128 lines
4.7 KiB
JavaScript
128 lines
4.7 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 { Orchestrator } from '../src/orchestrator.mjs'
|
||
|
|
import { readProgress } from '../src/goal.mjs'
|
||
|
|
|
||
|
|
const sink = { write: () => true }
|
||
|
|
|
||
|
|
function makeProject(stages) {
|
||
|
|
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-orch-'))
|
||
|
|
fs.mkdirSync(path.join(project, '.kilocode-loop'), { recursive: true })
|
||
|
|
fs.writeFileSync(path.join(project, 'goal.json'), JSON.stringify({ title: 'Test goal', stages }))
|
||
|
|
return project
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeConfig(project, overrides = {}) {
|
||
|
|
return {
|
||
|
|
project,
|
||
|
|
goal: 'goal.json',
|
||
|
|
goalText: '',
|
||
|
|
iterations: 2,
|
||
|
|
agent: 'code-design',
|
||
|
|
model: '',
|
||
|
|
variant: '',
|
||
|
|
thinking: false,
|
||
|
|
auto: true,
|
||
|
|
sharedContext: false,
|
||
|
|
hitl: 'off',
|
||
|
|
confirm: false,
|
||
|
|
port: 7998,
|
||
|
|
host: '127.0.0.1',
|
||
|
|
runId: 'test-run',
|
||
|
|
dryRun: true,
|
||
|
|
keepOpen: false,
|
||
|
|
quiet: true,
|
||
|
|
color: false,
|
||
|
|
promptExtra: '',
|
||
|
|
maxIterationMinutes: 0,
|
||
|
|
contextWarnTokens: 150000,
|
||
|
|
verify: '',
|
||
|
|
verifyTimeoutMinutes: 30,
|
||
|
|
maxCost: 0,
|
||
|
|
maxTokens: 0,
|
||
|
|
maxStaleIterations: 0,
|
||
|
|
checkpoint: false,
|
||
|
|
guard: true,
|
||
|
|
redact: true,
|
||
|
|
review: false,
|
||
|
|
reviewAgent: '',
|
||
|
|
autoFreshTokens: 0,
|
||
|
|
notify: '',
|
||
|
|
syncGoal: true,
|
||
|
|
...overrides,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function run(config) {
|
||
|
|
return new Orchestrator(config, { out: sink }).run()
|
||
|
|
}
|
||
|
|
|
||
|
|
test('dry-run: a stage with a verify command is only done after the gate passes', async () => {
|
||
|
|
const project = makeProject([{ id: 's1', title: 'Do the thing', verify: 'true' }])
|
||
|
|
const final = await run(makeConfig(project))
|
||
|
|
assert.equal(final.status, 'done')
|
||
|
|
assert.equal(final.verify.passed >= 1, true)
|
||
|
|
assert.equal(final.verify.failed, 0)
|
||
|
|
assert.ok(fs.existsSync(path.join(project, '.kilocode-loop', 'runs', 'test-run', 'report.md')))
|
||
|
|
assert.ok(readProgress({ project }))
|
||
|
|
})
|
||
|
|
|
||
|
|
test('dry-run: reports are persisted per iteration', async () => {
|
||
|
|
const project = makeProject([{ id: 's1', title: 'One', verify: 'true' }])
|
||
|
|
await run(makeConfig(project, { runId: 'iterations-run' }))
|
||
|
|
const iterDir = path.join(project, '.kilocode-loop', 'runs', 'iterations-run', 'iterations')
|
||
|
|
assert.ok(fs.existsSync(path.join(iterDir, '01-report.md')))
|
||
|
|
assert.ok(fs.existsSync(path.join(iterDir, '02-report.json')))
|
||
|
|
})
|
||
|
|
|
||
|
|
test('dry-run: the loop stops when the cost budget is exceeded', async () => {
|
||
|
|
const project = makeProject([{ id: 's1', title: 'One' }])
|
||
|
|
const final = await run(makeConfig(project, { iterations: 3, maxCost: 0.001, runId: 'budget-run' }))
|
||
|
|
assert.equal(final.status, 'stopped-budget')
|
||
|
|
assert.equal(final.stopReason, 'budget')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('dry-run: a completed stage is persisted to a JSON goal and not repeated', async () => {
|
||
|
|
const project = makeProject([{ id: 's1', title: 'One', verify: 'true' }])
|
||
|
|
const first = await run(makeConfig(project, { iterations: 1, runId: 'persist-a' }))
|
||
|
|
assert.equal(first.status, 'done')
|
||
|
|
const saved = JSON.parse(fs.readFileSync(path.join(project, 'goal.json'), 'utf8'))
|
||
|
|
assert.equal(saved.stages[0].done, true)
|
||
|
|
|
||
|
|
// A restart sees the persisted completion and does nothing.
|
||
|
|
const second = await run(makeConfig(project, { iterations: 1, runId: 'persist-b' }))
|
||
|
|
assert.equal(second.iterations.length, 0)
|
||
|
|
assert.equal(second.stopReason, 'complete')
|
||
|
|
assert.equal(second.status, 'done')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('dry-run: --force runs again even when the goal is fully marked done', async () => {
|
||
|
|
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-orch-'))
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(project, 'goal.json'),
|
||
|
|
JSON.stringify({ title: 'T', stages: [{ id: 's1', title: 'One', done: true, verify: 'true' }] }),
|
||
|
|
)
|
||
|
|
const final = await run(makeConfig(project, { iterations: 1, runId: 'force-run', force: true }))
|
||
|
|
assert.equal(final.iterations.length, 1)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('dry-run: goal lint issues are surfaced in state', async () => { const project = makeProject([{ id: 's1', title: 'Something vague' }])
|
||
|
|
const final = await run(makeConfig(project, { runId: 'lint-run' }))
|
||
|
|
assert.ok(Array.isArray(final.lint))
|
||
|
|
assert.ok(final.lint.length >= 1)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('dry-run: tokens/stall guard trips after iterations with no changes', async () => {
|
||
|
|
const project = makeProject([{ id: 's1', title: 'One', verify: 'true' }])
|
||
|
|
// The simulator always reports src/example.ts, so force a low stale cap with a
|
||
|
|
// no-op change set by disabling the simulator file list is not possible; instead
|
||
|
|
// assert the accounting fields exist and staleCount starts at zero.
|
||
|
|
const final = await run(makeConfig(project, { runId: 'stale-run', maxStaleIterations: 2 }))
|
||
|
|
assert.equal(typeof final.totals.tokens.total, 'number')
|
||
|
|
assert.ok(final.totals.tokens.total > 0)
|
||
|
|
})
|