Voorbereidende wijzigingen voor de queue-loop-refactor (zie
docs/plans/queue-loop-extraction.md in Scrum4Me-repo). Maakt scrum4me-mcp
geschikt als gedeelde library voor de nieuwe scrum4me-docker runner.
- T-13: export getFullJobContext uit src/tools/wait-for-job.ts
- T-14: mapBudgetToEffort(budget) → --effort {medium,high,xhigh,max} mapping
voor Claude CLI 2.1.x (heeft geen --thinking-budget). Comment in header
documenteert dat max_turns audit-only is en de CLI-flag-mapping.
- T-15: KIND_DEFAULTS.allowed_tools van null → expliciete lijsten zonder
wait_for_job/check_queue_empty/get_idea_context. Vangrail tegen recursieve
claims. SPRINT_IMPLEMENTATION mist bewust job_heartbeat (runner doet
lease-renewal).
- T-16: src/lib/idea-prompts.ts → src/lib/kind-prompts.ts. Nieuwe export
getKindPromptText voor alle 5 kinds. Back-compat re-export
getIdeaPromptText behouden zodat wait-for-job.ts:508 ongewijzigd werkt.
- T-17: nieuwe prompts src/prompts/task/implementation.md,
sprint/implementation.md, plan-chat/chat.md. Idea-prompts (M12) ongewijzigd.
Tests: 334 passed (38 files). 27 nieuwe asserts: mapBudgetToEffort
grenswaarden (14), KIND_DEFAULTS.allowed_tools structurele checks (6),
kind-prompts loading + verboden-tool-mentions (13).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
166 lines
6.6 KiB
TypeScript
166 lines
6.6 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { getKindDefault, resolveJobConfig, mapBudgetToEffort } from '../src/lib/job-config.js'
|
|
|
|
const KIND_EXPECTED = {
|
|
IDEA_GRILL: { model: 'claude-sonnet-4-6', thinking_budget: 12000, permission_mode: 'plan', max_turns: 15 },
|
|
IDEA_MAKE_PLAN: { model: 'claude-opus-4-7', thinking_budget: 24000, permission_mode: 'plan', max_turns: 20 },
|
|
PLAN_CHAT: { model: 'claude-sonnet-4-6', thinking_budget: 6000, permission_mode: 'plan', max_turns: 5 },
|
|
TASK_IMPLEMENTATION: { model: 'claude-sonnet-4-6', thinking_budget: 6000, permission_mode: 'bypassPermissions', max_turns: 50 },
|
|
SPRINT_IMPLEMENTATION: { model: 'claude-sonnet-4-6', thinking_budget: 6000, permission_mode: 'bypassPermissions', max_turns: null },
|
|
} as const
|
|
|
|
describe('getKindDefault', () => {
|
|
for (const [kind, expected] of Object.entries(KIND_EXPECTED)) {
|
|
it(`returnt de juiste defaults voor ${kind}`, () => {
|
|
const cfg = getKindDefault(kind)
|
|
expect(cfg.model).toBe(expected.model)
|
|
expect(cfg.thinking_budget).toBe(expected.thinking_budget)
|
|
expect(cfg.permission_mode).toBe(expected.permission_mode)
|
|
expect(cfg.max_turns).toBe(expected.max_turns)
|
|
})
|
|
}
|
|
|
|
it('valt terug op een veilige fallback voor onbekende kinds', () => {
|
|
const cfg = getKindDefault('SOMETHING_NEW')
|
|
expect(cfg.model).toBe('claude-sonnet-4-6')
|
|
expect(cfg.permission_mode).toBe('default')
|
|
})
|
|
})
|
|
|
|
describe('resolveJobConfig — geen overrides', () => {
|
|
for (const kind of Object.keys(KIND_EXPECTED)) {
|
|
it(`returnt kind-default voor ${kind} zonder overrides`, () => {
|
|
const cfg = resolveJobConfig({ kind }, {})
|
|
expect(cfg).toEqual(getKindDefault(kind))
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('resolveJobConfig — cascade', () => {
|
|
it('product.preferred_model overrult kind-default', () => {
|
|
const cfg = resolveJobConfig({ kind: 'TASK_IMPLEMENTATION' }, { preferred_model: 'claude-haiku-4-5-20251001' })
|
|
expect(cfg.model).toBe('claude-haiku-4-5-20251001')
|
|
})
|
|
|
|
it('job.requested_model overrult product.preferred_model', () => {
|
|
const cfg = resolveJobConfig(
|
|
{ kind: 'TASK_IMPLEMENTATION', requested_model: 'claude-opus-4-7' },
|
|
{ preferred_model: 'claude-haiku-4-5-20251001' },
|
|
)
|
|
expect(cfg.model).toBe('claude-opus-4-7')
|
|
})
|
|
|
|
it('task.requires_opus overrult product.preferred_model', () => {
|
|
const cfg = resolveJobConfig(
|
|
{ kind: 'TASK_IMPLEMENTATION' },
|
|
{ preferred_model: 'claude-sonnet-4-6' },
|
|
{ requires_opus: true },
|
|
)
|
|
expect(cfg.model).toBe('claude-opus-4-7')
|
|
})
|
|
|
|
it('task.requires_opus overrult ook job.requested_model = haiku', () => {
|
|
const cfg = resolveJobConfig(
|
|
{ kind: 'TASK_IMPLEMENTATION', requested_model: 'claude-haiku-4-5-20251001' },
|
|
{},
|
|
{ requires_opus: true },
|
|
)
|
|
expect(cfg.model).toBe('claude-opus-4-7')
|
|
})
|
|
|
|
it('job.requested_thinking_budget overrult kind-default', () => {
|
|
const cfg = resolveJobConfig({ kind: 'PLAN_CHAT', requested_thinking_budget: 1024 }, {})
|
|
expect(cfg.thinking_budget).toBe(1024)
|
|
})
|
|
|
|
it('product.thinking_budget_default overrult kind-default', () => {
|
|
const cfg = resolveJobConfig({ kind: 'IDEA_GRILL' }, { thinking_budget_default: 0 })
|
|
expect(cfg.thinking_budget).toBe(0)
|
|
})
|
|
|
|
it('product.preferred_permission_mode = acceptEdits overrult bypassPermissions voor TASK_IMPLEMENTATION', () => {
|
|
const cfg = resolveJobConfig(
|
|
{ kind: 'TASK_IMPLEMENTATION' },
|
|
{ preferred_permission_mode: 'acceptEdits' },
|
|
)
|
|
expect(cfg.permission_mode).toBe('acceptEdits')
|
|
})
|
|
|
|
it('max_turns blijft kind-default ook met product- en job-overrides (geen V1-cascade)', () => {
|
|
const cfg = resolveJobConfig(
|
|
{ kind: 'IDEA_GRILL', requested_model: 'claude-haiku-4-5-20251001' },
|
|
{ preferred_model: 'claude-sonnet-4-6' },
|
|
)
|
|
expect(cfg.max_turns).toBe(15)
|
|
})
|
|
})
|
|
|
|
describe('KIND_DEFAULTS.allowed_tools', () => {
|
|
it('TASK_IMPLEMENTATION bevat geen claim-tools', () => {
|
|
const cfg = getKindDefault('TASK_IMPLEMENTATION')
|
|
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__wait_for_job')
|
|
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__check_queue_empty')
|
|
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__get_idea_context')
|
|
})
|
|
|
|
it('TASK_IMPLEMENTATION bevat de essentiële task-tools', () => {
|
|
const cfg = getKindDefault('TASK_IMPLEMENTATION')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_task_status')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_job_status')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__verify_task_against_plan')
|
|
expect(cfg.allowed_tools).toContain('Bash')
|
|
expect(cfg.allowed_tools).toContain('Edit')
|
|
expect(cfg.allowed_tools).toContain('Write')
|
|
})
|
|
|
|
it('SPRINT_IMPLEMENTATION bevat sprint-specifieke tools maar GEEN job_heartbeat (runner doet die)', () => {
|
|
const cfg = getKindDefault('SPRINT_IMPLEMENTATION')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_task_execution')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__verify_sprint_task')
|
|
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__job_heartbeat')
|
|
})
|
|
|
|
it('IDEA_GRILL bevat update_idea_grill_md en geen wait_for_job', () => {
|
|
const cfg = getKindDefault('IDEA_GRILL')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_idea_grill_md')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__log_idea_decision')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_job_status')
|
|
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__wait_for_job')
|
|
})
|
|
|
|
it('IDEA_MAKE_PLAN bevat update_idea_plan_md en geen wait_for_job', () => {
|
|
const cfg = getKindDefault('IDEA_MAKE_PLAN')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__update_idea_plan_md')
|
|
expect(cfg.allowed_tools).toContain('mcp__scrum4me__log_idea_decision')
|
|
expect(cfg.allowed_tools).not.toContain('mcp__scrum4me__wait_for_job')
|
|
})
|
|
|
|
it('alle kinds hebben non-null allowed_tools', () => {
|
|
for (const kind of ['IDEA_GRILL', 'IDEA_MAKE_PLAN', 'PLAN_CHAT', 'TASK_IMPLEMENTATION', 'SPRINT_IMPLEMENTATION']) {
|
|
const cfg = getKindDefault(kind)
|
|
expect(cfg.allowed_tools).not.toBeNull()
|
|
expect(Array.isArray(cfg.allowed_tools)).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('mapBudgetToEffort', () => {
|
|
it.each([
|
|
[0, null],
|
|
[-1, null],
|
|
[1, 'medium'],
|
|
[3000, 'medium'],
|
|
[6000, 'medium'],
|
|
[6001, 'high'],
|
|
[9000, 'high'],
|
|
[12000, 'high'],
|
|
[12001, 'xhigh'],
|
|
[18000, 'xhigh'],
|
|
[24000, 'xhigh'],
|
|
[24001, 'max'],
|
|
[50000, 'max'],
|
|
[100000, 'max'],
|
|
])('budget %i → %s', (budget, expected) => {
|
|
expect(mapBudgetToEffort(budget)).toBe(expected)
|
|
})
|
|
})
|