Files
nexu-io__open-design/apps/daemon/tests/daemon-lifecycle.test.ts
Sid 11f2b994e5 Keep integrations CLI guidance aligned with run flow (#3465)
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
2026-06-03 09:23:15 +00:00

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);
});
});