Files
Codeman/test/pane-history-memory.test.ts
T
Codeman maintainer d41ad16818 fix: apply the review items for #681 at merge
_notePaneHistory JSDoc names the responses it skips (an error envelope on any path, a refresh that lost its owner, a pull whose select generation moved)
selectSession records the pane count only for a non-error envelope (capture.json?.success !== false), pinned in the static guard of test/pane-history-memory.test.ts; the two invariant paragraphs now name the tab switch beside the refresh and the pull

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 14:02:21 +02:00

98 lines
4.5 KiB
TypeScript

/**
* @fileoverview What the client remembers about a pane's tmux scrollback.
*
* `selectSession` serves a tab switch from the small `full=1` capture when the
* session's last terminal response reported `paneHistoryLines: 0` (a fullscreen
* CLI in the alternate screen), instead of the 1 MiB byte tail. The memory that
* decides it must treat ONLY a reported 0 as empty: a missing field (an older
* server, a byte-history fallback) has to forget, so the tab switch goes back to
* the bounded tail rather than trusting a stale answer.
*
* Runs the real `_notePaneHistory` / `_clearHistoryTruncation` from app.js in a
* `vm`. The switch itself is driven end to end in
* fullscreen-tab-switch-capture.browser.test.ts (browser suite).
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import vm from 'node:vm';
import { describe, expect, it } from 'vitest';
const APP = readFileSync(resolve(import.meta.dirname, '../src/web/public/app.js'), 'utf8');
function methodSource(source: string, method: string): string {
const start = source.search(new RegExp(`^ {2}(?:async )?${method}\\(`, 'm'));
expect(start, `${method} not found`).toBeGreaterThan(-1);
const next = /^ {2}(?:async )?[A-Za-z_$][\w$]*\(/m.exec(source.slice(start + 1));
return next ? source.slice(start, start + 1 + next.index) : source.slice(start);
}
function makeApp() {
const methods = vm.runInContext(
`({ ${methodSource(APP, '_notePaneHistory')}, ${methodSource(APP, '_clearHistoryTruncation')} })`,
vm.createContext({})
) as Record<string, (...args: unknown[]) => void>;
return {
activeSessionId: null,
_paneHistoryLines: new Map<string, number>(),
_renderHistoryTruncationBanner() {},
...methods,
} as Record<string, any>;
}
describe('_notePaneHistory', () => {
it('records the count a capture reported, 0 included', () => {
const app = makeApp();
app._notePaneHistory('s1', { paneHistoryLines: 0 });
app._notePaneHistory('s2', { paneHistoryLines: 40000 });
expect(app._paneHistoryLines.get('s1')).toBe(0);
expect(app._paneHistoryLines.get('s2')).toBe(40000);
});
it('forgets on a response without a usable count, so unknown never reads as empty', () => {
const app = makeApp();
for (const payload of [{}, { paneHistoryLines: null }, { paneHistoryLines: NaN }, { paneHistoryLines: -1 }, null]) {
app._notePaneHistory('s1', { paneHistoryLines: 0 });
app._notePaneHistory('s1', payload);
expect(app._paneHistoryLines.has('s1')).toBe(false);
}
});
it('ignores a call without a session', () => {
const app = makeApp();
app._notePaneHistory(null, { paneHistoryLines: 0 });
expect(app._paneHistoryLines.size).toBe(0);
});
it('is dropped with the session', () => {
const app = makeApp();
app._notePaneHistory('s1', { paneHistoryLines: 0 });
app._clearHistoryTruncation('s1');
expect(app._paneHistoryLines.has('s1')).toBe(false);
});
});
describe('every terminal response feeds it (static guard)', () => {
it('the tab switch, the refresh and the history re-pull all record the pane', () => {
expect(methodSource(APP, 'selectSession')).toContain('this._notePaneHistory?.(sessionId, data);');
expect(methodSource(APP, '_onSessionNeedsRefresh')).toContain('this._notePaneHistory?.(sessionId, data);');
expect(methodSource(APP, '_maybeRefetchFullHistory')).toContain('this._notePaneHistory?.(sessionId, payload);');
});
it('the refresh records the pane only once it still owns the terminal, and no caller records an error envelope', () => {
// A refresh that lost its owner is older than what the switch back read; an
// error envelope has no count, and forgetting on it would hand a scroll back
// to the paging fallback (see _maybePageCliTranscript).
const refresh = methodSource(APP, '_onSessionNeedsRefresh');
const ownerCheck = refresh.indexOf('this._terminalRefreshOwner !== refreshOwner) return false;');
expect(ownerCheck).toBeGreaterThan(-1);
expect(refresh.indexOf('this._notePaneHistory?.(sessionId, data);')).toBeGreaterThan(ownerCheck);
expect(refresh).toContain('capture.json?.success !== false) this._notePaneHistory?.(sessionId, data);');
expect(methodSource(APP, 'selectSession')).toContain(
'capture.json?.success !== false) this._notePaneHistory?.(sessionId, data);'
);
expect(methodSource(APP, '_maybeRefetchFullHistory')).toContain(
'capture.json?.success !== false) this._notePaneHistory?.(sessionId, payload);'
);
});
});