Files
Codeman/test/terminal-history.test.ts
Codeman maintainer 246f7b532d fix(review): clamp env-path trim below max; revert unwired scrollback raise (PR #138)
- UNBOUNDED-MEMORY: DEFAULT_TERMINAL_BUFFER_TRIM_BYTES from CODEMAN_TRIM_TERMINAL_TO
  had no relation to DEFAULT_TERMINAL_BUFFER_MAX_BYTES — setting only
  CODEMAN_MAX_TERMINAL_BUFFER=2097152 left the 24MB trim default in force, making
  BufferAccumulator.trim() (slice(-trimSize)) a no-op: unbounded growth past the cap
  plus a full string re-join on every append (O(n²)). Trim default is now clamped to
  75% of the resolved max (the 24MB/32MB default ratio, preserved as hysteresis);
  regression test re-evaluates the module under the env via vi.resetModules.
- OVERCLAIM: reverted DEFAULT_TERMINAL_SCROLLBACK_LINES 100k -> 50k — it has zero
  consumers; browser xterm scrollback is the separate hardcoded DEFAULT_SCROLLBACK
  (50k) in constants.js and deliberately stays 50k (mobile-memory hazard). The tmux
  history-limit raise (50k -> 100k) and PTY 32MB/24MB raise remain (those are wired).
  Module docstring now claims only what is wired; fixed the stale tmux-manager.ts
  comment saying the tmux limit "matches the xterm-side default in constants.js".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:39:46 +02:00

134 lines
5.8 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import {
resolveTerminalHistoryConfig,
DEFAULT_TERMINAL_SCROLLBACK_LINES,
DEFAULT_TMUX_HISTORY_LIMIT,
DEFAULT_TERMINAL_BUFFER_MAX_BYTES,
DEFAULT_TERMINAL_BUFFER_TRIM_BYTES,
MIN_TERMINAL_SCROLLBACK_LINES,
MAX_TERMINAL_SCROLLBACK_LINES,
MIN_TERMINAL_BUFFER_BYTES,
MAX_TERMINAL_BUFFER_BYTES,
} from '../src/config/terminal-history.js';
describe('resolveTerminalHistoryConfig', () => {
it('returns the documented defaults for empty settings', () => {
const cfg = resolveTerminalHistoryConfig({});
expect(cfg).toEqual({
terminalScrollbackLines: DEFAULT_TERMINAL_SCROLLBACK_LINES,
tmuxHistoryLimit: DEFAULT_TMUX_HISTORY_LIMIT,
terminalBufferMaxBytes: DEFAULT_TERMINAL_BUFFER_MAX_BYTES,
terminalBufferTrimBytes: DEFAULT_TERMINAL_BUFFER_TRIM_BYTES,
});
});
it('defaults raise tmux history to 100k and buffer caps to 32MB/24MB; browser scrollback stays 50k', () => {
expect(DEFAULT_TMUX_HISTORY_LIMIT).toBe(100_000);
// Matches the hardcoded browser-side DEFAULT_SCROLLBACK in src/web/public/constants.js —
// deliberately NOT raised (100k xterm lines per tab is a mobile-memory hazard).
expect(DEFAULT_TERMINAL_SCROLLBACK_LINES).toBe(50_000);
expect(DEFAULT_TERMINAL_BUFFER_MAX_BYTES).toBe(32 * 1024 * 1024);
expect(DEFAULT_TERMINAL_BUFFER_TRIM_BYTES).toBe(24 * 1024 * 1024);
});
it('clamps the env-derived trim default below an env-lowered max (CODEMAN_MAX_TERMINAL_BUFFER only)', async () => {
const originalMax = process.env.CODEMAN_MAX_TERMINAL_BUFFER;
const originalTrim = process.env.CODEMAN_TRIM_TERMINAL_TO;
vi.resetModules();
process.env.CODEMAN_MAX_TERMINAL_BUFFER = String(2 * 1024 * 1024);
delete process.env.CODEMAN_TRIM_TERMINAL_TO;
try {
const mod = await import('../src/config/terminal-history.js');
expect(mod.DEFAULT_TERMINAL_BUFFER_MAX_BYTES).toBe(2 * 1024 * 1024);
// trim >= max would make BufferAccumulator.trim() a no-op (unbounded growth + O(n²) appends).
expect(mod.DEFAULT_TERMINAL_BUFFER_TRIM_BYTES).toBeLessThan(mod.DEFAULT_TERMINAL_BUFFER_MAX_BYTES);
expect(mod.DEFAULT_TERMINAL_BUFFER_TRIM_BYTES).toBe(Math.floor(2 * 1024 * 1024 * 0.75));
} finally {
if (originalMax === undefined) delete process.env.CODEMAN_MAX_TERMINAL_BUFFER;
else process.env.CODEMAN_MAX_TERMINAL_BUFFER = originalMax;
if (originalTrim === undefined) delete process.env.CODEMAN_TRIM_TERMINAL_TO;
else process.env.CODEMAN_TRIM_TERMINAL_TO = originalTrim;
vi.resetModules();
}
});
it('passes valid in-range values through unchanged', () => {
const cfg = resolveTerminalHistoryConfig({
terminalScrollbackLines: 50_000,
tmuxHistoryLimit: 75_000,
terminalBufferMaxBytes: 16 * 1024 * 1024,
terminalBufferTrimBytes: 8 * 1024 * 1024,
});
expect(cfg).toEqual({
terminalScrollbackLines: 50_000,
tmuxHistoryLimit: 75_000,
terminalBufferMaxBytes: 16 * 1024 * 1024,
terminalBufferTrimBytes: 8 * 1024 * 1024,
});
});
it('clamps scrollback/history below MIN up to the floor', () => {
const cfg = resolveTerminalHistoryConfig({
terminalScrollbackLines: 1,
tmuxHistoryLimit: 0,
});
expect(cfg.terminalScrollbackLines).toBe(MIN_TERMINAL_SCROLLBACK_LINES);
expect(cfg.tmuxHistoryLimit).toBe(MIN_TERMINAL_SCROLLBACK_LINES);
});
it('clamps scrollback/history above MAX down to the ceiling', () => {
const cfg = resolveTerminalHistoryConfig({
terminalScrollbackLines: 999_999_999,
tmuxHistoryLimit: 999_999_999,
});
expect(cfg.terminalScrollbackLines).toBe(MAX_TERMINAL_SCROLLBACK_LINES);
expect(cfg.tmuxHistoryLimit).toBe(MAX_TERMINAL_SCROLLBACK_LINES);
});
it('clamps the buffer byte caps to their MIN/MAX bounds', () => {
const tooSmall = resolveTerminalHistoryConfig({ terminalBufferMaxBytes: 1 });
expect(tooSmall.terminalBufferMaxBytes).toBe(MIN_TERMINAL_BUFFER_BYTES);
const tooLarge = resolveTerminalHistoryConfig({ terminalBufferMaxBytes: 1024 * 1024 * 1024 });
expect(tooLarge.terminalBufferMaxBytes).toBe(MAX_TERMINAL_BUFFER_BYTES);
});
it('keeps trim <= max (trim is capped to the resolved max)', () => {
const cfg = resolveTerminalHistoryConfig({
terminalBufferMaxBytes: 4 * 1024 * 1024,
terminalBufferTrimBytes: 64 * 1024 * 1024,
});
expect(cfg.terminalBufferTrimBytes).toBeLessThanOrEqual(cfg.terminalBufferMaxBytes);
expect(cfg.terminalBufferTrimBytes).toBe(4 * 1024 * 1024);
});
it('caps the default trim to a lowered max', () => {
// Default trim (24MB) exceeds a 2MB max → trim must fall to the max.
const cfg = resolveTerminalHistoryConfig({ terminalBufferMaxBytes: 2 * 1024 * 1024 });
expect(cfg.terminalBufferTrimBytes).toBe(2 * 1024 * 1024);
});
it('truncates fractional inputs to integers', () => {
const cfg = resolveTerminalHistoryConfig({ terminalScrollbackLines: 12_345.9 });
expect(cfg.terminalScrollbackLines).toBe(12_345);
});
it('falls back to defaults for non-number / non-finite inputs', () => {
const cfg = resolveTerminalHistoryConfig({
terminalScrollbackLines: 'lots' as unknown as number,
tmuxHistoryLimit: NaN,
terminalBufferMaxBytes: null as unknown as number,
terminalBufferTrimBytes: Infinity,
});
expect(cfg.terminalScrollbackLines).toBe(DEFAULT_TERMINAL_SCROLLBACK_LINES);
expect(cfg.tmuxHistoryLimit).toBe(DEFAULT_TMUX_HISTORY_LIMIT);
expect(cfg.terminalBufferMaxBytes).toBe(DEFAULT_TERMINAL_BUFFER_MAX_BYTES);
// trim default is min(DEFAULT_TRIM, resolvedMax) — here resolvedMax is the default max.
expect(cfg.terminalBufferTrimBytes).toBe(DEFAULT_TERMINAL_BUFFER_TRIM_BYTES);
});
it('uses an empty object when called with no argument', () => {
expect(resolveTerminalHistoryConfig().tmuxHistoryLimit).toBe(DEFAULT_TMUX_HISTORY_LIMIT);
});
});