dsh-im-ops/test/history-i18n.test.mjs
xmanrui 8c6c31a1b6 feat: add bounded history previews across nine channels
Add shared /history parsing, read-only session access, message filtering, and bounded replies. Default to three messages, cap at five, and exclude saved history commands.

Include channel integration tests, translations, command help, and the Issue #62 implementation plan.
2026-08-28 03:01:53 +08:00

110 lines
5.1 KiB
JavaScript

import assert from 'node:assert/strict';
import { afterEach, beforeEach, test } from 'node:test';
import { runHistoryCommand } from '../src/channels/shared/history-command.mjs';
import { getImHostLanguage, setImHostLanguage, t } from '../src/channels/shared/i18n.mjs';
let previousLanguage;
beforeEach(() => {
previousLanguage = getImHostLanguage();
setImHostLanguage('en');
});
afterEach(() => setImHostLanguage(previousLanguage));
function entry(seq, type, data) {
return { event: { seq, type, data, surfaceOp: 'append' } };
}
function user(seq, content) {
return entry(seq, 'user/message', { source: { kind: 'user' }, content });
}
function assistant(seq, turn, content) {
return entry(seq, 'assistant/message', { turn, message: { content } });
}
function end(seq, turn) {
return entry(seq, 'turn/end', { turn, reason: { kind: 'completed' } });
}
function textBlock(text) {
return { type: 'text', text };
}
function run(text, { events = [], error, sessionId = 'history-test', ...options } = {}) {
return runHistoryCommand(text, {
workspaceSession() {
return {
async readHistory() {
if (error) throw error;
return { events, hasMore: false };
},
};
},
}, { sessionFor: () => sessionId }, 'direct:test', { isDirect: true, ...options });
}
test('/history English preview translates labels, truncation, and media placeholders', async () => {
const result = await run('/history', {
events: [
user(1, [textBlock('Earlier user message')]),
assistant(2, 1, [textBlock('Earlier assistant answer')]),
end(3, 1),
user(4, [textBlock('Recent user message'), { type: 'image' }, { type: 'file' }]),
assistant(5, 2, [textBlock('Long final answer. '.repeat(100))]),
end(6, 2),
],
});
assert.match(result.message, /^Session history \| history-test \| Recent messages: 3/);
assert.match(result.message, /1\. Assistant\nEarlier assistant answer/);
assert.match(result.message, /2\. User\nRecent user message\n\[Image\]\n\[File\]/);
assert.match(result.message, /3\. Assistant\nLong final answer\./);
assert.match(result.message, / \(truncated\)/);
assert.ok(result.message.endsWith('These are history records, not a new reply.'));
assert.doesNotMatch(result.message, /Earlier user message|[一-鿿]/);
assert.ok(result.messages.length <= 3);
});
test('/history English usage covers the indirect HISTORY_USAGE translation key', async () => {
for (const command of ['/history 0', '/history -1', '/history 1.5', '/history abc', '/history 1 2']) {
assert.equal((await run(command)).message, 'Usage: /history [count] (default 3, maximum 5)');
}
assert.equal(
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
'/history [count] Preview recent messages (default 3, maximum 5)',
);
assert.equal(t('发送 /history 查看最近对话。'), 'Send /history to preview recent conversation messages.');
});
test('/history English validation and empty-history messages never fall back to Chinese', async () => {
assert.equal((await run('/history', { isDirect: false })).message, 'Please use /history in a direct chat with the bot.');
for (const options of [{ hasImages: true }, { hasFiles: true }]) {
assert.equal((await run('/history', options)).message, '/history supports text commands only. Remove images or files and try again.');
}
assert.equal((await run('/history', { sessionId: null })).message, 'This chat has no bound Session. Send a message or use /session to bind one first.');
assert.equal((await run('/history')).message, 'This Session has no history messages available to preview yet.');
const result = await run('/history', {
events: [assistant(1, 1, []), end(2, 1)],
});
assert.match(result.message, /1\. Assistant\nThis message has no text to preview\./);
assert.match(result.message, /Messages available to preview in this Session: 1\./);
assert.doesNotMatch(result.message, /[一-鿿]/);
});
test('/history English errors preserve safe messages and hide raw details', async () => {
const cases = [
[{ code: 'session-not-found' }, 'The Session bound to this chat no longer exists. Please bind a Session again.'],
[{ code: 'workspace-session-stale' }, 'The Session, workspace, or bot state has changed. Please run /history again.'],
[{ code: 'workspace-bot-not-found' }, 'The Session, workspace, or bot state has changed. Please run /history again.'],
[{ code: 'session-binding-changed' }, 'The Session, workspace, or bot state has changed. Please run /history again.'],
[{ code: 'harness-api-not-found' }, 'This Harness does not support reading Session history.'],
[{ name: 'AbortError' }, 'History reading was cancelled.'],
[{ name: 'TimeoutError' }, 'Reading history timed out. Please try again later.'],
[{ code: 'harness-timeout' }, 'Reading history timed out. Please try again later.'],
[{}, 'Unable to read Session history right now. Please try again later.'],
];
for (const [properties, expected] of cases) {
const error = Object.assign(new Error('private upstream history details'), properties);
assert.equal((await run('/history', { error })).message, expected);
}
});