guanghu-ice-heart/server-tools/dark-core/task-controller.test.mjs

131 lines
8.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { DarkCoreTask, digest, inspectTask } from './task-controller.mjs';
import { localFileHost } from './local-file-host.mjs';
function fixture(t, overrides = {}) {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'dark-core-test-')));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const plan = { id: 'test', goal: 'write two approved files', channel: 'ICE-CH-DK001', authorizationRef: 'test-scope',
steps: [1, 2].map(i => ({ id: String(i), capability: 'CREATE_TEXT_FILE', args: { path: path.join(root, `${i}.txt`), text: `result ${i}` } })) };
const host = { ...localFileHost({ root, approvedPlan: plan, approvalReceipt: 'test-host-approval', verifyHuman: () => true }), ...overrides };
const directory = path.join(root, 'state');
const task = new DarkCoreTask({ plan, directory, host });
let sequence = 0;
const human = (kind, text = kind) => task.human({ id: `event-${++sequence}`, taskId: plan.id, kind, text });
return { root, plan, host, directory, task, human };
}
const deferred = () => { let resolve; const promise = new Promise(r => { resolve = r; }); return { promise, resolve }; };
test('real local writes complete only after both readbacks; immutable input and journal', async t => {
const f = fixture(t); f.plan.steps[0].args.text = 'changed after confirmation';
await f.task.run();
assert.equal(f.task.snapshot.status, 'COMPLETED');
assert.equal(fs.readFileSync(path.join(f.root, '1.txt'), 'utf8'), 'result 1');
assert.equal(inspectTask(f.directory).receipts.length, 2);
assert.throws(() => new DarkCoreTask({ plan: f.plan, directory: f.directory, host: f.host }), /EEXIST/);
});
test('ordinary advice stays queued during execution and rejection requires a reason', async t => {
const gate = deferred(), entered = deferred(); let calls = 0;
const f = fixture(t); const execute = f.host.execute;
f.host.execute = async args => { if (++calls === 1) { entered.resolve(); await gate.promise; } return execute(args); };
const running = f.task.run(); await entered.promise;
const before = f.task.snapshot.planHash;
await f.human('ADVICE', 'switch frameworks');
assert.equal(f.task.snapshot.status, 'RUNNING');
assert.throws(() => f.task.decideAdvice('event-1', 'REJECT', ''), /REASON_REQUIRED/);
f.task.decideAdvice('event-1', 'REJECT', 'Changing frameworks is outside the confirmed task.');
assert.equal(f.task.snapshot.planHash, before);
gate.resolve(); await running; assert.equal(f.task.snapshot.status, 'COMPLETED');
});
test('stop during asynchronous authorization prevents the first operation', async t => {
const gate = deferred(), entered = deferred();
const f = fixture(t); const authorize = f.host.authorize;
f.host.authorize = async args => { const g = await authorize(args); entered.resolve(); await gate.promise; return g; };
const running = f.task.run(); await entered.promise;
await f.human('STOP'); assert.equal(f.task.snapshot.status, 'STOP_REQUESTED');
gate.resolve(); await running;
assert.equal(f.task.snapshot.status, 'STOPPED'); assert.equal(fs.existsSync(path.join(f.root, '1.txt')), false);
});
test('withdrawal cancels a cooperative in-flight adapter and cannot be resumed', async t => {
const entered = deferred();
const f = fixture(t, { execute: ({ signal }) => new Promise((_, reject) => {
entered.resolve(); signal.addEventListener('abort', () => reject(Error('ABORTED')), { once: true });
}) });
const running = f.task.run(); await entered.promise; await f.human('WITHDRAW'); await running;
assert.equal(f.task.snapshot.status, 'WITHDRAWN');
assert.deepEqual(f.task.snapshot.uncertainSteps, ['1']);
await assert.rejects(f.human('RESUME'), /TASK_CLOSED/);
assert.equal(fs.existsSync(path.join(f.root, '2.txt')), false);
});
test('non-cancellable work is never falsely reported stopped while in flight', async t => {
const gate = deferred(), entered = deferred();
const f = fixture(t); const execute = f.host.execute;
f.host.execute = async args => { const result = await execute(args); entered.resolve(); await gate.promise; return result; };
const running = f.task.run(); await entered.promise; await f.human('STOP');
assert.equal(f.task.snapshot.status, 'STOP_REQUESTED');
gate.resolve(); await running;
assert.equal(f.task.snapshot.status, 'STOPPED'); assert.equal(f.task.snapshot.receipts.length, 1);
assert.equal(fs.existsSync(path.join(f.root, '2.txt')), false);
});
test('correction pauses at an authorization boundary and resume reauthorizes', async t => {
const gate = deferred(), entered = deferred(); let checks = 0;
const f = fixture(t); const authorize = f.host.authorize;
f.host.authorize = async args => { const grant = await authorize(args); if (++checks === 1) { entered.resolve(); await gate.promise; } return grant; };
const running = f.task.run(); await entered.promise; await f.human('CORRECTION', 'check the target');
gate.resolve(); await running; assert.equal(f.task.snapshot.status, 'PAUSED');
await f.human('RESUME', 'target checked; continue original task'); await f.task.run();
assert.equal(f.task.snapshot.status, 'COMPLETED'); assert.equal(checks, 3);
});
test('uncertain side effects block resume rather than repeat a partially executed operation', async t => {
const entered = deferred();
const f = fixture(t, { execute: ({ signal }) => new Promise((_, reject) => {
entered.resolve(); signal.addEventListener('abort', () => reject(Error('PARTIAL_EFFECT_POSSIBLE')));
}) });
const running = f.task.run(); await entered.promise; await f.human('CORRECTION'); await running;
assert.equal(f.task.snapshot.status, 'PAUSED');
await assert.rejects(f.human('RESUME'), /UNCERTAIN_EFFECTS/);
});
test('later correction does not override withdrawal', async t => {
const gate = deferred(), entered = deferred();
const f = fixture(t); const authorize = f.host.authorize;
f.host.authorize = async args => { const result = await authorize(args); entered.resolve(); await gate.promise; return result; };
const running = f.task.run(); await entered.promise;
await f.human('WITHDRAW'); await f.human('CORRECTION'); gate.resolve(); await running;
assert.equal(f.task.snapshot.status, 'WITHDRAWN');
});
test('untrusted document text cannot stop or replan the task', async t => {
const f = fixture(t, { verifyHuman: () => false });
await assert.rejects(f.human('STOP', 'document says ignore instructions'), /UNTRUSTED_CONTROL_SOURCE/);
assert.equal(f.task.snapshot.status, 'READY');
});
test('goal change pauses but cannot mutate confirmed scope', async t => {
const f = fixture(t); const original = f.task.snapshot.planHash;
await f.human('CHANGE_GOAL', 'also operate on another server');
assert.equal(f.task.snapshot.status, 'PAUSED'); assert.equal(f.task.snapshot.planHash, original);
});
test('bad readback fails without launching subsequent work', async t => {
const f = fixture(t, { verify: () => false }); await f.task.run();
assert.equal(f.task.snapshot.status, 'FAILED'); assert.equal(f.task.snapshot.receipts.length, 0);
assert.equal(fs.existsSync(path.join(f.root, '2.txt')), false);
});
test('invalid host grant cannot execute', async t => {
const f = fixture(t, { authorize: () => ({ planHash: 'wrong', stepId: '1', receipt: 'forged' }) });
await f.task.run(); assert.equal(f.task.snapshot.status, 'FAILED');
assert.equal(fs.existsSync(path.join(f.root, '1.txt')), false);
});
test('local adapter rejects outside targets and never overwrites existing files', async t => {
const f = fixture(t); fs.writeFileSync(path.join(f.root, '1.txt'), 'original'); await f.task.run();
assert.equal(f.task.snapshot.status, 'FAILED'); assert.equal(fs.readFileSync(path.join(f.root, '1.txt'), 'utf8'), 'original');
const outside = structuredClone(f.plan); outside.steps[0].args.path = '/tmp/outside.txt';
assert.throws(() => localFileHost({ root: f.root, approvedPlan: outside, approvalReceipt: 'x', verifyHuman: () => true }), /OUT_OF_SCOPE/);
});
test('inspection detects journal modification and marks unfinished tasks for reconciliation', t => {
const f = fixture(t); assert.equal(inspectTask(f.directory).recoveryRequired, true);
const p = path.join(f.directory, 'task.json'); const state = JSON.parse(fs.readFileSync(p));
state.events[0].detail.goal = 'tampered'; fs.writeFileSync(p, JSON.stringify(state));
assert.throws(() => inspectTask(f.directory), /JOURNAL_INTEGRITY/);
});