89 lines
3.1 KiB
TypeScript
89 lines
3.1 KiB
TypeScript
import { NEWLINE } from '$lib/constants';
|
|
import { MessageRole, MessageType } from '$lib/enums';
|
|
import { ConversationTransferService } from '$lib/services/conversation-transfer.service';
|
|
import type { ExportedConversation } from '$lib/types/database';
|
|
import { strToU8, zipSync } from 'fflate';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
function makeSession(id: string): ExportedConversation {
|
|
return {
|
|
conv: { currNode: `${id}-msg`, id, lastModified: 0, name: `Chat ${id}` },
|
|
messages: [
|
|
{
|
|
children: [],
|
|
content: `hello from ${id}`,
|
|
convId: id,
|
|
id: `${id}-msg`,
|
|
parent: null,
|
|
role: MessageRole.USER,
|
|
timestamp: 0,
|
|
type: MessageType.TEXT
|
|
}
|
|
]
|
|
} as unknown as ExportedConversation;
|
|
}
|
|
|
|
/**
|
|
* `parseImportFile` detects the format from the file contents. iOS has no UTI
|
|
* for `.jsonl`, so the picker cannot filter on it and the filename carries no
|
|
* guarantee: a JSONL export must import under any name.
|
|
*/
|
|
describe('ConversationTransferService.parseImportFile', () => {
|
|
it('imports a JSONL export whose name has no meaningful extension', async () => {
|
|
const jsonl = ConversationTransferService.serializeSessionToJsonl(makeSession('a'));
|
|
const sessions = await ConversationTransferService.parseImportFile(new File([jsonl], 'export'));
|
|
|
|
expect(sessions).toHaveLength(1);
|
|
expect(sessions[0].conv.id).toBe('a');
|
|
expect(sessions[0].messages[0].content).toBe('hello from a');
|
|
});
|
|
|
|
it('imports several sessions from one JSONL file', async () => {
|
|
const jsonl = [makeSession('a'), makeSession('b')]
|
|
.map((session) => ConversationTransferService.serializeSessionToJsonl(session))
|
|
.join(NEWLINE);
|
|
const sessions = await ConversationTransferService.parseImportFile(
|
|
new File([jsonl], 'export.txt')
|
|
);
|
|
|
|
expect(sessions.map((session) => session.conv.id)).toEqual(['a', 'b']);
|
|
});
|
|
|
|
it('imports a ZIP archive whose name has no meaningful extension', async () => {
|
|
const zipped = zipSync({
|
|
'a.jsonl': strToU8(ConversationTransferService.serializeSessionToJsonl(makeSession('a'))),
|
|
'b.jsonl': strToU8(ConversationTransferService.serializeSessionToJsonl(makeSession('b'))),
|
|
'notes.txt': strToU8('ignored')
|
|
});
|
|
const sessions = await ConversationTransferService.parseImportFile(
|
|
new File([zipped], 'archive')
|
|
);
|
|
|
|
expect(sessions.map((session) => session.conv.id).sort()).toEqual(['a', 'b']);
|
|
});
|
|
|
|
it('imports the legacy JSON array format', async () => {
|
|
const json = JSON.stringify([makeSession('a')], null, 2);
|
|
const sessions = await ConversationTransferService.parseImportFile(
|
|
new File([json], 'export.jsonl')
|
|
);
|
|
|
|
expect(sessions).toHaveLength(1);
|
|
expect(sessions[0].conv.id).toBe('a');
|
|
});
|
|
|
|
it('imports the legacy JSON single object format', async () => {
|
|
const json = JSON.stringify(makeSession('a'));
|
|
const sessions = await ConversationTransferService.parseImportFile(new File([json], 'export'));
|
|
|
|
expect(sessions).toHaveLength(1);
|
|
expect(sessions[0].conv.id).toBe('a');
|
|
});
|
|
|
|
it('rejects a file that holds neither format', async () => {
|
|
await expect(
|
|
ConversationTransferService.parseImportFile(new File(['not an export'], 'export.jsonl'))
|
|
).rejects.toThrow();
|
|
});
|
|
});
|