mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
11f2b994e5
The agent guide still described the old one-shot od run shorthand, while daemon startup duplicated lifecycle code and could exit after the top-level dispatcher completed. This aligns the documented headless path with the current project creation and run start flow, and routes daemon start through the shared startup runtime so the process stays alive until termination. Constraint: Issue #3454 reports the integrations guide no longer matches current CLI behavior Rejected: Keep documenting the old od run --plugin shorthand | it no longer reflects the project plus conversation flow users must run Confidence: high Scope-risk: narrow Directive: Keep CLI guide snippets backed by regression tests when the od command flow changes Tested: pnpm exec vitest run -c vitest.config.ts tests/cli-startup.test.ts tests/daemon-lifecycle.test.ts Tested: pnpm exec vitest run -c vitest.config.ts tests/components/use-everywhere-agent-guide.test.ts Tested: pnpm --filter @open-design/daemon typecheck Tested: pnpm --filter @open-design/web typecheck Tested: git diff --check Not-tested: lint unavailable because affected packages have no lint script Related: #3454
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
// Plan §3.F2 / spec §11.7 — daemon lifecycle endpoints.
|
|
//
|
|
// `od daemon status` and `od daemon stop` (both Phase 1.5) talk to the
|
|
// new /api/daemon/status + /api/daemon/shutdown routes. This suite spins
|
|
// the real Express app via `startServer` and asserts the public shape so
|
|
// the CLI can rely on it without re-checking each release.
|
|
|
|
import type http from 'node:http';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import { startServer } from '../src/server.js';
|
|
|
|
let server: http.Server;
|
|
let baseUrl: string;
|
|
let shutdown: (() => Promise<void> | void) | undefined;
|
|
|
|
beforeAll(async () => {
|
|
const started = (await startServer({ port: 0, returnServer: true })) as {
|
|
url: string;
|
|
server: http.Server;
|
|
shutdown?: () => Promise<void> | void;
|
|
};
|
|
baseUrl = started.url;
|
|
server = started.server;
|
|
shutdown = started.shutdown;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await Promise.resolve(shutdown?.());
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
});
|
|
|
|
describe('GET /api/daemon/status', () => {
|
|
it('returns a runtime snapshot covering version + bindHost + port + pid + installedPlugins', async () => {
|
|
const resp = await fetch(`${baseUrl}/api/daemon/status`);
|
|
expect(resp.status).toBe(200);
|
|
const body = (await resp.json()) as {
|
|
ok: boolean;
|
|
version: unknown;
|
|
bindHost: unknown;
|
|
port: unknown;
|
|
sandboxMode: boolean;
|
|
sandbox: { enabled: boolean };
|
|
pid: unknown;
|
|
installedPlugins: unknown;
|
|
shuttingDown: boolean;
|
|
namespace?: unknown;
|
|
};
|
|
expect(body.ok).toBe(true);
|
|
expect(typeof body.version === 'string' || typeof body.version === 'object').toBe(true);
|
|
expect(typeof body.bindHost).toBe('string');
|
|
expect(typeof body.port).toBe('number');
|
|
expect(body.port).toBe(Number(new URL(baseUrl).port));
|
|
expect(typeof body.pid).toBe('number');
|
|
expect(typeof body.installedPlugins).toBe('number');
|
|
expect(body.sandboxMode).toBe(false);
|
|
expect(body.sandbox).toEqual({ enabled: false });
|
|
expect(body.shuttingDown).toBe(false);
|
|
expect(body).not.toHaveProperty('namespace');
|
|
});
|
|
});
|
|
|
|
describe('GET /api/ready', () => {
|
|
it('returns a readiness snapshot for headless launchers', async () => {
|
|
const resp = await fetch(`${baseUrl}/api/ready`);
|
|
expect(resp.status).toBe(200);
|
|
const body = (await resp.json()) as {
|
|
ok: boolean;
|
|
ready: boolean;
|
|
version: unknown;
|
|
};
|
|
|
|
expect(body.ok).toBe(true);
|
|
expect(body.ready).toBe(true);
|
|
expect(typeof body.version === 'string' || typeof body.version === 'object').toBe(true);
|
|
expect(body).not.toHaveProperty('dataDir');
|
|
expect(body).not.toHaveProperty('sandboxMode');
|
|
expect(body).not.toHaveProperty('sandbox');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/daemon/shutdown', () => {
|
|
it('only accepts requests from local-daemon-allowed origins', async () => {
|
|
// Without the local-daemon header, the route is rejected. The
|
|
// exact rejection mode is owned by `requireLocalDaemonRequest`;
|
|
// we just check that a default test client gets a non-2xx.
|
|
const resp = await fetch(`${baseUrl}/api/daemon/shutdown`, {
|
|
method: 'POST',
|
|
headers: { origin: 'https://example.com' },
|
|
});
|
|
expect([401, 403, 404]).toContain(resp.status);
|
|
});
|
|
});
|