fix: preserve session titles without context enhancement

This commit is contained in:
xmanrui 2026-09-01 22:30:23 +08:00
parent e8c9817598
commit d044f1b0aa
17 changed files with 527 additions and 223 deletions

View file

@ -88,6 +88,10 @@ function fixture(channel, { contextEnhancement, onAsk } = {}) {
const harness = {
ensureRunning: async () => { calls.push(['ensureRunning']); },
createSession: async () => { calls.push(['createSession']); return 'session-existing'; },
renameSession: async (sessionId, title) => {
calls.push(['renameSession', sessionId, title]);
return { title, seq: 0 };
},
sessionExists: async (id) => { calls.push(['sessionExists', id]); return true; },
hasActiveTurn: async () => false,
isSessionRunning: async () => false,
@ -281,6 +285,7 @@ for (const channel of CHANNELS) {
await baseline.bridge.accept(baseline.event(1, undefined, { kind, media, poisonName: true }));
assert.equal(baseline.prompts.length, 1);
assert.equal(baseline.sourceReads, 0);
assert.equal(baseline.calls.some(([operation]) => operation === 'renameSession'), false);
for (const contextEnhancement of variants) {
const current = fixture(channel, { contextEnhancement });
await current.bridge.accept(current.event(1, undefined, { kind, media, poisonName: true }));
@ -308,6 +313,10 @@ for (const channel of CHANNELS) {
assert.deepEqual(sourceOf(current.prompts[0]), expected);
assert.ok(current.prompts[0].endsWith('\n\nhello'));
assert.doesNotMatch(current.prompts[0], /source_guidance|private-token|private-secret/);
assert.deepEqual(
current.calls.filter(([operation]) => operation === 'renameSession'),
[['renameSession', 'session-existing', 'hello']],
);
// Explicit null represents a provider event that does not include a nickname.
await current.bridge.accept(current.event(2, 'missing name', { kind, name: null }));
assert.equal(Object.hasOwn(sourceOf(current.prompts[1]), 'senderName'), false);
@ -355,7 +364,18 @@ for (const channel of CHANNELS) {
assert.ok(enhanced.endsWith(`\n\n${original}`));
}
assert.deepEqual(enabled.calls.filter(([op]) => op === 'file'), plain.calls.filter(([op]) => op === 'file'));
assert.deepEqual(withoutPrompt(enabled.calls), withoutPrompt(plain.calls), 'enhancement adds no provider or Harness requests');
const expectedTitle = media === 'file' && ['feishu', 'dingtalk'].includes(channel)
? 'report.txt'
: 'caption';
assert.deepEqual(
enabled.calls.filter(([op]) => op === 'renameSession'),
[['renameSession', 'session-existing', expectedTitle]],
);
assert.deepEqual(
withoutPrompt(enabled.calls.filter(([op]) => op !== 'renameSession')),
withoutPrompt(plain.calls),
'enhancement only adds the initial title request',
);
}
});
@ -446,7 +466,10 @@ for (const channel of CHANNELS) {
await current.bridge.accept(current.event(1, 'hello'));
if (config.direct.fields.length) assert.deepEqual(sourceOf(current.prompts[0]), { channel });
else if (config.direct.guidance) assert.equal(current.prompts[0], '<dsh_im_source_guidance>\ncustom\n</dsh_im_source_guidance>\n\nhello');
else assert.equal(current.prompts[0], 'hello');
else {
assert.equal(current.prompts[0], 'hello');
assert.equal(current.calls.some(([operation]) => operation === 'renameSession'), false);
}
}
});

View file

@ -112,7 +112,7 @@ test('in-process RPC preserves IDs, payloads, namespace receivers and errors', a
const calls = [];
const apiProxy = {};
const methods = ['host.describe', 'workspace.list', 'workspace.create', 'session.list',
'session.create', 'session.history', 'session.prompt', 'session.cancel',
'session.create', 'session.history', 'session.prompt', 'session.rename', 'session.cancel',
'session.models', 'session.selectModel', 'llm.models'];
for (const method of methods) {
const [domain, action] = method.split('.');
@ -191,6 +191,29 @@ test('explicit baseUrl still selects the existing HTTP transport', async () => {
assert.equal(requests, 1);
});
test('Session rename uses the public Harness RPC and validates local input', async () => {
const calls = [];
const success = (rpcId, value) => ({ rpcId, result: { ok: true, value } });
const client = localClient({
host: { describe: ({ rpcId }) => success(rpcId, {}) },
sessions: {
rename: ({ rpcId, payload }) => {
calls.push(payload);
return success(rpcId, { title: payload.title, seq: 3 });
},
},
});
assert.deepEqual(await client.renameSession('session', '查询订单'), {
title: '查询订单',
seq: 3,
});
assert.deepEqual(calls, [{ sessionId: 'session', title: '查询订单' }]);
await assert.rejects(client.renameSession('', 'title'), TypeError);
await assert.rejects(client.renameSession('session', ' '), TypeError);
assert.equal(calls.length, 1);
});
test('history reading uses only the existing read RPC in both Host connection modes', async () => {
const page = { events: [], hasMore: false };
for (const mode of ['local', 'http']) {