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.
175 lines
5.7 KiB
TypeScript
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');
|
|
});
|
|
});
|