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 { execFileSync } from 'node:child_process' import { fileURLToPath } from 'node:url' import { discoverGoalFiles } from '../src/goals.mjs' const SCRIPT = fileURLToPath(new URL('../new-plan.mjs', import.meta.url)) function tmpProject() { return fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-new-plan-')) } function run(args, project) { return execFileSync(process.execPath, [SCRIPT, ...args, '--project', project], { encoding: 'utf8' }) } test('new-plan writes a discoverable plan from the project template', () => { const project = tmpProject() fs.mkdirSync(path.join(project, '.kilo', 'templates'), { recursive: true }) fs.writeFileSync( path.join(project, '.kilo', 'templates', 'plan.md'), '# {{TITLE}}\n\n## Stages (kilo-loop)\n\n- [ ] Ship it :: verify: echo ok :: acceptance: shipped\n', ) const out = run(['My Feature'], project).trim() const abs = path.join(project, out) assert.ok(fs.existsSync(abs), `plan file exists: ${out}`) assert.match(path.basename(out), /^\d+-my-feature\.md$/) const body = fs.readFileSync(abs, 'utf8') assert.match(body, /^# My Feature$/m) assert.match(body, /- \[ \] Ship it/) // The whole point: kilo-loop must discover it. assert.ok(discoverGoalFiles(project).some((f) => path.resolve(f) === path.resolve(abs))) }) test('new-plan falls back to a built-in template when the project has none', () => { const project = tmpProject() const out = run(['Standalone'], project).trim() const body = fs.readFileSync(path.join(project, out), 'utf8') assert.match(body, /^# Standalone$/m) assert.match(body, /## Stages \(kilo-loop\)/) assert.match(body, /- \[ \] Stage 1/) }) test('new-plan --print does not write a file', () => { const project = tmpProject() const out = run(['Preview only', '--print'], project) assert.match(out, /^# Preview only$/m) assert.equal(fs.existsSync(path.join(project, '.kilo', 'plans')), false) }) test('new-plan targets the CURRENT version folder when a marker exists', () => { const project = tmpProject() fs.mkdirSync(path.join(project, '.kilo', 'plans'), { recursive: true }) fs.writeFileSync(path.join(project, '.kilo', 'plans', 'CURRENT'), 'v1.2\n') const out = run(['Versioned', '--epoch', '1700000000000'], project).trim() assert.equal(out, '.kilo/plans/v1.2/1700000000000-versioned.md') // Recursive discovery must still find it. const abs = path.join(project, out) assert.ok(discoverGoalFiles(project).some((f) => path.resolve(f) === path.resolve(abs))) }) test('new-plan --version targets an explicit version folder', () => { const project = tmpProject() const out = run(['Next', '--version', 'v1.3', '--epoch', '1700000000000'], project).trim() assert.equal(out, '.kilo/plans/v1.3/1700000000000-next.md') }) test('new-plan refuses to overwrite without --force', () => { const project = tmpProject() const first = run(['Same', '--slug', 'same', '--epoch', '1700000000000'], project).trim() assert.equal(first, '.kilo/plans/1700000000000-same.md') assert.throws(() => run(['Same', '--slug', 'same', '--epoch', '1700000000000'], project), /already exists/) // --force makes it succeed again. assert.doesNotThrow(() => execFileSync(process.execPath, [ SCRIPT, 'Same', '--slug', 'same', '--epoch', '1700000000000', '--force', '--project', project, ]), ) assert.ok(fs.existsSync(path.join(project, first))) })