mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 07:53:17 +08:00
fix: complete per-bot agent preset lifecycle
This commit is contained in:
parent
8ab4b9e7de
commit
01576f65c6
27 changed files with 721 additions and 208 deletions
137
test/agent-preset-session-lifecycle.test.mjs
Normal file
137
test/agent-preset-session-lifecycle.test.mjs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, realpath, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
BotWorkspaceStore,
|
||||
createBotWorkspaceScope,
|
||||
createWorkspaceAwareController,
|
||||
} from '../src/channels/shared/bot-workspace-store.mjs';
|
||||
import { ConversationStateStore } from '../src/channels/shared/conversation-state-store.mjs';
|
||||
import { TextHarnessBridge } from '../src/channels/shared/text-harness-bridge.mjs';
|
||||
import {
|
||||
TOKEN_BOT_ENDPOINTS,
|
||||
createTokenBotRpcHandler,
|
||||
} from '../plugin-src/host/channels/shared/rpc.mjs';
|
||||
|
||||
function message(messageId, content) {
|
||||
return {
|
||||
messageId,
|
||||
senderId: 'actor-one',
|
||||
senderIsBot: false,
|
||||
kind: 'direct',
|
||||
conversationId: 'chat-one',
|
||||
content,
|
||||
addressed: true,
|
||||
replyTarget: { id: 'chat-one' },
|
||||
};
|
||||
}
|
||||
|
||||
test('an agent preset change applies only after /new creates the next session', async (t) => {
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), 'dsh-im-preset-lifecycle-')));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
|
||||
const botId = 'bot-one';
|
||||
const conversationKey = 'direct:chat-one';
|
||||
const workspaces = await new BotWorkspaceStore(join(root, 'workspaces.json'), {
|
||||
defaultWorkspace: root,
|
||||
}).load();
|
||||
await workspaces.ensure(botId, { defaultAgentPreset: 'preset-old' });
|
||||
const state = await new ConversationStateStore(join(root, 'state.json')).load();
|
||||
|
||||
const creations = [];
|
||||
const asks = [];
|
||||
const sessions = new Set();
|
||||
const harness = {
|
||||
async createSession(options) {
|
||||
const sessionId = `session-${creations.length === 0 ? 'a' : 'b'}`;
|
||||
creations.push({ sessionId, options });
|
||||
sessions.add(sessionId);
|
||||
return sessionId;
|
||||
},
|
||||
async sessionExists(sessionId) {
|
||||
return sessions.has(sessionId);
|
||||
},
|
||||
async ask(sessionId, text) {
|
||||
asks.push({ sessionId, text });
|
||||
return `answer-from-${sessionId}`;
|
||||
},
|
||||
};
|
||||
const scope = createBotWorkspaceScope(harness, { botId, workspaces, state });
|
||||
const sent = [];
|
||||
const bridge = new TextHarnessBridge({
|
||||
descriptor: { key: 'test', label: 'Test' },
|
||||
bot: { async sendText(_target, text) { sent.push(text); } },
|
||||
harness: scope.harness,
|
||||
state: scope.state,
|
||||
logger: { warn() {}, error() {} },
|
||||
});
|
||||
|
||||
const baseController = {
|
||||
status() { return { bots: [{ botId, connected: true }] }; },
|
||||
bindCredentials() { return this.status(); },
|
||||
reconnectBot() { return this.status(); },
|
||||
deleteBot() { return { bots: [] }; },
|
||||
};
|
||||
const controller = createWorkspaceAwareController(baseController, {
|
||||
workspaces,
|
||||
stateFor: async () => state,
|
||||
agentPresetCatalog: {
|
||||
defaultId: 'preset-old',
|
||||
items: [
|
||||
{ id: 'preset-old', label: 'Old preset' },
|
||||
{ id: 'preset-new', label: 'New preset' },
|
||||
],
|
||||
},
|
||||
});
|
||||
const rpc = createTokenBotRpcHandler(controller, { channel: 'Test' });
|
||||
|
||||
await bridge.accept(message('message-one', 'first prompt'));
|
||||
assert.equal(state.sessionFor(conversationKey), 'session-a');
|
||||
assert.deepEqual(creations, [{
|
||||
sessionId: 'session-a',
|
||||
options: { workspace: root, agentPreset: 'preset-old' },
|
||||
}]);
|
||||
|
||||
const updated = await rpc(TOKEN_BOT_ENDPOINTS.setAgentPreset, {
|
||||
botId,
|
||||
agentPreset: 'preset-new',
|
||||
});
|
||||
assert.equal(updated.ok, true);
|
||||
assert.equal(updated.value.bots[0].agentPreset, 'preset-new');
|
||||
assert.equal(state.sessionFor(conversationKey), 'session-a');
|
||||
|
||||
await bridge.accept(message('message-two', 'still in the current chat'));
|
||||
assert.equal(creations.length, 1, 'the current chat must reuse session A');
|
||||
assert.equal(state.sessionFor(conversationKey), 'session-a');
|
||||
|
||||
await bridge.accept(message('message-new', '/new'));
|
||||
assert.equal(state.sessionFor(conversationKey), null);
|
||||
assert.equal(creations.length, 1, '/new itself must not create a session');
|
||||
|
||||
await bridge.accept(message('message-three', 'first prompt after /new'));
|
||||
assert.equal(state.sessionFor(conversationKey), 'session-b');
|
||||
assert.deepEqual(creations, [
|
||||
{
|
||||
sessionId: 'session-a',
|
||||
options: { workspace: root, agentPreset: 'preset-old' },
|
||||
},
|
||||
{
|
||||
sessionId: 'session-b',
|
||||
options: { workspace: root, agentPreset: 'preset-new' },
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(asks, [
|
||||
{ sessionId: 'session-a', text: 'first prompt' },
|
||||
{ sessionId: 'session-a', text: 'still in the current chat' },
|
||||
{ sessionId: 'session-b', text: 'first prompt after /new' },
|
||||
]);
|
||||
assert.deepEqual(sent, [
|
||||
'answer-from-session-a',
|
||||
'answer-from-session-a',
|
||||
'已开启新会话。请发送你的问题。',
|
||||
'answer-from-session-b',
|
||||
]);
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp } from 'node:fs/promises';
|
||||
import { mkdtemp, readFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
|
@ -87,6 +87,14 @@ test('production assembly keeps secrets in credentials and creates per-bot runti
|
|||
createConnectionSupervisor: () => supervisor,
|
||||
});
|
||||
|
||||
assert.equal(seen.harnessOptions.agentPreset, 'router-standard');
|
||||
assert.equal(Object.hasOwn(seen.harnessOptions, 'agentPreset'), false);
|
||||
await seen.controllerOptions.createRuntime({
|
||||
botId: 'dt_new',
|
||||
config: { botId: 'dt_new', clientId: 'dingnew' },
|
||||
clientSecret: 'host-only-secret',
|
||||
});
|
||||
const stored = JSON.parse(await readFile(join(directory, 'workspaces.json'), 'utf8'));
|
||||
assert.equal(stored.agentPresets.dt_new, 'router-standard');
|
||||
assert.equal(Object.hasOwn(stored.agentPresets, 'dt_abc'), false);
|
||||
await productionWithPreset.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -96,3 +96,33 @@ test('supervisor retries an offline bot and leaves the recovered connection on t
|
|||
|
||||
await supervisor.close();
|
||||
});
|
||||
|
||||
test('supervisor awaits an asynchronous controller status before resolving readiness', async () => {
|
||||
const timers = scheduler();
|
||||
let resolveStatus;
|
||||
let readyResolved = false;
|
||||
const pendingStatus = new Promise((resolve) => { resolveStatus = resolve; });
|
||||
const supervisor = new ConnectionSupervisor({
|
||||
controller: {
|
||||
async initialize() {},
|
||||
status() { return pendingStatus; },
|
||||
},
|
||||
harness: { async ensureRunning() {} },
|
||||
logger: { warn() {} },
|
||||
healthyIntervalMs: 103,
|
||||
setTimeoutImpl: timers.setTimeoutImpl,
|
||||
clearTimeoutImpl: timers.clearTimeoutImpl,
|
||||
}).start();
|
||||
void supervisor.ready.then(() => { readyResolved = true; });
|
||||
|
||||
await timers.runNext();
|
||||
assert.equal(readyResolved, false);
|
||||
assert.equal(timers.pending.length, 0);
|
||||
|
||||
resolveStatus({ totals: { configured: 1, connected: 1 } });
|
||||
assert.equal((await supervisor.ready).totals.connected, 1);
|
||||
await flush();
|
||||
assert.equal(timers.pending[0].delay, 103);
|
||||
|
||||
await supervisor.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -110,6 +110,94 @@ test('Host exposes configured offline as a redacted capability fact', async () =
|
|||
assert.equal('credentials' in result.value, false);
|
||||
});
|
||||
|
||||
test('Host exposes only browser-safe Agent Preset fields in status and update responses', async () => {
|
||||
let selectedPreset = 'marketing-jeep';
|
||||
const calls = [];
|
||||
const current = () => status({
|
||||
schemaVersion: 2,
|
||||
revision: 3,
|
||||
configured: true,
|
||||
agentPresetCatalog: {
|
||||
defaultId: 'standard',
|
||||
root: 'private-preset-root',
|
||||
failures: [{ message: 'private-catalog-failure' }],
|
||||
items: [
|
||||
{
|
||||
id: 'standard',
|
||||
name: 'Standard',
|
||||
path: 'private-standard-path',
|
||||
trust: 'private-trust-level',
|
||||
},
|
||||
{
|
||||
id: 'marketing-jeep',
|
||||
label: '营销吉普',
|
||||
error: { message: 'private-preset-error' },
|
||||
},
|
||||
{
|
||||
id: 'broken-preset',
|
||||
label: 'Broken',
|
||||
broken: { message: 'private-broken-reason' },
|
||||
},
|
||||
{ id: 'INVALID', label: 'invalid-item-must-be-filtered' },
|
||||
{ id: 'standard', label: 'duplicate-item-must-be-filtered' },
|
||||
],
|
||||
},
|
||||
bots: [{
|
||||
botId: 'bot_safe',
|
||||
phase: 'connected',
|
||||
connected: true,
|
||||
configured: true,
|
||||
agentPreset: selectedPreset,
|
||||
bot: { name: '安全机器人', domain: 'feishu' },
|
||||
connection: {
|
||||
ready: true,
|
||||
feishuLongConnectionState: 'connected',
|
||||
harnessReachable: true,
|
||||
},
|
||||
}],
|
||||
});
|
||||
const controller = {
|
||||
status: async () => current(),
|
||||
startRegistration: async () => current(),
|
||||
cancelRegistration: async () => current(),
|
||||
disconnect: async () => status(),
|
||||
updateAgentPreset: async (botId, agentPreset) => {
|
||||
calls.push({ botId, agentPreset });
|
||||
selectedPreset = agentPreset;
|
||||
return current();
|
||||
},
|
||||
};
|
||||
const fx = await rpcFixture(controller);
|
||||
|
||||
const listed = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
|
||||
assert.equal(listed.ok, true);
|
||||
assert.equal(listed.value.bots[0].agentPreset, 'marketing-jeep');
|
||||
assert.deepEqual(listed.value.agentPresetCatalog, {
|
||||
defaultId: 'standard',
|
||||
items: [
|
||||
{ id: 'standard', label: 'Standard' },
|
||||
{ id: 'marketing-jeep', label: '营销吉普' },
|
||||
],
|
||||
});
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(listed),
|
||||
/private-preset-root|private-catalog-failure|private-standard-path|private-trust-level|private-preset-error|private-broken-reason|invalid-item|duplicate-item/,
|
||||
);
|
||||
|
||||
const cleared = await fx.registration.handler(
|
||||
FEISHU_ENDPOINTS.setAgentPreset,
|
||||
{ botId: 'bot_safe', agentPreset: null },
|
||||
signal(),
|
||||
);
|
||||
assert.equal(cleared.ok, true);
|
||||
assert.deepEqual(calls, [{ botId: 'bot_safe', agentPreset: null }]);
|
||||
assert.equal(cleared.value.bots[0].agentPreset, null);
|
||||
assert.deepEqual(cleared.value.agentPresetCatalog, listed.value.agentPresetCatalog);
|
||||
assert.doesNotMatch(JSON.stringify(cleared), /private-|invalid-item|duplicate-item/);
|
||||
|
||||
await fx.dispose();
|
||||
});
|
||||
|
||||
test('RPC dispatch matches every endpoint in client/api.js', async () => {
|
||||
const calls = [];
|
||||
let current = status({
|
||||
|
|
@ -1068,7 +1156,7 @@ test('a corrupt legacy state file cannot prevent a healthy v2 bot from starting'
|
|||
});
|
||||
|
||||
await production.ready;
|
||||
const statusValue = production.controller.status();
|
||||
const statusValue = await production.controller.status();
|
||||
assert.equal(statusValue.bots.find((entry) => entry.botId === 'bot_legacy').phase, 'error');
|
||||
assert.equal(statusValue.bots.find((entry) => entry.botId === 'bot_healthy').connected, true);
|
||||
assert.equal(statusValue.totals.connected, 1);
|
||||
|
|
|
|||
|
|
@ -171,14 +171,22 @@ test('all nine production channels receive the Host command executor', async ()
|
|||
}
|
||||
});
|
||||
|
||||
test('all nine production channels defer an omitted agent preset to the Harness Host', async () => {
|
||||
test('all nine production channels use channel presets only as bot creation defaults', async () => {
|
||||
for (const path of PRODUCTION_FILES) {
|
||||
const source = await readFile(new URL(`../${path}`, import.meta.url), 'utf8');
|
||||
assert.doesNotMatch(source, /agentPreset:\s*config\.agentPreset\s*\?\?\s*['"]standard['"]/, path);
|
||||
assert.doesNotMatch(source, /\bagentPreset:\s*config\.agentPreset/, path);
|
||||
const creationDefaults = source.match(
|
||||
/workspaces\.ensure\([^;]*\{\s*defaultAgentPreset:\s*config\.agentPreset,?\s*\}\)/g,
|
||||
) ?? [];
|
||||
assert.equal(
|
||||
creationDefaults.length,
|
||||
2,
|
||||
`${path} must initialize both restored and newly connected bots`,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/\.\.\.\(config\.agentPreset == null \? \{\} : \{ agentPreset: config\.agentPreset \}\)/,
|
||||
path,
|
||||
/agentPresetCatalog:\s*\(\)\s*=>\s*listAgentPresetCatalog\(ctx\)/,
|
||||
`${path} must read the Host preset catalog dynamically`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -694,6 +694,37 @@ test('AgentPresetEditor lists Host presets and keeps the catalog label for the c
|
|||
assert.deepEqual(optionValues(select), ['', 'coding', 'default']);
|
||||
assert.equal(textOf(select.children[0]), '跟随 Host 默认');
|
||||
assert.equal(textOf(select.children[1]), 'Coding(coding)');
|
||||
assert.equal(
|
||||
textOf(renderer.root.findByProps({ className: 'dim-presetHelp' })),
|
||||
'只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。',
|
||||
);
|
||||
renderer.unmount();
|
||||
});
|
||||
|
||||
test('AgentPresetEditor marks a removed current preset and still allows clearing it', async () => {
|
||||
const saved = [];
|
||||
const renderer = create(React.createElement(
|
||||
AgentPresetCatalogContext.Provider,
|
||||
{ value: PRESET_CATALOG },
|
||||
React.createElement(AgentPresetEditor, {
|
||||
agentPreset: 'removed-preset',
|
||||
onSave(value) { saved.push(value); },
|
||||
}),
|
||||
));
|
||||
const select = renderer.root.findByProps({ className: 'dim-presetSelect' });
|
||||
assert.equal(select.props.value, 'removed-preset');
|
||||
assert.deepEqual(optionValues(select), ['', 'coding', 'default', 'removed-preset']);
|
||||
assert.equal(textOf(select.children[3]), 'removed-preset(已不可用)');
|
||||
assert.equal(
|
||||
textOf(renderer.root.findByProps({ role: 'status' })),
|
||||
'当前 Agent Preset 已不可用,请选择其他 Preset 或跟随 Host 默认。',
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
select.props.onChange({ target: { value: '' } });
|
||||
await flushMicrotasks();
|
||||
});
|
||||
assert.deepEqual(saved, [null]);
|
||||
renderer.unmount();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
observeBotWorkspaceRemovals,
|
||||
validateWorkspacePath,
|
||||
} from '../src/channels/shared/bot-workspace-store.mjs';
|
||||
import { listAgentPresetCatalog } from '../src/channels/shared/agent-preset.mjs';
|
||||
import {
|
||||
connectionTestTarget,
|
||||
rememberConnectionTestTarget,
|
||||
|
|
@ -125,8 +126,9 @@ test('workspace writes roll back updates while committed removals stay retired i
|
|||
const blockedParent = join(root, 'blocked-parent');
|
||||
await writeFile(blockedParent, 'not a directory');
|
||||
const broken = new BotWorkspaceStore(join(blockedParent, 'workspaces.json'), { defaultWorkspace });
|
||||
await assert.rejects(broken.ensure('bot_new'));
|
||||
await assert.rejects(broken.ensure('bot_new', { defaultAgentPreset: 'router-standard' }));
|
||||
assert.equal(broken.workspaceFor('bot_new'), defaultWorkspace);
|
||||
assert.equal(broken.agentPresetFor('bot_new'), null);
|
||||
});
|
||||
|
||||
test('workspace validation rejects relative, missing, and file paths', async (t) => {
|
||||
|
|
@ -1322,6 +1324,81 @@ test('BotWorkspaceStore persists per-bot agent presets without changing workspac
|
|||
assert.equal(reloaded.agentPresetFor('bot_one'), 'standard-claude');
|
||||
});
|
||||
|
||||
test('BotWorkspaceStore applies a channel preset only when a bot is first created', async (t) => {
|
||||
const { path, defaultWorkspace } = await fixture(t);
|
||||
const store = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
||||
|
||||
await store.ensure('bot_seeded', { defaultAgentPreset: 'marketing-jeep' });
|
||||
await store.ensure('bot_existing');
|
||||
await store.ensure('bot_existing', { defaultAgentPreset: 'router-standard' });
|
||||
assert.equal(store.agentPresetFor('bot_seeded'), 'marketing-jeep');
|
||||
assert.equal(store.agentPresetFor('bot_existing'), null);
|
||||
|
||||
await store.setAgentPreset('bot_seeded', null);
|
||||
await store.ensure('bot_seeded', { defaultAgentPreset: 'router-standard' });
|
||||
assert.equal(store.agentPresetFor('bot_seeded'), null);
|
||||
|
||||
await store.ensure('bot_existing', { defaultAgentPreset: 'Not Valid' });
|
||||
await assert.rejects(
|
||||
store.ensure('bot_invalid', { defaultAgentPreset: 'Not Valid' }),
|
||||
{ code: 'agent-preset-invalid' },
|
||||
);
|
||||
assert.equal(store.has('bot_invalid'), false);
|
||||
|
||||
await store.remove('bot_seeded');
|
||||
await store.ensure('bot_seeded', { defaultAgentPreset: 'router-standard' });
|
||||
assert.equal(store.agentPresetFor('bot_seeded'), 'router-standard');
|
||||
});
|
||||
|
||||
test('Agent Preset catalog filters broken entries and exposes public fields only', async () => {
|
||||
const catalog = await listAgentPresetCatalog({
|
||||
get(name) {
|
||||
assert.equal(name, 'agentPresets');
|
||||
return {
|
||||
defaultId: 'standard',
|
||||
async list() {
|
||||
return [
|
||||
{ id: 'standard', name: 'Standard', path: '/secret/standard', trust: 'trusted' },
|
||||
{ id: 'broken-one', name: 'Broken', broken: 'missing file', path: '/secret/broken' },
|
||||
{ id: 'marketing-jeep', label: 'Marketing' },
|
||||
{ id: 'marketing-jeep', label: 'Duplicate' },
|
||||
{ id: 'Not Valid', label: 'Invalid' },
|
||||
];
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(catalog, {
|
||||
defaultId: 'standard',
|
||||
items: [
|
||||
{ id: 'standard', label: 'Standard' },
|
||||
{ id: 'marketing-jeep', label: 'Marketing' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('Agent Preset catalog lookup failures fail soft', async () => {
|
||||
const empty = { defaultId: '', items: [] };
|
||||
assert.deepEqual(await listAgentPresetCatalog({
|
||||
get() {
|
||||
throw new Error('service was unloaded');
|
||||
},
|
||||
}), empty);
|
||||
|
||||
const service = {
|
||||
async list() {
|
||||
return [{ id: 'standard', name: 'Standard' }];
|
||||
},
|
||||
};
|
||||
Object.defineProperty(service, 'defaultId', {
|
||||
get() {
|
||||
throw new Error('service was replaced');
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await listAgentPresetCatalog({ agentPresets: service }), empty);
|
||||
});
|
||||
|
||||
test('BotWorkspaceStore rejects invalid agent preset ids and missing bots', async (t) => {
|
||||
const { path, defaultWorkspace } = await fixture(t);
|
||||
const store = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
||||
|
|
@ -1420,3 +1497,62 @@ test('workspace RPC can set a bot agent preset without switching workspace', asy
|
|||
assert.equal(invalid.ok, false);
|
||||
assert.equal(invalid.error.code, 'bad-request');
|
||||
});
|
||||
|
||||
test('workspace RPC refreshes the Agent Preset catalog and rejects unavailable choices', async (t) => {
|
||||
const { path, defaultWorkspace } = await fixture(t);
|
||||
const workspaces = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
||||
await workspaces.ensure('bot_one');
|
||||
let catalog = {
|
||||
defaultId: 'standard',
|
||||
items: [{ id: 'standard', label: 'Standard' }],
|
||||
};
|
||||
const base = {
|
||||
status() { return { bots: [{ botId: 'bot_one', connected: true }] }; },
|
||||
bindCredentials() { return this.status(); },
|
||||
reconnectBot() { return this.status(); },
|
||||
deleteBot() { return { bots: [] }; },
|
||||
};
|
||||
const controller = createWorkspaceAwareController(base, {
|
||||
workspaces,
|
||||
stateFor: async () => ({ async clearSessions() {} }),
|
||||
agentPresetCatalog: async () => catalog,
|
||||
});
|
||||
const handler = createTokenBotRpcHandler(controller, { channel: 'Telegram' });
|
||||
|
||||
const initial = await handler(TOKEN_BOT_ENDPOINTS.status, {});
|
||||
assert.deepEqual(initial.value.agentPresetCatalog, catalog);
|
||||
|
||||
catalog = {
|
||||
defaultId: 'marketing-jeep',
|
||||
items: [
|
||||
{ id: 'standard', label: 'Broken Standard', broken: 'missing entrypoint' },
|
||||
{ id: 'marketing-jeep', label: 'Marketing' },
|
||||
],
|
||||
};
|
||||
const refreshed = await handler(TOKEN_BOT_ENDPOINTS.status, {});
|
||||
assert.deepEqual(refreshed.value.agentPresetCatalog, {
|
||||
defaultId: 'marketing-jeep',
|
||||
items: [{ id: 'marketing-jeep', label: 'Marketing' }],
|
||||
});
|
||||
|
||||
const unavailable = await handler(TOKEN_BOT_ENDPOINTS.setAgentPreset, {
|
||||
botId: 'bot_one', agentPreset: 'standard',
|
||||
});
|
||||
assert.equal(unavailable.ok, false);
|
||||
assert.equal(unavailable.error.code, 'agent-preset-unavailable');
|
||||
assert.equal(workspaces.agentPresetFor('bot_one'), null);
|
||||
|
||||
const selected = await handler(TOKEN_BOT_ENDPOINTS.setAgentPreset, {
|
||||
botId: 'bot_one', agentPreset: 'marketing-jeep',
|
||||
});
|
||||
assert.equal(selected.ok, true);
|
||||
assert.equal(selected.value.bots[0].agentPreset, 'marketing-jeep');
|
||||
|
||||
catalog = { defaultId: '', items: [] };
|
||||
const cleared = await handler(TOKEN_BOT_ENDPOINTS.setAgentPreset, {
|
||||
botId: 'bot_one', agentPreset: null,
|
||||
});
|
||||
assert.equal(cleared.ok, true);
|
||||
assert.equal(cleared.value.bots[0].agentPreset, null);
|
||||
assert.deepEqual(cleared.value.agentPresetCatalog, catalog);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue