mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-13 18:29:32 +01:00
Gate inspect, stage and submit behind the synced agentCoordinationEnabled setting (default OFF, App Settings > Cross-session features, zh-CN strings). While off, all three routes answer 403 FORBIDDEN naming the setting, checked before session lookup; the CLI verbs print it verbatim. Remember the attributed Codex rollout per session and re-verify only its head on later calls, rescanning only date directories not older than it for a /new conflict. The first walk reads 1 KiB prefixes first, so a store of 2,100+ unrelated rollouts still attributes. Answer unexpected inspection errors with 422 OPERATION_FAILED and log the session id and error code only. Read the guarded composer glyph and footer from the CLI registry: codex workDetect.promptGlyph and a new workDetect.composerFooterLine capability, compiled through compileVersionRegex. Codex footer effort words are one shared list with modelDetect.screenLine. Fix the wiki API reference link, frame the versioning policy entry as a case of the experimental rule, join the CLAUDE.md Agent CLI paragraph, and update the changeset, skill reference and architecture notes.
100 lines
4.1 KiB
TypeScript
100 lines
4.1 KiB
TypeScript
/**
|
|
* @fileoverview `new WebServer(0, …)`: the OS picks the port and every reader of
|
|
* `this.port` sees that number, not the 0 that was asked for (#440).
|
|
*
|
|
* Port: ephemeral.
|
|
*/
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import type { AddressInfo } from 'node:net';
|
|
import { WebServer } from '../src/web/server.js';
|
|
|
|
describe('WebServer on an ephemeral port', () => {
|
|
let server: WebServer;
|
|
/** Built BEFORE start(), as setupRoutes() builds it, so a by-value snapshot would read 0. */
|
|
let ctx: { port: number };
|
|
const savedApiUrl = process.env.CODEMAN_API_URL;
|
|
/** The socket's own answer, so each test stands on its own instead of trusting boundPort. */
|
|
const socketPort = () =>
|
|
((server as unknown as { app: { server: { address(): AddressInfo } } }).app.server.address() as AddressInfo).port;
|
|
|
|
beforeAll(async () => {
|
|
server = new WebServer(0, false, true);
|
|
ctx = (server as unknown as { createRouteContext(): { port: number } }).createRouteContext();
|
|
await server.start();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await server.stop();
|
|
if (savedApiUrl === undefined) delete process.env.CODEMAN_API_URL;
|
|
else process.env.CODEMAN_API_URL = savedApiUrl;
|
|
});
|
|
|
|
it('reports the port the OS assigned', async () => {
|
|
expect(socketPort()).toBeGreaterThan(0);
|
|
expect(server.boundPort).toBe(socketPort());
|
|
const res = await fetch(`http://127.0.0.1:${server.boundPort}/api/status`);
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it('the route context reads it live (the tunnel start gets the real port, not 0)', () => {
|
|
expect(ctx.port).toBe(socketPort());
|
|
});
|
|
|
|
it('registers bounded agent routes with the server route modules', async () => {
|
|
const { Session } = await import('../src/session.js');
|
|
const session = new Session({ id: 'agent-route-wiring', workingDir: process.env.HOME!, mode: 'shell' });
|
|
const sessions = (server as unknown as { sessions: Map<string, typeof session> }).sessions;
|
|
sessions.set(session.id, session);
|
|
const { mkdirSync, rmSync, writeFileSync } = await import('node:fs');
|
|
const { dirname } = await import('node:path');
|
|
const { SETTINGS_PATH } = await import('../src/web/route-helpers.js');
|
|
const setEnabled = (on: boolean) => {
|
|
mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
|
|
writeFileSync(SETTINGS_PATH, JSON.stringify({ agentCoordinationEnabled: on }));
|
|
};
|
|
try {
|
|
const prefix = `http://127.0.0.1:${server.boundPort}/api/v1/sessions/${session.id}`;
|
|
// Opt-in: off, the /api/v1 alias of every route refuses with 403 FORBIDDEN.
|
|
setEnabled(false);
|
|
for (const [method, verb] of [
|
|
['GET', 'inspect'],
|
|
['POST', 'stage'],
|
|
['POST', 'submit'],
|
|
] as const) {
|
|
const refused = await fetch(prefix + '/' + verb, {
|
|
method,
|
|
...(method === 'POST' ? { headers: { 'Content-Type': 'application/json' }, body: '{}' } : {}),
|
|
});
|
|
expect(refused.status).toBe(403);
|
|
const body = await refused.json();
|
|
expect(body.errorCode).toBe('FORBIDDEN');
|
|
expect(body.error).toContain('agentCoordinationEnabled');
|
|
}
|
|
setEnabled(true);
|
|
const inspected = await fetch(prefix + '/inspect');
|
|
expect(inspected.status).toBe(200);
|
|
expect((await inspected.json()).data.source).toBe('pane-snapshot');
|
|
for (const [verb, body] of [
|
|
['stage', { message: 'hello' }],
|
|
['submit', { stageId: '11111111-1111-4111-8111-111111111111', confirmSha: 'a'.repeat(64) }],
|
|
] as const) {
|
|
const response = await fetch(prefix + '/' + verb, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
expect(response.status).toBe(409);
|
|
expect((await response.json()).errorCode).toBe('CONFLICT');
|
|
}
|
|
} finally {
|
|
rmSync(SETTINGS_PATH, { force: true });
|
|
sessions.delete(session.id);
|
|
await session.stop(false);
|
|
}
|
|
});
|
|
|
|
it('exports the real port to the panes as CODEMAN_API_URL', () => {
|
|
expect(process.env.CODEMAN_API_URL).toBe(`http://127.0.0.1:${socketPort()}`);
|
|
});
|
|
});
|