mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
Merge master (PR #112 terminal pane-buffer rework) into v1-readiness-hardening
Conflict in src/web/public/app.js selectSession: combined #112's _clearTerminalLoadState cleanup on stale select with #113's {success,data} envelope unwrap of the terminal fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
function cssRuleBody(css: string, selector: string): string {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const match = css.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`));
|
||||
return match?.[1] ?? '';
|
||||
}
|
||||
|
||||
describe('terminal layout CSS', () => {
|
||||
it('pins the xterm root to the terminal container width while scrollback moves', () => {
|
||||
const css = readFileSync(resolve(import.meta.dirname, '../src/web/public/styles.css'), 'utf8');
|
||||
const xtermRule = cssRuleBody(css, '.terminal-container .xterm');
|
||||
|
||||
expect(xtermRule).toContain('width: 100%');
|
||||
expect(xtermRule).toContain('min-width: 0');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('tmux styled pane capture', () => {
|
||||
it('requests SGR style escapes for canonical browser replay frames', () => {
|
||||
const source = readFileSync(resolve(import.meta.dirname, '../src/tmux-manager.ts'), 'utf8');
|
||||
const captureMethod = source.indexOf('capturePaneBuffer(muxName: string');
|
||||
const captureCommand = source.indexOf('capture-pane -p', captureMethod);
|
||||
|
||||
expect(captureMethod).toBeGreaterThan(-1);
|
||||
expect(captureCommand).toBeGreaterThan(captureMethod);
|
||||
expect(source.slice(captureCommand, captureCommand + 80)).toContain('capture-pane -p -e');
|
||||
});
|
||||
});
|
||||
+106
-2
@@ -8,8 +8,8 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { TmuxManager, parsePaneList } from '../src/tmux-manager.js';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { TmuxManager, formatPaneSnapshot, parsePaneList, resolveActivePaneTarget } from '../src/tmux-manager.js';
|
||||
import { execSync, exec } from 'node:child_process';
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests (mocked)
|
||||
@@ -64,6 +64,7 @@ vi.mock('node:fs/promises', async () => {
|
||||
describe('TmuxManager (unit)', () => {
|
||||
let manager: TmuxManager;
|
||||
const mockedExecSync = vi.mocked(execSync);
|
||||
const mockedExec = vi.mocked(exec);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -118,6 +119,109 @@ describe('TmuxManager (unit)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('window sizing', () => {
|
||||
it('pins a tmux window to manual sizing before browser attach', () => {
|
||||
expect(manager.setManualWindowSize('codeman-abc12345')).toBe(true);
|
||||
|
||||
expect(mockedExecSync).toHaveBeenCalledWith(
|
||||
"tmux -L 'codeman' set-window-option -t 'codeman-abc12345' window-size manual",
|
||||
expect.objectContaining({ stdio: 'ignore' })
|
||||
);
|
||||
});
|
||||
|
||||
it('resizes the tmux window when Codeman accepts a desktop resize', () => {
|
||||
expect(manager.resizeWindow('codeman-abc12345', 140, 42)).toBe(true);
|
||||
|
||||
// Non-blocking exec (not execSync) on the interactive resize hot path.
|
||||
expect(mockedExec).toHaveBeenCalledWith(
|
||||
"tmux -L 'codeman' resize-window -t 'codeman-abc12345' -x 140 -y 42",
|
||||
expect.objectContaining({ timeout: expect.any(Number) }),
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('environment exports', () => {
|
||||
it('keeps COLORTERM unset for OpenCode sessions', () => {
|
||||
const exports = (
|
||||
manager as unknown as {
|
||||
buildEnvExports(sessionId: string, muxName: string, mode: string): string[];
|
||||
}
|
||||
).buildEnvExports('session-1', 'codeman-abc12345', 'opencode');
|
||||
|
||||
expect(exports).toContain('unset COLORTERM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPaneSnapshot', () => {
|
||||
it('paints captured rows with absolute cursor positions to avoid newline autowrap scroll', () => {
|
||||
const fullWidthLine = 'x'.repeat(10);
|
||||
|
||||
const snapshot = formatPaneSnapshot([fullWidthLine, 'next line'], {
|
||||
cols: 10,
|
||||
rows: 4,
|
||||
cursorX: 2,
|
||||
cursorY: 1,
|
||||
});
|
||||
|
||||
expect(snapshot).toBe(`\x1b[1;1H${'x'.repeat(9)}\x1b[2;1Hnext line\x1b[2;3H`);
|
||||
expect(snapshot).not.toContain('\n');
|
||||
});
|
||||
|
||||
it('preserves SGR color while stripping non-style pane controls', () => {
|
||||
const snapshot = formatPaneSnapshot(['\x1b[32mgreen\x1b[0m\x1b[2K\x1b[10;20Htail'], {
|
||||
cols: 40,
|
||||
rows: 2,
|
||||
cursorX: 0,
|
||||
cursorY: 0,
|
||||
});
|
||||
|
||||
expect(snapshot).toContain('\x1b[32mgreen\x1b[0m');
|
||||
expect(snapshot).toContain('tail');
|
||||
expect(snapshot).not.toContain('\x1b[2K');
|
||||
expect(snapshot).not.toContain('\x1b[10;20H');
|
||||
});
|
||||
|
||||
it('truncates styled rows by visible columns without cutting SGR escapes', () => {
|
||||
const snapshot = formatPaneSnapshot(['\x1b[31mabcdef\x1b[0m'], {
|
||||
cols: 4,
|
||||
rows: 1,
|
||||
cursorX: 0,
|
||||
cursorY: 0,
|
||||
});
|
||||
|
||||
expect(snapshot).toBe('\x1b[1;1H\x1b[31mabc\x1b[0m\x1b[1;1H');
|
||||
});
|
||||
|
||||
it('does not let full-width glyphs cross the paint boundary', () => {
|
||||
const snapshot = formatPaneSnapshot(['abc\u754cdef'], {
|
||||
cols: 5,
|
||||
rows: 1,
|
||||
cursorX: 0,
|
||||
cursorY: 0,
|
||||
});
|
||||
|
||||
expect(snapshot).toBe('\x1b[1;1Habc\x1b[1;1H');
|
||||
});
|
||||
|
||||
it('keeps combining marks attached without consuming a terminal column', () => {
|
||||
const snapshot = formatPaneSnapshot(['a\u0301bc'], {
|
||||
cols: 4,
|
||||
rows: 1,
|
||||
cursorX: 0,
|
||||
cursorY: 0,
|
||||
});
|
||||
|
||||
expect(snapshot).toBe('\x1b[1;1Ha\u0301bc\x1b[1;1H');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveActivePaneTarget', () => {
|
||||
it('selects the active pane instead of assuming pane zero', () => {
|
||||
expect(resolveActivePaneTarget('%1:0\n%18:1\n')).toBe('%18');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAvailable', () => {
|
||||
it('should return true when tmux is found', () => {
|
||||
mockedExecSync.mockImplementation((cmd: string) => {
|
||||
|
||||
Reference in New Issue
Block a user