mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
403 lines
15 KiB
JavaScript
403 lines
15 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
|
||
import {
|
||
isPresetCommand,
|
||
PRESET_LIST_SNAPSHOT_MAX_ENTRIES,
|
||
PRESET_LIST_SNAPSHOT_TTL_MS,
|
||
runPresetCommand,
|
||
} from '../src/channels/shared/preset-command.mjs';
|
||
import { withSessionBindingLock } from '../src/channels/shared/session-binding-lock.mjs';
|
||
|
||
const CATALOG = Object.freeze({
|
||
defaultId: 'standard',
|
||
items: Object.freeze([
|
||
Object.freeze({ id: 'standard', label: 'Standard' }),
|
||
Object.freeze({ id: 'coding', label: 'Coding' }),
|
||
Object.freeze({ id: 'marketing', label: 'Marketing' }),
|
||
]),
|
||
});
|
||
|
||
function fixture({
|
||
agentPreset = null,
|
||
catalog = CATALOG,
|
||
settingsError = null,
|
||
updateError = null,
|
||
updateHook = null,
|
||
} = {}) {
|
||
const calls = [];
|
||
let selected = agentPreset;
|
||
const harness = {
|
||
async agentPresetSettings(options) {
|
||
calls.push(['agentPresetSettings', options]);
|
||
if (settingsError) throw settingsError;
|
||
const currentCatalog = typeof catalog === 'function' ? catalog() : catalog;
|
||
return { agentPreset: selected, agentPresetCatalog: currentCatalog };
|
||
},
|
||
async updateAgentPreset(value, options) {
|
||
calls.push(['updateAgentPreset', value, options]);
|
||
if (updateHook) await updateHook({ value, options });
|
||
if (updateError) throw updateError;
|
||
selected = value;
|
||
const currentCatalog = typeof catalog === 'function' ? catalog() : catalog;
|
||
return { agentPreset: selected, agentPresetCatalog: currentCatalog };
|
||
},
|
||
};
|
||
return { calls, harness, state: {}, selected: () => selected };
|
||
}
|
||
|
||
test('isPresetCommand recognizes only /presetlist and /preset command prefixes', () => {
|
||
for (const command of [
|
||
'/presetlist', ' /PRESETLIST ', '/presetlist ignored',
|
||
'/preset', '/PrEsEt coding',
|
||
]) {
|
||
assert.equal(isPresetCommand(command), true, command);
|
||
}
|
||
for (const value of [
|
||
null, '', 'preset', '/presetx', '/presetlisting', 'hello /preset',
|
||
]) {
|
||
assert.equal(isPresetCommand(value), false, String(value));
|
||
}
|
||
});
|
||
|
||
test('/presetlist dynamically lists, annotates, and snapshots presets without updating', async () => {
|
||
const { calls, harness, state } = fixture({ agentPreset: 'coding' });
|
||
const signal = new AbortController().signal;
|
||
const result = await runPresetCommand('/PRESETLIST', harness, state, 'direct:one', { signal });
|
||
|
||
assert.match(result.message, /当前机器人用于新会话/);
|
||
assert.match(result.message, /Host 默认:Standard(standard)/);
|
||
assert.match(result.message, /1\. Standard(standard)(Host 默认)/);
|
||
assert.match(result.message, /2\. Coding(coding)(当前选择)/);
|
||
assert.match(result.message, /\/preset --default/);
|
||
assert.deepEqual(calls, [['agentPresetSettings', { signal }]]);
|
||
|
||
const selected = await runPresetCommand('/preset 2', harness, state, 'direct:one');
|
||
assert.match(selected.message, /Coding(coding)/);
|
||
assert.deepEqual(calls.at(-1), ['updateAgentPreset', 'coding', {}]);
|
||
});
|
||
|
||
test('/presetlist shows following Host default as the effective preset', async () => {
|
||
const { harness, state } = fixture();
|
||
const result = await runPresetCommand('/presetlist', harness, state, 'direct:one');
|
||
|
||
assert.match(result.message, /跟随 Host 默认:Standard(standard)/);
|
||
assert.match(result.message, /Standard(standard)(Host 默认,当前生效)/);
|
||
});
|
||
|
||
test('/presetlist filters invalid, duplicate, and broken entries and sanitizes labels', async () => {
|
||
const catalog = {
|
||
defaultId: 'safe',
|
||
items: [
|
||
{ id: 'safe', label: 'Safe\nPreset\u202e' },
|
||
{ id: 'safe', label: 'Duplicate' },
|
||
{ id: 'UPPER', label: 'Invalid' },
|
||
{ id: 'broken', label: 'Broken', broken: { message: 'secret' } },
|
||
],
|
||
};
|
||
const { harness, state } = fixture({ catalog });
|
||
const result = await runPresetCommand('/presetlist', harness, state, 'direct:one');
|
||
|
||
assert.match(result.message, /可用 Agent Preset(1)/);
|
||
assert.match(result.message, /Safe Preset(safe)/);
|
||
assert.doesNotMatch(result.message, /Duplicate|Invalid|Broken|secret|\u202e/);
|
||
});
|
||
|
||
test('/preset reports an explicit unavailable current preset without changing it', async () => {
|
||
const { calls, harness, state, selected } = fixture({ agentPreset: 'removed' });
|
||
const result = await runPresetCommand('/preset', harness, state, 'direct:one');
|
||
|
||
assert.match(result.message, /removed(已不可用)/);
|
||
assert.match(result.message, /已有会话不会受此设置影响/);
|
||
assert.equal(selected(), 'removed');
|
||
assert.equal(calls.some(([name]) => name === 'updateAgentPreset'), false);
|
||
});
|
||
|
||
test('/preset and /presetlist expose an unavailable Host default safely', async () => {
|
||
const catalog = { defaultId: 'removed-default', items: CATALOG.items };
|
||
const current = fixture({ catalog });
|
||
assert.match(
|
||
(await runPresetCommand('/preset', current.harness, current.state, 'direct:one')).message,
|
||
/跟随 Host 默认(Host 默认当前不可用)/,
|
||
);
|
||
|
||
const listed = fixture({ catalog });
|
||
assert.match(
|
||
(await runPresetCommand('/presetlist', listed.harness, listed.state, 'direct:one')).message,
|
||
/Host 默认:removed-default(当前不可用)/,
|
||
);
|
||
});
|
||
|
||
test('numeric selection requires a snapshot for the same state and conversation key', async () => {
|
||
const first = fixture();
|
||
assert.match(
|
||
(await runPresetCommand('/preset 1', first.harness, first.state, 'direct:one')).message,
|
||
/先执行 \/presetlist/,
|
||
);
|
||
assert.equal(first.calls.length, 0);
|
||
|
||
await runPresetCommand('/presetlist', first.harness, first.state, 'direct:one');
|
||
assert.match(
|
||
(await runPresetCommand('/preset 1', first.harness, first.state, 'direct:two')).message,
|
||
/先执行 \/presetlist/,
|
||
);
|
||
|
||
const otherState = {};
|
||
assert.match(
|
||
(await runPresetCommand('/preset 1', first.harness, otherState, 'direct:one')).message,
|
||
/先执行 \/presetlist/,
|
||
);
|
||
});
|
||
|
||
test('numeric selection rejects an expired snapshot and requires a new /presetlist', async (t) => {
|
||
let now = 1_000;
|
||
t.mock.method(Date, 'now', () => now);
|
||
const { calls, harness, state } = fixture();
|
||
await runPresetCommand('/presetlist', harness, state, 'direct:one');
|
||
|
||
now += PRESET_LIST_SNAPSHOT_TTL_MS - 1;
|
||
const stillFresh = await runPresetCommand('/preset 1', harness, state, 'direct:one');
|
||
assert.match(stillFresh.message, /Standard(standard)/);
|
||
|
||
now += 1;
|
||
const expired = await runPresetCommand('/preset 1', harness, state, 'direct:one');
|
||
|
||
assert.match(expired.message, /请先执行 \/presetlist/);
|
||
assert.equal(calls.filter(([name]) => name === 'updateAgentPreset').length, 1);
|
||
});
|
||
|
||
test('numeric snapshots are capacity-bounded and evicted in LRU order', async () => {
|
||
const { calls, harness, state } = fixture();
|
||
for (let index = 0; index < PRESET_LIST_SNAPSHOT_MAX_ENTRIES; index += 1) {
|
||
await runPresetCommand('/presetlist', harness, state, `direct:${index}`);
|
||
}
|
||
|
||
await runPresetCommand('/preset 1', harness, state, 'direct:0');
|
||
await runPresetCommand(
|
||
'/presetlist',
|
||
harness,
|
||
state,
|
||
`direct:${PRESET_LIST_SNAPSHOT_MAX_ENTRIES}`,
|
||
);
|
||
|
||
const evicted = await runPresetCommand('/preset 1', harness, state, 'direct:1');
|
||
assert.match(evicted.message, /请先执行 \/presetlist/);
|
||
const retained = await runPresetCommand('/preset 1', harness, state, 'direct:0');
|
||
assert.match(retained.message, /Standard(standard)/);
|
||
assert.equal(calls.filter(([name]) => name === 'updateAgentPreset').length, 2);
|
||
});
|
||
|
||
test('numeric selection keeps the listed ID when the fresh catalog is reordered', async () => {
|
||
let currentCatalog = CATALOG;
|
||
const fixtureValue = fixture({ catalog: () => currentCatalog });
|
||
await runPresetCommand(
|
||
'/presetlist',
|
||
fixtureValue.harness,
|
||
fixtureValue.state,
|
||
'direct:one',
|
||
);
|
||
currentCatalog = {
|
||
defaultId: 'standard',
|
||
items: [CATALOG.items[2], CATALOG.items[0], CATALOG.items[1]],
|
||
};
|
||
|
||
await runPresetCommand('/preset 2', fixtureValue.harness, fixtureValue.state, 'direct:one');
|
||
assert.deepEqual(
|
||
fixtureValue.calls.findLast(([name]) => name === 'updateAgentPreset'),
|
||
['updateAgentPreset', 'coding', {}],
|
||
);
|
||
});
|
||
|
||
test('/preset rejects invalid and out-of-range numeric selections without updating', async () => {
|
||
const { calls, harness, state } = fixture();
|
||
await runPresetCommand('/presetlist', harness, state, 'direct:one');
|
||
|
||
for (const requested of ['0', '4', '9007199254740992']) {
|
||
const result = await runPresetCommand(`/preset ${requested}`, harness, state, 'direct:one');
|
||
assert.match(result.message, /序号.*无效|序号不存在/, requested);
|
||
}
|
||
assert.equal(calls.filter(([name]) => name === 'updateAgentPreset').length, 0);
|
||
});
|
||
|
||
test('/preset accepts exact IDs and id: for a purely numeric ID', async () => {
|
||
const direct = fixture();
|
||
const directResult = await runPresetCommand(
|
||
'/preset marketing',
|
||
direct.harness,
|
||
direct.state,
|
||
'direct:one',
|
||
);
|
||
assert.match(directResult.message, /Marketing(marketing)/);
|
||
assert.deepEqual(direct.calls.at(-1), ['updateAgentPreset', 'marketing', {}]);
|
||
|
||
const numericCatalog = {
|
||
defaultId: '123',
|
||
items: [{ id: '123', label: 'Numeric' }],
|
||
};
|
||
const numeric = fixture({ catalog: numericCatalog });
|
||
const numericResult = await runPresetCommand(
|
||
'/preset id:123',
|
||
numeric.harness,
|
||
numeric.state,
|
||
'direct:one',
|
||
);
|
||
assert.match(numericResult.message, /Numeric(123)/);
|
||
assert.deepEqual(numeric.calls.at(-1), ['updateAgentPreset', '123', {}]);
|
||
});
|
||
|
||
test('/preset --default clears the override without first reading the catalog', async () => {
|
||
const settingsError = new Error('catalog private failure');
|
||
const { calls, harness, state, selected } = fixture({
|
||
agentPreset: 'coding',
|
||
settingsError,
|
||
});
|
||
const signal = new AbortController().signal;
|
||
const result = await runPresetCommand(
|
||
'/preset --DEFAULT',
|
||
harness,
|
||
state,
|
||
'direct:one',
|
||
{ signal },
|
||
);
|
||
|
||
assert.match(result.message, /跟随 Host 默认/);
|
||
assert.equal(selected(), null);
|
||
assert.deepEqual(calls, [['updateAgentPreset', null, { signal }]]);
|
||
assert.doesNotMatch(result.message, /private/);
|
||
});
|
||
|
||
test('/preset updates even while an interaction is pending and never consults Session state', async () => {
|
||
const { calls, harness, state } = fixture();
|
||
state.sessionFor = () => {
|
||
throw new Error('must not inspect current Session');
|
||
};
|
||
const result = await runPresetCommand(
|
||
'/preset coding',
|
||
harness,
|
||
state,
|
||
'direct:one',
|
||
{ pendingInteraction: true, control: { owner: 'one' } },
|
||
);
|
||
|
||
assert.match(result.message, /已设置为/);
|
||
assert.match(result.message, /先发送 \/new/);
|
||
assert.deepEqual(calls, [['updateAgentPreset', 'coding', {}]]);
|
||
});
|
||
|
||
test('/preset serializes with the next Session-binding transaction', async () => {
|
||
let releaseUpdate;
|
||
let markUpdateStarted;
|
||
const updateStarted = new Promise((resolve) => { markUpdateStarted = resolve; });
|
||
const updateGate = new Promise((resolve) => { releaseUpdate = resolve; });
|
||
const order = [];
|
||
const { harness, state } = fixture({
|
||
updateHook: async () => {
|
||
markUpdateStarted();
|
||
await updateGate;
|
||
order.push('preset');
|
||
},
|
||
});
|
||
const updating = runPresetCommand('/preset coding', harness, state, 'direct:one');
|
||
await updateStarted;
|
||
const binding = withSessionBindingLock(state, 'direct:one', async () => {
|
||
order.push('session');
|
||
});
|
||
|
||
await Promise.resolve();
|
||
assert.deepEqual(order, []);
|
||
releaseUpdate();
|
||
await Promise.all([updating, binding]);
|
||
assert.deepEqual(order, ['preset', 'session']);
|
||
});
|
||
|
||
test('preset commands validate syntax and reject images without calling the harness', async () => {
|
||
const { calls, harness, state } = fixture();
|
||
for (const command of ['/presetlist extra', '/preset coding extra', '/preset id:abc']) {
|
||
const result = await runPresetCommand(command, harness, state, 'direct:one');
|
||
assert.match(result.message, /用法|格式无效/, command);
|
||
}
|
||
|
||
for (const command of ['/preset', '/presetlist', '/preset coding']) {
|
||
const result = await runPresetCommand(command, harness, state, 'direct:one', {
|
||
hasImages: true,
|
||
});
|
||
assert.match(result.message, /仅支持纯文字/, command);
|
||
}
|
||
assert.equal(calls.length, 0);
|
||
});
|
||
|
||
test('fresh validation failures and internal errors use safe messages', async () => {
|
||
const unavailableError = new Error('path=/private/preset token=secret');
|
||
unavailableError.code = 'agent-preset-unavailable';
|
||
const unavailable = fixture({ updateError: unavailableError });
|
||
const unavailableResult = await runPresetCommand(
|
||
'/preset coding',
|
||
unavailable.harness,
|
||
unavailable.state,
|
||
'direct:one',
|
||
);
|
||
assert.match(unavailableResult.message, /不存在或当前不可用/);
|
||
assert.doesNotMatch(unavailableResult.message, /private|secret/);
|
||
|
||
const missingError = new Error('bot details');
|
||
missingError.code = 'workspace-bot-not-found';
|
||
const missing = fixture({ updateError: missingError });
|
||
assert.match(
|
||
(await runPresetCommand('/preset coding', missing.harness, missing.state, 'direct:one')).message,
|
||
/机器人状态已发生变化/,
|
||
);
|
||
|
||
const listing = fixture({ settingsError: new Error('private catalog endpoint') });
|
||
const listingResult = await runPresetCommand(
|
||
'/presetlist',
|
||
listing.harness,
|
||
listing.state,
|
||
'direct:one',
|
||
);
|
||
assert.match(listingResult.message, /暂时无法获取 Agent Preset 列表/);
|
||
assert.doesNotMatch(listingResult.message, /private catalog endpoint/);
|
||
});
|
||
|
||
test('cancelled and malformed harness results fail safely', async () => {
|
||
const cancelledError = new Error('secret abort details');
|
||
cancelledError.name = 'AbortError';
|
||
const cancelled = fixture({ settingsError: cancelledError });
|
||
assert.match(
|
||
(await runPresetCommand(
|
||
'/presetlist',
|
||
cancelled.harness,
|
||
cancelled.state,
|
||
'direct:one',
|
||
)).message,
|
||
/列表已取消/,
|
||
);
|
||
|
||
const badHarness = {
|
||
async agentPresetSettings() {
|
||
return { agentPreset: null, agentPresetCatalog: { items: null } };
|
||
},
|
||
};
|
||
const result = await runPresetCommand('/preset', badHarness, {}, 'direct:one');
|
||
assert.match(result.message, /暂时无法获取 Agent Preset 设置/);
|
||
});
|
||
|
||
test('/presetlist splits long output into lossless 1,800-character messages', async () => {
|
||
const catalog = {
|
||
defaultId: 'preset-000',
|
||
items: Array.from({ length: 80 }, (_, index) => ({
|
||
id: `preset-${String(index).padStart(3, '0')}`,
|
||
label: `Preset ${index} ${'x'.repeat(80)}`,
|
||
})),
|
||
};
|
||
const { harness, state } = fixture({ catalog });
|
||
const result = await runPresetCommand('/presetlist', harness, state, 'direct:one');
|
||
|
||
assert.ok(result.messages.length > 1);
|
||
assert.ok(result.messages.every((message) => message.length <= 1_800));
|
||
assert.equal(result.messages.join(''), result.message);
|
||
assert.match(result.message, /80\. Preset 79/);
|
||
});
|
||
|
||
test('non-preset input is left for ordinary message routing', async () => {
|
||
assert.equal(await runPresetCommand('hello', {}, {}, 'direct:one'), null);
|
||
});
|