mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 06:30:46 +08:00
Keep IM tool approvals in chat and add WhatsApp default model.
Claim in-flight IM turns before ownership flips active so approvals no longer stick only in DSH Web, auto-recover stale session models once, and let WhatsApp bots pick a shared default model for new sessions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2723e94541
commit
74c58bdcde
21 changed files with 1388 additions and 395 deletions
97
test/channels/shared/default-model.test.mjs
Normal file
97
test/channels/shared/default-model.test.mjs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
catalogHasModel,
|
||||
defaultModelKey,
|
||||
normalizeDefaultModel,
|
||||
normalizeModelCatalog,
|
||||
validateDefaultModel,
|
||||
} from '../../../src/channels/shared/default-model.mjs';
|
||||
import {
|
||||
BotWorkspaceStore,
|
||||
createBotWorkspaceScope,
|
||||
} from '../../../src/channels/shared/bot-workspace-store.mjs';
|
||||
|
||||
test('normalizeDefaultModel accepts object and provider/model strings', () => {
|
||||
assert.deepEqual(normalizeDefaultModel({ provider: 'deepseek', model: 'chat' }), {
|
||||
provider: 'deepseek',
|
||||
model: 'chat',
|
||||
});
|
||||
assert.deepEqual(normalizeDefaultModel('openai/gpt-4.1'), {
|
||||
provider: 'openai',
|
||||
model: 'gpt-4.1',
|
||||
});
|
||||
assert.equal(normalizeDefaultModel(''), null);
|
||||
assert.equal(normalizeDefaultModel({ provider: '', model: 'x' }), null);
|
||||
assert.equal(defaultModelKey({ provider: 'a', model: 'b' }), 'a/b');
|
||||
});
|
||||
|
||||
test('validateDefaultModel rejects junk', () => {
|
||||
assert.equal(validateDefaultModel(null), null);
|
||||
assert.throws(() => validateDefaultModel({ provider: 'x' }), (error) => (
|
||||
error.code === 'default-model-invalid'
|
||||
));
|
||||
});
|
||||
|
||||
test('model catalog helpers flatten groups', () => {
|
||||
const catalog = normalizeModelCatalog({
|
||||
groups: [{
|
||||
id: 'deepseek',
|
||||
name: 'DeepSeek',
|
||||
models: [{ id: 'chat', name: 'Chat' }, { id: 'reasoner', name: 'Reasoner' }],
|
||||
}],
|
||||
failures: [],
|
||||
});
|
||||
assert.equal(catalog.items.length, 2);
|
||||
assert.equal(catalogHasModel(catalog, 'deepseek/chat'), true);
|
||||
assert.equal(catalogHasModel(catalog, 'deepseek/missing'), false);
|
||||
});
|
||||
|
||||
test('BotWorkspaceStore persists and applies default model on createSession', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-im-default-model-'));
|
||||
try {
|
||||
const store = await new BotWorkspaceStore(join(root, 'workspaces.json'), {
|
||||
defaultWorkspace: root,
|
||||
}).load();
|
||||
await store.ensure('bot-a');
|
||||
await store.setDefaultModel('bot-a', { provider: 'deepseek', model: 'chat' });
|
||||
assert.deepEqual(store.defaultModelFor('bot-a'), {
|
||||
provider: 'deepseek',
|
||||
model: 'chat',
|
||||
});
|
||||
|
||||
const selected = [];
|
||||
const harness = {
|
||||
async createSession() { return 'session-1'; },
|
||||
async selectSessionModel(sessionId, selection) {
|
||||
selected.push({ sessionId, selection });
|
||||
return { selected: selection };
|
||||
},
|
||||
};
|
||||
const state = {
|
||||
async clearSessions() {},
|
||||
sessionFor() { return null; },
|
||||
async setSession() { return true; },
|
||||
};
|
||||
const scoped = createBotWorkspaceScope(harness, {
|
||||
botId: 'bot-a',
|
||||
workspaces: store,
|
||||
state,
|
||||
});
|
||||
const sessionId = await scoped.harness.createSession();
|
||||
assert.equal(sessionId, 'session-1');
|
||||
assert.deepEqual(selected, [{
|
||||
sessionId: 'session-1',
|
||||
selection: { provider: 'deepseek', model: 'chat' },
|
||||
}]);
|
||||
|
||||
await store.setDefaultModel('bot-a', null);
|
||||
assert.equal(store.defaultModelFor('bot-a'), null);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
@ -198,7 +198,7 @@ test('never submits the next FIFO approval before its operation is presented', a
|
|||
assert.equal(sent.some(({ toolName, text }) => (
|
||||
toolName === 'second-tool' && text.includes('second-tool --run')
|
||||
)), true);
|
||||
assert.equal(earlyTexts[0].includes('请精准回复'), true);
|
||||
assert.equal(earlyTexts[0].includes('精准回复'), true);
|
||||
|
||||
await queue.claimReply({
|
||||
key: 'direct:actor-a',
|
||||
|
|
@ -237,7 +237,7 @@ test('a failed presentation cannot be followed by a blind approval', async () =>
|
|||
}).process();
|
||||
assert.equal(responses.length, 0);
|
||||
assert.equal(sent.some((text) => text.includes('bash --run')), true);
|
||||
assert.equal(sent.some((text) => text.includes('请精准回复')), true);
|
||||
assert.equal(sent.some((text) => text.includes('精准回复')), true);
|
||||
|
||||
await queue.claimReply({
|
||||
key: 'direct:actor-a',
|
||||
|
|
@ -321,7 +321,7 @@ test('resolved waits for an in-flight presentation before showing the next appro
|
|||
assert.match(sent[0], /first-tool --run/);
|
||||
assert.equal(sent[1], '已拒绝此次操作。');
|
||||
assert.match(sent[2], /second-tool --run/);
|
||||
assert.match(sent[3], /请精准回复/);
|
||||
assert.match(sent[3], /精准回复/);
|
||||
assert.equal(secondResponses.length, 0);
|
||||
|
||||
await queue.claimReply({
|
||||
|
|
@ -539,7 +539,7 @@ test('approval decisions stay bound to the initiating actor, route, and group me
|
|||
assert.equal(responses.length, 1);
|
||||
assert.equal(responses[0].value.outcome, 'allowed-once');
|
||||
assert.equal(sent.filter((text) => text.includes('只有发起当前任务')).length, 2);
|
||||
assert.equal(sent.some((text) => text.includes('请精准回复')), true);
|
||||
assert.equal(sent.some((text) => text.includes('精准回复')), true);
|
||||
});
|
||||
|
||||
test('a deferred approval reply stays silent when the approval resolves with its question unfinished', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue