kilo-loop/test/goal.test.mjs

111 lines
3.8 KiB
JavaScript
Raw Normal View History

2026-09-13 10:46:28 +00:00
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 {
classifyProgress,
extractQuestionsFromText,
loadGoal,
normaliseQuestions,
resetProgress,
readProgress,
buildIterationPrompt,
} from '../src/goal.mjs'
function tmpProject() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-loop-'))
return dir
}
test('loadGoal parses markdown checkboxes', () => {
const project = tmpProject()
const file = path.join(project, 'goal.md')
fs.writeFileSync(file, '# My goal\n\nBody text.\n\n- [x] done one\n- [ ] next one\n')
const goal = loadGoal({ project, goal: 'goal.md', goalText: '' })
assert.equal(goal.title, 'My goal')
assert.equal(goal.stages.length, 2)
assert.equal(goal.stages[0].done, true)
assert.equal(goal.stages[1].title, 'next one')
})
test('loadGoal parses json stages', () => {
const project = tmpProject()
const file = path.join(project, 'goal.json')
fs.writeFileSync(file, JSON.stringify({ title: 'T', stages: [{ id: 'a', title: 'A' }] }))
const goal = loadGoal({ project, goal: 'goal.json', goalText: '' })
assert.equal(goal.stages[0].id, 'a')
})
test('loadGoal falls back to a single stage for inline text', () => {
const goal = loadGoal({ project: '/tmp', goal: '', goalText: 'Do the thing' })
assert.equal(goal.stages.length, 1)
assert.equal(goal.title, 'Do the thing')
})
test('classifyProgress prefers explicit percent and normalises questions', () => {
const goal = { title: 'G', stages: [{ id: 's1', title: 'One' }, { id: 's2', title: 'Two' }] }
const cls = classifyProgress(
{ topic: 'topic', percent: 42, stageId: 's2', stageStatus: 'in_progress', nextStage: 'Two', questions: ['Why?'] },
goal,
1,
)
assert.equal(cls.percent, 42)
assert.equal(cls.topic, 'topic')
assert.equal(cls.nextStage, 'Two')
assert.equal(cls.needsInput, true)
assert.equal(cls.questions[0].text, 'Why?')
})
test('classifyProgress derives percent from completed stages', () => {
const goal = { title: 'G', stages: [{ id: 's1', title: 'One' }, { id: 's2', title: 'Two' }] }
const cls = classifyProgress({ stageId: 's1', stageStatus: 'done' }, goal, 1)
assert.equal(cls.percent, 50)
assert.equal(cls.stageStatus, 'done')
})
test('classifyProgress clamps out-of-range percentages', () => {
const goal = { title: 'G', stages: [{ id: 's1', title: 'One' }] }
assert.equal(classifyProgress({ percent: 900 }, goal, 1).percent, 100)
assert.equal(classifyProgress({ percent: -5 }, goal, 1).percent, 0)
})
test('normaliseQuestions handles strings and objects, filters blanks', () => {
const qs = normaliseQuestions(['a?', { id: 'x', text: 'b?', options: [1, 2] }, { text: ' ' }])
assert.equal(qs.length, 2)
assert.deepEqual(qs[1].options, ['1', '2'])
})
test('extractQuestionsFromText pulls question lines only', () => {
const qs = extractQuestionsFromText(['Line one\ndoes this work?\nhttp://x?y=1\nAnother question?'])
assert.equal(qs.length, 2)
assert.match(qs[0].text, /does this work\?/)
})
test('resetProgress + readProgress round-trip', () => {
const project = tmpProject()
resetProgress({ project }, 'run-1', 3)
const p = readProgress({ project })
assert.equal(p.runId, 'run-1')
assert.equal(p.iteration, 3)
})
test('buildIterationPrompt embeds the progress path and answers', () => {
const project = tmpProject()
const goal = { title: 'G', description: 'd', stages: [{ id: 's1', title: 'One' }] }
const prompt = buildIterationPrompt({
config: { project, promptExtra: '' },
goal,
iteration: 2,
total: 5,
progress: { stageId: 's1', summary: 'prev', iteration: 1 },
answers: [{ question: 'Q?', answer: 'A!' }],
currentStageIndex: 0,
})
assert.match(prompt, /iteration 2\/5/i)
assert.match(prompt, /progress\.json/)
assert.match(prompt, /Q:/)
assert.match(prompt, /A!/)
})