mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-12 02:09:42 +02:00
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:
co-authored by
Claude Opus 5.5
parent
3a0cee6b90
commit
aa4217b8b1
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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) }));
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`, {
|
||||
|
||||
@@ -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
@@ -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,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 () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
@@ -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 () => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user