import { test } from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { allStagesDone, lintGoal, loadGoal, syncGoalCheckbox, syncGoalDone, syncGoalStage } from '../src/goal.mjs' function tmpProject() { return fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-goal-')) } test('loadGoal parses verify/acceptance/model directives from a JSON stage', () => { const project = tmpProject() const file = path.join(project, 'goal.json') fs.writeFileSync( file, JSON.stringify({ title: 'T', stages: [{ id: 's1', title: 'A', verify: 'pnpm test', acceptance: 'green', model: 'p/m', agent: 'code', variant: 'high' }], }), ) const goal = loadGoal({ project, goal: 'goal.json', goalText: '' }) assert.equal(goal.stages[0].verify, 'pnpm test') assert.equal(goal.stages[0].acceptance, 'green') assert.equal(goal.stages[0].model, 'p/m') assert.equal(goal.stages[0].agent, 'code') assert.equal(goal.stages[0].variant, 'high') }) test('loadGoal parses ":: verify: ..." directives from markdown checkboxes', () => { const project = tmpProject() const file = path.join(project, 'goal.md') fs.writeFileSync(file, '# G\n\n- [ ] One :: verify: pnpm test :: acceptance: all green\n- [ ] Two\n') const goal = loadGoal({ project, goal: 'goal.md', goalText: '' }) assert.equal(goal.stages[0].verify, 'pnpm test') assert.equal(goal.stages[0].acceptance, 'all green') assert.equal(goal.stages[0].title, 'One') assert.equal(goal.stages[1].verify, '') }) test('lintGoal warns when a stage has no verify command', () => { const goal = { stages: [{ id: 's1', title: 'Rebuild the parser' }, { id: 's2', title: 'x', verify: 'true' }] } const issues = lintGoal(goal, {}) assert.ok(issues.some((i) => i.stageId === 's1' && /no verify/.test(i.message))) assert.ok(!issues.some((i) => i.stageId === 's2' && /no verify/.test(i.message))) }) test('lintGoal reports duplicate ids as errors and vague titles as warnings', () => { const goal = { stages: [{ id: 'dup', title: 'Fix stuff' }, { id: 'dup', title: 'Another thing' }] } const issues = lintGoal(goal, { verify: 'true' }) assert.ok(issues.some((i) => i.level === 'error' && /duplicate stage id/.test(i.message))) assert.ok(issues.some((i) => i.level === 'warn' && /vague/.test(i.message))) }) test('syncGoalCheckbox ticks the matching markdown checkbox only', () => { const project = tmpProject() const file = path.join(project, 'plan.md') fs.writeFileSync(file, '# G\n\n- [ ] First :: verify: t\n- [ ] Second\n') const goal = { source: file, stages: [{ id: 's1', title: 'First' }, { id: 's2', title: 'Second' }] } const res = syncGoalCheckbox(goal, goal.stages[0]) assert.equal(res.changed, true) const text = fs.readFileSync(file, 'utf8') assert.match(text, /- \[x\] First :: verify: t/) assert.match(text, /- \[ \] Second/) }) test('syncGoalCheckbox is a no-op for non-markdown goals', () => { const res = syncGoalCheckbox({ source: '/tmp/goal.json' }, { title: 'x' }) assert.equal(res.ok, false) assert.match(res.reason, /not a markdown file/) }) test('syncGoalDone marks a JSON stage done by id and preserves formatting', () => { const project = tmpProject() const file = path.join(project, 'goal.json') fs.writeFileSync(file, JSON.stringify({ title: 'G', stages: [{ id: 's1', title: 'One' }, { id: 's2', title: 'Two' }] })) const res = syncGoalDone({ source: file }, { id: 's1', title: 'One' }) assert.equal(res.changed, true) const saved = JSON.parse(fs.readFileSync(file, 'utf8')) assert.equal(saved.stages[0].done, true) assert.equal(saved.stages[1].done, undefined) // Idempotent: a second call reports no change. assert.equal(syncGoalDone({ source: file }, { id: 's1', title: 'One' }).changed, false) }) test('syncGoalDone falls back to a title match', () => { const project = tmpProject() const file = path.join(project, 'goal.json') fs.writeFileSync(file, JSON.stringify({ title: 'G', stages: [{ title: 'Only by title' }] })) const res = syncGoalDone({ source: file }, { id: 'missing', title: 'Only by title' }) assert.equal(res.changed, true) assert.equal(JSON.parse(fs.readFileSync(file, 'utf8')).stages[0].done, true) }) test('syncGoalStage dispatches by file extension', () => { const project = tmpProject() const md = path.join(project, 'g.md') fs.writeFileSync(md, '# G\n\n- [ ] One\n') assert.equal(syncGoalStage({ source: md }, { title: 'One' }).ok, true) const json = path.join(project, 'g.json') fs.writeFileSync(json, JSON.stringify({ stages: [{ id: 's1', title: 'One' }] })) assert.equal(syncGoalStage({ source: json }, { id: 's1', title: 'One' }).changed, true) assert.equal(syncGoalStage({ source: '/tmp/other.txt' }, { title: 'One' }).ok, false) }) test('allStagesDone requires a non-empty goal where every stage is done', () => { assert.equal(allStagesDone({ stages: [{ done: true }, { done: true }] }), true) assert.equal(allStagesDone({ stages: [{ done: true }, { done: false }] }), false) assert.equal(allStagesDone({ stages: [] }), false) assert.equal(allStagesDone({}), false) })