open-design/apps/daemon/tests/craft-route.test.ts
Cursor Agent 712c18dbc2
feat(cli): od atoms / skills / design-systems / craft / status / version + marketplace search
Plan H2 + H3 + H4 / spec §12.2 / §11.7.

Closes the CLI parity remainder: every UI feature reachable via
/api/* now has a matching CLI verb.

  od atoms list                       List first-party atoms.
  od atoms show <id>                  Print one atom (status + taskKinds).

  od skills list / show <id>          Wraps GET /api/skills{,/:id}.
  od design-systems list / show <id>  Wraps GET /api/design-systems{,/:id}.
  od craft list / show <id>           NEW endpoints GET /api/craft{,/:id}
                                      walk the craft/ directory and
                                      emit { id, label, bytes }.
  od status                           Alias of `od daemon status`.
  od version                          Wraps GET /api/version; --json
                                      emits the structured shape.

  od marketplace search "<query>"     New verb. Walks every configured
                  [--tag <tag>]         marketplace's plugins[] and
                                       matches by substring on
                                       name + description + tags.

The HTTP additions are GET /api/craft (catalog summary) and
GET /api/craft/:id (raw markdown body), both safe-slug-validated.

Daemon tests: 1472 → 1475 (+3 cases on craft-route covering catalog
shape, malformed slug rejection, miss-as-404).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 12:41:06 +00:00

56 lines
1.8 KiB
TypeScript

// Plan §3.H2 — `/api/craft` and `/api/craft/:id` route shape.
//
// `od craft list` and `od craft show <id>` are thin HTTP wrappers
// around these endpoints. We pin the JSON shape so the CLI doesn't
// drift from the daemon contract.
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/craft', () => {
it('returns a craft array of { id, label, bytes }', async () => {
const resp = await fetch(`${baseUrl}/api/craft`);
expect(resp.status).toBe(200);
const body = (await resp.json()) as { craft?: Array<{ id: string; label: string; bytes: number }> };
expect(Array.isArray(body.craft)).toBe(true);
if (body.craft && body.craft.length > 0) {
const first = body.craft[0]!;
expect(typeof first.id).toBe('string');
expect(typeof first.label).toBe('string');
expect(typeof first.bytes).toBe('number');
}
});
});
describe('GET /api/craft/:id', () => {
it('rejects malformed slugs with 400', async () => {
const resp = await fetch(`${baseUrl}/api/craft/Bad%20ID`);
expect([400, 404]).toContain(resp.status);
});
it('returns 404 for an unknown slug', async () => {
const resp = await fetch(`${baseUrl}/api/craft/no-such-craft-section-9z`);
expect(resp.status).toBe(404);
});
});