mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
test: raw test servers listen on port 0 (#440)
http/Fastify servers in daemon-control, deepseek-status-shim, session-input-wait and the three tui tests listen on 0 and read address().port. The two "nothing listens here" ports (a fixed 3243, and the server's port + 1) come from closedPort(): bind 0, read the port, close. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6f87186deb
commit
eceaecf076
6 files changed
+81
-46
No files matched your search
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
|
import type { AddressInfo } from 'node:net';
|
||||||
import {
|
import {
|
||||||
buildBaseUrl,
|
buildBaseUrl,
|
||||||
buildStatusUrl,
|
buildStatusUrl,
|
||||||
@@ -16,7 +17,17 @@ import {
|
|||||||
probeServer,
|
probeServer,
|
||||||
} from '../src/daemon-control.js';
|
} from '../src/daemon-control.js';
|
||||||
|
|
||||||
const PORT = 3216;
|
/**
|
||||||
|
* A port nothing listens on: bind 0, read what the OS handed out, close. Free at the
|
||||||
|
* moment of use, unlike "the server's port + 1", which anything may hold.
|
||||||
|
*/
|
||||||
|
async function closedPort(): Promise<number> {
|
||||||
|
const probe = http.createServer();
|
||||||
|
await new Promise<void>((resolve) => probe.listen(0, '127.0.0.1', resolve));
|
||||||
|
const { port: free } = probe.address() as AddressInfo;
|
||||||
|
await new Promise<void>((resolve) => probe.close(() => resolve()));
|
||||||
|
return free;
|
||||||
|
}
|
||||||
|
|
||||||
describe('buildWebArgs', () => {
|
describe('buildWebArgs', () => {
|
||||||
it('always passes host and port through explicitly', () => {
|
it('always passes host and port through explicitly', () => {
|
||||||
@@ -143,6 +154,7 @@ describe('isProcessAlive', () => {
|
|||||||
|
|
||||||
describe('probeServer', () => {
|
describe('probeServer', () => {
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
|
let port: number;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
server = http.createServer((req, res) => {
|
server = http.createServer((req, res) => {
|
||||||
@@ -157,7 +169,8 @@ describe('probeServer', () => {
|
|||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ success: true, data: { version: '9.9.9' } }));
|
res.end(JSON.stringify({ success: true, data: { version: '9.9.9' } }));
|
||||||
});
|
});
|
||||||
await new Promise<void>((resolve) => server.listen(PORT, '127.0.0.1', resolve));
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
port = (server.address() as AddressInfo).port;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -165,23 +178,23 @@ describe('probeServer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('reports up and reads the version back', async () => {
|
it('reports up and reads the version back', async () => {
|
||||||
const result = await probeServer(`http://127.0.0.1:${PORT}/api/status`);
|
const result = await probeServer(`http://127.0.0.1:${port}/api/status`);
|
||||||
expect(result.up).toBe(true);
|
expect(result.up).toBe(true);
|
||||||
expect(result.version).toBe('9.9.9');
|
expect(result.version).toBe('9.9.9');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('counts a 401 as up, because auth being active proves a server is there', async () => {
|
it('counts a 401 as up, because auth being active proves a server is there', async () => {
|
||||||
const result = await probeServer(`http://127.0.0.1:${PORT}/unauthorized`);
|
const result = await probeServer(`http://127.0.0.1:${port}/unauthorized`);
|
||||||
expect(result.up).toBe(true);
|
expect(result.up).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not mistake an unrelated service squatting on the port for Codeman', async () => {
|
it('does not mistake an unrelated service squatting on the port for Codeman', async () => {
|
||||||
const result = await probeServer(`http://127.0.0.1:${PORT}/foreign`);
|
const result = await probeServer(`http://127.0.0.1:${port}/foreign`);
|
||||||
expect(result.up).toBe(false);
|
expect(result.up).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reports down when nothing is listening', async () => {
|
it('reports down when nothing is listening', async () => {
|
||||||
const result = await probeServer(`http://127.0.0.1:${PORT + 1}/api/status`, 1000);
|
const result = await probeServer(`http://127.0.0.1:${await closedPort()}/api/status`, 1000);
|
||||||
expect(result.up).toBe(false);
|
expect(result.up).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest';
|
import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest';
|
||||||
import { execFileSync, spawn } from 'node:child_process';
|
import { execFileSync, spawn } from 'node:child_process';
|
||||||
import { createServer, type Server } from 'node:http';
|
import { createServer, type Server } from 'node:http';
|
||||||
|
import type { AddressInfo } from 'node:net';
|
||||||
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, chmodSync } from 'node:fs';
|
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, chmodSync } from 'node:fs';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import {
|
import {
|
||||||
@@ -25,8 +26,6 @@ import {
|
|||||||
DEEPSEEK_STATE_TO_HOOK_EVENT,
|
DEEPSEEK_STATE_TO_HOOK_EVENT,
|
||||||
} from '../src/deepseek-status-shim.js';
|
} from '../src/deepseek-status-shim.js';
|
||||||
|
|
||||||
const PORT = 3251;
|
|
||||||
|
|
||||||
describe('DeepSeek status shim: provisioning', () => {
|
describe('DeepSeek status shim: provisioning', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetDeepSeekStatusShimForTest();
|
resetDeepSeekStatusShimForTest();
|
||||||
@@ -73,6 +72,7 @@ describe('DeepSeek status shim: provisioning', () => {
|
|||||||
|
|
||||||
describe('DeepSeek status shim: the supervisor contract', () => {
|
describe('DeepSeek status shim: the supervisor contract', () => {
|
||||||
let server: Server | undefined;
|
let server: Server | undefined;
|
||||||
|
let port: number;
|
||||||
const received: Array<{ body: unknown; secret: string | undefined }> = [];
|
const received: Array<{ body: unknown; secret: string | undefined }> = [];
|
||||||
let status = 200;
|
let status = 200;
|
||||||
|
|
||||||
@@ -96,7 +96,10 @@ describe('DeepSeek status shim: the supervisor contract', () => {
|
|||||||
res.end('{}');
|
res.end('{}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
server.listen(PORT, '127.0.0.1', resolve);
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
port = (server!.address() as AddressInfo).port;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => listen());
|
beforeAll(() => listen());
|
||||||
@@ -120,7 +123,7 @@ describe('DeepSeek status shim: the supervisor contract', () => {
|
|||||||
const child = spawn(process.execPath, [path, ...args], {
|
const child = spawn(process.execPath, [path, ...args], {
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
CODEMAN_API_URL: `http://127.0.0.1:${PORT}`,
|
CODEMAN_API_URL: `http://127.0.0.1:${port}`,
|
||||||
CODEMAN_SESSION_ID: 'sess-from-env',
|
CODEMAN_SESSION_ID: 'sess-from-env',
|
||||||
...env,
|
...env,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -451,13 +451,12 @@ describe('POST /api/sessions/:id/input: delivered reports the write, not just th
|
|||||||
* exactly why this regression needs a POST and a real socket to catch.
|
* exactly why this regression needs a POST and a real socket to catch.
|
||||||
*/
|
*/
|
||||||
describe('POST /api/sessions/:id/input over real HTTP: hang-up handling', () => {
|
describe('POST /api/sessions/:id/input over real HTTP: hang-up handling', () => {
|
||||||
const PORT = 3181;
|
let base: string;
|
||||||
const base = `http://127.0.0.1:${PORT}`;
|
|
||||||
let app: FastifyInstance;
|
let app: FastifyInstance;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = (await harness()).app;
|
app = (await harness()).app;
|
||||||
await app.listen({ port: PORT, host: '127.0.0.1' });
|
base = await app.listen({ port: 0, host: '127.0.0.1' });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
|
import type { AddressInfo } from 'node:net';
|
||||||
import { TuiClient, type TuiApprovalEvent, type TuiSseStatusDetail } from '../../src/tui/tui-client.js';
|
import { TuiClient, type TuiApprovalEvent, type TuiSseStatusDetail } from '../../src/tui/tui-client.js';
|
||||||
|
|
||||||
const PORT = 3242;
|
let port: number;
|
||||||
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
let baseUrl: string;
|
||||||
|
|
||||||
interface Connection {
|
interface Connection {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -57,7 +58,9 @@ beforeAll(async () => {
|
|||||||
res.flushHeaders();
|
res.flushHeaders();
|
||||||
connections.push({ url: req.url, headers: req.headers, res });
|
connections.push({ url: req.url, headers: req.headers, res });
|
||||||
});
|
});
|
||||||
await new Promise<void>((resolve) => server.listen(PORT, '127.0.0.1', resolve));
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
port = (server.address() as AddressInfo).port;
|
||||||
|
baseUrl = `http://127.0.0.1:${port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -82,7 +85,7 @@ describe('subscribeEvents', () => {
|
|||||||
let planUsage: unknown = null;
|
let planUsage: unknown = null;
|
||||||
let init: unknown = null;
|
let init: unknown = null;
|
||||||
|
|
||||||
client = new TuiClient({ baseUrl: BASE_URL, password: 's3cret' });
|
client = new TuiClient({ baseUrl, password: 's3cret' });
|
||||||
client.subscribeEvents({
|
client.subscribeEvents({
|
||||||
onInit: (state) => {
|
onInit: (state) => {
|
||||||
init = state;
|
init = state;
|
||||||
@@ -117,7 +120,7 @@ describe('subscribeEvents', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('suppresses the terminal firehose by default and carries the auth header', async () => {
|
it('suppresses the terminal firehose by default and carries the auth header', async () => {
|
||||||
client = new TuiClient({ baseUrl: BASE_URL, password: 's3cret' });
|
client = new TuiClient({ baseUrl, password: 's3cret' });
|
||||||
client.subscribeEvents({});
|
client.subscribeEvents({});
|
||||||
await until(() => connections.length === 1);
|
await until(() => connections.length === 1);
|
||||||
expect(connections[0].url).toBe('/api/events?sessions=tui-no-terminal');
|
expect(connections[0].url).toBe('/api/events?sessions=tui-no-terminal');
|
||||||
@@ -126,7 +129,7 @@ describe('subscribeEvents', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('subscribes to the terminal stream of named sessions when asked', async () => {
|
it('subscribes to the terminal stream of named sessions when asked', async () => {
|
||||||
client = new TuiClient({ baseUrl: BASE_URL });
|
client = new TuiClient({ baseUrl });
|
||||||
client.subscribeEvents({}, { sessionIds: ['a', 'b'] });
|
client.subscribeEvents({}, { sessionIds: ['a', 'b'] });
|
||||||
await until(() => connections.length === 1);
|
await until(() => connections.length === 1);
|
||||||
expect(connections[0].url).toBe('/api/events?sessions=a%2Cb');
|
expect(connections[0].url).toBe('/api/events?sessions=a%2Cb');
|
||||||
@@ -134,7 +137,7 @@ describe('subscribeEvents', () => {
|
|||||||
|
|
||||||
it('reconnects when the stream ends', async () => {
|
it('reconnects when the stream ends', async () => {
|
||||||
const statuses: Array<[string, TuiSseStatusDetail]> = [];
|
const statuses: Array<[string, TuiSseStatusDetail]> = [];
|
||||||
client = new TuiClient({ baseUrl: BASE_URL });
|
client = new TuiClient({ baseUrl });
|
||||||
const stream = client.subscribeEvents(
|
const stream = client.subscribeEvents(
|
||||||
{ onStatus: (status, detail) => statuses.push([status, detail]) },
|
{ onStatus: (status, detail) => statuses.push([status, detail]) },
|
||||||
{ baseBackoffMs: 10, maxBackoffMs: 20 }
|
{ baseBackoffMs: 10, maxBackoffMs: 20 }
|
||||||
@@ -148,7 +151,7 @@ describe('subscribeEvents', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('reconnects when a live stream goes silent, which no socket error reports', async () => {
|
it('reconnects when a live stream goes silent, which no socket error reports', async () => {
|
||||||
client = new TuiClient({ baseUrl: BASE_URL });
|
client = new TuiClient({ baseUrl });
|
||||||
client.subscribeEvents({}, { staleTimeoutMs: 150, checkIntervalMs: 25, baseBackoffMs: 10, maxBackoffMs: 20 });
|
client.subscribeEvents({}, { staleTimeoutMs: 150, checkIntervalMs: 25, baseBackoffMs: 10, maxBackoffMs: 20 });
|
||||||
|
|
||||||
await until(() => connections.length === 1);
|
await until(() => connections.length === 1);
|
||||||
@@ -161,7 +164,7 @@ describe('subscribeEvents', () => {
|
|||||||
it('recommends polling once connecting keeps failing', async () => {
|
it('recommends polling once connecting keeps failing', async () => {
|
||||||
refuse = true;
|
refuse = true;
|
||||||
const details: TuiSseStatusDetail[] = [];
|
const details: TuiSseStatusDetail[] = [];
|
||||||
client = new TuiClient({ baseUrl: BASE_URL });
|
client = new TuiClient({ baseUrl });
|
||||||
const stream = client.subscribeEvents(
|
const stream = client.subscribeEvents(
|
||||||
{ onStatus: (_status, detail) => details.push(detail) },
|
{ onStatus: (_status, detail) => details.push(detail) },
|
||||||
{ baseBackoffMs: 10, maxBackoffMs: 20, pollingAfterFailures: 2 }
|
{ baseBackoffMs: 10, maxBackoffMs: 20, pollingAfterFailures: 2 }
|
||||||
@@ -175,7 +178,7 @@ describe('subscribeEvents', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('stops reconnecting after close, so the process can exit', async () => {
|
it('stops reconnecting after close, so the process can exit', async () => {
|
||||||
client = new TuiClient({ baseUrl: BASE_URL });
|
client = new TuiClient({ baseUrl });
|
||||||
const stream = client.subscribeEvents({}, { baseBackoffMs: 10, maxBackoffMs: 20 });
|
const stream = client.subscribeEvents({}, { baseBackoffMs: 10, maxBackoffMs: 20 });
|
||||||
await until(() => connections.length === 1);
|
await until(() => connections.length === 1);
|
||||||
|
|
||||||
@@ -187,7 +190,7 @@ describe('subscribeEvents', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes every stream the client opened', async () => {
|
it('closes every stream the client opened', async () => {
|
||||||
client = new TuiClient({ baseUrl: BASE_URL });
|
client = new TuiClient({ baseUrl });
|
||||||
client.subscribeEvents({});
|
client.subscribeEvents({});
|
||||||
client.subscribeEvents({});
|
client.subscribeEvents({});
|
||||||
await until(() => connections.length === 2);
|
await until(() => connections.length === 2);
|
||||||
@@ -200,7 +203,7 @@ describe('subscribeEvents', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('refuses to subscribe before the client knows where the server is', () => {
|
it('refuses to subscribe before the client knows where the server is', () => {
|
||||||
const disconnected = new TuiClient({ port: 3999 });
|
const disconnected = new TuiClient({ port });
|
||||||
expect(() => disconnected.subscribeEvents({})).toThrow(/connect\(\)/);
|
expect(() => disconnected.subscribeEvents({})).toThrow(/connect\(\)/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+33
-17
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
|
import type { AddressInfo } from 'node:net';
|
||||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { dataPath } from '../../src/config/instance.js';
|
import { dataPath } from '../../src/config/instance.js';
|
||||||
@@ -32,10 +33,22 @@ import {
|
|||||||
type TuiExecFile,
|
type TuiExecFile,
|
||||||
} from '../../src/tui/tui-client.js';
|
} from '../../src/tui/tui-client.js';
|
||||||
|
|
||||||
const PORT = 3241;
|
/**
|
||||||
/** Nothing ever listens here: the "no server" path. */
|
* A port nothing listens on: bind 0, read what the OS handed out, close. Free at the
|
||||||
const DEAD_PORT = 3243;
|
* moment of use, unlike "the server's port + 1", which anything may hold.
|
||||||
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
*/
|
||||||
|
async function closedPort(): Promise<number> {
|
||||||
|
const probe = http.createServer();
|
||||||
|
await new Promise<void>((resolve) => probe.listen(0, '127.0.0.1', resolve));
|
||||||
|
const { port: free } = probe.address() as AddressInfo;
|
||||||
|
await new Promise<void>((resolve) => probe.close(() => resolve()));
|
||||||
|
return free;
|
||||||
|
}
|
||||||
|
|
||||||
|
let port: number;
|
||||||
|
/** Nothing listens one above the bound port: the "no server" path. */
|
||||||
|
let deadPort: number;
|
||||||
|
let baseUrl: string;
|
||||||
|
|
||||||
interface Recorded {
|
interface Recorded {
|
||||||
method: string;
|
method: string;
|
||||||
@@ -115,7 +128,7 @@ const defaultResponder: Responder = (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function client(overrides: Record<string, unknown> = {}): TuiClient {
|
function client(overrides: Record<string, unknown> = {}): TuiClient {
|
||||||
return new TuiClient({ baseUrl: BASE_URL, timeoutMs: 4000, ...overrides });
|
return new TuiClient({ baseUrl, timeoutMs: 4000, ...overrides });
|
||||||
}
|
}
|
||||||
|
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
@@ -140,7 +153,10 @@ beforeAll(async () => {
|
|||||||
(responder ?? defaultResponder)(req, res, body);
|
(responder ?? defaultResponder)(req, res, body);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await new Promise<void>((resolve) => server.listen(PORT, '127.0.0.1', resolve));
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
port = (server.address() as AddressInfo).port;
|
||||||
|
deadPort = await closedPort();
|
||||||
|
baseUrl = `http://127.0.0.1:${port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -391,8 +407,8 @@ describe('TuiClient remaining API surface', () => {
|
|||||||
|
|
||||||
describe('TuiClient.connect', () => {
|
describe('TuiClient.connect', () => {
|
||||||
it('discovers the loopback server and reports its identity', async () => {
|
it('discovers the loopback server and reports its identity', async () => {
|
||||||
const info = await new TuiClient({ port: PORT, probeTimeoutMs: 1000 }).connect();
|
const info = await new TuiClient({ port, probeTimeoutMs: 1000 }).connect();
|
||||||
expect(info?.baseUrl).toBe(BASE_URL);
|
expect(info?.baseUrl).toBe(baseUrl);
|
||||||
expect(info?.version).toBe('9.9.9');
|
expect(info?.version).toBe('9.9.9');
|
||||||
expect(info?.hostname).toBeTruthy();
|
expect(info?.hostname).toBeTruthy();
|
||||||
expect(info?.authRequired).toBeUndefined();
|
expect(info?.authRequired).toBeUndefined();
|
||||||
@@ -403,17 +419,17 @@ describe('TuiClient.connect', () => {
|
|||||||
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="codeman"' });
|
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="codeman"' });
|
||||||
res.end('Unauthorized');
|
res.end('Unauthorized');
|
||||||
};
|
};
|
||||||
const info = await new TuiClient({ port: PORT, probeTimeoutMs: 1000 }).connect();
|
const info = await new TuiClient({ port, probeTimeoutMs: 1000 }).connect();
|
||||||
expect(info?.baseUrl).toBe(BASE_URL);
|
expect(info?.baseUrl).toBe(baseUrl);
|
||||||
expect(info?.authRequired).toBe(true);
|
expect(info?.authRequired).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null when nothing answers', async () => {
|
it('returns null when nothing answers', async () => {
|
||||||
await expect(new TuiClient({ port: DEAD_PORT, probeTimeoutMs: 500 }).connect()).resolves.toBeNull();
|
await expect(new TuiClient({ port: deadPort, probeTimeoutMs: 500 }).connect()).resolves.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('refuses to talk to an unconnected client', async () => {
|
it('refuses to talk to an unconnected client', async () => {
|
||||||
await expect(new TuiClient({ port: DEAD_PORT }).fetchApprovals()).rejects.toThrow(/not connected/);
|
await expect(new TuiClient({ port: deadPort }).fetchApprovals()).rejects.toThrow(/not connected/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -501,7 +517,7 @@ describe('degraded-mode tmux enumeration', () => {
|
|||||||
describe('attach window sizing', () => {
|
describe('attach window sizing', () => {
|
||||||
/** A client that only ever needs its injected exec: none of this talks to a server. */
|
/** A client that only ever needs its injected exec: none of this talks to a server. */
|
||||||
function sizingClient(exec: TuiExecFile): TuiClient {
|
function sizingClient(exec: TuiExecFile): TuiClient {
|
||||||
return new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec });
|
return new TuiClient({ baseUrl, socket: 'codeman-beta', exec });
|
||||||
}
|
}
|
||||||
|
|
||||||
it('parses the sizing format, and rejects a window tmux could not measure', () => {
|
it('parses the sizing format, and rejects a window tmux could not measure', () => {
|
||||||
@@ -615,7 +631,7 @@ describe('TuiClient.bindSwitchKey', () => {
|
|||||||
calls.push([...args]);
|
calls.push([...args]);
|
||||||
return { stdout: '', stderr: '' };
|
return { stdout: '', stderr: '' };
|
||||||
};
|
};
|
||||||
const client = new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec });
|
const client = new TuiClient({ baseUrl, socket: 'codeman-beta', exec });
|
||||||
return client.bindSwitchKey('M-2', 'codeman-aaaa1111').then(() => {
|
return client.bindSwitchKey('M-2', 'codeman-aaaa1111').then(() => {
|
||||||
const bind = calls.find((args) => args.includes('bind-key'));
|
const bind = calls.find((args) => args.includes('bind-key'));
|
||||||
expect(bind).toBeDefined();
|
expect(bind).toBeDefined();
|
||||||
@@ -672,7 +688,7 @@ describe('TuiClient.clearLeakedAttachBanners', () => {
|
|||||||
}
|
}
|
||||||
return { stdout: '', stderr: '' };
|
return { stdout: '', stderr: '' };
|
||||||
};
|
};
|
||||||
return { client: new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec }), calls };
|
return { client: new TuiClient({ baseUrl, socket: 'codeman-beta', exec }), calls };
|
||||||
}
|
}
|
||||||
|
|
||||||
it('takes down a bar a killed TUI left behind, and puts status back off', async () => {
|
it('takes down a bar a killed TUI left behind, and puts status back off', async () => {
|
||||||
@@ -713,7 +729,7 @@ describe('TuiClient.clearLeakedAttachBanners', () => {
|
|||||||
const exec: TuiExecFile = async () => {
|
const exec: TuiExecFile = async () => {
|
||||||
throw new Error('no server running on /tmp/tmux-1000/codeman-beta');
|
throw new Error('no server running on /tmp/tmux-1000/codeman-beta');
|
||||||
};
|
};
|
||||||
const client = new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec });
|
const client = new TuiClient({ baseUrl, socket: 'codeman-beta', exec });
|
||||||
await expect(client.clearLeakedAttachBanners()).resolves.toBe(0);
|
await expect(client.clearLeakedAttachBanners()).resolves.toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -761,7 +777,7 @@ describe('parseDetachKey', () => {
|
|||||||
|
|
||||||
describe('attach status bar options', () => {
|
describe('attach status bar options', () => {
|
||||||
function optionsClient(exec: TuiExecFile): TuiClient {
|
function optionsClient(exec: TuiExecFile): TuiClient {
|
||||||
return new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec });
|
return new TuiClient({ baseUrl, socket: 'codeman-beta', exec });
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHOW = [
|
const SHOW = [
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
|
import type { AddressInfo } from 'node:net';
|
||||||
import { mkdtempSync, rmSync } from 'node:fs';
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
@@ -35,8 +36,7 @@ import type { SearchResponseData } from '../../src/types/search.js';
|
|||||||
import type { ApprovalItem } from '../../src/web/approval-inbox.js';
|
import type { ApprovalItem } from '../../src/web/approval-inbox.js';
|
||||||
import type { AwayDigestResponse } from '../../src/web/away-digest.js';
|
import type { AwayDigestResponse } from '../../src/web/away-digest.js';
|
||||||
|
|
||||||
const PORT = 3244;
|
let baseUrl: string;
|
||||||
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
||||||
const ROOT = resolve(import.meta.dirname, '..', '..');
|
const ROOT = resolve(import.meta.dirname, '..', '..');
|
||||||
const COLS = 100;
|
const COLS = 100;
|
||||||
const ROWS = 30;
|
const ROWS = 30;
|
||||||
@@ -234,7 +234,7 @@ function childEnv(): Record<string, string> {
|
|||||||
delete env.CODEMAN_PORT;
|
delete env.CODEMAN_PORT;
|
||||||
return {
|
return {
|
||||||
...env,
|
...env,
|
||||||
CODEMAN_API_URL: BASE_URL,
|
CODEMAN_API_URL: baseUrl,
|
||||||
CODEMAN_DATA_DIR: dataDir,
|
CODEMAN_DATA_DIR: dataDir,
|
||||||
CODEMAN_TMUX_SOCKET: 'codeman-tui-e2e',
|
CODEMAN_TMUX_SOCKET: 'codeman-tui-e2e',
|
||||||
CODEMAN_TUI_GLYPHS: 'ascii',
|
CODEMAN_TUI_GLYPHS: 'ascii',
|
||||||
@@ -347,7 +347,8 @@ beforeAll(async () => {
|
|||||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ success: false, error: 'no route', errorCode: 'NOT_FOUND' }));
|
res.end(JSON.stringify({ success: false, error: 'no route', errorCode: 'NOT_FOUND' }));
|
||||||
});
|
});
|
||||||
await new Promise<void>((done) => server.listen(PORT, '127.0.0.1', done));
|
await new Promise<void>((done) => server.listen(0, '127.0.0.1', done));
|
||||||
|
baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user