test: isolate runtime state and PTY integration

This commit is contained in:
lior
2026-07-29 09:19:46 +03:00
parent 7c07284b95
commit bba3d80971
2 changed files with 57 additions and 12 deletions
+16 -7
View File
@@ -180,6 +180,8 @@ export function isAltScreenStripMode(mode: SessionMode): boolean {
const DEFAULT_PTY_COLS = 120;
const DEFAULT_PTY_ROWS = 40;
const TMUX_DISPLAY_TIMEOUT_MS = 2000;
const IS_TEST_MODE = !!process.env.VITEST;
const TEST_PTY_SCRIPT = 'process.stdin.pipe(process.stdout);';
/** Delay before the in-container Claude CLI version probe (lets the container start). */
const DOCKER_CLI_VERSION_PROBE_DELAY_MS = 3000;
@@ -1248,16 +1250,23 @@ export class Session extends EventEmitter {
// No extra sleep — createSession() already waits for tmux readiness
}
// Attach to the mux session via PTY
// Prevent tmux from letting the newest browser attach dictate global window
// size; accepted Codeman resize events update it explicitly below.
mux.setManualWindowSize?.(this._muxSession!.muxName);
// Integration tests need a live input/output transport without attaching to
// the host's tmux server or agent CLI. Production still uses the real mux.
if (!IS_TEST_MODE) {
// Prevent tmux from letting the newest browser attach dictate global window
// size; accepted Codeman resize events update it explicitly below.
mux.setManualWindowSize?.(this._muxSession!.muxName);
}
// Query existing tmux window size so re-attach matches (avoids flicker from 120x40 default).
// MUST go through the dedicated socket (mux.muxSocket); a bare `tmux display` hits the
// default server, always fails for our socketed sessions, and silently falls back to 120x40.
const { cols: ptyCols, rows: ptyRows } = queryTmuxWindowSize(this._muxSession!.muxName, mux.muxSocket);
const { cols: ptyCols, rows: ptyRows } = IS_TEST_MODE
? { cols: DEFAULT_PTY_COLS, rows: DEFAULT_PTY_ROWS }
: queryTmuxWindowSize(this._muxSession!.muxName, mux.muxSocket);
const attachCommand = IS_TEST_MODE ? process.execPath : mux.getAttachCommand();
const attachArgs = IS_TEST_MODE ? ['-e', TEST_PTY_SCRIPT] : mux.getAttachArgs(this._muxSession!.muxName);
try {
this.ptyProcess = pty.spawn(mux.getAttachCommand(), mux.getAttachArgs(this._muxSession!.muxName), {
this.ptyProcess = pty.spawn(attachCommand, attachArgs, {
name: 'xterm-256color',
cols: ptyCols,
rows: ptyRows,
@@ -2683,7 +2692,7 @@ export class Session extends EventEmitter {
if (this.ptyProcess && (dimsChanged || options.force)) {
this._ptyCols = cols;
this._ptyRows = rows;
if (this._mux && this._muxSession) {
if (!IS_TEST_MODE && this._mux && this._muxSession) {
this._mux.resizeWindow?.(this._muxSession.muxName, cols, rows);
}
this.ptyProcess.resize(cols, rows);
+41 -5
View File
@@ -1,16 +1,36 @@
/**
* @fileoverview Global test setup for Codeman tests
*
* SAFETY: TmuxManager has built-in test mode detection
* (via process.env.VITEST) that makes ALL shell commands no-ops.
* This means tests CANNOT kill, create, or interact with real tmux
* sessions regardless of what the test code does.
* SAFETY: The suite gets a temporary HOME and explicitly enables runtime test
* mode before application modules load. Tests therefore cannot touch the real
* Codeman state/cases tree or launch external tmux-backed agent sessions.
*
* This setup file strips shell-level auth configuration that can leak from a
* running Codeman instance, then handles mock/timer cleanup between tests.
*/
import { afterEach, vi } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, afterEach, vi } from 'vitest';
const originalHome = process.env.HOME;
const originalUserProfile = process.env.USERPROFILE;
const originalVitest = process.env.VITEST;
const originalPlaywrightBrowsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH;
const testHome = mkdtempSync(join(tmpdir(), 'codeman-vitest-'));
if (originalPlaywrightBrowsersPath === undefined && originalHome) {
process.env.PLAYWRIGHT_BROWSERS_PATH =
process.platform === 'darwin'
? join(originalHome, 'Library', 'Caches', 'ms-playwright')
: process.platform === 'win32'
? join(process.env.LOCALAPPDATA || originalHome, 'ms-playwright')
: join(originalHome, '.cache', 'ms-playwright');
}
process.env.HOME = testHome;
process.env.USERPROFILE = testHome;
process.env.VITEST = 'true';
delete process.env.CODEMAN_PASSWORD;
delete process.env.CODEMAN_USERNAME;
@@ -23,3 +43,19 @@ afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
});
afterAll(() => {
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = originalUserProfile;
if (originalVitest === undefined) delete process.env.VITEST;
else process.env.VITEST = originalVitest;
if (originalPlaywrightBrowsersPath === undefined) delete process.env.PLAYWRIGHT_BROWSERS_PATH;
else process.env.PLAYWRIGHT_BROWSERS_PATH = originalPlaywrightBrowsersPath;
rmSync(testHome, { recursive: true, force: true });
});