test: build every in-process WebServer on port 0 (#440)

The files on the guard's legacy list build new WebServer(0, …) and read server.boundPort after start(), as sse-tile-grid-filter does. Files with several servers read each server's own port; quick-start's local helper lost its port parameter so the literal 0 sits where the server is built. Header comments that named a port say "ephemeral". No assertion changed except where it embedded the port.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Randalix
2026-10-09 16:28:26 +02:00
co-authored by Claude Opus 5.5
parent 3a0cee6b90
commit aa4217b8b1
42 changed files with 163 additions and 204 deletions
+3 -4
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview Phase 5 admin API tests (live server, port 3173).
* @fileoverview Phase 5 admin API tests (live server, ephemeral port).
*
* Covers the admin user-management endpoints: multi-user gate, requireAdmin,
* create (one-time password), patch + last-admin invariant, reset-password,
@@ -16,9 +16,8 @@ import { createUser, invalidateUsersCache } from '../src/user-store.js';
vi.spyOn(TmuxManager, 'isTmuxAvailable').mockReturnValue(true);
const PORT = 3173;
const basic = (u: string, p: string) => 'Basic ' + Buffer.from(`${u}:${p}`).toString('base64');
const url = (p: string) => `http://localhost:${PORT}${p}`;
const url = (p: string) => `http://localhost:${server.boundPort}${p}`;
const admin = { Authorization: basic('root', 'rootpass123'), 'Content-Type': 'application/json' };
const adminNoBody = { Authorization: basic('root', 'rootpass123') };
const regular = { Authorization: basic('joe', 'joepass1234'), 'Content-Type': 'application/json' };
@@ -48,7 +47,7 @@ beforeAll(async () => {
invalidateUsersCache();
await createUser({ username: 'root', role: 'admin', password: 'rootpass123' });
await createUser({ username: 'joe', role: 'user', password: 'joepass1234' });
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
});
+4 -4
View File
@@ -8,14 +8,12 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { WebServer } from '../src/web/server.js';
const PORT = 3197;
describe('reverse-proxy base path: server wiring', () => {
let server: WebServer;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let app: any;
beforeAll(async () => {
server = new WebServer(PORT, false, true, '127.0.0.1', undefined, false, '/codeman');
server = new WebServer(0, false, true, '127.0.0.1', undefined, false, '/codeman');
await server.start();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
app = (server as any).app;
@@ -66,7 +64,9 @@ describe('reverse-proxy base path: server wiring', () => {
const { WebSocket } = await import('ws');
const close = (path: string) =>
new Promise<{ code: number; reason: string }>((resolve) => {
const ws = new WebSocket(`ws://127.0.0.1:${PORT}${path}`, { headers: { origin: `http://127.0.0.1:${PORT}` } });
const ws = new WebSocket(`ws://127.0.0.1:${server.boundPort}${path}`, {
headers: { origin: `http://127.0.0.1:${server.boundPort}` },
});
ws.on('close', (code, reason) => resolve({ code, reason: reason.toString() }));
ws.on('error', (e) => resolve({ code: -1, reason: String(e) }));
});
+5 -5
View File
@@ -20,7 +20,7 @@
* because the mismatch itself needs two viewports to stage against live tmux.
* Without the fix the first assertion below sees one fetch instead of two.
*
* Port: 3252 (capture geometry retry)
* Port: ephemeral
*
* Run: npx vitest run --config config/vitest.browser.config.ts test/capture-geometry-retry.browser.test.ts
*/
@@ -29,15 +29,15 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3252;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
let server: WebServer;
let browser: Browser;
beforeAll(async () => {
server = new WebServer(PORT, false, true); // testMode
server = new WebServer(0, false, true); // testMode
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
}, 60_000);
@@ -176,7 +176,7 @@ async function stubTerminalAtRequestedSize(page: Page, counter: { n: number; url
const WIDER_THAN_ANY_TERMINAL_COLS = 500;
async function openSession(page: Page): Promise<string> {
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => document.body.classList.contains('app-loaded'), { timeout: 10_000 });
// xterm is loaded from /vendor, so the terminal appears a beat after the app.
// Without it `app.terminal.rows` reads 0 and every height comparison below
+5 -5
View File
@@ -14,7 +14,7 @@
* own `json()` call, which is the one place guaranteed to land after the
* headers and before the chunked write.
*
* Port: 3256 (capture load window)
* Port: ephemeral
*
* Run: npx vitest run --config config/vitest.browser.config.ts test/capture-load-window.browser.test.ts
*/
@@ -23,16 +23,16 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3256;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
const MARKER = 'ARRIVED-AFTER-THE-CAPTURE';
let server: WebServer;
let browser: Browser;
beforeAll(async () => {
server = new WebServer(PORT, false, true); // testMode
server = new WebServer(0, false, true); // testMode
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
}, 60_000);
@@ -115,7 +115,7 @@ async function runLoad(page: Page, sessionId: string, source: string): Promise<n
}
async function openSession(page: Page): Promise<string> {
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => document.body.classList.contains('app-loaded'), { timeout: 10_000 });
// xterm loads from /vendor, so the terminal appears a beat after the app.
// Without it every buffer assertion below would throw rather than compare.
+2 -4
View File
@@ -8,8 +8,6 @@ import { WebServer } from '../src/web/server.js';
declare const PathPicker: any; // evaluated inside the page, where it is a global
const PORT = 3193;
describe('Create a case in a custom folder', () => {
let server: WebServer;
let browser: Browser;
@@ -18,11 +16,11 @@ describe('Create a case in a custom folder', () => {
beforeAll(async () => {
parent = mkdtempSync(join(homedir(), 'custom-case-'));
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(`http://localhost:${PORT}`, { waitUntil: 'domcontentloaded' });
await page.goto(`http://localhost:${server.boundPort}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
}, 90000);
+2 -4
View File
@@ -3,8 +3,6 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3196;
const REPORT = {
platform: { environment: 'linux' },
summary: { ok: 1, requiredMissing: 1, optionalMissing: 0, exitCode: 1 },
@@ -46,13 +44,13 @@ describe('Diagnostics panel in a real browser', () => {
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
browser = await chromium.launch({ headless: true });
// A controlling service worker can swallow requests before page.route() sees them, letting the
// real /api/doctor (a forked Node process) answer instead; block it so the stub is reliable.
page = await (await browser.newContext({ serviceWorkers: 'block' })).newPage();
await page.goto(`http://localhost:${PORT}`, { waitUntil: 'domcontentloaded' });
await page.goto(`http://localhost:${server.boundPort}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
await page.evaluate(() => (window as any).app.openAppSettings());
}, 90000);
+6 -7
View File
@@ -4,7 +4,6 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
import { safeRmHomeTree } from './mocks/index.js';
const TEST_PORT = 3110;
const CASES_DIR = join(homedir(), 'codeman-cases');
describe('Edge Cases and Error Handling', () => {
@@ -13,9 +12,9 @@ describe('Edge Cases and Error Handling', () => {
const createdCases: string[] = [];
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterEach(() => {
@@ -263,9 +262,9 @@ describe('Concurrent Session Handling', () => {
let baseUrl: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT + 1, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 1}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -360,9 +359,9 @@ describe('API Request Validation', () => {
let baseUrl: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT + 2, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 2}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+4 -4
View File
@@ -4,7 +4,7 @@
* Tests that file paths displayed in terminal output are clickable
* and open the log viewer window correctly.
*
* Port allocation: 3154 (see CLAUDE.md test port table)
* Port allocation: ephemeral port
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
@@ -14,8 +14,7 @@ import { writeFileSync, mkdirSync, rmSync, appendFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
const TEST_PORT = 3154;
const baseUrl = `http://localhost:${TEST_PORT}`;
let baseUrl: string;
const BROWSER_TIMEOUT = 30000;
// Helper to run agent-browser commands
@@ -97,8 +96,9 @@ describe('File Link Click Tests', () => {
testLogFile = join(testDir, 'test.log');
writeFileSync(testLogFile, '=== Test Log Started ===\n');
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
await new Promise((r) => setTimeout(r, 1000));
// Test if browser is available
+2 -3
View File
@@ -7,7 +7,6 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3192;
const ENV = {
...process.env,
GIT_AUTHOR_NAME: 'T',
@@ -85,7 +84,7 @@ describe('Git status indicator in a real browser', () => {
write('a.txt', '2\n');
write('new file.txt', 'n\n');
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
browser = await chromium.launch({ headless: true });
page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
@@ -96,7 +95,7 @@ describe('Git status indicator in a real browser', () => {
page.on('response', (r) => {
if (r.request().method() === 'PUT' && r.url().endsWith('/api/settings')) settingsPutStatuses.push(r.status());
});
await page.goto(`http://localhost:${PORT}`, { waitUntil: 'domcontentloaded' });
await page.goto(`http://localhost:${server.boundPort}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
repoSession = await createSession(repo);
plainSession = await createSession(plain);
+5 -7
View File
@@ -782,21 +782,19 @@ describe('subagent stop guard helper', () => {
});
// ========== Hook Event API Integration Tests ==========
// Port 3130 reserved for hooks integration tests
// Hooks integration tests use an ephemeral port
import { WebServer } from '../src/web/server.js';
const TEST_PORT = 3130;
describe('Hook Event API', () => {
let server: WebServer;
let baseUrl: string;
let testSessionId: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
// Create a test session
const createRes = await fetch(`${baseUrl}/api/sessions`, {
@@ -976,9 +974,9 @@ describe('Hook Data Sanitization', () => {
let testSessionId: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT + 1, false, true); // Port 3131
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 1}`;
baseUrl = `http://localhost:${server.boundPort}`;
// Create a test session
const createRes = await fetch(`${baseUrl}/api/sessions`, {
+3 -4
View File
@@ -11,15 +11,14 @@ import { flattenOwnerSessionOrder, type TabLayout } from '../src/tab-layout.js';
import { WebServer } from '../src/web/server.js';
import { SseEvent } from '../src/web/sse-events.js';
const PORT = 3168;
describe('Stable HTTP contract (live server)', () => {
let server: WebServer;
const base = `http://localhost:${PORT}`;
let base: string;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
base = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+10 -14
View File
@@ -13,18 +13,14 @@
* Strategy: stub a synthetic .tab-name node and a fake session entry, then
* drive the rename function directly via page.evaluate(). No real PTY/tmux.
*
* Ports: 3164, plus 3165 and 3192 for the two server-backed describes below
* (per MEMORY.md, ports 3150+ for tests)
* Ports: ephemeral, for this and the two server-backed describes below
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3164;
const ORDERING_PORT = 3165;
const LONG_PREFIX_PORT = 3192;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
describe('Inline rename input', () => {
let server: WebServer;
@@ -32,11 +28,12 @@ describe('Inline rename input', () => {
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true); // testMode = true
server = new WebServer(0, false, true); // testMode = true
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
// Wait for app.js to expose window.app and finish constructor init.
await page.waitForFunction(
() =>
@@ -667,11 +664,11 @@ describe('Inline rename write ordering', () => {
type Pending = { body: string; resolve: (response: Response) => void };
beforeAll(async () => {
server = new WebServer(ORDERING_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(`http://localhost:${ORDERING_PORT}`, { waitUntil: 'domcontentloaded' });
await page.goto(`http://localhost:${server.boundPort}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
typeof (window as { app?: unknown }).app !== 'undefined' &&
@@ -1010,14 +1007,13 @@ describe('Inline rename write ordering', () => {
describe('Vertical rail rename editor with a long prefix', () => {
let server: WebServer;
let browser: Browser;
const port = LONG_PREFIX_PORT;
const NAME = 'w3-this_is_a_very_long_valid_prefix: charlie';
let sessionId = '';
beforeAll(async () => {
server = new WebServer(port, false, true);
server = new WebServer(0, false, true);
await server.start();
const res = await fetch(`http://localhost:${port}/api/sessions`, {
const res = await fetch(`http://localhost:${server.boundPort}/api/sessions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: NAME, mode: 'shell' }),
@@ -1048,7 +1044,7 @@ describe('Vertical rail rename editor with a long prefix', () => {
settings
);
const page = await context.newPage();
await page.goto(`http://localhost:${port}`, { waitUntil: 'domcontentloaded' });
await page.goto(`http://localhost:${server.boundPort}`, { waitUntil: 'domcontentloaded' });
// One #sessionTabs list, moved into the rail or the sidebar by the layout.
const row = page.locator(`#sessionTabs .session-tab[data-id="${sessionId}"]`);
await row.waitFor({ state: 'visible', timeout: 15000 });
+4 -5
View File
@@ -4,7 +4,6 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
import { safeRmHomeTree } from './mocks/index.js';
const TEST_PORT = 3115;
const CASES_DIR = join(homedir(), 'codeman-cases');
/**
@@ -18,9 +17,9 @@ describe('Integration Flows', () => {
const createdSessions: string[] = [];
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterEach(() => {
@@ -282,9 +281,9 @@ describe('SSE Event Flow', () => {
const createdSessions: string[] = [];
beforeAll(async () => {
server = new WebServer(TEST_PORT + 1, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 1}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+2 -4
View File
@@ -3,19 +3,17 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3194;
describe('Key tester in a real browser', () => {
let server: WebServer;
let browser: Browser;
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(`http://localhost:${PORT}`, { waitUntil: 'domcontentloaded' });
await page.goto(`http://localhost:${server.boundPort}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
await page.evaluate(() => (window as any).app.openAppSettings());
await page.focus('#keyTesterInput');
+5 -5
View File
@@ -6,7 +6,7 @@
* - Close modal shows "Kill Tmux & OpenCode" (not "Claude Code")
* - needsRefresh handler sends resize
*
* Port: 3211 (opencode UI tests)
* Port: ephemeral
*
* Run: npx vitest run test/opencode-resize.test.ts
*/
@@ -16,8 +16,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3211;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
const HAS_OPENCODE = (() => {
try {
@@ -40,15 +39,16 @@ async function freshPage(): Promise<{ context: BrowserContext; page: Page }> {
}
async function navigateAndWait(page: Page): Promise<void> {
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => document.body.classList.contains('app-loaded'), {
timeout: 5000,
});
}
beforeAll(async () => {
server = new WebServer(PORT, false, true); // testMode
server = new WebServer(0, false, true); // testMode
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
}, 30_000);
+2 -3
View File
@@ -15,7 +15,6 @@ import { WebServer } from '../src/web/server.js';
import { safeRmHomeTree } from './mocks/index.js';
import { homedir } from 'node:os';
import { join } from 'node:path';
const TEST_PORT = 3215;
// Helper to parse SSE events from raw text
function parseSSEEvents(text: string): Array<{ event: string; data: unknown }> {
@@ -106,9 +105,9 @@ describe('Operation Lightspeed', () => {
let baseUrl: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+3 -4
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview Phase 3 ownership-scoping tests (live server, port 3172).
* @fileoverview Phase 3 ownership-scoping tests (live server, ephemeral port).
*
* Verifies multi-user isolation at the API level: case lists are disjoint per user,
* a non-admin cannot read/kill another user's session, workingDir confinement +
@@ -17,14 +17,13 @@ import { canAccessOwned, findSessionOrFail, sessionCapacityState } from '../src/
vi.spyOn(TmuxManager, 'isTmuxAvailable').mockReturnValue(true);
const PORT = 3172;
const basic = (u: string, p: string) => 'Basic ' + Buffer.from(`${u}:${p}`).toString('base64');
let server: WebServer;
let dataDir: string;
let spacesDir: string;
const saved: Record<string, string | undefined> = {};
const url = (p: string) => `http://localhost:${PORT}${p}`;
const url = (p: string) => `http://localhost:${server.boundPort}${p}`;
// Route returns are wrapped in the {success,data} envelope; unwrap to the payload.
async function getJson(p: string, headers: Record<string, string>): Promise<unknown> {
@@ -58,7 +57,7 @@ beforeAll(async () => {
await createUser({ username: 'alice', role: 'user', password: 'alicepass1' });
await createUser({ username: 'bob', role: 'user', password: 'bobpass1234' });
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
});
+4 -5
View File
@@ -18,7 +18,7 @@
* 4. **Only local mux-backed sessions.** A remote session's local pane is its
* ssh client, whose death may be a transport drop.
*
* Port: 3189
* Port: ephemeral
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import { Session } from '../src/session.js';
@@ -33,7 +33,6 @@ import {
shouldCloseCleanlyExitedSession,
} from '../src/pane-exit-sweep.js';
const PORT = 3189;
const AT = 1_700_000_000_000;
describe('isCleanPaneExit', () => {
@@ -144,7 +143,7 @@ describe('the sweep on a real server', () => {
/** A server whose mux reports `exit` for every pane, seen by `reads` reads. */
const build = (exit: PaneExit | undefined, reads: number) => {
const web = new WebServer(PORT, false, true);
const web = new WebServer(0, false, true);
server = web;
const mux = (web as unknown as { mux: Record<string, unknown> }).mux;
const state = { exit, reads };
@@ -245,7 +244,7 @@ describe('the sweep on a real server', () => {
it('removes the session for real through cleanupSession', async () => {
// No spy on cleanupSession here: the close runs the same path as the X
// button, and the session leaves the server's map.
const web = new WebServer(PORT, false, true);
const web = new WebServer(0, false, true);
server = web;
const mux = (web as unknown as { mux: Record<string, unknown> }).mux;
mux.getPaneExit = () => ({ status: 0, at: AT });
@@ -276,7 +275,7 @@ describe('a session the server is closing', () => {
});
it('is cleared again when the server gives the close up', async () => {
const web = new WebServer(PORT, false, true);
const web = new WebServer(0, false, true);
try {
const s = session();
const sessions = (web as unknown as { sessions: Map<string, Session> }).sessions;
+4 -5
View File
@@ -9,7 +9,7 @@
* closes sessions unattended, which turns that from an occasional loss into a
* routine one.
*
* Port: 3188
* Port: ephemeral
*/
import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
@@ -18,8 +18,6 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { WebServer } from '../src/web/server.js';
import { pasteImageDirInUseByOtherSession } from '../src/web/paste-image-gc.js';
const PORT = 3188;
describe('pasteImageDirInUseByOtherSession', () => {
const none = new Set<string>();
const check = (
@@ -108,12 +106,13 @@ describe('pasteImageDirInUseByOtherSession', () => {
describe('deleting a session that shares its working directory', () => {
let server: WebServer;
let workingDir: string;
const base = `http://localhost:${PORT}`;
let base: string;
beforeAll(async () => {
workingDir = mkdtempSync(join(tmpdir(), 'codeman-paste-shared-'));
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
base = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+8 -8
View File
@@ -5,7 +5,7 @@
* terminal input latency, subagent window management, settings modal,
* SSE connection speed, and rendering under load.
*
* Port: 3210 (perf-browser tests)
* Port: ephemeral
*
* Run: npx vitest run test/perf-browser.test.ts
*/
@@ -14,8 +14,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3210;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
// Thresholds (ms)
const THRESHOLDS = {
@@ -53,7 +52,7 @@ async function freshPage(): Promise<{ context: BrowserContext; page: Page }> {
}
async function navigateAndWait(page: Page): Promise<void> {
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
// Wait for app.js to initialize (app-loaded class added to body)
await page.waitForFunction(() => document.body.classList.contains('app-loaded'), {
timeout: 5000,
@@ -120,8 +119,9 @@ async function getHeapMB(page: Page): Promise<number> {
// ─── Setup / Teardown ─────────────────────────────────────
beforeAll(async () => {
server = new WebServer(PORT, false, true); // testMode
server = new WebServer(0, false, true); // testMode
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
}, 30_000);
@@ -144,7 +144,7 @@ describe('Page load performance', () => {
({ context, page } = await freshPage());
const wallTiming = await measure(async () => {
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
});
const navigationTiming = await getBrowserNavigationTiming(page);
@@ -158,7 +158,7 @@ describe('Page load performance', () => {
({ context, page } = await freshPage());
const timing = await measure(async () => {
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => document.body.classList.contains('app-loaded'), {
timeout: 5000,
});
@@ -190,7 +190,7 @@ describe('Page load performance', () => {
it('loading skeleton disappears after init', async () => {
({ context, page } = await freshPage());
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
// Skeleton should be visible initially
const skeletonVisible = await page.locator('.loading-skeleton').isVisible();
+10 -11
View File
@@ -4,7 +4,6 @@ import { existsSync, rmSync, mkdirSync, mkdtempSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
const TEST_PORT = 3299;
const ORIGINAL_HOME = process.env.HOME;
const TEST_HOME = mkdtempSync(join(tmpdir(), 'codeman-quick-start-'));
const CASES_DIR = join(TEST_HOME, 'codeman-cases');
@@ -12,10 +11,10 @@ let webServerModule: Promise<typeof import('../src/web/server.js')> | undefined;
process.env.HOME = TEST_HOME;
async function createTestServer(port: number): Promise<WebServer> {
async function createTestServer(): Promise<WebServer> {
webServerModule ??= import('../src/web/server.js');
const { WebServer: TestWebServer } = await webServerModule;
return new TestWebServer(port, false, true);
return new TestWebServer(0, false, true);
}
afterAll(() => {
@@ -30,9 +29,9 @@ describe('Quick Start API', () => {
const createdCases: string[] = [];
beforeAll(async () => {
server = await createTestServer(TEST_PORT);
server = await createTestServer();
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
}, 30000);
afterEach(() => {
@@ -164,9 +163,9 @@ describe('Session Management', () => {
let baseUrl: string;
beforeAll(async () => {
server = await createTestServer(TEST_PORT + 1);
server = await createTestServer();
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 1}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -223,9 +222,9 @@ describe('Case Management', () => {
const createdCases: string[] = [];
beforeAll(async () => {
server = await createTestServer(TEST_PORT + 2);
server = await createTestServer();
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 2}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -339,9 +338,9 @@ describe('Agent skill injection (agentSkillEnabled)', () => {
const createdCases: string[] = [];
beforeAll(async () => {
server = await createTestServer(TEST_PORT + 4); // 3103
server = await createTestServer();
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 4}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+3 -4
View File
@@ -8,7 +8,7 @@
* - Todo item detection and management
* - Ralph configuration via API
*
* Test port: 3125
* Test port: ephemeral
*/
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
@@ -17,7 +17,6 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
import { safeRmHomeTree } from './mocks/index.js';
const TEST_PORT = 3125;
const CASES_DIR = join(homedir(), 'codeman-cases');
describe('Ralph Integration Tests', () => {
@@ -27,9 +26,9 @@ describe('Ralph Integration Tests', () => {
const createdSessions: string[] = [];
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterEach(() => {
+4 -6
View File
@@ -1,17 +1,15 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { WebServer } from '../src/web/server.js';
const TEST_PORT = 3105;
describe('Scheduled Runs API', () => {
let server: WebServer;
let baseUrl: string;
const createdRuns: string[] = [];
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -181,9 +179,9 @@ describe('Quick Run API', () => {
let baseUrl: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT + 1, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 1}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+6 -8
View File
@@ -11,7 +11,7 @@
* - text/plain bodies kept RAW (the closed "simple request" CSRF vector)
* - WebSocket anti-CSWSH (Origin/Host validated on upgrade → close 4003)
*
* Port: 3167
* Port: ephemeral
*/
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import http from 'node:http';
@@ -19,8 +19,6 @@ import WebSocket from 'ws';
import { WebServer } from '../src/web/server.js';
import { TmuxManager } from '../src/tmux-manager.js';
const PORT = 3167;
vi.spyOn(TmuxManager, 'isTmuxAvailable').mockReturnValue(true);
interface RawResponse {
@@ -32,7 +30,7 @@ interface RawResponse {
/** Raw HTTP request with full control over Host/Origin headers (fetch/undici rewrites Host). */
function raw(method: string, path: string, headers: Record<string, string> = {}, body?: string): Promise<RawResponse> {
return new Promise((resolve, reject) => {
const req = http.request({ host: '127.0.0.1', port: PORT, path, method, headers }, (res) => {
const req = http.request({ host: '127.0.0.1', port: server.boundPort, path, method, headers }, (res) => {
let data = '';
res.on('data', (c) => (data += c));
res.on('end', () => resolve({ status: res.statusCode ?? 0, headers: res.headers, body: data }));
@@ -46,7 +44,7 @@ function raw(method: string, path: string, headers: Record<string, string> = {},
/** Open a WS to `path` with optional Origin and resolve with the close code the server sends. */
function wsCloseCode(path: string, origin?: string): Promise<number> {
return new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://127.0.0.1:${PORT}${path}`, {
const ws = new WebSocket(`ws://127.0.0.1:${server.boundPort}${path}`, {
headers: origin ? { origin } : {},
});
const timer = setTimeout(() => {
@@ -71,7 +69,7 @@ let server: WebServer;
beforeAll(async () => {
delete process.env.CODEMAN_PASSWORD; // guard must work even on the no-auth default
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
});
@@ -87,7 +85,7 @@ describe('Host-header allowlist (anti DNS-rebinding), wired', () => {
});
it('allows a loopback Host', async () => {
const res = await raw('GET', '/api/status', { Host: `localhost:${PORT}` });
const res = await raw('GET', '/api/status', { Host: `localhost:${server.boundPort}` });
expect(res.status).toBe(200);
});
@@ -114,7 +112,7 @@ describe('cross-site Origin / CSRF guard, wired', () => {
});
it('allows a state-changing request from a same-site Origin', async () => {
const res = await raw('POST', PROBE, { Origin: `http://localhost:${PORT}` });
const res = await raw('POST', PROBE, { Origin: `http://localhost:${server.boundPort}` });
expect(res.status).not.toBe(403);
});
+4 -5
View File
@@ -5,7 +5,6 @@ import { join } from 'node:path';
import { homedir, tmpdir } from 'node:os';
import { safeRmHomeTree } from './mocks/index.js';
const TEST_PORT = 3120;
const CASES_DIR = join(homedir(), 'codeman-cases');
/**
@@ -22,9 +21,9 @@ describe('Session Cleanup', () => {
beforeAll(async () => {
testWorkingDir = mkdtempSync(join(tmpdir(), 'codeman-cleanup-test-'));
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterEach(() => {
@@ -218,9 +217,9 @@ describe('Resource Management', () => {
const createdCases: string[] = [];
beforeAll(async () => {
server = new WebServer(TEST_PORT + 1, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 1}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+2 -4
View File
@@ -24,7 +24,7 @@
* server and the persisted record is the only thing left that can say the
* agent was already gone.
*
* Port: 3187
* Port: ephemeral
*/
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
@@ -37,8 +37,6 @@ import type { PaneExit, SessionRemote, SessionDocker, SessionState } from '../sr
import type { MuxSession, TerminalMultiplexer } from '../src/mux-interface.js';
import { hasObservablePaneSession } from '../src/tmux-manager.js';
const PORT = 3187;
const EXIT: PaneExit = { status: 0, at: 1_700_000_000_000 };
/** The two mux members `Session` reads when it decides whether the field applies. */
@@ -294,7 +292,7 @@ describe('a pane read reaching the session record', () => {
});
const build = () => {
const web = new WebServer(PORT, false, true);
const web = new WebServer(0, false, true);
const mux = (web as unknown as { mux: Record<string, unknown> }).mux;
const session = localMuxSession();
(web as unknown as { sessions: Map<string, Session> }).sessions.set(session.id, session);
+2 -4
View File
@@ -2,8 +2,6 @@ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import { mkdirSync } from 'node:fs';
import { WebServer } from '../src/web/server.js';
const TEST_PORT = 3102;
describe('Interactive Session Lifecycle', () => {
let server: WebServer;
let baseUrl: string;
@@ -12,9 +10,9 @@ describe('Interactive Session Lifecycle', () => {
// The 'custom working directory' test creates a session in /tmp/test; workingDir
// validation requires the dir to exist, so ensure it does (idempotent, CI-safe).
mkdirSync('/tmp/test', { recursive: true });
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+5 -5
View File
@@ -18,15 +18,14 @@
* The CI gate's guard on the same handlers is the source check in
* test/shift-enter-keypress-swallowed.test.ts.
*
* Port: 3267 (per CLAUDE.md, ports 3150+ for tests)
* Port: ephemeral
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3267;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
const MAIN_ID = 'shift-enter-probe-main';
const PANE_B_ID = 'shift-enter-probe-pane-b';
@@ -45,11 +44,12 @@ describe('Shift/Ctrl+Enter through the shipped key handlers (keypress must be sw
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
await page.evaluate(
({ paneBId }) => {
@@ -4,8 +4,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3177;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
describe('split-pane auto-collapse in a real browser', () => {
let server: WebServer;
@@ -13,11 +12,12 @@ describe('split-pane auto-collapse in a real browser', () => {
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
}, 90000);
@@ -3,8 +3,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3176;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
describe('split-pane orchestration in a real browser', () => {
let server: WebServer;
@@ -12,11 +11,12 @@ describe('split-pane orchestration in a real browser', () => {
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
}, 90000);
+4 -4
View File
@@ -3,8 +3,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3175;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
describe('TerminalTile in a real browser', () => {
let server: WebServer;
@@ -12,11 +11,12 @@ describe('TerminalTile in a real browser', () => {
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
}, 90000);
+2 -3
View File
@@ -17,7 +17,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { WebServer } from '../src/web/server.js';
const TEST_PORT = 3119;
const LOCAL_ORIGIN = 'http://localhost:5173';
/** Open /api/events, read the response headers, then abort — it never ends on its own. */
@@ -42,9 +41,9 @@ describe('GET /api/events header inheritance', () => {
let baseUrl: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+4 -6
View File
@@ -5,8 +5,6 @@ import { safeRmHomeTree } from './mocks/index.js';
import { homedir } from 'node:os';
import { join } from 'node:path';
const TEST_PORT = 3107;
// Helper to parse SSE events
function parseSSEEvents(text: string): Array<{ event: string; data: unknown }> {
const events: Array<{ event: string; data: unknown }> = [];
@@ -40,9 +38,9 @@ describe('SSE Events', () => {
let baseUrl: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -186,9 +184,9 @@ describe('SSE Event Types', () => {
let baseUrl: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT + 1, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT + 1}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+1 -1
View File
@@ -13,7 +13,7 @@ import { WebServer } from '../src/web/server.js';
type Hint = { owner?: string; username?: string; adminOnly?: boolean; sessionScoped?: boolean } | undefined;
function hintFor(event: string, payload: Record<string, unknown>, owners: Record<string, string> = {}): Hint {
const server = new WebServer(3999, false, true) as unknown as {
const server = new WebServer(0, false, true) as unknown as {
sessions: Map<string, { owner?: string }>;
deriveSseHint(event: string, data: unknown): Hint;
};
+2 -4
View File
@@ -4,8 +4,6 @@ import { safeRmHomeTree } from './mocks/index.js';
import { homedir } from 'node:os';
import { join } from 'node:path';
const TEST_PORT = 3212;
// Helper to parse SSE events from raw text
function parseSSEEvents(text: string): Array<{ event: string; data: unknown }> {
const events: Array<{ event: string; data: unknown }> = [];
@@ -76,9 +74,9 @@ describe('SSE Subscription Filtering', () => {
let baseUrl: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+2 -4
View File
@@ -22,8 +22,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { WebServer } from '../src/web/server.js';
const TEST_PORT = 3183;
/**
* Fetch and fully drain the body. Both halves matter for teardown: an unconsumed
* body leaves undici holding the socket, and a pooled keep-alive socket makes
@@ -40,9 +38,9 @@ describe('static asset Cache-Control headers', () => {
let baseUrl: string;
beforeAll(async () => {
server = new WebServer(TEST_PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${TEST_PORT}`;
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
+6 -6
View File
@@ -14,15 +14,14 @@
* Browser-driven, so it is excluded from `npm run test:ci` like the other
* Playwright suites. Run locally: npm run test:browser -- test/terminal-copy-shortcut.test.ts
*
* Port: 3174 (per MEMORY.md, ports 3150+ for tests)
* Port: ephemeral
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3174;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
const IME_PUNCTUATION = ',。!?;:“”、《》、()';
describe('terminal Ctrl+C smart copy', () => {
@@ -31,12 +30,13 @@ describe('terminal Ctrl+C smart copy', () => {
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ permissions: ['clipboard-read', 'clipboard-write'] });
page = await context.newPage();
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
// The first write after load can be dropped while the app finishes wiring
// its render pipeline, so poll until one really lands in the buffer.
@@ -226,7 +226,7 @@ describe('terminal Ctrl+C smart copy', () => {
const touchContext = await browser.newContext({ hasTouch: true });
try {
const touchPage = await touchContext.newPage();
await touchPage.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await touchPage.goto(baseUrl, { waitUntil: 'domcontentloaded' });
const touchChunks = await captureImeInput(touchPage);
expect(touchChunks.join('')).toBe('中文' + IME_PUNCTUATION);
} finally {
@@ -20,15 +20,14 @@
* Playwright suites. Run locally:
* npm run test:browser -- test/terminal-keycode229-recovery.browser.test.ts
*
* Port: 3186 (per CLAUDE.md, ports 3150+ for tests)
* Port: ephemeral
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3186;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
describe('orphaned terminal input recovery wiring', () => {
let server: WebServer;
@@ -36,11 +35,12 @@ describe('orphaned terminal input recovery wiring', () => {
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
await page.waitForFunction(() => (window as any).app?._keyCode229Recovery, null, { timeout: 30000 });
}, 90000);
+5 -5
View File
@@ -18,15 +18,14 @@
* the trip math is pure and the dispose path is a couple of property
* mutations, both of which run on any page where app.js loaded.
*
* Port: 3166 (per MEMORY.md, ports 3150+ for tests)
* Port: ephemeral
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3166;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
describe('WebGL longtask auto-fallback', () => {
let server: WebServer;
@@ -34,11 +33,12 @@ describe('WebGL longtask auto-fallback', () => {
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
baseUrl = `http://localhost:${server.boundPort}`;
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
// Wait for constants.js + app.js to have loaded — both expose globals.
await page.waitForFunction(
() =>
+2 -3
View File
@@ -5,7 +5,6 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3195;
const SECRET = 'SUPERSECRET-topic-123';
describe('Webhook settings in a real browser', () => {
@@ -30,11 +29,11 @@ describe('Webhook settings in a real browser', () => {
await new Promise<void>((r) => receiver.listen(0, '127.0.0.1', r));
receiverPort = (receiver.address() as AddressInfo).port;
server = new WebServer(PORT, false, true);
server = new WebServer(0, false, true);
await server.start();
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
await page.goto(`http://localhost:${PORT}`, { waitUntil: 'domcontentloaded' });
await page.goto(`http://localhost:${server.boundPort}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
await page.evaluate(() => (window as any).app.openAppSettings());
await page.waitForSelector('#webhookGroup', { state: 'attached' });
+2 -3
View File
@@ -12,12 +12,11 @@
* real WebServer in test mode for the passwordless one, where the index route
* itself has to answer.
*
* Port: 3198
* Port: ephemeral
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { WebServer } from '../src/web/server.js';
const PORT = 3198;
const lostFrame = { 'sec-fetch-dest': 'iframe', 'sec-fetch-mode': 'navigate', accept: 'text/html,*/*;q=0.8' };
describe('landing-page reload of a proxied dashboard, passwordless install', () => {
@@ -25,7 +24,7 @@ describe('landing-page reload of a proxied dashboard, passwordless install', ()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let app: any;
beforeAll(async () => {
server = new WebServer(PORT);
server = new WebServer(0);
await server.start();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
app = (server as any).app;
+1 -1
View File
@@ -34,7 +34,7 @@ function client() {
/** The server's real event → routing-hint derivation, without starting the server. */
function serverHint(event: string, data: unknown): SseRoutingHint | undefined {
const server = new WebServer(3999, false, true) as unknown as {
const server = new WebServer(0, false, true) as unknown as {
deriveSseHint(event: string, data: unknown): SseRoutingHint | undefined;
};
return server.deriveSseHint(event, data);