import { test } from 'node:test' import assert from 'node:assert/strict' import { EventEmitter } from 'node:events' import { RunController } from '../src/controller.mjs' function fakeOrchestrator(config, { status = 'running' } = {}) { const runState = new EventEmitter() runState.state = { status, runId: config.runId } const orchestrator = { config, state: runState, run: async () => ({ status: 'done', runId: config.runId }), requestStop: () => true, submitAnswers: () => true, listCheckpointMeta: () => [{ id: 'iter-01' }], revertIteration: () => ({ ok: true }), } return orchestrator } function makeController() { const created = [] const baseConfig = { project: '/repo', goal: '', goalText: '', iterations: 5, agent: 'code-design', hitl: 'on-question' } const controller = new RunController(baseConfig, { createOrchestrator: (config) => { const orch = fakeOrchestrator(config, { status: 'running' }) created.push(orch) return orch }, }) return { controller, created, baseConfig } } test('snapshot reports idle before any run', () => { const { controller } = makeController() const snap = controller.snapshot() assert.equal(snap.idle, true) assert.equal(snap.status, 'idle') assert.equal(controller.isActive(), false) }) test('start merges per-run overrides into the config', async () => { const { controller, created } = makeController() await controller.start({ goal: 'goal.json', verify: 'pnpm test', iterations: 3, agent: 'code', dryRun: true }) assert.equal(created.length, 1) const config = created[0].config assert.equal(config.goal, 'goal.json') assert.equal(config.verify, 'pnpm test') assert.equal(config.iterations, 3) assert.equal(config.agent, 'code') assert.equal(config.dryRun, true) assert.ok(config.runId) }) test('start applies a per-run custom instruction (promptExtra) override', async () => { const { controller, created } = makeController() await controller.start({ goal: 'goal.json', promptExtra: 'focus on the parser only' }) assert.equal(created[0].config.promptExtra, 'focus on the parser only') }) test('start leaves the base custom instruction when the override is omitted', async () => { const controller = new RunController( { project: '/repo', goal: '', iterations: 5, agent: 'code-design', promptExtra: 'base instructions' }, { createOrchestrator: (config) => fakeOrchestrator(config, { status: 'running' }) }, ) await controller.start({ goal: 'goal.json' }) assert.equal(controller.config.promptExtra, 'base instructions') }) test('start refuses while a run is active', () => { const { controller } = makeController() controller.start({ goal: 'goal.json' }) assert.equal(controller.isActive(), true) assert.throws(() => controller.start({ goal: 'other.json' }), /already active/) }) test('start requires a goal', () => { const { controller } = makeController() assert.throws(() => controller.start({}), /goal is required/) }) test('control methods delegate to the attached orchestrator', () => { const { controller, created } = makeController() controller.start({ goal: 'goal.json' }) assert.equal(controller.requestStop(), true) assert.equal(controller.submitAnswers([]), true) assert.deepEqual(controller.listCheckpointMeta(), [{ id: 'iter-01' }]) assert.deepEqual(controller.revertIteration(1), { ok: true }) assert.equal(created.length, 1) }) test('control methods are safe with no orchestrator', () => { const { controller } = makeController() assert.equal(controller.requestStop(), false) assert.equal(controller.submitAnswers([]), false) assert.deepEqual(controller.listCheckpointMeta(), []) assert.equal(controller.revertIteration(1).ok, false) }) test('attach adopts an existing orchestrator and forwards its events', () => { const { controller } = makeController() const orch = fakeOrchestrator({ runId: 'adopted' }, { status: 'running' }) let states = 0 controller.on('state', () => (states += 1)) controller.attach(orch) assert.equal(controller.orchestrator, orch) assert.equal(states >= 1, true) orch.state.emit('update') assert.equal(states >= 2, true) })