dsh-im-ops/test/model-command.test.mjs
2026-08-25 00:41:44 +08:00

1008 lines
34 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict';
import test from 'node:test';
import {
isModelCommand,
runModelCommand,
} from '../src/channels/shared/model-command.mjs';
const CATALOG = Object.freeze({
groups: [
{
id: 'deepseek-official',
name: 'DeepSeek',
models: [
{ id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash' },
{ id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro' },
],
},
{
id: 'openrouter',
name: 'OpenRouter',
models: [{ id: 'anthropic/claude-sonnet-4', name: 'Claude Sonnet 4' }],
},
],
failures: [],
});
const REASONING_CATALOG = Object.freeze({
groups: [
{
id: 'deepseek-official',
name: 'DeepSeek',
models: [
{
id: 'deepseek-v4-flash',
name: 'DeepSeek V4 Flash',
reasoning: {
efforts: [
{ id: 'off', name: 'Off' },
{ id: 'high', name: 'High', description: '复杂任务' },
{ id: 'max', name: 'Max' },
],
defaultEffort: 'high',
},
},
{
id: 'deepseek-v4-pro',
name: 'DeepSeek V4 Pro',
reasoning: {
efforts: [
{ id: 'medium', name: 'Medium' },
{ id: 'xhigh', name: 'XHigh' },
],
defaultEffort: 'medium',
},
},
],
},
CATALOG.groups[1],
],
failures: [],
});
function reasoningSessionCatalog(current = {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: 'high',
}) {
return {
...REASONING_CATALOG,
current,
routable: true,
};
}
function deferred() {
let resolve;
const promise = new Promise((settle) => { resolve = settle; });
return { promise, resolve };
}
function fixture({
initialSessionId = null,
existing = true,
globalCatalog = CATALOG,
sessionCatalog = {
...CATALOG,
current: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
routable: true,
},
running = false,
activeTurn = false,
selectionError = null,
selectionResult,
selectionReadback,
selectModelHook,
setSessionResult,
} = {}) {
const calls = [];
let boundId = initialSessionId;
const selectedModels = new Map();
const session = (sessionId) => ({
async sessionExists(options) {
calls.push(['sessionExists', sessionId, options]);
return typeof existing === 'function' ? existing(sessionId) : existing;
},
async models(options) {
calls.push(['models', sessionId, options]);
if (sessionCatalog instanceof Error) throw sessionCatalog;
const current = selectedModels.get(sessionId) ?? sessionCatalog.current;
return { ...sessionCatalog, current };
},
async isRunning(options) {
calls.push(['isRunning', sessionId, options]);
return running;
},
async hasActiveTurn(control, options) {
calls.push(['hasActiveTurn', sessionId, control, options]);
return activeTurn;
},
async selectModel(selection, options) {
calls.push(['selectModel', sessionId, selection, options]);
if (selectModelHook) await selectModelHook({ sessionId, selection, options });
if (selectionError) throw selectionError;
const selected = selectionResult ?? selection;
selectedModels.set(sessionId, selectionReadback ?? selection);
return { selected };
},
});
const state = {
sessionFor(key) {
calls.push(['sessionFor', key]);
return boundId;
},
async setSession(key, sessionId) {
calls.push(['setSession', key, sessionId]);
if (setSessionResult === false) return false;
boundId = sessionId;
return setSessionResult;
},
async clearSession(key) {
calls.push(['clearSession', key]);
boundId = null;
},
};
const harness = {
async listModels(options) {
calls.push(['listModels', options]);
if (globalCatalog instanceof Error) throw globalCatalog;
return globalCatalog;
},
workspaceSession(sessionId) {
calls.push(['workspaceSession', sessionId]);
return session(sessionId);
},
async createSession(options) {
calls.push(['createSession', options]);
return 'session-created';
},
};
return { calls, harness, state, boundId: () => boundId };
}
test('isModelCommand recognizes model and reasoning command prefixes', () => {
for (const command of [
'/models', ' /MODELS ', '/models ignored', '/model', '/MoDeL openai/gpt-5',
'/reasoning', '/REASONING high', '/reasoninglist', '/ReAsOnInGs',
]) {
assert.equal(isModelCommand(command), true, command);
}
for (const value of [
null, '', 'model', '/modelx', '/modelsx', 'hello /models',
'/reasoningx', '/reasoninglists', 'hello /reasoning',
]) {
assert.equal(isModelCommand(value), false, String(value));
}
});
test('/models lists the global catalog without creating a Session', async () => {
const { calls, harness, state } = fixture();
const signal = new AbortController().signal;
const result = await runModelCommand('/MODELS', harness, state, 'direct:one', { signal });
assert.match(result.message, /DeepSeek/);
assert.match(result.message, /1\. deepseek-official\/deepseek-v4-flash/);
assert.match(result.message, /2\. deepseek-official\/deepseek-v4-pro/);
assert.match(result.message, /3\. openrouter\/anthropic\/claude-sonnet-4/);
assert.match(result.message, /切换模型:\/model <序号>/);
assert.deepEqual(calls, [
['sessionFor', 'direct:one'],
['listModels', { signal }],
]);
});
test('/models marks the current Session model and contains provider-local failures', async () => {
const sessionCatalog = {
groups: CATALOG.groups,
current: { provider: 'deepseek-official', model: 'deepseek-v4-pro' },
routable: true,
failures: [{
id: 'private-provider',
name: 'Private Provider',
message: 'https://private.example.invalid failed with secret=abc',
}],
};
const { harness, state } = fixture({ initialSessionId: 'session-one', sessionCatalog });
const result = await runModelCommand('/models', harness, state, 'direct:one');
assert.match(result.message, /2\. deepseek-official\/deepseek-v4-pro(当前)/);
assert.match(result.message, /Private Provider/);
assert.doesNotMatch(result.message, /private\.example|secret=abc/);
});
test('/models validates its no-argument and text-only syntax', async () => {
const { harness, state } = fixture();
assert.match(
(await runModelCommand('/models openai', harness, state, 'direct:one')).message,
/不带参数/,
);
assert.match(
(await runModelCommand('/models', harness, state, 'direct:one', { hasImages: true })).message,
/仅支持纯文字/,
);
});
test('/models splits a long catalog into lossless 1,800-character messages', async () => {
const longCatalog = {
groups: [{
id: 'provider',
name: 'Large Provider',
models: Array.from({ length: 80 }, (_, index) => ({
id: `model-${String(index).padStart(3, '0')}-${'x'.repeat(40)}`,
name: `Model ${index}`,
})),
}],
failures: [],
};
const { harness, state } = fixture({ globalCatalog: longCatalog });
const result = await runModelCommand('/models', 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\. provider\/model-079/);
});
test('/model reports current state without creating or selecting', async () => {
const missing = fixture();
const noSession = await runModelCommand('/model', missing.harness, missing.state, 'direct:one');
assert.match(noSession.message, /还没有会话/);
assert.equal(missing.calls.some(([name]) => name === 'createSession'), false);
const existingFixture = fixture({ initialSessionId: 'session-one' });
const existing = await runModelCommand(
'/MODEL',
existingFixture.harness,
existingFixture.state,
'direct:one',
);
assert.match(existing.message, /deepseek-official\/deepseek-v4-flash/);
assert.equal(existingFixture.calls.some(([name]) => name === 'selectModel'), false);
});
test('/reasoninglist and /reasonings are identical read-only aliases', async () => {
const first = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
});
const second = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
});
const listed = await runModelCommand(
'/reasoninglist', first.harness, first.state, 'direct:one',
);
const aliased = await runModelCommand(
'/REASONINGS', second.harness, second.state, 'direct:one',
);
assert.equal(aliased.message, listed.message);
assert.match(listed.message, /deepseek-official\/deepseek-v4-flash/);
assert.match(listed.message, /2\. High \(high\)(当前、默认)/);
assert.match(listed.message, /复杂任务/);
assert.match(listed.message, /\/reasoning --default/);
assert.equal(first.calls.some(([name]) => name === 'selectModel'), false);
assert.equal(second.calls.some(([name]) => name === 'selectModel'), false);
});
test('/reasoninglist keeps default reset available without current-model metadata', async () => {
for (const sessionCatalog of [
{
...CATALOG,
current: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
routable: true,
},
{
...CATALOG,
current: {
provider: 'private-provider',
model: 'private-model',
reasoningEffort: 'custom-high',
},
routable: true,
},
]) {
const current = fixture({ initialSessionId: 'session-one', sessionCatalog });
const listed = await runModelCommand(
'/reasoninglist', current.harness, current.state, 'direct:one',
);
assert.match(listed.message, /不提供可切换的推理等级/);
assert.match(listed.message, /恢复默认等级:\/reasoning --default/);
assert.equal(current.calls.some(([name]) => name === 'selectModel'), false);
}
});
test('reasoning read commands require an existing Session and validate syntax', async () => {
const missing = fixture();
for (const command of ['/reasoning', '/reasoninglist', '/reasonings']) {
const result = await runModelCommand(command, missing.harness, missing.state, 'direct:one');
assert.match(result.message, /还没有会话/, command);
}
assert.equal(missing.calls.some(([name]) => name === 'createSession'), false);
const existing = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
});
assert.match(
(await runModelCommand(
'/reasoninglist extra', existing.harness, existing.state, 'direct:one',
)).message,
/不带参数/,
);
assert.match(
(await runModelCommand(
'/reasoning high extra', existing.harness, existing.state, 'direct:one',
)).message,
/用法/,
);
});
test('/reasoning reports the explicit or model-default effort without selecting', async () => {
const explicit = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
});
const explicitResult = await runModelCommand(
'/reasoning', explicit.harness, explicit.state, 'direct:one',
);
assert.match(explicitResult.message, /当前推理等级:High \(high\)/);
const inherited = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog({
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
}),
});
const inheritedResult = await runModelCommand(
'/reasoning', inherited.harness, inherited.state, 'direct:one',
);
assert.match(inheritedResult.message, /当前推理等级:High \(high\)/);
assert.equal(explicit.calls.some(([name]) => name === 'selectModel'), false);
assert.equal(inherited.calls.some(([name]) => name === 'selectModel'), false);
});
test('/reasoning preserves an unknown current effort and represents Provider Default', async () => {
const groups = structuredClone(REASONING_CATALOG.groups);
delete groups[0].models[0].reasoning.defaultEffort;
const raw = fixture({
initialSessionId: 'session-one',
sessionCatalog: {
groups,
failures: [],
current: {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: 'gateway-ultra',
},
routable: true,
},
});
const listed = await runModelCommand(
'/reasoninglist', raw.harness, raw.state, 'direct:one',
);
assert.match(listed.message, /当前推理等级:gateway-ultra/);
assert.doesNotMatch(listed.message, /(当前/);
const providerDefault = fixture({
initialSessionId: 'session-one',
sessionCatalog: {
groups,
failures: [],
current: {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
},
routable: true,
},
});
const current = await runModelCommand(
'/reasoning', providerDefault.harness, providerDefault.state, 'direct:one',
);
assert.match(current.message, /Default(由模型或 Provider 决定)/);
});
test('/reasoning switches the current model effort by index or exact ID', async () => {
for (const [requested, expected] of [['3', 'max'], ['off', 'off']]) {
const { calls, harness, state } = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
});
const result = await runModelCommand(
`/reasoning ${requested}`, harness, state, 'direct:one',
);
assert.match(result.message, new RegExp(`推理等级已切换为:[\\s\\S]*${expected}`));
assert.deepEqual(calls.find(([name]) => name === 'selectModel'), [
'selectModel',
'session-one',
{
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: expected,
},
{},
]);
}
});
test('/reasoning prefers an exact opaque numeric effort ID before an index', async () => {
const groups = structuredClone(REASONING_CATALOG.groups);
groups[0].models[0].reasoning = {
efforts: [
{ id: '3', name: 'Literal Three' },
{ id: '0', name: 'Literal Zero' },
{ id: '9007199254740992', name: 'Huge Numeric' },
],
defaultEffort: '3',
};
for (const requested of ['3', '0', '9007199254740992']) {
const { calls, harness, state } = fixture({
initialSessionId: 'session-one',
sessionCatalog: {
groups,
failures: [],
current: {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: '3',
},
routable: true,
},
});
const result = await runModelCommand(
`/reasoning ${requested}`, harness, state, 'direct:one',
);
assert.match(result.message, /推理等级已切换为/);
assert.equal(
calls.find(([name]) => name === 'selectModel')?.[2]?.reasoningEffort,
requested,
);
}
});
test('/reasoning --default restores the current model default effort', async () => {
const { calls, harness, state } = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog({
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: 'max',
}),
selectionResult: {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: 'high',
},
selectionReadback: {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: 'high',
},
});
const result = await runModelCommand(
'/reasoning --default', harness, state, 'direct:one',
);
assert.match(result.message, /推理等级已切换为:[\s\S]*High \(high\)/);
assert.deepEqual(calls.find(([name]) => name === 'selectModel')?.[2], {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
});
});
test('/reasoning --default can reset an advisory-unlisted current model', async () => {
const sessionCatalog = {
...CATALOG,
current: {
provider: 'private-provider',
model: 'private-model',
reasoningEffort: 'custom-high',
},
routable: true,
};
const { calls, harness, state } = fixture({
initialSessionId: 'session-one',
sessionCatalog,
});
const result = await runModelCommand(
'/reasoning --default', harness, state, 'direct:one',
);
assert.match(result.message, /推理等级已切换为/);
assert.match(result.message, /Default/);
assert.deepEqual(calls.find(([name]) => name === 'selectModel')?.[2], {
provider: 'private-provider',
model: 'private-model',
});
});
test('/reasoning rejects unsupported levels without changing the selection', async () => {
for (const requested of ['0', '4', '9007199254740992', 'medium']) {
const { calls, harness, state } = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
});
const result = await runModelCommand(
`/reasoning ${requested}`, harness, state, 'direct:one',
);
assert.match(result.message, /序号无效|不支持推理等级/, requested);
assert.equal(calls.some(([name]) => name === 'selectModel'), false, requested);
}
const unsupported = fixture({ initialSessionId: 'session-one' });
const result = await runModelCommand(
'/reasoning high', unsupported.harness, unsupported.state, 'direct:one',
);
assert.match(result.message, /不提供可切换的推理等级/);
assert.equal(unsupported.calls.some(([name]) => name === 'selectModel'), false);
const noSession = fixture({ globalCatalog: REASONING_CATALOG });
const noSessionResult = await runModelCommand(
'/reasoning high', noSession.harness, noSession.state, 'direct:one',
);
assert.match(noSessionResult.message, /还没有会话/);
assert.equal(noSession.calls.some(([name]) => name === 'createSession'), false);
});
test('reasoning commands reject image-bearing control messages', async () => {
const current = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
});
for (const command of ['/reasoning', '/reasoninglist', '/reasonings', '/reasoning high']) {
const result = await runModelCommand(
command, current.harness, current.state, 'direct:one', { hasImages: true },
);
assert.match(result.message, /仅支持纯文字/, command);
}
assert.equal(current.calls.some(([name]) => name === 'models'), false);
assert.equal(current.calls.some(([name]) => name === 'selectModel'), false);
});
test('/model uses an exact catalog ID and preserves slashes inside the model ID', async () => {
const { calls, harness, state } = fixture({ initialSessionId: 'session-one' });
const control = Object.freeze({ route: 'direct:one' });
const result = await runModelCommand(
'/model openrouter/anthropic/claude-sonnet-4',
harness,
state,
'direct:one',
{ control },
);
assert.match(result.message, /openrouter\/anthropic\/claude-sonnet-4/);
assert.deepEqual(calls.find(([name]) => name === 'selectModel'), [
'selectModel',
'session-one',
{ provider: 'openrouter', model: 'anthropic/claude-sonnet-4' },
{},
]);
});
test('/model accepts the current catalog\'s global 1-based model number', async () => {
const { calls, harness, state } = fixture({ initialSessionId: 'session-one' });
const result = await runModelCommand('/model 3', harness, state, 'direct:one');
assert.match(result.message, /openrouter\/anthropic\/claude-sonnet-4/);
assert.deepEqual(calls.find(([name]) => name === 'selectModel'), [
'selectModel',
'session-one',
{ provider: 'openrouter', model: 'anthropic/claude-sonnet-4' },
{},
]);
});
test('/model accepts an optional effort and otherwise applies the target model default', async () => {
for (const [command, expectedEffort] of [
['/model 2 xhigh', 'xhigh'],
['/model deepseek-official/deepseek-v4-pro', 'medium'],
]) {
const resolvedDefault = command.includes(' xhigh') ? undefined : {
provider: 'deepseek-official',
model: 'deepseek-v4-pro',
reasoningEffort: 'medium',
};
const { calls, harness, state } = fixture({
initialSessionId: 'session-one',
globalCatalog: REASONING_CATALOG,
sessionCatalog: reasoningSessionCatalog(),
selectionResult: resolvedDefault,
selectionReadback: resolvedDefault,
});
const result = await runModelCommand(command, harness, state, 'direct:one');
assert.match(result.message, /deepseek-official\/deepseek-v4-pro/);
assert.match(result.message, new RegExp(`推理等级:.*${expectedEffort}`));
assert.deepEqual(calls.find(([name]) => name === 'selectModel')?.[2], {
provider: 'deepseek-official',
model: 'deepseek-v4-pro',
...(command.includes(' xhigh') ? { reasoningEffort: expectedEffort } : {}),
});
}
});
test('/model validates an optional effort against the target model atomically', async () => {
const invalid = fixture({ globalCatalog: REASONING_CATALOG });
const failed = await runModelCommand(
'/model 2 high', invalid.harness, invalid.state, 'direct:one',
);
assert.match(failed.message, /不支持推理等级:high/);
assert.match(failed.message, /medium, xhigh/);
assert.equal(invalid.calls.some(([name]) => name === 'createSession'), false);
assert.equal(invalid.calls.some(([name]) => name === 'selectModel'), false);
const valid = fixture({ globalCatalog: REASONING_CATALOG });
const switched = await runModelCommand(
'/model 2 xhigh', valid.harness, valid.state, 'direct:one',
);
assert.match(switched.message, /模型已切换为/);
assert.equal(valid.boundId(), 'session-created');
assert.deepEqual(valid.calls.find(([name]) => name === 'selectModel')?.[2], {
provider: 'deepseek-official',
model: 'deepseek-v4-pro',
reasoningEffort: 'xhigh',
});
});
test('/model reports the current reasoning effort when the Session exposes it', async () => {
const current = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
});
const result = await runModelCommand('/model', current.harness, current.state, 'direct:one');
assert.match(result.message, /当前推理等级:High \(high\)/);
assert.match(result.message, /\/reasoninglist/);
assert.equal(current.calls.some(([name]) => name === 'selectModel'), false);
});
test('/model rejects invalid model numbers without creating or selecting a Session', async () => {
for (const requested of ['0', '4', '9007199254740992']) {
const { calls, harness, state } = fixture();
const result = await runModelCommand(`/model ${requested}`, harness, state, 'direct:one');
assert.match(result.message, /模型序号无效/, requested);
assert.match(result.message, /\/models/, requested);
assert.equal(calls.some(([name]) => name === 'createSession'), false, requested);
assert.equal(calls.some(([name]) => name === 'selectModel'), false, requested);
}
});
test('/model rejects unknown IDs before creating a Session', async () => {
const { calls, harness, state } = fixture();
const result = await runModelCommand(
'/model DEEPSEEK-OFFICIAL/deepseek-v4-flash',
harness,
state,
'direct:one',
);
assert.match(result.message, /没有找到模型/);
assert.equal(calls.some(([name]) => name === 'createSession'), false);
assert.equal(calls.some(([name]) => name === 'selectModel'), false);
assert.match(
(await runModelCommand('/model missing-slash', harness, state, 'direct:one')).message,
/用法/,
);
});
test('/model creates and selects a blank Session before exposing its binding', async () => {
const { calls, harness, state, boundId } = fixture();
const signal = new AbortController().signal;
const result = await runModelCommand(
'/model 2',
harness,
state,
'direct:one',
{ signal, control: 'control-one' },
);
assert.match(result.message, /模型已切换为/);
assert.equal(boundId(), 'session-created');
const operations = calls.map(([name]) => name);
assert.ok(operations.indexOf('listModels') < operations.indexOf('createSession'));
assert.ok(operations.indexOf('createSession') < operations.indexOf('selectModel'));
assert.ok(operations.lastIndexOf('models') > operations.indexOf('selectModel'));
assert.ok(operations.lastIndexOf('models') < operations.indexOf('setSession'));
assert.ok(operations.indexOf('selectModel') < operations.indexOf('setSession'));
assert.deepEqual(calls.find(([name]) => name === 'selectModel'), [
'selectModel',
'session-created',
{ provider: 'deepseek-official', model: 'deepseek-v4-pro' },
{ signal },
]);
});
test('a mismatched selectModel result is reported as a failed switch', async () => {
const { calls, harness, state } = fixture({
initialSessionId: 'session-one',
selectionResult: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
});
const result = await runModelCommand(
'/model deepseek-official/deepseek-v4-pro',
harness,
state,
'direct:one',
);
assert.match(result.message, /模型切换失败/);
assert.match(result.message, /requested: deepseek-official\/deepseek-v4-pro/);
assert.match(result.message, /selectModel\.selected: deepseek-official\/deepseek-v4-flash/);
assert.equal(calls.filter(([name]) => name === 'models').length, 1);
});
test('an authoritative current-model mismatch is reported and leaves a new Session unbound', async () => {
const { calls, harness, state, boundId } = fixture({
selectionReadback: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
});
const result = await runModelCommand(
'/model deepseek-official/deepseek-v4-pro',
harness,
state,
'direct:one',
);
assert.match(result.message, /模型切换失败/);
assert.match(result.message, /requested: deepseek-official\/deepseek-v4-pro/);
assert.match(result.message, /当前模型: deepseek-official\/deepseek-v4-flash/);
assert.equal(boundId(), null);
assert.equal(calls.filter(([name]) => name === 'models').length, 1);
assert.equal(calls.some(([name]) => name === 'setSession'), false);
});
test('a failed first model selection leaves the conversation unbound', async () => {
const selectionError = new Error('provider unavailable');
selectionError.code = 'model-unavailable';
const { calls, harness, state, boundId } = fixture({ selectionError });
const result = await runModelCommand(
'/model deepseek-official/deepseek-v4-pro',
harness,
state,
'direct:one',
);
assert.match(result.message, /当前不可用|图片/);
assert.equal(boundId(), null);
assert.equal(calls.some(([name]) => name === 'createSession'), true);
assert.equal(calls.some(([name]) => name === 'selectModel'), true);
assert.equal(calls.some(([name]) => name === 'setSession'), false);
});
test('two concurrent first model switches share one created Session', async () => {
const { calls, harness, state, boundId } = fixture();
const results = await Promise.all([
runModelCommand(
'/model 2',
harness,
state,
'direct:one',
),
runModelCommand(
'/model openrouter/anthropic/claude-sonnet-4',
harness,
state,
'direct:one',
),
]);
assert.ok(results.every(({ message }) => /模型已切换为/.test(message)));
assert.equal(boundId(), 'session-created');
assert.equal(calls.filter(([name]) => name === 'createSession').length, 1);
assert.equal(calls.filter(([name]) => name === 'setSession').length, 1);
assert.deepEqual(
calls.filter(([name]) => name === 'selectModel').map(([, sessionId]) => sessionId),
['session-created', 'session-created'],
);
});
test('a concurrent external binding is preserved after selecting an unbound Session', async () => {
const selectionStarted = deferred();
const releaseSelection = deferred();
const fixtureValue = fixture({
selectModelHook: async ({ sessionId }) => {
if (sessionId !== 'session-created') return;
selectionStarted.resolve();
await releaseSelection.promise;
},
});
const switching = runModelCommand(
'/model deepseek-official/deepseek-v4-pro',
fixtureValue.harness,
fixtureValue.state,
'direct:one',
);
await selectionStarted.promise;
await fixtureValue.state.setSession('direct:one', 'session-bound-elsewhere');
releaseSelection.resolve();
const result = await switching;
assert.match(result.message, /会话已发生变化.*重试/);
assert.equal(fixtureValue.boundId(), 'session-bound-elsewhere');
assert.equal(fixtureValue.calls.some((call) => (
call[0] === 'setSession' && call[2] === 'session-created'
)), false);
});
test('a concurrent external rebind is preserved after selecting a bound Session', async () => {
const selectionStarted = deferred();
const releaseSelection = deferred();
const fixtureValue = fixture({
initialSessionId: 'session-one',
selectModelHook: async ({ sessionId }) => {
if (sessionId !== 'session-one') return;
selectionStarted.resolve();
await releaseSelection.promise;
},
});
const switching = runModelCommand(
'/model deepseek-official/deepseek-v4-pro',
fixtureValue.harness,
fixtureValue.state,
'direct:one',
);
await selectionStarted.promise;
await fixtureValue.state.setSession('direct:one', 'session-two');
releaseSelection.resolve();
const result = await switching;
assert.match(result.message, /会话已发生变化.*重试/);
assert.equal(fixtureValue.boundId(), 'session-two');
assert.doesNotMatch(result.message, /模型已切换为/);
});
test('/model refuses pending interactions and active or running Sessions', async () => {
const pending = fixture({ initialSessionId: 'session-one' });
const pendingResult = await runModelCommand(
'/model deepseek-official/deepseek-v4-pro',
pending.harness,
pending.state,
'direct:one',
{ pendingInteraction: true },
);
assert.match(pendingResult.message, /等待你的回答或审批/);
assert.equal(pending.calls.some(([name]) => name === 'selectModel'), false);
for (const state of [{ running: true }, { activeTurn: true }]) {
const active = fixture({ initialSessionId: 'session-one', ...state });
const result = await runModelCommand(
'/model deepseek-official/deepseek-v4-pro',
active.harness,
active.state,
'direct:one',
{ control: 'owner-one' },
);
assert.match(result.message, /当前任务正在运行/);
assert.equal(active.calls.some(([name]) => name === 'models'), false);
assert.equal(active.calls.some(([name]) => name === 'selectModel'), false);
}
});
test('/reasoning mutations refuse pending or busy Sessions while reads remain available', async () => {
const pending = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
});
const pendingResult = await runModelCommand(
'/reasoning off', pending.harness, pending.state, 'direct:one',
{ pendingInteraction: true },
);
assert.match(pendingResult.message, /等待你的回答或审批/);
assert.equal(pending.calls.some(([name]) => name === 'selectModel'), false);
for (const runState of [{ running: true }, { activeTurn: true }]) {
const active = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
...runState,
});
const failed = await runModelCommand(
'/reasoning off', active.harness, active.state, 'direct:one',
{ control: 'owner-one' },
);
assert.match(failed.message, /当前任务正在运行/);
assert.equal(active.calls.some(([name]) => name === 'selectModel'), false);
const listed = await runModelCommand(
'/reasoninglist', active.harness, active.state, 'direct:one',
);
assert.match(listed.message, /可用推理等级/);
}
});
test('/reasoning verifies both the selected effort and authoritative readback', async () => {
const wrongSelection = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
selectionResult: {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: 'high',
},
});
const selectionFailure = await runModelCommand(
'/reasoning max',
wrongSelection.harness,
wrongSelection.state,
'direct:one',
);
assert.match(selectionFailure.message, /推理等级切换失败/);
assert.match(selectionFailure.message, /reasoningEffort=max/);
assert.match(selectionFailure.message, /reasoningEffort=high/);
const wrongReadback = fixture({
initialSessionId: 'session-one',
sessionCatalog: reasoningSessionCatalog(),
selectionReadback: {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: 'off',
},
});
const readbackFailure = await runModelCommand(
'/reasoning max',
wrongReadback.harness,
wrongReadback.state,
'direct:one',
);
assert.match(readbackFailure.message, /推理等级切换失败/);
assert.match(readbackFailure.message, /reasoningEffort=max/);
assert.match(readbackFailure.message, /reasoningEffort=off/);
});
test('a missing bound Session is cleared and /models falls back to the global catalog', async () => {
const { calls, harness, state, boundId } = fixture({
initialSessionId: 'session-missing',
existing: false,
});
const result = await runModelCommand('/models', harness, state, 'direct:one');
assert.match(result.message, /deepseek-official\/deepseek-v4-flash/);
assert.equal(boundId(), null);
assert.equal(calls.some(([name]) => name === 'clearSession'), true);
assert.equal(calls.some(([name]) => name === 'listModels'), true);
});
test('model command failures use safe user-facing messages', async () => {
const privateError = new Error('provider leaked API key sk-private');
privateError.code = 'model-unavailable';
const selection = fixture({
initialSessionId: 'session-one',
selectionError: privateError,
});
const failed = await runModelCommand(
'/model deepseek-official/deepseek-v4-pro',
selection.harness,
selection.state,
'direct:one',
);
assert.match(failed.message, /当前不可用|图片/);
assert.doesNotMatch(failed.message, /sk-private/);
const listing = fixture({ globalCatalog: new Error('private endpoint') });
const unavailable = await runModelCommand('/models', listing.harness, listing.state, 'direct:one');
assert.match(unavailable.message, /暂时无法获取模型列表/);
assert.doesNotMatch(unavailable.message, /private endpoint/);
const bad = fixture({ globalCatalog: { groups: null, failures: [] } });
assert.match(
(await runModelCommand('/models', bad.harness, bad.state, 'direct:one')).message,
/暂时无法获取模型列表/,
);
});
test('non-model input is left for ordinary message routing', async () => {
assert.equal(await runModelCommand('hello', {}, {}, 'direct:one'), null);
});