mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
- Skip snapshot save for shell sessions (restore is gated on mode!=='shell', so they only burned a serialize() + cache slot + localStorage quota). - In-memory cache: delete-before-set so eviction is true LRU, not FIFO that could drop the most-recently-used session. - localStorage: extract _persistXtermSnapshot — evict to a fixed key budget regardless of session liveness (the old prune only dropped dead keys, so >10 live sessions at the 20-session target deadlocked the quota) and evict-and-retry on quota errors (the old prune ran only after a successful setItem, so a full quota permanently disabled persistence). - Tighten the OSC-strip regex in _isUsableXtermSnapshot to stop at ST. - Update the structural test's usability-gate assertion to not depend on a fixed byte window.
95 lines
5.0 KiB
TypeScript
95 lines
5.0 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
// Structural tests for the xterm snapshot/replay slice (COD-81). app.js has no
|
|
// bundler and is hard to drive through a real DOM, so — following the repo's
|
|
// existing pattern for app.js — these assert the source structure that makes
|
|
// the snapshot first-paint correct rather than executing it.
|
|
describe('xterm snapshot/replay (codex tab-switch)', () => {
|
|
const appSource = () => readFileSync(resolve(import.meta.dirname, '../src/web/public/app.js'), 'utf8');
|
|
|
|
it('rejects blank xterm snapshots before saving or restoring them', () => {
|
|
const source = appSource();
|
|
const helper = source.indexOf('_isUsableXtermSnapshot(snapshot)');
|
|
const save = source.indexOf('this._xtermSnapshots.set(this.activeSessionId, snapshot)');
|
|
const restore = source.indexOf('SNAPSHOT_RESTORE:', save);
|
|
const restoreBlock = source.slice(save, restore);
|
|
|
|
expect(helper).toBeGreaterThan(-1);
|
|
// The save is gated on a usability check immediately above it.
|
|
const usabilityGate = source.lastIndexOf('if (this._isUsableXtermSnapshot(snapshot))', save);
|
|
expect(usabilityGate).toBeGreaterThan(-1);
|
|
expect(usabilityGate).toBeLessThan(save);
|
|
// …and so is each restore path (in-memory + persisted).
|
|
expect(restoreBlock).toContain('if (snapshot && !this._isUsableXtermSnapshot(snapshot))');
|
|
expect(restoreBlock).toContain('persisted && this._isUsableXtermSnapshot(persisted)');
|
|
});
|
|
|
|
it('declares the snapshot-restore flag before selectSession uses it', () => {
|
|
const source = appSource();
|
|
const selectStart = source.indexOf('async selectSession(sessionId, options = {})');
|
|
const declaration = source.indexOf('let restoredSnapshot = false;', selectStart);
|
|
const snapshotBranch = source.indexOf("if (snapshot && !sessionIsBusy && session?.mode !== 'shell')", selectStart);
|
|
const rewriteDecision = source.indexOf(
|
|
'restoredSnapshot || clearedForBusy || data.terminalBuffer !== cachedBuffer',
|
|
selectStart
|
|
);
|
|
|
|
expect(selectStart).toBeGreaterThan(-1);
|
|
expect(declaration).toBeGreaterThan(selectStart);
|
|
expect(declaration).toBeLessThan(snapshotBranch);
|
|
expect(declaration).toBeLessThan(rewriteDecision);
|
|
});
|
|
|
|
it('uses xterm snapshots as first paint but still fetches the canonical terminal frame', () => {
|
|
const source = appSource();
|
|
const snapshotRestore = source.indexOf('SNAPSHOT_RESTORE:');
|
|
const cacheRestore = source.indexOf('Instant cache restore', snapshotRestore);
|
|
const fetchStart = source.indexOf("FETCH_START'", snapshotRestore);
|
|
const needsRewrite = source.indexOf('const needsRewrite', fetchStart);
|
|
const snapshotBlock = source.slice(snapshotRestore, cacheRestore);
|
|
const postSnapshotRestore = source.slice(snapshotRestore, needsRewrite + 160);
|
|
|
|
expect(snapshotRestore).toBeGreaterThan(-1);
|
|
expect(cacheRestore).toBeGreaterThan(snapshotRestore);
|
|
expect(fetchStart).toBeGreaterThan(cacheRestore);
|
|
expect(needsRewrite).toBeGreaterThan(fetchStart);
|
|
// Snapshot restore must NOT short-circuit the canonical fetch.
|
|
expect(snapshotBlock).not.toContain('this._finishBufferLoad();');
|
|
expect(postSnapshotRestore).toContain('restoredSnapshot');
|
|
expect(postSnapshotRestore).toContain('restoredSnapshot || clearedForBusy || data.terminalBuffer !== cachedBuffer');
|
|
});
|
|
|
|
it('forces replay after clearing a busy tab even when the fetched frame matches cache', () => {
|
|
const source = appSource();
|
|
const cacheRestore = source.indexOf('Instant cache restore');
|
|
const busyClear = source.indexOf('CACHE_SKIP_BUSY', cacheRestore);
|
|
const needsRewrite = source.indexOf('const needsRewrite', busyClear);
|
|
const replayBlock = source.slice(cacheRestore, needsRewrite + 160);
|
|
|
|
expect(cacheRestore).toBeGreaterThan(-1);
|
|
expect(busyClear).toBeGreaterThan(cacheRestore);
|
|
expect(needsRewrite).toBeGreaterThan(busyClear);
|
|
expect(replayBlock).toContain('clearedForBusy');
|
|
expect(replayBlock).toContain('restoredSnapshot || clearedForBusy || data.terminalBuffer !== cachedBuffer');
|
|
});
|
|
|
|
it('loads the SerializeAddon and keeps a per-session snapshot map', () => {
|
|
const terminalSource = readFileSync(resolve(import.meta.dirname, '../src/web/public/terminal-ui.js'), 'utf8');
|
|
expect(terminalSource).toContain('this._xtermSnapshots = new Map()');
|
|
expect(terminalSource).toContain('new SerializeAddon.SerializeAddon()');
|
|
expect(terminalSource).toContain('this.terminal.loadAddon(this._serializeAddon)');
|
|
});
|
|
|
|
it('evicts the in-memory snapshot cache and persists with a bounded localStorage budget', () => {
|
|
const source = appSource();
|
|
// In-memory cache is LRU-bounded…
|
|
expect(source).toContain('if (this._xtermSnapshots.size > 20)');
|
|
// …per-snapshot localStorage writes are size-capped…
|
|
expect(source).toContain('snapshot.length < 256 * 1024');
|
|
// …and the persisted key set is pruned of dead sessions.
|
|
expect(source).toContain("k.startsWith('codeman-xs-')");
|
|
});
|
|
});
|