dsh-im-ops/test/channels/shared/harness-control.test.mjs
2026-08-20 01:28:51 +08:00

483 lines
18 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createBotWorkspaceScope } from '../../../src/channels/shared/bot-workspace-store.mjs';
import { HarnessClient } from '../../../src/channels/shared/harness-client.mjs';
import { WORKSPACE_SESSION_STALE } from '../../../src/channels/shared/workspace-session.mjs';
const CATALOG = {
groups: [{
id: 'deepseek-official',
name: 'DeepSeek',
models: [{ id: 'deepseek-v4', name: 'DeepSeek V4' }],
}],
failures: [],
};
function deferred() {
let resolve;
const promise = new Promise((promiseResolve) => { resolve = promiseResolve; });
return { promise, resolve };
}
function modelClient(options = {}) {
const calls = [];
const responses = new Map([
['llm.models', CATALOG],
['session.models', {
...CATALOG,
current: { provider: 'deepseek-official', model: 'deepseek-v4' },
routable: true,
}],
['session.selectModel', {
selected: { provider: 'deepseek-official', model: 'deepseek-v4' },
}],
['session.list', {
items: [
{ sessionId: 'session-one', running: true },
{ sessionId: 'session-two', running: false },
],
}],
]);
const client = new HarnessClient({
baseUrl: 'http://127.0.0.1:3981',
workspace: '/tmp/workspace',
...options,
});
client.ensureRunning = async (options) => calls.push(['ensureRunning', options]);
client.rpc = async (method, payload, timeoutMs, options) => {
calls.push(['rpc', method, payload, timeoutMs, options]);
return responses.get(method);
};
return { calls, client, responses };
}
test('HarnessClient exposes and validates model and run-state RPCs', async () => {
const { calls, client } = modelClient();
const signal = new AbortController().signal;
const options = { signal };
assert.equal(await client.listModels(options), CATALOG);
assert.deepEqual((await client.getSessionModels('session-one', options)).current, {
provider: 'deepseek-official', model: 'deepseek-v4',
});
assert.deepEqual(await client.selectSessionModel('session-one', {
provider: 'deepseek-official', model: 'deepseek-v4',
}, options), {
selected: { provider: 'deepseek-official', model: 'deepseek-v4' },
});
assert.equal(await client.isSessionRunning('session-one', options), true);
assert.equal(await client.isSessionRunning('missing', options), false);
assert.deepEqual(calls.filter(([type]) => type === 'rpc').map(([, method, payload]) => (
[method, payload]
)), [
['llm.models', {}],
['session.models', { sessionId: 'session-one' }],
['session.selectModel', {
sessionId: 'session-one',
provider: 'deepseek-official',
model: 'deepseek-v4',
}],
['session.list', {}],
['session.list', {}],
]);
assert.ok(calls.filter(([type]) => type === 'ensureRunning').every(([, value]) => value === options));
});
test('HarnessClient rejects malformed model and run-state responses', async () => {
const { client, responses } = modelClient();
responses.set('llm.models', { groups: null, failures: [] });
await assert.rejects(client.listModels(), /invalid response for llm\.models/);
responses.set('session.models', {
...CATALOG,
current: { provider: 'deepseek-official' },
routable: true,
});
await assert.rejects(
client.getSessionModels('session-one'),
/invalid response for session\.models/,
);
responses.set('session.selectModel', { selected: { provider: '', model: 'bad' } });
await assert.rejects(
client.selectSessionModel('session-one', { provider: 'p', model: 'm' }),
/invalid response for session\.selectModel/,
);
responses.set('session.list', { items: [{ sessionId: 'session-one', running: 'yes' }] });
await assert.rejects(client.isSessionRunning('session-one'), /invalid response for session\.list/);
});
test('selectSessionModel holds the maintenance executor across its RPC', async () => {
const maintenanceController = new AbortController();
const stages = [];
const { calls, client } = modelClient({
sessionMaintenanceExecutor({ sessionId, operation }) {
stages.push(['maintenance', sessionId]);
const result = operation(maintenanceController.signal);
stages.push(['operation-started']);
return result;
},
});
const callerController = new AbortController();
await client.selectSessionModel('session-one', {
provider: 'deepseek-official', model: 'deepseek-v4',
}, { signal: callerController.signal });
assert.deepEqual(stages, [
['maintenance', 'session-one'],
['operation-started'],
]);
const rpcOptions = calls.find((entry) => entry[0] === 'rpc' && entry[1] === 'session.selectModel')[4];
assert.notEqual(rpcOptions.signal, callerController.signal);
assert.equal(rpcOptions.signal.aborted, false);
maintenanceController.abort();
assert.equal(rpcOptions.signal.aborted, true, 'maintenance cancellation must abort the RPC');
});
function controlledTurn({ sessionId, initialEnd = false, controlExecutor } = {}) {
const id = sessionId ?? `session-${Math.random()}`;
const calls = [];
const admitted = deferred();
let promptRpcId = null;
let ended = initialEnd;
let answer = initialEnd ? 'already complete' : '';
let endReason = initialEnd ? 'completed' : null;
let historyFailure = null;
const client = new HarnessClient({
baseUrl: 'http://127.0.0.1:3982',
workspace: '/tmp/workspace',
...(controlExecutor ? { controlExecutor } : {}),
});
client.ensureRunning = async () => true;
const history = () => {
if (!promptRpcId) return { events: [] };
const events = [
{ event: { seq: 1, type: 'turn/start', data: { turn: 7 } } },
{ event: {
seq: 2,
type: 'user/message',
data: { turn: 7, source: { rpcId: promptRpcId } },
} },
];
if (answer) {
events.push({ event: {
seq: 3,
type: 'assistant/message',
data: {
turn: 7,
message: { content: [{ type: 'text', text: answer }] },
},
} });
}
if (ended) {
events.push({ event: {
seq: 4,
type: 'turn/end',
data: { turn: 7, reason: endReason },
} });
}
return { events };
};
client.rpc = async (method, payload, _timeoutMs, options) => {
calls.push({ method, payload, options });
if (method === 'session.history') {
if (historyFailure) throw historyFailure;
return history();
}
if (method === 'session.prompt' && payload.mode === 'queue') {
promptRpcId = options.rpcId;
admitted.resolve();
return {};
}
if (method === 'session.prompt' && payload.mode === 'steer') return {};
if (method === 'session.cancel') return {};
throw new Error(`Unexpected RPC ${method}`);
};
return {
id,
calls,
client,
admitted: admitted.promise,
setText(text) { answer = text; },
failHistory(error = new Error('history unavailable')) { historyFailure = error; },
finish({ text = '', reason = 'cancelled' } = {}) {
answer = text;
endReason = reason;
ended = true;
},
};
}
test('control methods require exact owner identity, key, and Session before any RPC', async () => {
const turn = controlledTurn();
const owner = {};
const control = { owner, key: 'direct:one' };
const asking = turn.client.ask(turn.id, 'work', { control, timeoutMs: 2_000 });
void asking.catch(() => undefined);
await turn.admitted;
for (const wrong of [
{ owner: {}, key: 'direct:one' },
{ owner, key: 'direct:two' },
null,
]) {
const before = turn.calls.length;
assert.equal(await turn.client.hasActiveTurn(turn.id, wrong), false);
assert.equal(await turn.client.stopActiveTurn(turn.id, wrong), false);
assert.equal(await turn.client.steerActiveTurn(turn.id, 'do more', wrong), false);
assert.equal(turn.calls.length, before, 'unowned controls must not observe or mutate Session');
}
const beforeOtherSession = turn.calls.length;
assert.equal(await turn.client.stopActiveTurn('different-session', control), false);
assert.equal(turn.calls.length, beforeOtherSession);
assert.equal(await turn.client.hasActiveTurn(turn.id, control), true);
assert.equal(await turn.client.stopActiveTurn(turn.id, control), true);
const cancelCount = () => turn.calls.filter(({ method }) => method === 'session.cancel').length;
assert.equal(cancelCount(), 1);
assert.equal(await turn.client.stopActiveTurn(turn.id, control), true, 'stop is idempotent');
assert.equal(cancelCount(), 1, 'an idempotent stop must not issue a second cancel RPC');
turn.finish();
await assert.rejects(asking, (error) => error?.code === 'turn-stopped');
assert.deepEqual(
turn.calls.find(({ method }) => method === 'session.cancel')?.payload,
{ sessionId: turn.id, keepInbox: true },
);
});
test('a stopped turn returns partial text instead of a generic failure', async () => {
const turn = controlledTurn();
const control = { owner: {}, key: 'direct:partial' };
const asking = turn.client.ask(turn.id, 'work', { control, timeoutMs: 2_000 });
await turn.admitted;
assert.equal(await turn.client.stopActiveTurn(turn.id, control), true);
turn.finish({ text: 'partial result' });
assert.equal(await asking, 'partial result');
});
test('accepted stop converts later polling failures to turn-stopped and preserves streamed text', async () => {
const withoutText = controlledTurn();
const firstControl = { owner: {}, key: 'direct:poll-error' };
const firstAsk = withoutText.client.ask(withoutText.id, 'work', {
control: firstControl, timeoutMs: 2_000,
});
await withoutText.admitted;
assert.equal(await withoutText.client.stopActiveTurn(withoutText.id, firstControl), true);
withoutText.failHistory();
await assert.rejects(firstAsk, (error) => error?.code === 'turn-stopped');
const withText = controlledTurn();
const secondControl = { owner: {}, key: 'direct:partial-poll-error' };
const updateSeen = deferred();
const secondAsk = withText.client.ask(withText.id, 'work', {
control: secondControl,
timeoutMs: 2_000,
onUpdate(update) {
if (update.type === 'text') updateSeen.resolve();
},
});
await withText.admitted;
withText.setText('partial before transport failure');
await updateSeen.promise;
assert.equal(await withText.client.stopActiveTurn(withText.id, secondControl), true);
withText.failHistory();
assert.equal(await secondAsk, 'partial before transport failure');
});
test('in-process control executor receives exact ownership and suppresses control RPCs', async () => {
const executions = [];
const turn = controlledTurn({
controlExecutor(request) {
executions.push(request);
return true;
},
});
const control = { owner: {}, key: 'direct:executor' };
const asking = turn.client.ask(turn.id, 'work', { control, timeoutMs: 2_000 });
await turn.admitted;
assert.equal(await turn.client.steerActiveTurn(turn.id, 'stay in this turn', control), true);
assert.equal(await turn.client.stopActiveTurn(turn.id, control), true);
assert.deepEqual(executions.map(({ sessionId, expectedTurn, action, text }) => ({
sessionId, expectedTurn, action, text,
})), [
{ sessionId: turn.id, expectedTurn: 7, action: 'steer', text: 'stay in this turn' },
{ sessionId: turn.id, expectedTurn: 7, action: 'stop', text: undefined },
]);
assert.ok(executions.every(({ promptRpcId }) => typeof promptRpcId === 'string' && promptRpcId));
assert.equal(turn.calls.some(({ method, payload }) => (
method === 'session.cancel' || (method === 'session.prompt' && payload.mode === 'steer')
)), false);
turn.finish();
await assert.rejects(asking, (error) => error?.code === 'turn-stopped');
});
test('a refused in-process control action never falls through to HTTP', async () => {
const turn = controlledTurn({ controlExecutor: () => false });
const control = { owner: {}, key: 'direct:executor-race' };
const asking = turn.client.ask(turn.id, 'work', { control, timeoutMs: 2_000 });
await turn.admitted;
assert.equal(await turn.client.steerActiveTurn(turn.id, 'must not wake', control), false);
assert.equal(await turn.client.stopActiveTurn(turn.id, control), false);
assert.equal(turn.calls.some(({ method, payload }) => (
method === 'session.cancel' || (method === 'session.prompt' && payload.mode === 'steer')
)), false);
turn.finish({ text: 'normal completion', reason: 'completed' });
assert.equal(await asking, 'normal completion');
});
test('steer uses mode steer only while the exact owned turn is still active', async () => {
const turn = controlledTurn();
const control = { owner: {}, key: 'direct:steer' };
const asking = turn.client.ask(turn.id, 'work', { control, timeoutMs: 2_000 });
await turn.admitted;
assert.equal(await turn.client.steerActiveTurn(
turn.id,
'first line\nsecond line',
control,
), true);
const steer = turn.calls.find(({ method, payload }) => (
method === 'session.prompt' && payload.mode === 'steer'
));
assert.deepEqual(steer.payload, {
sessionId: turn.id,
mode: 'steer',
content: [{ type: 'text', text: 'first line\nsecond line' }],
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
turn.finish({ text: 'done', reason: 'completed' });
assert.equal(await asking, 'done');
});
test('steer refuses a turn that ended during its ownership refresh', async () => {
const turn = controlledTurn({ initialEnd: true });
const control = { owner: {}, key: 'direct:ended' };
const asking = turn.client.ask(turn.id, 'work', { control, timeoutMs: 2_000 });
await turn.admitted;
assert.equal(await turn.client.steerActiveTurn(turn.id, 'must not wake', control), false);
assert.equal(turn.calls.some(({ method, payload }) => (
method === 'session.prompt' && payload.mode === 'steer'
)), false);
assert.equal(await asking, 'already complete');
});
function scopedHarnessFixture() {
const calls = [];
let generation = 1;
let present = true;
const workspaces = {
incarnationFor() { return 'incarnation-one'; },
has() { return present; },
generationFor() { return generation; },
workspaceFor() { return '/tmp/workspace'; },
};
const harness = {
async listModels(...args) {
calls.push(['listModels', ...args]);
return CATALOG;
},
async getSessionModels(...args) {
calls.push(['getSessionModels', ...args]);
return { ...CATALOG, current: { provider: 'p', model: 'm' }, routable: true };
},
async selectSessionModel(...args) {
calls.push(['selectSessionModel', ...args]);
return { selected: args[1] };
},
async isSessionRunning(...args) {
calls.push(['isSessionRunning', ...args]);
return true;
},
async hasActiveTurn(...args) {
calls.push(['hasActiveTurn', ...args]);
return true;
},
async stopActiveTurn(...args) {
calls.push(['stopActiveTurn', ...args]);
return true;
},
async steerActiveTurn(...args) {
calls.push(['steerActiveTurn', ...args]);
return true;
},
};
const scope = createBotWorkspaceScope(harness, {
botId: 'bot-one', workspaces, state: {},
});
return {
calls,
harness,
scope,
advanceGeneration() { generation += 1; },
removeBot() { present = false; },
};
}
test('workspace session handles forward every model and control API through one generation fence', async () => {
const fixture = scopedHarnessFixture();
const session = fixture.scope.harness.workspaceSession('session-one');
const signal = new AbortController().signal;
const options = { signal };
const control = { owner: {}, key: 'direct:one' };
await session.models(options);
await session.selectModel({ provider: 'p', model: 'm' }, options);
assert.equal(await session.isRunning(options), true);
assert.equal(await session.hasActiveTurn(control, options), true);
assert.equal(await session.stopActiveTurn(control, options), true);
assert.equal(await session.steerActiveTurn('more', control, options), true);
assert.deepEqual(fixture.calls, [
['getSessionModels', 'session-one', options],
['selectSessionModel', 'session-one', { provider: 'p', model: 'm' }, options],
['isSessionRunning', 'session-one', options],
['hasActiveTurn', 'session-one', control, options],
['stopActiveTurn', 'session-one', control, options],
['steerActiveTurn', 'session-one', 'more', control, options],
]);
const methods = [
['models', [options]],
['selectModel', [{ provider: 'p', model: 'm' }, options]],
['isRunning', [options]],
['hasActiveTurn', [control, options]],
['stopActiveTurn', [control, options]],
['steerActiveTurn', ['more', control, options]],
];
for (const [method, args] of methods) {
const stale = fixture.scope.harness.workspaceSession(`stale-${method}`);
fixture.advanceGeneration();
const before = fixture.calls.length;
await assert.rejects(
stale[method](...args),
(error) => error?.code === WORKSPACE_SESSION_STALE,
method,
);
assert.equal(fixture.calls.length, before, `${method} crossed a stale generation`);
}
});
test('workspace session and listModels re-check their scope after an in-flight RPC', async () => {
const fixture = scopedHarnessFixture();
const modelGate = deferred();
fixture.harness.getSessionModels = async () => modelGate.promise;
const session = fixture.scope.harness.workspaceSession('session-race');
const listing = session.models();
fixture.advanceGeneration();
modelGate.resolve({ ...CATALOG, current: { provider: 'p', model: 'm' }, routable: true });
await assert.rejects(listing, (error) => error?.code === WORKSPACE_SESSION_STALE);
const listGate = deferred();
fixture.harness.listModels = async () => listGate.promise;
const globalListing = fixture.scope.harness.listModels();
fixture.removeBot();
listGate.resolve(CATALOG);
await assert.rejects(globalListing, (error) => error?.code === 'workspace-bot-not-found');
});