kilo-loop/test/goals.test.mjs
Vitali sharp8n 1eb9ec22c2 kilo loop
2026-09-13 19:29:03 +03:00

200 lines
8.2 KiB
JavaScript

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 {
deleteGoalFile,
discoverGoalFiles,
listGoalSummaries,
loadGoalSummary,
resolveGoalPath,
setGlobalVerify,
setGoalAllDone,
setGoalVerify,
} from '../src/goals.mjs'
function tmpProject() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-goals-'))
}
test('discoverGoalFiles finds curated goals, the legacy goal, and checkbox plans', () => {
const project = tmpProject()
fs.mkdirSync(path.join(project, '.kilocode-loop', 'goals'), { recursive: true })
fs.writeFileSync(path.join(project, '.kilocode-loop', 'goals', 'a.json'), JSON.stringify({ title: 'A', stages: [] }))
fs.writeFileSync(path.join(project, '.kilocode-loop', 'goals', 'b.md'), '# B\n\n- [ ] one\n')
fs.writeFileSync(path.join(project, '.kilocode-loop', 'goal.json'), JSON.stringify({ title: 'Legacy', stages: [] }))
fs.mkdirSync(path.join(project, 'plans'), { recursive: true })
fs.writeFileSync(path.join(project, 'plans', 'with.md'), '# P\n\n- [ ] step\n')
fs.writeFileSync(path.join(project, 'plans', 'without.md'), '# Q\n\nno checkboxes here\n')
const found = discoverGoalFiles(project).map((f) => path.relative(project, f).split(path.sep).join('/'))
assert.ok(found.includes('.kilocode-loop/goals/a.json'))
assert.ok(found.includes('.kilocode-loop/goals/b.md'))
assert.ok(found.includes('.kilocode-loop/goal.json'))
assert.ok(found.includes('plans/with.md'))
assert.ok(!found.includes('plans/without.md'))
})
test('loadGoalSummary computes completion, verify and the last run', () => {
const project = tmpProject()
const goalFile = path.join(project, '.kilocode-loop', 'goal.json')
fs.mkdirSync(path.dirname(goalFile), { recursive: true })
fs.writeFileSync(
goalFile,
JSON.stringify({
title: 'My plan',
stages: [
{ id: 's1', title: 'One', done: true, verify: 'pnpm test' },
{ id: 's2', title: 'Two' },
],
}),
)
fs.writeFileSync(path.join(project, '.kilocode-loop', 'config.json'), JSON.stringify({ verify: 'dotnet test' }))
const runDir = path.join(project, '.kilocode-loop', 'runs', 'r1')
fs.mkdirSync(runDir, { recursive: true })
fs.writeFileSync(
path.join(runDir, 'state.json'),
JSON.stringify({
runId: 'r1',
startedAt: '2026-09-13T00:00:00Z',
status: 'done',
goal: { title: 'My plan', source: goalFile },
iterations: [{ index: 1 }],
totals: { cost: 0.5, tokens: { total: 1234 } },
}),
)
const summary = loadGoalSummary(project, '.kilocode-loop/goal.json')
assert.equal(summary.title, 'My plan')
assert.equal(summary.total, 2)
assert.equal(summary.done, 1)
assert.equal(summary.percent, 50)
assert.equal(summary.globalVerify, 'dotnet test')
assert.equal(summary.stages[0].verify, 'pnpm test')
assert.equal(summary.lastRun.runId, 'r1')
assert.equal(summary.lastRun.status, 'done')
})
test('setGoalVerify edits a JSON stage and the JSON global verify', () => {
const project = tmpProject()
const file = path.join(project, 'goal.json')
fs.writeFileSync(file, JSON.stringify({ title: 'T', stages: [{ id: 's1', title: 'One' }, { id: 's2', title: 'Two' }] }))
assert.equal(setGoalVerify(project, 'goal.json', { stageIndex: 1, verify: 'pnpm build' }).ok, true)
let data = JSON.parse(fs.readFileSync(file, 'utf8'))
assert.equal(data.stages[1].verify, 'pnpm build')
assert.equal(setGoalVerify(project, 'goal.json', { verify: 'dotnet test' }).ok, true)
data = JSON.parse(fs.readFileSync(file, 'utf8'))
assert.equal(data.verify, 'dotnet test')
})
test('setGoalVerify adds and replaces the directive on a markdown stage', () => {
const project = tmpProject()
const file = path.join(project, 'plan.md')
fs.writeFileSync(file, '# P\n\n- [ ] First :: acceptance: green\n- [ ] Second\n')
assert.equal(setGoalVerify(project, 'plan.md', { stageIndex: 0, verify: 'pnpm test' }).ok, true)
let text = fs.readFileSync(file, 'utf8')
assert.match(text, /- \[ \] First :: acceptance: green :: verify: pnpm test/)
assert.equal(setGoalVerify(project, 'plan.md', { stageIndex: 0, verify: 'pnpm build' }).ok, true)
text = fs.readFileSync(file, 'utf8')
assert.match(text, /verify: pnpm build/)
assert.doesNotMatch(text, /pnpm test/)
assert.match(text, /- \[ \] Second/)
})
test('setGlobalVerify writes .kilocode-loop/config.json', () => {
const project = tmpProject()
assert.equal(setGlobalVerify(project, 'dotnet test').ok, true)
const config = JSON.parse(fs.readFileSync(path.join(project, '.kilocode-loop', 'config.json'), 'utf8'))
assert.equal(config.verify, 'dotnet test')
})
test('setGoalAllDone marks and clears markdown checkboxes, keeping directives', () => {
const project = tmpProject()
const file = path.join(project, 'plan.md')
fs.writeFileSync(file, '# P\n\n- [ ] First :: verify: pnpm test\n- [x] Second\n- plain note\n')
assert.equal(setGoalAllDone(project, 'plan.md', { done: true }).ok, true)
let text = fs.readFileSync(file, 'utf8')
assert.match(text, /- \[x\] First :: verify: pnpm test/)
assert.match(text, /- \[x\] Second/)
assert.match(text, /- plain note/)
assert.equal(loadGoalSummary(project, 'plan.md').percent, 100)
assert.equal(setGoalAllDone(project, 'plan.md', { done: false }).ok, true)
text = fs.readFileSync(file, 'utf8')
assert.match(text, /- \[ \] First :: verify: pnpm test/)
assert.match(text, /- \[ \] Second/)
assert.equal(loadGoalSummary(project, 'plan.md').percent, 0)
})
test('setGoalAllDone marks and clears JSON stages', () => {
const project = tmpProject()
const file = path.join(project, 'goal.json')
fs.writeFileSync(file, JSON.stringify({ title: 'T', stages: [{ id: 's1', title: 'One' }, { id: 's2', title: 'Two', done: true }] }))
assert.equal(setGoalAllDone(project, 'goal.json', { done: true }).ok, true)
let data = JSON.parse(fs.readFileSync(file, 'utf8'))
assert.deepEqual(data.stages.map((s) => s.done), [true, true])
assert.equal(setGoalAllDone(project, 'goal.json', { done: false }).ok, true)
data = JSON.parse(fs.readFileSync(file, 'utf8'))
assert.deepEqual(data.stages.map((s) => s.done), [false, false])
})
test('setGoalAllDone rejects a plan without checkboxes or stages', () => {
const project = tmpProject()
fs.writeFileSync(path.join(project, 'empty.md'), '# P\n\nno checkboxes\n')
const result = setGoalAllDone(project, 'empty.md', { done: true })
assert.equal(result.ok, false)
assert.match(result.error, /no checkbox stages/)
})
test('deleteGoalFile moves a plan to .kilocode-loop/trash (recoverable)', () => {
const project = tmpProject()
const file = path.join(project, 'plans', 'x.md')
fs.mkdirSync(path.dirname(file), { recursive: true })
fs.writeFileSync(file, '# X\n\n- [ ] one\n')
const result = deleteGoalFile(project, 'plans/x.md')
assert.equal(result.ok, true)
assert.equal(fs.existsSync(file), false)
const trashed = path.join(project, result.trashed)
assert.ok(fs.existsSync(trashed))
assert.match(fs.readFileSync(trashed, 'utf8'), /one/)
})
test('deleteGoalFile purges when asked', () => {
const project = tmpProject()
const file = path.join(project, 'gone.json')
fs.writeFileSync(file, JSON.stringify({ title: 'G', stages: [] }))
const result = deleteGoalFile(project, 'gone.json', { purge: true })
assert.equal(result.ok, true)
assert.equal(result.purged, true)
assert.equal(fs.existsSync(file), false)
assert.equal(fs.existsSync(path.join(project, '.kilocode-loop', 'trash')), false)
})
test('resolveGoalPath rejects paths outside the project', () => {
const project = tmpProject()
assert.throws(() => resolveGoalPath(project, '../../etc/passwd'), /inside the project/)
assert.throws(() => resolveGoalPath(project, 'missing.json'), /not found/)
})
test('listGoalSummaries skips broken goal files', () => {
const project = tmpProject()
fs.mkdirSync(path.join(project, '.kilocode-loop', 'goals'), { recursive: true })
fs.writeFileSync(path.join(project, '.kilocode-loop', 'goals', 'bad.json'), '{ not json')
fs.writeFileSync(path.join(project, '.kilocode-loop', 'goals', 'ok.json'), JSON.stringify({ title: 'OK', stages: [{ id: 's1', title: 'x' }] }))
const list = listGoalSummaries(project)
assert.equal(list.length, 1)
assert.equal(list[0].title, 'OK')
})