Files
openchamber/packages/ui/src/components/chat/inputHistory.test.ts
T
Bohdan Triapitsyn f46fb718c5 fix(chat): recall the current session's prompts by default; tidy the six merged PRs
Input history (#3035) shipped with "All projects" as the default scope and
only recorded prompts sent after the upgrade, so ArrowUp showed other
sessions' prompts and, once switched to "Current session", nothing at all.
Default to the current session and merge the visible transcript's prompts
with the persisted bucket. Existing sessions recall as they did before
#3035, while new prompts keep their attachments and stay recallable after
a revert hides them from the transcript.

Cleanup across #1855, #2297, #3072, #3178, #3035 and #3135: drop the
duplicate poll guards in the file content poller, the zod schema the
VS Code package cannot depend on, a copied file-URL helper and stray
whitespace; move the Enter-to-send strings into the settings namespace;
document OPENCHAMBER_CHATS_DIR, resolve the chats root once on the server
and warm it alongside the other bootstrap calls.
2026-09-05 20:16:14 +03:00

175 lines
5.7 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import type { AttachedFile } from '@/stores/types/sessionTypes';
import { createInputHistorySubmission, type InputHistoryAttachment, type InputHistoryEntry } from '@/stores/useInputHistoryStore';
import {
buildChatInputHistorySubmissions,
buildInputHistoryNavigatorIdentity,
mapInputHistoryEntriesToValues,
mergeSessionInputHistory,
} from './inputHistory';
const ATTACHMENT: AttachedFile = {
id: 'file-1',
file: new File(['hello'], 'notes.txt', { type: 'text/plain' }),
dataUrl: 'file:///repo/notes.txt',
mimeType: 'text/plain',
filename: 'notes.txt',
size: 5,
source: 'local',
serverPath: '/repo/notes.txt',
};
describe('buildChatInputHistorySubmissions', () => {
test('keeps raw queued submissions first and raw composer submission last', () => {
const submissions = buildChatInputHistorySubmissions({
inputMode: 'normal',
queuedMessages: [
{ content: '/queued one', attachments: [ATTACHMENT] },
{ content: '/queued two', attachments: [] },
],
composerText: '/composer raw',
composerAttachments: [ATTACHMENT],
includeComposer: true,
});
expect(submissions?.map((submission) => submission.text)).toEqual([
'/queued one',
'/queued two',
'/composer raw',
]);
expect(submissions?.[0]).toEqual(createInputHistorySubmission('/queued one', [ATTACHMENT]));
expect(submissions?.[2]).toEqual(createInputHistorySubmission('/composer raw', [ATTACHMENT]));
});
test('omits history submissions for shell mode', () => {
expect(buildChatInputHistorySubmissions({
inputMode: 'shell',
queuedMessages: [{ content: 'echo hello', attachments: [ATTACHMENT] }],
composerText: 'pwd',
composerAttachments: [ATTACHMENT],
includeComposer: true,
})).toBe(undefined);
});
});
describe('mapInputHistoryEntriesToValues', () => {
test('keeps chronological order and materializes supported attachments', () => {
const entries: InputHistoryEntry[] = [
{
text: 'oldest',
attachmentKeys: ['a'],
restorableAttachments: [{
key: 'server-file',
source: 'file-url',
filename: 'server.txt',
mimeType: 'text/plain',
size: 11,
reference: '/repo/server.txt',
}],
submittedAt: 1,
},
{
text: 'newest',
attachmentKeys: ['b'],
restorableAttachments: [{
key: 'vscode-file',
source: 'vscode-file',
filename: 'editor.ts',
mimeType: 'text/plain',
size: 22,
reference: '/repo/editor.ts',
}],
submittedAt: 2,
},
];
const values = mapInputHistoryEntriesToValues(entries);
expect(values.map((value) => value.text)).toEqual(['oldest', 'newest']);
expect(values[0]?.attachments[0]?.filename).toBe('server.txt');
expect(values[0]?.attachments[0]?.dataUrl).toBe('/repo/server.txt');
expect(values[0]?.attachments[0]?.source).toBe('local');
expect(values[1]?.attachments[0]?.filename).toBe('editor.ts');
expect(values[1]?.attachments[0]?.vscodePath).toBe('/repo/editor.ts');
expect(values[1]?.attachments[0]?.vscodeSource).toBe('file');
expect(values[1]?.attachments[0]?.source).toBe('vscode');
});
test('drops unsupported attachment descriptors', () => {
const unsupported: InputHistoryAttachment = {
key: 'bad',
source: 'file-url',
filename: 'bad.txt',
mimeType: 'text/plain',
size: 1,
reference: 'data:text/plain;base64,Zm9v',
};
const entries: InputHistoryEntry[] = [{
text: 'value',
attachmentKeys: ['bad'],
restorableAttachments: [unsupported],
submittedAt: 1,
}];
expect(mapInputHistoryEntriesToValues(entries)[0]?.attachments).toEqual([]);
});
});
describe('buildInputHistoryNavigatorIdentity', () => {
test('includes scope and full identity so bucket changes reset navigation', () => {
expect(buildInputHistoryNavigatorIdentity('global', {
runtimeKey: 'runtime-a',
directory: '/repo',
sessionId: 'session-1',
})).toBe('global\nruntime-a\n/repo\nsession-1');
expect(buildInputHistoryNavigatorIdentity('session', {
runtimeKey: 'runtime-a',
directory: '/repo',
sessionId: 'session-1',
})).toBe('session\nruntime-a\n/repo\nsession-1');
});
});
describe('mergeSessionInputHistory', () => {
const entry = (text: string, submittedAtMs: number): InputHistoryEntry => ({
text,
attachmentKeys: [],
restorableAttachments: [],
submittedAt: submittedAtMs * 1000,
});
test('recalls transcript prompts when nothing is persisted yet', () => {
const values = mergeSessionInputHistory(
[{ text: 'first', createdAt: 10 }, { text: 'second', createdAt: 20 }],
[],
);
expect(values.map((value) => value.text)).toEqual(['first', 'second']);
expect(values[0]?.attachments).toEqual([]);
});
test('interleaves persisted entries by time and collapses duplicates onto the persisted entry', () => {
const persisted: InputHistoryEntry = {
...entry('second', 20),
restorableAttachments: [{
key: 'server-file',
source: 'file-url',
filename: 'server.txt',
mimeType: 'text/plain',
size: 11,
reference: '/repo/server.txt',
}],
};
const values = mergeSessionInputHistory(
[{ text: 'first', createdAt: 10 }, { text: 'second', createdAt: 20 }, { text: 'fourth', createdAt: 40 }],
[persisted, entry('reverted', 30)],
);
expect(values.map((value) => value.text)).toEqual(['first', 'second', 'reverted', 'fourth']);
expect(values[1]?.attachments[0]?.filename).toBe('server.txt');
});
});