Merge branch 'upstream' into concedo_experimental

# Conflicts:
#	.github/workflows/build-webgpu.yml
#	CMakeLists.txt
#	common/CMakeLists.txt
#	docs/development/HOWTO-add-model.md
#	ggml/src/ggml-opencl/ggml-opencl.cpp
#	ggml/src/ggml-sycl/CMakeLists.txt
#	tests/test-arg-parser.cpp
#	tests/test-jinja.cpp
#	tests/test-llama-archs.cpp
#	tests/test-save-load-state.cpp
#	tools/cli/README.md
#	tools/completion/README.md
#	tools/llama-bench/llama-bench.cpp
#	tools/mtmd/CMakeLists.txt
#	tools/server/README.md
This commit is contained in:
Concedo
2026-07-27 22:28:38 +08:00
74 changed files with 2516 additions and 361 deletions
@@ -0,0 +1,68 @@
import { afterEach, describe, expect, it } from 'vitest';
import { DatabaseService } from '$lib/services/database.service';
import { MessageRole, MessageType } from '$lib/enums';
import type { ExportedConversation } from '$lib/types/database';
function makeSession(id: string): ExportedConversation {
return {
conv: { id, currNode: `${id}-msg`, lastModified: 0, name: `Chat ${id}` },
messages: [
{
id: `${id}-msg`,
convId: id,
type: MessageType.TEXT,
timestamp: 0,
role: MessageRole.USER,
content: `hello from ${id}`,
parent: null,
children: []
}
]
} as unknown as ExportedConversation;
}
afterEach(async () => {
const conversations = await DatabaseService.getAllConversations();
await DatabaseService.bulkDeleteConversations(conversations.map((conv) => conv.id));
});
/**
* An import leaves a conversation already in the database untouched, so the
* caller needs to know what was written to report it instead of echoing the
* selection back at the user.
*/
describe('DatabaseService.importConversations', () => {
it('reports the conversations it wrote', async () => {
const { imported, skipped } = await DatabaseService.importConversations([
makeSession('a'),
makeSession('b')
]);
expect(imported.map((conv) => conv.id)).toEqual(['a', 'b']);
expect(skipped).toEqual([]);
expect(await DatabaseService.getConversationMessages('a')).toHaveLength(1);
});
it('reports an existing conversation as skipped and leaves it untouched', async () => {
await DatabaseService.importConversations([makeSession('a')]);
await DatabaseService.updateConversation('a', { name: 'Renamed locally' });
const { imported, skipped } = await DatabaseService.importConversations([makeSession('a')]);
expect(imported).toEqual([]);
expect(skipped.map((conv) => conv.id)).toEqual(['a']);
expect((await DatabaseService.getConversation('a'))?.name).toBe('Renamed locally');
});
it('imports the new conversations of a partially known selection', async () => {
await DatabaseService.importConversations([makeSession('a')]);
const { imported, skipped } = await DatabaseService.importConversations([
makeSession('a'),
makeSession('b')
]);
expect(imported.map((conv) => conv.id)).toEqual(['b']);
expect(skipped.map((conv) => conv.id)).toEqual(['a']);
});
});
@@ -0,0 +1,112 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { zipSync, strToU8 } from 'fflate';
import { MessageRole, MessageType } from '$lib/enums';
import { NEWLINE } from '$lib/constants';
import type { ExportedConversation } from '$lib/types/database';
let conversationsStore: typeof import('$lib/stores/conversations.svelte').conversationsStore;
// node env unit project has no DOM, install a minimal localStorage backed by a
// Map before the store module reads it. Transforming the store takes seconds,
// so import it once for the whole file.
beforeAll(async () => {
const store = new Map<string, string>();
const polyfill: Storage = {
get length() {
return store.size;
},
clear: () => store.clear(),
getItem: (k) => (store.has(k) ? store.get(k)! : null),
key: (i) => Array.from(store.keys())[i] ?? null,
removeItem: (k) => {
store.delete(k);
},
setItem: (k, v) => {
store.set(k, String(v));
}
};
(globalThis as unknown as { localStorage: Storage }).localStorage = polyfill;
({ conversationsStore } = await import('$lib/stores/conversations.svelte'));
}, 30000);
function makeSession(id: string): ExportedConversation {
return {
conv: { id, currNode: `${id}-msg`, lastModified: 0, name: `Chat ${id}` },
messages: [
{
id: `${id}-msg`,
convId: id,
type: MessageType.TEXT,
timestamp: 0,
role: MessageRole.USER,
content: `hello from ${id}`,
parent: null,
children: []
}
]
} 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('conversationsStore.parseImportFile', () => {
it('imports a JSONL export whose name has no meaningful extension', async () => {
const jsonl = conversationsStore.serializeSessionToJsonl(makeSession('a'));
const sessions = await conversationsStore.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) => conversationsStore.serializeSessionToJsonl(session))
.join(NEWLINE);
const sessions = await conversationsStore.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(conversationsStore.serializeSessionToJsonl(makeSession('a'))),
'b.jsonl': strToU8(conversationsStore.serializeSessionToJsonl(makeSession('b'))),
'notes.txt': strToU8('ignored')
});
const sessions = await conversationsStore.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 conversationsStore.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 conversationsStore.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(
conversationsStore.parseImportFile(new File(['not an export'], 'export.jsonl'))
).rejects.toThrow();
});
});