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!/) })