mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 07:59:42 +02:00
COD-80 centralize terminal history/scrollback/buffer limits into config
Introduce src/config/terminal-history.ts: one place for terminal scrollback, tmux history-limit, and PTY buffer byte caps, each overridable via env var or the settings object and bounds-clamped via resolveTerminalHistoryConfig(). Defaults match the prior hardcoded values, so this is behavior-neutral. Wires the resolver through buffer-limits, tmux-manager (incl. a setHistoryLimit so a settings change applies live), session, server, system-routes, session-routes, schemas, and the config port. Adds 4 optional settings keys (terminalScrollback Lines, tmuxHistoryLimit, terminalBufferMaxBytes, terminalBufferTrimBytes) with bounds + a trim<=max cross-check.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
import { vi } from 'vitest';
|
||||
import { MockSession, createMockSession } from './mock-session.js';
|
||||
import { resolveTerminalHistoryConfig } from '../../src/config/terminal-history.js';
|
||||
|
||||
/**
|
||||
* Creates a mock context that satisfies all port interfaces.
|
||||
@@ -76,6 +77,7 @@ export function createMockRouteContext(options?: { sessionId?: string }) {
|
||||
getGlobalNiceConfig: vi.fn(async () => undefined),
|
||||
getModelConfig: vi.fn(async () => null),
|
||||
getClaudeModeConfig: vi.fn(async () => ({})),
|
||||
getTerminalHistoryConfig: vi.fn(async () => resolveTerminalHistoryConfig({})),
|
||||
getDefaultClaudeMdPath: vi.fn(async () => undefined),
|
||||
getLightState: vi.fn(() => ({ sessions: [], status: 'ok' })),
|
||||
getLightSessionsState: vi.fn(() => {
|
||||
@@ -99,6 +101,7 @@ export function createMockRouteContext(options?: { sessionId?: string }) {
|
||||
getSession: vi.fn(() => null),
|
||||
clearRespawnConfig: vi.fn(),
|
||||
updateRespawnConfig: vi.fn(),
|
||||
setHistoryLimit: vi.fn(async () => {}),
|
||||
},
|
||||
runSummaryTrackers: new Map(),
|
||||
activePlanOrchestrators: new Map(),
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SettingsUpdateSchema } from '../src/web/schemas.js';
|
||||
import {
|
||||
MAX_TERMINAL_SCROLLBACK_LINES,
|
||||
MIN_TERMINAL_SCROLLBACK_LINES,
|
||||
MAX_TERMINAL_BUFFER_BYTES,
|
||||
MIN_TERMINAL_BUFFER_BYTES,
|
||||
} from '../src/config/terminal-history.js';
|
||||
|
||||
describe('SettingsUpdateSchema — terminal history keys', () => {
|
||||
it('accepts valid in-range terminal-history settings', () => {
|
||||
const res = SettingsUpdateSchema.safeParse({
|
||||
terminalScrollbackLines: 100_000,
|
||||
tmuxHistoryLimit: 100_000,
|
||||
terminalBufferMaxBytes: 32 * 1024 * 1024,
|
||||
terminalBufferTrimBytes: 24 * 1024 * 1024,
|
||||
});
|
||||
expect(res.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a settings object that omits the terminal-history keys', () => {
|
||||
const res = SettingsUpdateSchema.safeParse({ allowedTools: 'Bash' });
|
||||
expect(res.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects scrollback below the minimum', () => {
|
||||
const res = SettingsUpdateSchema.safeParse({ terminalScrollbackLines: MIN_TERMINAL_SCROLLBACK_LINES - 1 });
|
||||
expect(res.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects scrollback above the maximum', () => {
|
||||
const res = SettingsUpdateSchema.safeParse({ tmuxHistoryLimit: MAX_TERMINAL_SCROLLBACK_LINES + 1 });
|
||||
expect(res.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects buffer bytes outside the byte bounds', () => {
|
||||
expect(SettingsUpdateSchema.safeParse({ terminalBufferMaxBytes: MIN_TERMINAL_BUFFER_BYTES - 1 }).success).toBe(
|
||||
false
|
||||
);
|
||||
expect(SettingsUpdateSchema.safeParse({ terminalBufferMaxBytes: MAX_TERMINAL_BUFFER_BYTES + 1 }).success).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects non-integer values', () => {
|
||||
expect(SettingsUpdateSchema.safeParse({ terminalScrollbackLines: 12_345.5 }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects trim > max via the cross-field superRefine', () => {
|
||||
const res = SettingsUpdateSchema.safeParse({
|
||||
terminalBufferMaxBytes: 4 * 1024 * 1024,
|
||||
terminalBufferTrimBytes: 8 * 1024 * 1024,
|
||||
});
|
||||
expect(res.success).toBe(false);
|
||||
if (!res.success) {
|
||||
expect(res.error.issues.some((i) => i.path.includes('terminalBufferTrimBytes'))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows trim == max', () => {
|
||||
const res = SettingsUpdateSchema.safeParse({
|
||||
terminalBufferMaxBytes: 8 * 1024 * 1024,
|
||||
terminalBufferTrimBytes: 8 * 1024 * 1024,
|
||||
});
|
||||
expect(res.success).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect } 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 match the prior hardcoded values (behavior-neutral)', () => {
|
||||
expect(DEFAULT_TMUX_HISTORY_LIMIT).toBe(50_000);
|
||||
expect(DEFAULT_TERMINAL_SCROLLBACK_LINES).toBe(50_000);
|
||||
expect(DEFAULT_TERMINAL_BUFFER_MAX_BYTES).toBe(2 * 1024 * 1024);
|
||||
expect(DEFAULT_TERMINAL_BUFFER_TRIM_BYTES).toBe(1.5 * 1024 * 1024);
|
||||
});
|
||||
|
||||
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 (1.5MB) exceeds a 1MB max → trim must fall to the max.
|
||||
const cfg = resolveTerminalHistoryConfig({ terminalBufferMaxBytes: 1 * 1024 * 1024 });
|
||||
expect(cfg.terminalBufferTrimBytes).toBe(1 * 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user