Files
united-workforce/packages/cli-workflow/__tests__/serve.test.ts
T
xingyue 3ed38c65ec feat(cli): add serve command — Hono HTTP API server
Adds `uncaged-workflow serve` command that exposes workflow data
via a local HTTP API for the upcoming Web UI (RFC #118 Phase 1).

Routes:
- GET /healthz — health check
- GET /api/workflows — list registered workflows
- GET /api/workflows/:name — show workflow details
- GET /api/workflows/:name/history — version history
- GET /api/threads — list threads (optional ?workflow= filter)
- GET /api/threads/running — list running threads
- GET /api/threads/:id — show thread records (parsed JSONL)
- GET /api/cas — list CAS hashes
- GET /api/cas/:hash — get CAS content
- POST /api/cas — store content, returns hash
- DELETE /api/cas/:hash — remove CAS entry
- POST /api/cas/gc — garbage collect

Default: 127.0.0.1:7860, configurable via --port/-p and --host.

Refs: #118
2026-05-08 11:07:13 +08:00

99 lines
3.3 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { createApp } from "../src/commands/serve/app.js";
function buildApp(storageRoot: string) {
const app = createApp(storageRoot);
return {
fetch: (path: string, init?: RequestInit) =>
app.fetch(new Request(`http://localhost${path}`, init)),
};
}
describe("serve /healthz", () => {
test("returns ok", async () => {
const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent");
const res = await fetch("/healthz");
expect(res.status).toBe(200);
const body = (await res.json()) as { ok: boolean };
expect(body.ok).toBe(true);
});
});
describe("serve /api/workflows", () => {
test("returns empty list for missing storage", async () => {
const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent");
const res = await fetch("/api/workflows");
// Registry file won't exist, should return error
expect(res.status).toBe(200);
});
});
describe("serve /api/threads", () => {
test("returns empty list for missing storage", async () => {
const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent");
const res = await fetch("/api/threads");
expect(res.status).toBe(200);
const body = (await res.json()) as { threads: unknown[] };
expect(body.threads).toEqual([]);
});
test("returns 404 for missing thread", async () => {
const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent");
const res = await fetch("/api/threads/nonexistent-id");
expect(res.status).toBe(404);
});
});
describe("serve /api/threads/running", () => {
test("returns empty list for missing storage", async () => {
const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent");
const res = await fetch("/api/threads/running");
expect(res.status).toBe(200);
const body = (await res.json()) as { threads: unknown[] };
expect(body.threads).toEqual([]);
});
});
describe("serve /api/cas", () => {
test("returns empty list for missing storage", async () => {
const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent");
const res = await fetch("/api/cas");
expect(res.status).toBe(200);
const body = (await res.json()) as { hashes: unknown[] };
expect(body.hashes).toEqual([]);
});
test("returns 404 for missing hash", async () => {
const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent");
const res = await fetch("/api/cas/nonexistent-hash");
expect(res.status).toBe(404);
});
});
describe("serve CAS round-trip", () => {
const tmpDir = `/tmp/uncaged-serve-cas-test-${Date.now()}`;
test("put then get", async () => {
const { fetch } = buildApp(tmpDir);
const putRes = await fetch("/api/cas", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: "hello world" }),
});
expect(putRes.status).toBe(201);
const putBody = (await putRes.json()) as { hash: string };
expect(typeof putBody.hash).toBe("string");
const getRes = await fetch(`/api/cas/${putBody.hash}`);
expect(getRes.status).toBe(200);
const getBody = (await getRes.json()) as { content: string };
expect(getBody.content).toBe("hello world");
// cleanup
const delRes = await fetch(`/api/cas/${putBody.hash}`, { method: "DELETE" });
expect(delRes.status).toBe(200);
});
});