2026-09-05 10:19:03 -06:00
|
|
|
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,
|
2026-09-05 20:13:35 +03:00
|
|
|
mergeSessionInputHistory,
|
2026-09-05 10:19:03 -06:00
|
|
|
} 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');
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-09-05 20:13:35 +03:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
});
|