Inital commit

This commit is contained in:
2026-07-29 01:00:10 -05:00
commit 23e8ea90e4
3301 changed files with 1376308 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
# Agentic thread perf harness
Two tiers, both reusing the existing vitest projects (see `vite.config.ts`).
## Tier 1 - `agentic-stream.perf.svelte.test.ts` (project: `client`, real Chromium)
Mounts `ChatMessageAgenticContent` and replays a stream, replacing the message
object on each chunk exactly as the real pipeline does:
- `chat.svelte.ts` `updateStreamingUI()` runs per SSE chunk
- `conversations.svelte.ts` `updateMessageAtIndex` does `{ ...old, ...updates }`
That new object identity is the thing under test: it cascades through
`deriveAgenticSections` (which returns fresh `AgenticSection` objects) into every
tool-call block in the message, including completed ones.
```
npx vitest --project=client --run tests/client/agentic-stream.perf.svelte.test.ts
```
### Reading the output
- `mean` / `p95` / `max` - the synchronous window per token: prop write,
`await tick()`, then a forced `offsetHeight` read so style and layout are
included rather than deferred.
- `sync` - sum of those windows. This is the number to optimize.
- `wall` - the whole run including work `MarkdownContent` defers into its own
`requestAnimationFrame`. It carries a ~16.7ms/token idle floor because the
harness yields a frame each iteration, so compare `wall` **across fixtures**,
never against `sync`.
### The knobs, and what each one discriminates
The point of the harness is the _scaling curve_, not any single number.
| Knob | Reads on |
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
| `priorToolCalls` (0/1/5/20) | the reactive fan-out. Flat => no fan-out. Linear => confirmed. |
| `toolResultBytes` | whole-blob string scans (`extractSearchResults`, `parseToolResultWithImages`, `classifyToolResult`). |
| `editFileEdits` | `computeLineDiff`, the O(m\*n) LCS. |
| `openCodeFence` | `hljs.highlightAuto` on partial code. |
Deliberately no hard assertions: CI timing is noisy and the value here is the
before/after delta, not a gate.
### Caveat
This measures one message's subtree. In the real app `ChatMessages.svelte`
rebuilds its whole `displayMessages` list per token, so multiply by the number
of rendered messages to get the conversation-level cost.
## Tier 2 - `../unit/agentic-hotpath.bench.ts` (project: `unit`, node)
Per-call costs for the pure functions the curve implicates.
```
npx vitest bench --project=unit --run tests/unit/agentic-hotpath.bench.ts
```
@@ -0,0 +1,410 @@
// Tier 1 perf harness for the agentic thread.
//
// Drives ChatMessageAgenticContent the way the real streaming pipeline does:
// chat.svelte.ts -> conversations.svelte.ts:184 replaces the message object per
// SSE chunk (`{ ...old, ...updates }`), which changes prop identity and cascades
// through deriveAgenticSections into every tool-call block in the message.
//
// The fixture is parameterized so the *scaling curve* identifies the culprit -
// a single number would not. See tests/client/README-perf.md.
//
// Run: npx vitest --project=client --run tests/client/agentic-stream.perf.svelte.test.ts
import { describe, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { tick } from 'svelte';
import AgenticPerfWrapper from './components/AgenticPerfWrapper.svelte';
import ChatMessagesPerfWrapper from './components/ChatMessagesPerfWrapper.svelte';
import { perfState } from './components/agentic-perf-state.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import type { DatabaseMessage } from '$lib/types';
import { MessageRole } from '$lib/enums';
// --- fixture construction -------------------------------------------------
interface FixtureOpts {
/** Completed tool-call sections preceding the streaming text. */
priorToolCalls: number;
/** Size of each tool result blob. */
toolResultBytes: number;
/** Number of edit_file calls (each a 400x400-line diff). */
editFileEdits: number;
/** Leave an unclosed ``` fence at the end of the streamed content. */
openCodeFence: boolean;
/**
* Emit a blank line every N chunks so the content forms real markdown
* blocks. 0 = one unbroken paragraph, which defeats MarkdownContent's
* stable-block cache entirely (worst case). Typical prose has breaks.
*/
paragraphEvery: number;
}
const DEFAULTS: FixtureOpts = {
priorToolCalls: 0,
toolResultBytes: 1024,
editFileEdits: 0,
openCodeFence: false,
paragraphEvery: 0
};
function blob(bytes: number, seed: string): string {
const line = `${seed} output line with some representative width to it`;
const n = Math.max(1, Math.ceil(bytes / (line.length + 1)));
const out: string[] = [];
for (let i = 0; i < n; i++) out.push(`${line} ${i}`);
return out.join('\n');
}
function diffLines(n: number, seed: string): string {
const out: string[] = [];
for (let i = 0; i < n; i++) out.push(`${seed} line ${i} const value_${i} = compute(${i});`);
return out.join('\n');
}
let msgSeq = 0;
function baseMessage(overrides: Partial<DatabaseMessage>): DatabaseMessage {
return {
id: `m${msgSeq++}`,
convId: 'perf-conv',
type: 'text',
timestamp: 0,
role: MessageRole.ASSISTANT,
content: '',
parent: null,
children: [],
...overrides
} as DatabaseMessage;
}
function buildFixture(opts: FixtureOpts): {
message: DatabaseMessage;
toolMessages: DatabaseMessage[];
} {
const toolCalls: unknown[] = [];
const toolMessages: DatabaseMessage[] = [];
for (let i = 0; i < opts.priorToolCalls; i++) {
const id = `call_${i}`;
toolCalls.push({
id,
type: 'function',
function: {
name: 'exec_shell_command',
arguments: JSON.stringify({ command: `grep -rn "thing_${i}" src/` })
}
});
toolMessages.push(
baseMessage({
role: MessageRole.TOOL,
toolCallId: id,
content: `${blob(opts.toolResultBytes, `t${i}`)}\n[exit code: 0]`
})
);
}
for (let i = 0; i < opts.editFileEdits; i++) {
const id = `edit_${i}`;
toolCalls.push({
id,
type: 'function',
function: {
name: 'edit_file',
arguments: JSON.stringify({
path: `/src/file_${i}.ts`,
edits: [{ old_text: diffLines(400, 'old'), new_text: diffLines(400, 'new') }]
})
}
});
toolMessages.push(
baseMessage({
role: MessageRole.TOOL,
toolCallId: id,
content: JSON.stringify({ result: 'ok', edits_applied: 1 })
})
);
}
const message = baseMessage({
toolCalls: toolCalls.length > 0 ? JSON.stringify(toolCalls) : undefined,
content: ''
});
return { message, toolMessages };
}
// --- the driver -----------------------------------------------------------
interface Sample {
label: string;
tokens: number;
mean: number;
p95: number;
max: number;
/** Sum of the synchronous per-token windows. */
total: number;
/** Wall-clock for the whole run incl. deferred rAF work, then settle. */
wall: number;
}
function nextFrame(): Promise<void> {
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
}
const results: Sample[] = [];
/**
* Replays `tokens` streamed chunks, replacing the message object each time
* exactly as conversations.svelte.ts:184 does, flushing Svelte and forcing
* layout so the measurement includes render + style/layout, not just script.
*/
async function measure(label: string, partial: Partial<FixtureOpts>, tokens = 60) {
const opts = { ...DEFAULTS, ...partial };
const { message, toolMessages } = buildFixture(opts);
perfState.message = message;
perfState.toolMessages = toolMessages;
perfState.isStreaming = true;
// Every wrapper reads the same module state, so a leftover mount from a
// previous fixture would re-render on each mutation and fold its cost into
// this measurement. Tear down explicitly between fixtures.
const { unmount } = render(AgenticPerfWrapper);
await tick();
const CHUNK = 'The quick brown fox jumps over the lazy dog. ';
let accumulated = opts.openCodeFence ? '```notalanguage\n' : '';
const durations: number[] = [];
const wallStart = performance.now();
for (let i = 0; i < tokens; i++) {
accumulated += CHUNK;
if (opts.paragraphEvery > 0 && (i + 1) % opts.paragraphEvery === 0) {
accumulated += '\n\n';
}
const t0 = performance.now();
// Mirrors updateMessageAtIndex: a brand-new object identity per chunk.
perfState.message = { ...perfState.message!, content: accumulated };
await tick();
void document.body.offsetHeight; // force style + layout
durations.push(performance.now() - t0);
// MarkdownContent coalesces its parse into a rAF, so that work lands
// outside the window above. Yield a frame each iteration so it is
// captured in `wall` - the gap between `wall` and `total` is the
// deferred cost.
await nextFrame();
}
// Let any trailing coalesced work drain before stopping the clock.
await nextFrame();
await nextFrame();
const wall = performance.now() - wallStart;
await unmount();
durations.sort((a, b) => a - b);
const total = durations.reduce((a, b) => a + b, 0);
results.push({
label,
tokens,
mean: total / durations.length,
p95: durations[Math.floor(durations.length * 0.95)],
max: durations[durations.length - 1],
total,
wall
});
}
function report() {
const pad = (s: string, n: number) => s.padEnd(n);
const num = (n: number) => n.toFixed(2).padStart(8);
const header = `${pad('fixture', 40)}${pad('tok', 5)}${'mean'.padStart(8)}${'p95'.padStart(8)}${'max'.padStart(8)}${'sync'.padStart(9)}${'wall'.padStart(9)}`;
const lines = [
'',
'=== Tier 1: ms per streamed token (agentic content subtree) ===',
'mean/p95/max = synchronous window per token (script + style + layout).',
'sync = sum of those windows.',
'wall = whole run, incl. work MarkdownContent defers into its own rAF.',
' NOTE: wall carries a ~16.7ms/token idle floor from the harness',
' yielding a frame each iteration (60 tokens => ~1000ms floor).',
' Compare wall ACROSS fixtures / against the baseline row,',
' never against sync.',
'',
header,
'-'.repeat(header.length)
];
for (const r of results) {
lines.push(
`${pad(r.label, 40)}${pad(String(r.tokens), 5)}${num(r.mean)}${num(r.p95)}${num(r.max)}${num(r.total)}${num(r.wall)}`
);
}
lines.push('');
console.log(lines.join('\n'));
}
// --- conversation-level driver --------------------------------------------
// The per-message driver above cannot see the fan-out in ChatMessages: a single
// token mutation invalidates `displayMessages`, which rebuilds a fresh
// toolMessages array for EVERY message in the conversation. Drive the real
// store through the real list component to measure that.
async function measureConversation(
label: string,
priorMessages: number,
tokens = 60,
/**
* Give each prior assistant turn a resolved tool call, so the fixture pays
* `hasAgenticContent`'s JSON.parse and the tool-message grouping walk that a
* real agent thread would - plain prose messages skip both.
*/
agentic = false
) {
const history: DatabaseMessage[] = [];
for (let i = 0; i < priorMessages; i++) {
const isAssistant = i % 2 !== 0;
if (isAssistant && agentic) {
const id = `prior_call_${i}`;
history.push(
baseMessage({
role: MessageRole.ASSISTANT,
content: `Message ${i}`,
toolCalls: JSON.stringify([
{
id,
type: 'function',
function: {
name: 'exec_shell_command',
arguments: JSON.stringify({ command: `grep -rn "thing_${i}" src/` })
}
}
])
})
);
history.push(
baseMessage({
role: MessageRole.TOOL,
toolCallId: id,
content: `${blob(1024, `r${i}`)}\n[exit code: 0]`
})
);
continue;
}
history.push(
baseMessage({
role: isAssistant ? MessageRole.ASSISTANT : MessageRole.USER,
content: `Message ${i}: ${blob(512, `m${i}`)}`
})
);
}
const streaming = baseMessage({ role: MessageRole.ASSISTANT, content: '' });
history.push(streaming);
conversationsStore.activeMessages = history;
const { unmount } = render(ChatMessagesPerfWrapper);
await tick();
const idx = conversationsStore.findMessageIndex(streaming.id);
const CHUNK = 'The quick brown fox jumps over the lazy dog. ';
let accumulated = '';
const durations: number[] = [];
const wallStart = performance.now();
for (let i = 0; i < tokens; i++) {
accumulated += CHUNK;
const t0 = performance.now();
// The real path: chat.svelte.ts -> conversations.svelte.ts.
conversationsStore.updateMessageAtIndex(idx, { content: accumulated });
await tick();
void document.body.offsetHeight;
durations.push(performance.now() - t0);
await nextFrame();
}
await nextFrame();
await nextFrame();
const wall = performance.now() - wallStart;
await unmount();
conversationsStore.activeMessages = [];
durations.sort((a, b) => a - b);
const total = durations.reduce((a, b) => a + b, 0);
results.push({
label,
tokens,
mean: total / durations.length,
p95: durations[Math.floor(durations.length * 0.95)],
max: durations[durations.length - 1],
total,
wall
});
}
// --- the matrix -----------------------------------------------------------
// Sequential, in one test, so the table prints together and the samples do not
// interleave with other suites competing for the main thread.
describe('agentic streaming perf', () => {
it('scales', { timeout: 600_000 }, async () => {
// Baseline: plain text streaming, nothing agentic.
await measure('baseline: no tool calls', {});
// Knob: priorToolCalls. Flat => no fan-out. Linear => fan-out confirmed.
await measure('priorToolCalls=1 (1KB results)', { priorToolCalls: 1 });
await measure('priorToolCalls=5 (1KB results)', { priorToolCalls: 5 });
await measure('priorToolCalls=20 (1KB results)', { priorToolCalls: 20 });
// Knob: toolResultBytes, at fixed section count.
await measure('5 calls x 200KB results', {
priorToolCalls: 5,
toolResultBytes: 200 * 1024
});
// Knob: editFileEdits (computeLineDiff, 400x400 LCS).
await measure('3 edit_file calls (400x400 diff)', { editFileEdits: 3 });
// Knob: openCodeFence (hljs highlightAuto on partial code).
await measure('open code fence, unknown language', { openCodeFence: true });
// Conversation level: does streaming one message cost more as the
// conversation grows? Flat => no fan-out. Linear => confirmed.
await measureConversation('convo: 1 prior message', 1);
await measureConversation('convo: 10 prior messages', 10);
await measureConversation('convo: 40 prior messages', 40);
// Same, but each prior assistant turn carries a resolved tool call.
await measureConversation('convo: 10 prior, agentic', 10, 60, true);
await measureConversation('convo: 40 prior, agentic', 40, 60, true);
// Message length: MarkdownContent re-parses the whole accumulated string
// each frame, so per-token cost should climb as the response grows.
// A rising mean across these three rows means O(n^2) over the stream.
// One unbroken paragraph: worst case, stable-block cache never applies.
await measure('len 60tok 1-para (worst case)', {}, 60);
await measure('len 250tok 1-para (worst case)', {}, 250);
await measure('len 600tok 1-para (worst case)', {}, 600);
// Same lengths, broken into paragraphs every 8 chunks: the typical shape,
// where only the trailing paragraph should be unstable.
await measure('len 60tok paras (typical)', { paragraphEvery: 8 }, 60);
await measure('len 250tok paras (typical)', { paragraphEvery: 8 }, 250);
await measure('len 600tok paras (typical)', { paragraphEvery: 8 }, 600);
report();
});
});
@@ -0,0 +1,41 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { validateApiKey } from '$lib/utils/api-key-validation';
import { settingsStore } from '$lib/stores/settings.svelte';
import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
function fakeFetch(status: number, capture: { auth?: string | null } = {}) {
return (async (_url: RequestInfo | URL, init?: RequestInit) => {
capture.auth = (init?.headers as Record<string, string>)?.['Authorization'] ?? null;
return new Response(status === 200 ? '{}' : 'Unauthorized', { status });
}) as typeof globalThis.fetch;
}
const is401 = (err: unknown) =>
typeof err === 'object' && err !== null && 'status' in err && err.status === 401;
describe('api key validation surfaces the splash', () => {
beforeEach(() => {
localStorage.removeItem(CONFIG_LOCALSTORAGE_KEY);
settingsStore.initialize();
});
it('keyed server, no stored key: throws 401 so the splash shows (onboarding)', async () => {
await expect(validateApiKey(fakeFetch(401))).rejects.toSatisfy(is401);
});
it('keyed server, wrong stored key: throws 401 so the splash shows', async () => {
settingsStore.updateConfig('apiKey', 'wrong-key');
await expect(validateApiKey(fakeFetch(401))).rejects.toSatisfy(is401);
});
it('open server, no stored key: passes silently', async () => {
await expect(validateApiKey(fakeFetch(200))).resolves.toBeUndefined();
});
it('valid stored key: passes and sends the bearer header', async () => {
settingsStore.updateConfig('apiKey', 'sk-good');
const capture: { auth?: string | null } = {};
await expect(validateApiKey(fakeFetch(200, capture))).resolves.toBeUndefined();
expect(capture.auth).toBe('Bearer sk-good');
});
});
@@ -0,0 +1,121 @@
import { describe, expect, it } from 'vitest';
import { capImageDataURLSize } from '$lib/utils/cap-img-size';
import { getJpegOrientationFromDataURL } from '$lib/utils/jpeg-orientation';
// Real 64x32 jpegs generated with Pillow, quality 90. The upright picture is
// four solid quadrants: top left red, top right green, bottom left blue,
// bottom right yellow. For each exif value the stored pixels are inverse
// transposed so a conforming decoder shows the upright picture, exactly like
// a rotated smartphone photo.
const EXIF1 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAAgAEADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD50ooor8MP9UwooooA9uooor4I/wCcwKKKKAPhSiiiv+gM/qgKKKKAP3Vooor/AJdz+lQooooA/9k=`;
const EXIF3 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAMAAAAAAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAAgAEADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD7Nooor/Mo/XQooooA/Cqiiiv+og/moKKKKAPuuiiiv+fw/lcKKKKAPEaKKK+9P+jMKKKKAP/Z`;
const EXIF5 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAUAAAAAAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCABAACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD50ooor8MP9UzwKiiiv9xj/HQ99ooor/Dk/wBizwKiiiv9xj/HQ+66KKK/5/D+Vz7qooor+ej/AE9PhWiiiv6FP8wj7qooor+ej/T0/9k=`;
const EXIF6 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCABAACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDCooor+Tz+Hz7qooor+ej/AE9PhWiiiv6FP8wj7qooor+ej/T0/Keiiiv7CP72PAqKKK/3GP8AHQ99ooor/Dk/2LPAqKKK/wBxj/HQ/9k=`;
const EXIF8 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAgAAAAAAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCABAACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD896KKK/1TPhz32iiiv8OT/Ys8Cooor/cY/wAdD32iiiv8OT/Ys/Viiiiv49P4JPhWiiiv6FP8wj7qooor+ej/AE9PhWiiiv6FP8wj/9k=`;
const NOEXIF = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAAgAEADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD50ooor8MP9UwooooA9uooor4I/wCcwKKKKAPhSiiiv+gM/qgKKKKAP3Vooor/AJdz+lQooooA/9k=`;
const RED: Rgb = [255, 0, 0];
const GREEN: Rgb = [0, 200, 0];
const BLUE: Rgb = [0, 0, 255];
const YELLOW: Rgb = [255, 220, 0];
// Wide tolerance per channel, jpeg compression shifts solid colors a bit
const COLOR_TOLERANCE = 70;
// 0.000512 megapixels is 512 pixels, a quarter of the area of the 2048 pixel fixtures
const QUARTER_AREA_MEGAPIXELS = 0.000512;
type Rgb = [number, number, number];
function loadImage(dataUrl: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Failed to decode image.'));
img.src = dataUrl;
});
}
// Decodes a data URL and samples the center of each quadrant of the picture
async function quadrantColors(dataUrl: string): Promise<Rgb[]> {
const img = await loadImage(dataUrl);
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
const points = [
[0.25, 0.25],
[0.75, 0.25],
[0.25, 0.75],
[0.75, 0.75]
];
return points.map(([fx, fy]) => {
const d = ctx.getImageData(
Math.floor(canvas.width * fx),
Math.floor(canvas.height * fy),
1,
1
).data;
return [d[0], d[1], d[2]];
});
}
function expectUpright(colors: Rgb[]) {
const targets = [RED, GREEN, BLUE, YELLOW];
for (let i = 0; i < 4; i++) {
for (let c = 0; c < 3; c++) {
expect(Math.abs(colors[i][c] - targets[i][c])).toBeLessThan(COLOR_TOLERANCE);
}
}
}
describe('capImageDataURLSize orientation and capping', () => {
it('passes upright jpegs through untouched when capping is disabled', async () => {
expect(await capImageDataURLSize(EXIF1, 0)).toBe(EXIF1);
expect(await capImageDataURLSize(NOEXIF, 0)).toBe(NOEXIF);
});
it('passes upright jpegs through untouched when under the cap threshold', async () => {
expect(await capImageDataURLSize(EXIF1, 1)).toBe(EXIF1);
});
it.each([
['orientation 3', EXIF3],
['orientation 5', EXIF5],
['orientation 6', EXIF6],
['orientation 8', EXIF8]
])('bakes %s into upright pixels without capping', async (_label, fixture) => {
const result = await capImageDataURLSize(fixture, 0);
expect(result).not.toBe(fixture);
const img = await loadImage(result);
expect(img.naturalWidth).toBe(64);
expect(img.naturalHeight).toBe(32);
expectUpright(await quadrantColors(result));
// The re-encoded jpeg carries no orientation tag anymore
expect(getJpegOrientationFromDataURL(result)).toBe(1);
});
it('caps and bakes the orientation in a single output', async () => {
const result = await capImageDataURLSize(EXIF6, QUARTER_AREA_MEGAPIXELS);
const img = await loadImage(result);
expect(img.naturalWidth).toBe(32);
expect(img.naturalHeight).toBe(16);
expectUpright(await quadrantColors(result));
expect(getJpegOrientationFromDataURL(result)).toBe(1);
});
it('caps upright jpegs without disturbing the picture', async () => {
const result = await capImageDataURLSize(EXIF1, QUARTER_AREA_MEGAPIXELS);
const img = await loadImage(result);
expect(img.naturalWidth).toBe(32);
expect(img.naturalHeight).toBe(16);
expectUpright(await quadrantColors(result));
});
});
@@ -0,0 +1,37 @@
// Guards the lazy-body behaviour of the shared collapsible wrappers.
//
// bits-ui's Collapsible.Content renders its children unconditionally and only
// sets `hidden`, so before this was gated a collapsed tool result kept its whole
// body in the DOM and re-rendered it on every streamed token (measured at
// ~41ms/section/token for a 200KB result). These tests pin the fix: closed means
// not rendered, and opening still mounts the body.
import { describe, it, expect } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { tick } from 'svelte';
import CollapsibleLazyBodyHarness from './components/CollapsibleLazyBodyHarness.svelte';
const MARKER = 'collapsible-body-marker';
describe('collapsible wrappers render their body lazily', () => {
for (const variant of ['content', 'terminal'] as const) {
it(`${variant}: body is absent while closed and present once open`, async () => {
const screen = render(CollapsibleLazyBodyHarness, { variant, open: false });
await tick();
expect(document.body.textContent).not.toContain(MARKER);
await screen.rerender({ variant, open: true });
await tick();
expect(document.body.textContent).toContain(MARKER);
// And it unmounts again on close, so a collapsed block stops costing
// anything during streaming.
await screen.rerender({ variant, open: false });
await tick();
expect(document.body.textContent).not.toContain(MARKER);
});
}
});
@@ -0,0 +1,16 @@
<script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip';
import ChatMessageAgenticContent from '$lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte';
import { perfState } from './agentic-perf-state.svelte';
</script>
<Tooltip.Provider>
{#if perfState.message}
<ChatMessageAgenticContent
message={perfState.message}
toolMessages={perfState.toolMessages}
isStreaming={perfState.isStreaming}
isLastAssistantMessage
/>
{/if}
</Tooltip.Provider>
@@ -0,0 +1,12 @@
<script lang="ts">
// Mounts the real ChatMessages list against the real conversations store, so
// the harness exercises `displayMessages` (which rebuilds every message's
// toolMessages array) rather than a single message subtree.
import * as Tooltip from '$lib/components/ui/tooltip';
import ChatMessages from '$lib/components/app/chat/ChatMessages/ChatMessages.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
</script>
<Tooltip.Provider>
<ChatMessages messages={conversationsStore.activeMessages} />
</Tooltip.Provider>
@@ -0,0 +1,21 @@
<script lang="ts">
import CollapsibleContentBlock from '$lib/components/app/content/CollapsibleContentBlock.svelte';
import CollapsibleTerminalBlock from '$lib/components/app/content/CollapsibleTerminalBlock.svelte';
interface Props {
variant: 'content' | 'terminal';
open: boolean;
}
let { variant, open }: Props = $props();
</script>
{#if variant === 'content'}
<CollapsibleContentBlock {open} title="Block">
<span>collapsible-body-marker</span>
</CollapsibleContentBlock>
{:else}
<CollapsibleTerminalBlock {open} title="Block">
<span>collapsible-body-marker</span>
</CollapsibleTerminalBlock>
{/if}
@@ -0,0 +1,37 @@
<script lang="ts">
import { untrack } from 'svelte';
import McpServerForm from '$lib/components/app/mcp/McpServerForm.svelte';
interface Props {
headers?: string;
}
let { headers = '' }: Props = $props();
let headersState = $state(untrack(() => headers));
let lastCapturedHeaders = $state(untrack(() => headers));
$effect(() => {
if (headers !== lastCapturedHeaders) {
headersState = headers;
lastCapturedHeaders = headers;
}
});
</script>
<!--
Drives McpServerForm with a controlled `headers` string and exposes the
latest captured value through `data-captured-headers` so the client test
can read it back without a custom binding API.
-->
<McpServerForm
url="https://example.test/mcp"
headers={headersState}
onUrlChange={() => {}}
onHeadersChange={(value) => {
headersState = value;
}}
id="mcp-server-form-test"
/>
<div data-testid="captured-headers" data-captured-headers={headersState} hidden></div>
@@ -0,0 +1,12 @@
<script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip';
import Page from '../../../src/routes/(chat)/+page.svelte';
</script>
<!--
Test wrapper that provides necessary context providers for component testing.
This mirrors the providers from +layout.svelte.
-->
<Tooltip.Provider>
<Page />
</Tooltip.Provider>
@@ -0,0 +1,17 @@
// Shared reactive fixture state for the Tier 1 perf harness.
//
// The wrapper component reads this module directly rather than taking props,
// so the driver can mutate it without depending on how the test renderer
// forwards props.
import type { DatabaseMessage } from '$lib/types';
export const perfState = $state<{
message: DatabaseMessage | null;
toolMessages: DatabaseMessage[];
isStreaming: boolean;
}>({
message: null,
toolMessages: [],
isStreaming: true
});
@@ -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,73 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { McpServerForm } from '$lib/components/app/mcp';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
describe('mcp server display name', () => {
beforeEach(() => {
settingsStore.updateConfig('mcpServers', '[]');
});
it('custom display name wins over the url fallback', () => {
const server = mcpStore.addServer({
enabled: false,
url: 'https://mcp.example.com/a',
displayName: 'My Tools'
});
expect(mcpStore.getServerLabel(server)).toBe('My Tools');
});
it('without a custom name the url is the label', () => {
const server = mcpStore.addServer({ enabled: false, url: 'https://mcp.example.com/a' });
expect(mcpStore.getServerLabel(server)).toBe('https://mcp.example.com/a');
});
it('identical labels get positional suffixes', () => {
const a = mcpStore.addServer({
enabled: false,
url: 'https://mcp.example.com/a',
displayName: 'GitHub'
});
const b = mcpStore.addServer({
enabled: false,
url: 'https://mcp.example.com/b',
displayName: 'GitHub'
});
expect(mcpStore.getServerLabel(a)).toBe('GitHub (1)');
expect(mcpStore.getServerLabel(b)).toBe('GitHub (2)');
});
it('renaming one twin dissolves the suffixes', () => {
const a = mcpStore.addServer({
enabled: false,
url: 'https://mcp.example.com/a',
displayName: 'GitHub'
});
const b = mcpStore.addServer({
enabled: false,
url: 'https://mcp.example.com/b',
displayName: 'GitHub'
});
mcpStore.updateServer(b.id, { displayName: 'GitHub Work' });
expect(mcpStore.getServerLabel(a)).toBe('GitHub');
expect(mcpStore.getServerLabel(mcpStore.getServerById(b.id)!)).toBe('GitHub Work');
});
it('the form exposes an editable display name field', async () => {
let captured = '';
const screen = await render(McpServerForm, {
url: 'https://mcp.example.com/a',
headers: '',
name: '',
onUrlChange: () => {},
onHeadersChange: () => {},
onNameChange: (v: string) => (captured = v)
});
const input = screen.getByLabelText('Display name');
await expect.element(input).toBeVisible();
await input.fill('My Custom Server');
expect(captured).toBe('My Custom Server');
});
});
@@ -0,0 +1,133 @@
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import McpServerFormWrapper from './components/McpServerFormWrapper.svelte';
const AUTHORIZATION_HEADER = 'Authorization';
const BEARER_PREFIX = 'Bearer ';
const BEARER_PLACEHOLDER = 'Paste token here';
/**
* Client-side tests for the McpServerForm bearer UI.
*
* The dedicated UI only "owns" Authorization headers that already carry a
* Bearer scheme (heuristic check on the value). Other Authorization values
* stay in the KV section so the user can still edit them verbatim. Storage
* always goes through the same custom-headers slot, so a round-trip via this
* UI produces exactly one `Authorization: Bearer <token>` entry.
*
* Equivalent parser coverage lives in `tests/unit/headers.test.ts`.
*/
describe('McpServerForm - Authorization / bearer UI', () => {
function bearerInput(screen: Awaited<ReturnType<typeof render>>) {
return screen.locator.getByPlaceholder(BEARER_PLACEHOLDER);
}
function capturedHeaders(screen: Awaited<ReturnType<typeof render>>) {
return screen.getByTestId('captured-headers');
}
it('mounts with the bearer input hidden when no auth header is present', async () => {
const screen = await render(McpServerFormWrapper, { headers: '' });
await expect.element(screen.getByRole('textbox', { name: /server url/i })).toBeVisible();
await expect.element(bearerInput(screen)).not.toBeInTheDocument();
});
it('toggling Authorization shows the bearer input', async () => {
const screen = await render(McpServerFormWrapper, { headers: '' });
await screen.getByRole('switch', { name: /authorization/i }).click();
await expect.element(bearerInput(screen)).toBeVisible();
});
it('typing a token writes the Authorization row with the Bearer prefix prepended', async () => {
const screen = await render(McpServerFormWrapper, { headers: '' });
await screen.getByRole('switch', { name: /authorization/i }).click();
const token = 'super-secret';
await bearerInput(screen).fill(token);
const expected = JSON.stringify({ [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${token}` });
await expect
.element(capturedHeaders(screen))
.toHaveAttribute('data-captured-headers', expected);
});
it('pre-existing Bearer header pre-fills the bearer input with the token stripped', async () => {
const existing = JSON.stringify({
'X-Trace-Id': 'abc',
[AUTHORIZATION_HEADER]: `${BEARER_PREFIX}preexisting`
});
const screen = await render(McpServerFormWrapper, { headers: existing });
await expect.element(bearerInput(screen)).toBeVisible();
await expect.element(bearerInput(screen)).toHaveValue('preexisting');
});
it('non-Bearer Authorization is ignored by the dedicated UI and stays in the KV section', async () => {
const existing = JSON.stringify({ [AUTHORIZATION_HEADER]: 'Basic czNjcjpwYXNz' });
const screen = await render(McpServerFormWrapper, { headers: existing });
await expect.element(bearerInput(screen)).not.toBeInTheDocument();
const headerKeyInput = screen.getByPlaceholder('Header name');
await expect.element(headerKeyInput).toBeVisible();
});
it('engaging the token UI replaces a non-Bearer Authorization with the Bearer scheme', async () => {
const existing = JSON.stringify({ [AUTHORIZATION_HEADER]: 'Basic old' });
const screen = await render(McpServerFormWrapper, { headers: existing });
await screen.getByRole('switch', { name: /authorization/i }).click();
await bearerInput(screen).fill('new');
const expected = JSON.stringify({ [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}new` });
await expect
.element(capturedHeaders(screen))
.toHaveAttribute('data-captured-headers', expected);
});
it('toggling Authorization off with no token drops the Bearer row but keeps non-Bearer schemes', async () => {
const existing = JSON.stringify({ [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}xyz` });
const screen = await render(McpServerFormWrapper, { headers: existing });
await screen.getByRole('switch', { name: /authorization/i }).click();
await expect.element(capturedHeaders(screen)).toHaveAttribute('data-captured-headers', '');
});
it('toggling Authorization off when no Bearer row is present leaves headers untouched', async () => {
const existing = JSON.stringify({ [AUTHORIZATION_HEADER]: 'Basic czNjcjpwYXNz' });
const screen = await render(McpServerFormWrapper, { headers: existing });
await screen.getByRole('switch', { name: /authorization/i }).click();
await screen.getByRole('switch', { name: /authorization/i }).click();
await expect
.element(capturedHeaders(screen))
.toHaveAttribute('data-captured-headers', existing);
});
it('clearing the bearer input drops the Authorization row', async () => {
const existing = JSON.stringify({ [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}xyz` });
const screen = await render(McpServerFormWrapper, { headers: existing });
await bearerInput(screen).fill('');
await expect.element(capturedHeaders(screen)).toHaveAttribute('data-captured-headers', '');
});
it('does not surface Bearer Authorization in the KV section even when pre-existing', async () => {
const existing = JSON.stringify({ [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}xyz` });
const screen = await render(McpServerFormWrapper, { headers: existing });
const headerKeyInput = screen.getByPlaceholder('Header name');
await expect.element(headerKeyInput).not.toBeInTheDocument();
});
});
+12
View File
@@ -0,0 +1,12 @@
import { describe, it, expect } from 'vitest';
import { render } from 'vitest-browser-svelte';
import TestWrapper from './components/TestWrapper.svelte';
describe('/+page.svelte', () => {
it('should render page without throwing', async () => {
// Basic smoke test - page should render without throwing errors.
// API calls are mocked in vitest-setup-client.ts.
await render(TestWrapper);
expect(true).toBe(true);
});
});
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { SandboxService } from '$lib/services/sandbox.service';
import { SANDBOX_TOOL_NAME } from '$lib/constants';
const run = (code: string, timeoutMs?: number) =>
SandboxService.executeTool(SANDBOX_TOOL_NAME, {
code,
...(timeoutMs !== undefined ? { timeout_ms: timeoutMs } : {})
});
describe('sandbox service', () => {
beforeEach(async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.config = {
...settingsStore.config,
symbolicMathEnabled: true
};
});
it('executes plain JavaScript', async () => {
const reply = await run('return 1 + 1;');
expect(reply.isError).toBe(false);
expect(reply.content).toContain('=> 2');
});
it('exposes nerdamer for symbolic computation', async () => {
const reply = await run("return nerdamer.diff('sin(x)/x', 'x').toString();");
expect(reply.isError).toBe(false);
expect(reply.content).toContain('cos(x)');
});
it('computes exact rational arithmetic', async () => {
const reply = await run("return nerdamer('1/3 + 1/6').toString();");
expect(reply.isError).toBe(false);
expect(reply.content).toContain('=> 1/2');
});
it('proves a polynomial identity symbolically', async () => {
const reply = await run(
"return nerdamer('expand((1+x*y)^3 - (1 + 3*x*y + 3*x^2*y^2 + x^3*y^3))').toString();"
);
expect(reply.isError).toBe(false);
expect(reply.content).toContain('=> 0');
});
it('blocks network egress in the worker via CSP', async () => {
const reply = await run(
"try { await fetch('https://example.com/'); return 'leaked'; } catch { return 'blocked'; }"
);
expect(reply.isError).toBe(false);
expect(reply.content).toContain('=> blocked');
});
it('enforces the timeout on runaway code', async () => {
const reply = await run('while (true) {}', 500);
expect(reply.isError).toBe(true);
expect(reply.content).toContain('timed out');
});
});
@@ -0,0 +1,107 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { settingsStore, config } from '$lib/stores/settings.svelte';
import { serverStore } from '$lib/stores/server.svelte';
import { ParameterSyncService } from '$lib/services/parameter-sync.service';
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-registry';
import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
import type { SettingsConfigType } from '$lib/types';
type Primitive = string | number | boolean;
const KEYS = Object.keys(SETTING_CONFIG_DEFAULT).filter(
(k) => ['string', 'number', 'boolean'].includes(typeof SETTING_CONFIG_DEFAULT[k]) && k !== 'theme'
);
function divergent(key: string, base: Primitive): Primitive {
if (typeof base === 'boolean') return !base;
if (typeof base === 'number') return base + 7;
return `user-${key}`;
}
function baselineFor(key: string, base: Primitive): Primitive {
if (typeof base === 'boolean') return !base;
if (typeof base === 'number') return base + 42;
return `admin-${key}`;
}
function mockProps(uiSettings: Record<string, Primitive>) {
Object.defineProperty(serverStore, 'props', {
configurable: true,
get: () =>
({
default_generation_settings: { params: { temperature: 0.8 } },
ui_settings: uiSettings
}) as unknown as typeof serverStore.props
});
}
const setUser = (key: string, value: Primitive) =>
settingsStore.updateConfig(key as keyof SettingsConfigType, value as never);
const current = (key: string) => (config() as Record<string, unknown>)[key];
describe('registry-wide invariants', () => {
beforeEach(() => {
localStorage.removeItem(CONFIG_LOCALSTORAGE_KEY);
});
it('I1: no load ever modifies a stored user value, for any key of any type', () => {
settingsStore.initialize();
const userValues: Record<string, Primitive> = {};
for (const key of KEYS) {
userValues[key] = divergent(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
setUser(key, userValues[key]);
}
// simulated F5 + adverse admin baseline on every key, synced twice
settingsStore.initialize();
const adverse: Record<string, Primitive> = {};
for (const key of KEYS)
adverse[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
mockProps(adverse);
settingsStore.syncWithServerDefaults();
settingsStore.syncWithServerDefaults();
for (const key of KEYS) {
expect(current(key), key).toBe(userValues[key]);
}
});
it('first visit: the baseline applies for every key, false and 0 included', () => {
settingsStore.initialize();
const baseline: Record<string, Primitive> = {};
for (const key of KEYS)
baseline[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
mockProps(baseline);
settingsStore.syncWithServerDefaults();
for (const key of KEYS) {
if (ParameterSyncService.canSyncParameter(key)) continue;
expect(current(key), key).toBe(baseline[key]);
}
});
it('I3: Reset returns every key to baseline when defined, factory default otherwise', () => {
settingsStore.initialize();
for (const key of KEYS) setUser(key, divergent(key, SETTING_CONFIG_DEFAULT[key] as Primitive));
const baseline: Record<string, Primitive> = {};
KEYS.filter((_, i) => i % 2 === 0).forEach((key) => {
baseline[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
});
mockProps(baseline);
settingsStore.forceSyncWithServerDefaults();
for (const key of KEYS) {
if (key in baseline) {
expect(current(key), key).toBe(baseline[key]);
} else if (ParameterSyncService.canSyncParameter(key)) {
expect(current(key), key).toBe('');
} else {
expect(current(key), key).toBe(SETTING_CONFIG_DEFAULT[key]);
}
}
});
});
@@ -0,0 +1,73 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { settingsStore, config } from '$lib/stores/settings.svelte';
import { serverStore } from '$lib/stores/server.svelte';
import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
function mockProps(uiSettings: Record<string, string | number | boolean>) {
Object.defineProperty(serverStore, 'props', {
configurable: true,
get: () =>
({
default_generation_settings: { params: { temperature: 0.8 } },
ui_settings: uiSettings
}) as unknown as typeof serverStore.props
});
}
describe('server ui_settings application semantics', () => {
beforeEach(() => {
localStorage.removeItem(CONFIG_LOCALSTORAGE_KEY);
});
it('applies the admin defaults once for a new user', () => {
settingsStore.initialize();
mockProps({ theme: 'dark', apiKey: '' });
settingsStore.syncWithServerDefaults();
expect(config().theme).toBe('dark');
});
it('never reapplies on later loads: the user config diverges freely', () => {
settingsStore.initialize();
settingsStore.updateConfig('theme', 'light');
settingsStore.updateConfig('apiKey', 'sk-user-key');
// simulated F5: config now exists in localStorage
settingsStore.initialize();
mockProps({ theme: 'dark', apiKey: '' });
settingsStore.syncWithServerDefaults();
settingsStore.syncWithServerDefaults();
expect(config().theme).toBe('light');
expect(config().apiKey).toBe('sk-user-key');
const stored = JSON.parse(localStorage.getItem(CONFIG_LOCALSTORAGE_KEY) ?? '{}');
expect(stored.apiKey).toBe('sk-user-key');
});
it('Reset to Default reapplies the full baseline, api key included', () => {
settingsStore.initialize();
settingsStore.updateConfig('theme', 'light');
settingsStore.updateConfig('apiKey', 'sk-user-key');
mockProps({ theme: 'dark', apiKey: '' });
settingsStore.forceSyncWithServerDefaults();
expect(config().theme).toBe('dark');
expect(config().apiKey).toBe('');
});
});
describe('syncable scope (spec section 4)', () => {
it('sampling params keep their live server twin, ui settings carry none', async () => {
const { ParameterSyncService } = await import('$lib/services/parameter-sync.service');
expect(ParameterSyncService.canSyncParameter('temperature')).toBe(true);
expect(ParameterSyncService.canSyncParameter('samplers')).toBe(true);
expect(ParameterSyncService.canSyncParameter('theme')).toBe(false);
expect(ParameterSyncService.canSyncParameter('systemMessage')).toBe(false);
expect(ParameterSyncService.canSyncParameter('apiKey')).toBe(false);
expect(ParameterSyncService.canSyncParameter('customCss')).toBe(false);
});
});
@@ -0,0 +1,64 @@
// Pins the contract that makes streaming cheap: updateMessageAtIndex mutates the
// existing message object instead of replacing it.
//
// Replacing it (`{ ...old, ...updates }`) changes the array slot, which
// invalidates every consumer that merely walks the list - ChatMessages'
// `displayMessages` rebuilds entries for EVERY message in the conversation. That
// made per-token cost scale with conversation length (1.26ms at 1 prior message
// -> 3.07ms at 40). Mutating in place keeps it flat.
import { describe, it, expect } from 'vitest';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import type { DatabaseMessage } from '$lib/types';
import { MessageRole } from '$lib/enums';
function makeMessage(id: string): DatabaseMessage {
return {
id,
convId: 'c1',
type: 'text',
timestamp: 0,
role: MessageRole.ASSISTANT,
content: '',
parent: null,
children: []
} as DatabaseMessage;
}
describe('conversationsStore.updateMessageAtIndex', () => {
it('mutates in place, preserving object identity', () => {
conversationsStore.activeMessages = [makeMessage('a'), makeMessage('b')];
const before = conversationsStore.activeMessages[1];
conversationsStore.updateMessageAtIndex(1, { content: 'hello' });
expect(conversationsStore.activeMessages[1].content).toBe('hello');
expect(conversationsStore.activeMessages[1]).toBe(before);
conversationsStore.activeMessages = [];
});
it('leaves other messages and unrelated fields untouched', () => {
conversationsStore.activeMessages = [makeMessage('a'), makeMessage('b')];
const untouched = conversationsStore.activeMessages[0];
conversationsStore.updateMessageAtIndex(1, { content: 'x', model: 'm1' });
expect(conversationsStore.activeMessages[0]).toBe(untouched);
expect(conversationsStore.activeMessages[0].content).toBe('');
expect(conversationsStore.activeMessages[1].model).toBe('m1');
expect(conversationsStore.activeMessages[1].id).toBe('b');
conversationsStore.activeMessages = [];
});
it('is a no-op for an index of -1 or out of range', () => {
conversationsStore.activeMessages = [makeMessage('a')];
expect(() => conversationsStore.updateMessageAtIndex(-1, { content: 'x' })).not.toThrow();
expect(() => conversationsStore.updateMessageAtIndex(9, { content: 'x' })).not.toThrow();
expect(conversationsStore.activeMessages[0].content).toBe('');
conversationsStore.activeMessages = [];
});
});