// A daemon E2E cannot observe the web client's byte cursor, retry policy, or polling lifetime. // Compile the real resource and stub only HTTP transport and document visibility. import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { stripTypeScriptTypes } from 'node:module'; import test from 'node:test'; import { compileModule } from 'svelte/compiler'; const moduleURL = (code) => `data:text/javascript;base64,${Buffer.from(code).toString('base64')}`; const apiURL = moduleURL(` const requests = { metadata: [], output: [], metadataQueue: [], outputQueue: [], command: null, bytes: Buffer.alloc(0) }; export class ApiError extends Error { constructor(code, status = 400) { super(code); this.code = code; this.status = status; } } export function reset(command, output) { requests.metadata = []; requests.output = []; requests.metadataQueue = []; requests.outputQueue = []; requests.command = command; requests.bytes = Buffer.from(output); } export async function getShellCommand(session, id, signal) { requests.metadata.push({ session, id, signal }); if (requests.metadataQueue.length) return requests.metadataQueue.shift()(); if (signal.aborted) throw new DOMException('aborted', 'AbortError'); return requests.command; } export async function readShellOutput(session, id, offset, signal, limit) { requests.output.push({ session, id, offset, limit, signal }); if (requests.outputQueue.length) return requests.outputQueue.shift()({ session, id, offset, limit, signal }); if (signal.aborted) throw new DOMException('aborted', 'AbortError'); if (offset > requests.bytes.length || (offset < requests.bytes.length && (requests.bytes[offset] & 0xc0) === 0x80)) throw new ApiError('invalid_shell_output_range'); let end = Math.min(requests.bytes.length, offset + limit); while (end < requests.bytes.length && (requests.bytes[end] & 0xc0) === 0x80) end--; const data = requests.bytes.subarray(offset, end); return { text: data.toString('utf8'), output_bytes: requests.bytes.length }; } export { requests }; `); const api = await import(apiURL); const source = await readFile(new URL('../src/lib/shell-commands.svelte.ts', import.meta.url), 'utf8'); let code = compileModule(stripTypeScriptTypes(source, { mode: 'transform' }), { filename: 'shell-commands.svelte.js', generate: 'client' }).js.code; code = code.replace(/from (['"])(.*?)\1/g, (_, _quote, path) => `from ${JSON.stringify(path === './api' ? apiURL : import.meta.resolve(path))}`); const { ShellOutput } = await import(moduleURL(code)); const visibilityListeners = new Set(); globalThis.document = { hidden: false, addEventListener(type, listener) { if (type === 'visibilitychange') visibilityListeners.add(listener); }, removeEventListener(type, listener) { if (type === 'visibilitychange') visibilityListeners.delete(listener); }, }; function visibility(hidden) { document.hidden = hidden; for (const listener of [...visibilityListeners]) listener(); } const timers = new Map(); let timerId = 0; const realSetTimeout = globalThis.setTimeout; const realClearTimeout = globalThis.clearTimeout; function fakeTimers() { globalThis.setTimeout = (fn, delay) => { const id = ++timerId; timers.set(id, { fn, delay }); return id; }; globalThis.clearTimeout = (id) => timers.delete(id); return () => { globalThis.setTimeout = realSetTimeout; globalThis.clearTimeout = realClearTimeout; timers.clear(); }; } async function settle() { for (let i = 0; i < 12; i++) await new Promise((resolve) => setImmediate(resolve)); } test('finished command viewers stop polling even while the receipt is queued for the agent', async () => { const restore = fakeTimers(); const output = 'finished output'; api.reset({ ...metadata(output), delivery: 'pending' }, output); const view = new ShellOutput('session', 'cmd'); try { view.watch(); await settle(); assert.equal(view.command.delivery, 'pending'); assert.equal(view.text, output); assert.equal(api.requests.metadata.length, 1); assert.equal(api.requests.output.length, 1); assert.equal(timers.size, 0, 'delivery belongs to the transcript; the viewer must stop polling at completion'); } finally { view.dispose(); restore(); } }); function metadata(output, status = 'completed') { return { id: 'cmd', command: 'printf output', status, output_bytes: Buffer.byteLength(output), exit_code: status === 'completed' ? 0 : null, error: null }; } // The assertions need to see requests made with exact offsets, including cursor repairs. test('initial tail advances off a UTF-8 continuation byte and exposes exact byte range', async () => { const restore = fakeTimers(); try { const output = 'x'.repeat(10) + '🌿' + 't'.repeat(65_533); api.reset(metadata(output), output); const view = new ShellOutput('session', 'cmd'); view.watch(); await settle(); assert.equal(view.text, 't'.repeat(65_533)); assert.equal(view.start, Buffer.byteLength(output) - Buffer.byteLength(view.text)); assert.equal(view.end, Buffer.byteLength(output)); assert.equal(timers.size, 0, 'completed command output must not keep polling'); assert.deepEqual(api.requests.output.map((r) => r.offset), [11, 12, 13, 14]); view.dispose(); } finally { restore(); } }); test('first, next, previous and latest use byte cursors without gaps', async () => { const restore = fakeTimers(); try { const output = 'a'.repeat(140_000); api.reset(metadata(output), output); const view = new ShellOutput('session', 'cmd'); view.watch(); await settle(); assert.equal(view.start, 74_464); view.first(); await settle(); assert.equal(view.start, 0); assert.equal(view.end, 65_536); view.next(); await settle(); assert.equal(view.start, 65_536); assert.equal(view.end, 131_072); assert.equal(view.text.length, 65_536); view.previous(); await settle(); assert.equal(view.start, 0); assert.equal(view.end, 65_536); assert.equal(view.text.length, 65_536); view.latest(); await settle(); assert.equal(view.text, output.slice(-65_536)); assert.equal(view.end, output.length); view.dispose(); } finally { restore(); } }); test('following drains growth beyond one page and caps text without splitting surrogate pairs', async () => { const restore = fakeTimers(); try { let output = 'x'.repeat(70_000); api.reset(metadata(output, 'running'), output); const view = new ShellOutput('session', 'cmd'); view.watch(); await settle(); // Each 320k-byte increment stays below KEEP, so the resource must append // pages rather than jump straight to the latest tail. for (let batch = 0; batch < 3; batch++) { output += '🌿'.repeat(80_000); api.requests.bytes = Buffer.from(output); api.requests.command = metadata(output, 'running'); for (let i = 0; i < 8 && view.end < Buffer.byteLength(output); i++) { const scheduled = [...timers.entries()].find(([, timer]) => timer.delay === 0); if (scheduled) { timers.delete(scheduled[0]); scheduled[1].fn(); } else view.refresh(); await settle(); } assert.equal(view.end, Buffer.byteLength(output), `batch ${batch} must drain all appended pages`); } const suffix = '🌿'.repeat(200_000); assert.equal(view.text, suffix); assert.equal(view.text.length, 400_000); assert.ok(view.start > 0); assert.equal(view.start, Buffer.byteLength(output) - Buffer.byteLength(suffix)); assert.ok(!view.text.includes('�')); assert.ok(!/[\uD800-\uDBFF]$/.test(view.text)); view.dispose(); } finally { restore(); } }); test('disposed in-flight reads abort and ignore late transport results', async () => { const restore = fakeTimers(); try { api.reset(metadata('hello'), 'hello'); let finish; api.requests.metadataQueue.push(() => new Promise((resolve) => { finish = resolve; })); const view = new ShellOutput('session', 'cmd'); view.watch(); await settle(); const signal = api.requests.metadata[0].signal; view.dispose(); finish(metadata('late')); await settle(); assert.equal(signal.aborted, true); assert.equal(view.command, null); assert.equal(view.loading, true); assert.equal(timers.size, 0); } finally { restore(); } }); test('404 is terminal while transient errors retry after the documented delay', async () => { const restore = fakeTimers(); try { api.reset(metadata(''), ''); api.requests.metadataQueue.push(() => { throw new api.ApiError('not_found', 404); }); const missing = new ShellOutput('session', 'cmd'); missing.watch(); await settle(); assert.match(missing.problem, /no longer available/); assert.equal(timers.size, 0); missing.dispose(); api.reset(metadata('ok'), 'ok'); api.requests.metadataQueue.push(() => { throw new Error('temporary network failure'); }); const transient = new ShellOutput('session', 'cmd'); transient.watch(); await settle(); assert.equal(timers.size, 1); assert.equal([...timers.values()][0].delay, 3000); transient.dispose(); } finally { restore(); } }); test('hidden watchers pause, resume on visibility, and detach on dispose', async () => { const restore = fakeTimers(); try { api.reset(metadata('first', 'running'), 'first'); document.hidden = true; const view = new ShellOutput('session', 'cmd'); view.watch(); await settle(); assert.equal(api.requests.metadata.length, 0, 'initially hidden watcher must not fetch metadata'); assert.equal(timers.size, 0); assert.equal(view.loading, false); visibility(false); await settle(); assert.equal(api.requests.metadata.length, 1); assert.equal(view.text, 'first'); assert.equal(timers.size, 1); const reads = api.requests.metadata.length; api.requests.command = metadata('first plus later output', 'running'); api.requests.bytes = Buffer.from('first plus later output'); visibility(true); assert.equal(timers.size, 0, 'hide clears scheduled polling'); assert.equal(view.loading, false); assert.equal(api.requests.metadata.length, reads, 'hide does not issue metadata requests'); visibility(false); await settle(); assert.equal(api.requests.metadata.length, reads + 1, 'visible resumes with fresh metadata'); assert.equal(view.text, 'first plus later output'); assert.equal(view.end, Buffer.byteLength('first plus later output')); assert.equal(timers.size, 1); const beforeDispose = api.requests.metadata.length; view.dispose(); assert.equal(visibilityListeners.size, 0, 'dispose removes the visibility listener'); visibility(true); visibility(false); await settle(); assert.equal(api.requests.metadata.length, beforeDispose, 'disposed view ignores future visibility events'); assert.equal(timers.size, 0); } finally { document.hidden = false; restore(); } });