131 lines
8.4 KiB
JavaScript
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/);
|
|
});
|