mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
feat: add model and turn control commands
This commit is contained in:
parent
fc8f120e13
commit
99ce38e9a4
38 changed files with 3451 additions and 169 deletions
168
test/control-command.test.mjs
Normal file
168
test/control-command.test.mjs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
isControlCommand,
|
||||
runControlCommand,
|
||||
} from '../src/channels/shared/control-command.mjs';
|
||||
import { HarnessApprovalQueue } from '../src/channels/shared/harness-approval.mjs';
|
||||
|
||||
function fixture({ sessionId = 'session-one', stopped = true, steered = true } = {}) {
|
||||
const calls = [];
|
||||
const state = {
|
||||
sessionFor(key) {
|
||||
calls.push(['sessionFor', key]);
|
||||
return sessionId;
|
||||
},
|
||||
};
|
||||
const harness = {
|
||||
workspaceSession(id) {
|
||||
calls.push(['workspaceSession', id]);
|
||||
return {
|
||||
async stopActiveTurn(control, options) {
|
||||
calls.push(['stopActiveTurn', id, control, options]);
|
||||
return stopped;
|
||||
},
|
||||
async steerActiveTurn(text, control, options) {
|
||||
calls.push(['steerActiveTurn', id, text, control, options]);
|
||||
return steered;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
return { calls, harness, state };
|
||||
}
|
||||
|
||||
test('isControlCommand reserves valid and malformed control command forms', () => {
|
||||
for (const value of [
|
||||
'/stop', ' /STOP ', '/stop now', '/steer', '/StEeR do this', '/steer line one\nline two',
|
||||
]) {
|
||||
assert.equal(isControlCommand(value), true, value);
|
||||
}
|
||||
for (const value of [null, '', 'stop', '/stopping', '/steering', 'hello /stop']) {
|
||||
assert.equal(isControlCommand(value), false, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
test('/stop is exact, text-only, and never creates a Session', async () => {
|
||||
const { calls, harness, state } = fixture();
|
||||
assert.match((await runControlCommand('/stop later', harness, state, 'direct:one')).message, /用法/);
|
||||
assert.match((await runControlCommand('/stop', harness, state, 'direct:one', {
|
||||
hasImages: true,
|
||||
})).message, /纯文字/);
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
|
||||
test('/stop controls only the bound active turn and marks accepted cancellation', async () => {
|
||||
const active = fixture();
|
||||
const signal = new AbortController().signal;
|
||||
const owner = {};
|
||||
const control = { owner, key: 'direct:one' };
|
||||
const result = await runControlCommand(
|
||||
'/STOP',
|
||||
active.harness,
|
||||
active.state,
|
||||
'direct:one',
|
||||
{ signal, pendingInteraction: true, control },
|
||||
);
|
||||
assert.deepEqual(result, { message: '已请求停止当前任务。', stopped: true });
|
||||
assert.deepEqual(active.calls, [
|
||||
['sessionFor', 'direct:one'],
|
||||
['workspaceSession', 'session-one'],
|
||||
['stopActiveTurn', 'session-one', control, { signal }],
|
||||
]);
|
||||
|
||||
const inactive = fixture({ stopped: false });
|
||||
const missing = await runControlCommand(
|
||||
'/stop', inactive.harness, inactive.state, 'direct:one', { control },
|
||||
);
|
||||
assert.match(missing.message, /没有正在运行/);
|
||||
assert.equal(missing.stopped, undefined);
|
||||
});
|
||||
|
||||
test('/stop and /steer return friendly no-session messages without creating one', async () => {
|
||||
const { harness, state, calls } = fixture({ sessionId: null });
|
||||
assert.match(
|
||||
(await runControlCommand('/stop', harness, state, 'direct:one')).message,
|
||||
/没有正在运行/,
|
||||
);
|
||||
assert.match(
|
||||
(await runControlCommand('/steer 补充', harness, state, 'direct:one')).message,
|
||||
/普通消息/,
|
||||
);
|
||||
assert.equal(calls.some(([method]) => method === 'workspaceSession'), false);
|
||||
});
|
||||
|
||||
test('/steer requires text, preserves multiple lines, and rejects images', async () => {
|
||||
const { harness, state, calls } = fixture();
|
||||
assert.match((await runControlCommand('/steer', harness, state, 'direct:one')).message, /用法/);
|
||||
assert.match((await runControlCommand('/steer ', harness, state, 'direct:one')).message, /用法/);
|
||||
assert.match((await runControlCommand('/steer text', harness, state, 'direct:one', {
|
||||
hasImages: true,
|
||||
})).message, /纯文字/);
|
||||
assert.equal(calls.length, 0);
|
||||
|
||||
const control = { owner: {}, key: 'direct:one' };
|
||||
const result = await runControlCommand(
|
||||
'/steer first line\nsecond line',
|
||||
harness,
|
||||
state,
|
||||
'direct:one',
|
||||
{ control },
|
||||
);
|
||||
assert.match(result.message, /已提交/);
|
||||
assert.deepEqual(calls.find(([method]) => method === 'steerActiveTurn'), [
|
||||
'steerActiveTurn',
|
||||
'session-one',
|
||||
'first line\nsecond line',
|
||||
control,
|
||||
{},
|
||||
]);
|
||||
});
|
||||
|
||||
test('/steer never reaches Harness while an interaction is pending', async () => {
|
||||
const { harness, state, calls } = fixture();
|
||||
const result = await runControlCommand('/steer continue', harness, state, 'direct:one', {
|
||||
pendingInteraction: true,
|
||||
control: { owner: {}, key: 'direct:one' },
|
||||
});
|
||||
assert.match(result.message, /等待你的回答或审批/);
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
|
||||
test('/steer reports a lost active-turn race instead of starting new work', async () => {
|
||||
const { harness, state, calls } = fixture({ steered: false });
|
||||
const result = await runControlCommand('/steer continue', harness, state, 'direct:one', {
|
||||
control: { owner: {}, key: 'direct:one' },
|
||||
});
|
||||
assert.match(result.message, /普通消息/);
|
||||
assert.equal(calls.filter(([method]) => method === 'steerActiveTurn').length, 1);
|
||||
});
|
||||
|
||||
test('HarnessApprovalQueue exposes whether a route has a live approval', async () => {
|
||||
const queue = new HarnessApprovalQueue({ logger: { warn() {}, error() {} } });
|
||||
const replies = [];
|
||||
const interaction = {
|
||||
kind: 'approval',
|
||||
rpcId: 'approval-rpc',
|
||||
sessionId: 'session-one',
|
||||
toolCall: { callId: 'call-one', name: 'bash', arguments: '{}' },
|
||||
payload: {
|
||||
type: 'approval/requested',
|
||||
sessionId: 'session-one',
|
||||
approvalId: 'approval-one',
|
||||
toolName: 'bash',
|
||||
callId: 'call-one',
|
||||
},
|
||||
async respond() {},
|
||||
};
|
||||
await queue.handleRequested(interaction, {
|
||||
key: 'direct:one',
|
||||
actor: 'user-one',
|
||||
send: async (text) => replies.push(text),
|
||||
});
|
||||
assert.equal(queue.hasPending('direct:one'), true);
|
||||
assert.equal(queue.hasPending('direct:other'), false);
|
||||
await queue.closeRoute('direct:one');
|
||||
assert.equal(queue.hasPending('direct:one'), false);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue