import { readFileSync } from "node:fs"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, rs } from "@rstest/core"; import { listAgents } from "@/core/agents/api"; import { fetch as apiFetch } from "@/core/api/fetcher"; import { staticCapabilityCatalog } from "@/core/capabilities/static"; import { listChannelConnections, listChannelProviders, } from "@/core/channels/api"; import { fetchFeatures } from "@/core/features/api"; import { loadLarkIntegrationStatus } from "@/core/integrations/lark/api"; import { loadMCPConfig } from "@/core/mcp/api"; import { loadMemory } from "@/core/memory/api"; import { createScheduledTask, fetchScheduledTaskRuns, fetchScheduledTasks, fetchThreadScheduledTasks, } from "@/core/scheduled-tasks/api"; import { loadSkills } from "@/core/skills/api"; import { listSubagents } from "@/core/subagents/api"; import { loadSuggestionsConfig } from "@/core/suggestions/api"; import { fetchSubtaskSteps } from "@/core/tasks/api"; import { fetchThreadTokenUsage } from "@/core/threads/api"; const { env } = rs.hoisted(() => ({ env: { NEXT_PUBLIC_STATIC_WEBSITE_ONLY: "true", NEXT_PUBLIC_BACKEND_BASE_URL: "", }, })); rs.mock("@/env", () => ({ env })); const network = rs.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => Response.json({}), ); beforeEach(() => { rs.stubEnv("HOSTNAME", undefined); rs.stubEnv("PORT", undefined); env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY = "true"; env.NEXT_PUBLIC_BACKEND_BASE_URL = ""; network.mockReset(); network.mockResolvedValue(Response.json({})); rs.stubGlobal("fetch", network); }); afterEach(() => { rs.unstubAllEnvs(); rs.unstubAllGlobals(); }); describe("static website API requests", () => { it("loads optional capabilities and empty catalogs without the Gateway", async () => { await expect(fetchFeatures()).resolves.toMatchObject({ agents_api: { enabled: false }, browser_control: { enabled: false }, mcp_tasks: { enabled: false }, subagent_batches: { repository_available: false, worker_running: false }, }); await expect(listChannelProviders()).resolves.toEqual({ enabled: false, providers: [], }); await expect(listChannelConnections()).resolves.toEqual([]); await expect(listAgents()).resolves.toEqual([]); await expect(listSubagents()).resolves.toEqual([]); await expect(loadSuggestionsConfig()).resolves.toMatchObject({ enabled: false, }); await expect(fetchScheduledTasks()).resolves.toEqual([]); await expect(fetchThreadScheduledTasks("demo / thread")).resolves.toEqual( [], ); await expect(fetchScheduledTaskRuns("task / id")).resolves.toEqual([]); await expect(fetchSubtaskSteps("thread", "run", "task")).resolves.toEqual( [], ); await expect(fetchThreadTokenUsage("demo")).resolves.toBeNull(); const history = await apiFetch("/api/threads/demo/messages/page?limit=50"); expect(await history.json()).toEqual({ data: [], has_more: false, next_before_seq: null, }); await expect(loadMemory()).resolves.toMatchObject({ facts: [], user: {}, history: {}, }); expect(network).not.toHaveBeenCalled(); }); it("uses the existing same-origin settings fixtures even with a configured Gateway", async () => { env.NEXT_PUBLIC_BACKEND_BASE_URL = "https://gateway.example/prefix"; network.mockResolvedValueOnce(Response.json({ skills: [] })); await expect(loadSkills()).resolves.toEqual([]); network.mockResolvedValueOnce(Response.json({ mcp_servers: {} })); await expect(loadMCPConfig()).resolves.toEqual({ mcp_servers: {} }); network.mockResolvedValueOnce(Response.json({ installed: false })); await expect(loadLarkIntegrationStatus()).resolves.toMatchObject({ installed: false, }); expect(network.mock.calls.map(([url]) => url)).toEqual([ "http://127.0.0.1:3000/mock/api/skills", "http://127.0.0.1:3000/mock/api/mcp/config", "http://127.0.0.1:3000/mock/api/integrations/lark/status", ]); await expect(fetchFeatures()).resolves.toMatchObject({ agents_api: { enabled: false }, }); expect(network).toHaveBeenCalledTimes(3); }); it("rejects writes and unsupported endpoints locally instead of reporting fake success", async () => { await expect( createScheduledTask({ context_mode: "fresh_thread_per_run", title: "Demo", prompt: "Demo", schedule_type: "once", schedule_spec: {}, timezone: "UTC", }), ).rejects.toThrow("Unavailable in static demo mode"); expect((await apiFetch("/api/new-feature")).status).toBe(404); expect( ( await apiFetch( new Request("http://127.0.0.1:3000/api/subagents", { method: "DELETE", }), ) ).status, ).toBe(405); expect(network).not.toHaveBeenCalled(); }); it.each([ ["demo.internal", undefined, "http://demo.internal:3000"], [undefined, "4000", "http://127.0.0.1:4000"], ["demo.internal", "4000", "http://demo.internal:4000"], ])( "uses the server origin with HOSTNAME=%s and PORT=%s", async (hostname, port, origin) => { rs.stubEnv("HOSTNAME", hostname); rs.stubEnv("PORT", port); await apiFetch("/api/skills"); expect(network).toHaveBeenCalledWith( `${origin}/mock/api/skills`, expect.anything(), ); network.mockClear(); const response = await apiFetch( new Request(`${origin}/api/subagents`, { method: "DELETE" }), ); expect(response.status).toBe(405); expect(network).not.toHaveBeenCalled(); }, ); it("leaves demo assets and unrelated external requests intact", async () => { await apiFetch("/demo/threads/demo/thread.json"); await apiFetch("https://external.example/api/features"); expect(network).toHaveBeenCalledTimes(2); }); it("uses the current browser origin for fixtures and respects cancellation", async () => { rs.stubEnv("HOSTNAME", "demo.internal"); rs.stubEnv("PORT", "4000"); rs.stubGlobal("window", { location: { origin: "http://127.0.0.1:3000" } }); await apiFetch("/api/skills"); expect(network).toHaveBeenCalledWith( "http://127.0.0.1:3000/mock/api/skills", expect.anything(), ); network.mockClear(); const controller = new AbortController(); controller.abort(); await expect( apiFetch("/api/features", { signal: controller.signal }), ).rejects.toThrow(); expect( await (await apiFetch("/api/features", { method: "HEAD" })).text(), ).toBe(""); expect(network).not.toHaveBeenCalled(); }); it("preserves normal backend requests unless the flag is exactly true", async () => { env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY = "false"; network.mockResolvedValue(Response.json({ agents_api: { enabled: true } })); await expect(fetchFeatures()).resolves.toMatchObject({ agents_api: { enabled: true }, }); expect(network).toHaveBeenCalledWith( "/api/features", expect.objectContaining({ credentials: "include" }), ); }); }); it("serves the canonical capability catalog locally and rejects writes", async () => { const response = await apiFetch("/api/capabilities/catalog"); expect(response.status).toBe(200); const catalog = (await response.json()) as { id: string }[]; expect(catalog).toEqual( JSON.parse( readFileSync( path.resolve( process.cwd(), "../backend/packages/harness/deerflow/capabilities/builtin.json", ), "utf8", ), ), ); expect(network).not.toHaveBeenCalled(); const write = await apiFetch("/api/capabilities/installations", { method: "POST", body: "{}", }); expect(write.status).toBe(405); expect(network).not.toHaveBeenCalled(); }); it("projects demo installations without copying secrets or calling the Gateway", async () => { env.NEXT_PUBLIC_BACKEND_BASE_URL = "https://gateway.example/prefix"; network.mockResolvedValueOnce( Response.json({ mcp_servers: { github: { enabled: true, env: { TOKEN: "private" }, url: "https://private.example", capability: { plugin_id: "github" }, }, }, }), ); const response = await apiFetch( "https://gateway.example/prefix/api/capabilities/installations/mcp", ); expect(response.status).toBe(200); const data = await response.json(); expect(data).toMatchObject({ can_manage: false, items: [ { name: "github", plugin_id: "github", adapter: "mcp", installed: true }, ], }); expect(JSON.stringify(data)).not.toContain("private"); expect(network.mock.calls[0]?.[0]).toContain("/mock/api/mcp/config"); }); it("provides Lark, skills and business projections from the owning fixtures", async () => { for (const [adapter, fixture, count] of [ ["lark", { installed: false }, 1], [ "skills", { skills: [ { name: "research", category: "public", enabled: true, description: "Research", }, ], }, 1, ], ["business", { mcp_servers: {} }, 0], ] as const) { network.mockResolvedValueOnce(Response.json(fixture)); const response = await apiFetch( `/api/capabilities/installations/${adapter}`, ); expect(response.status).toBe(200); const data = (await response.json()) as { items: unknown[]; can_manage: boolean; }; expect(data.items).toHaveLength(count); expect(data.can_manage).toBe(false); } const unknown = await apiFetch("/api/capabilities/installations/unknown"); expect(unknown.status).toBe(404); }); it("discovers newly cataloged business adapters without a provider allowlist", async () => { const template = staticCapabilityCatalog.find( (plugin) => plugin.adapter === "business", )!; staticCapabilityCatalog.push({ ...template, id: "future-business" }); try { network.mockResolvedValueOnce( Response.json({ mcp_servers: { future: { enabled: true, capability: { plugin_id: "future-business" }, }, github: { enabled: true, capability: { plugin_id: "github" } }, unknown: { enabled: true, capability: { plugin_id: "unknown" } }, }, }), ); const response = await apiFetch("/api/capabilities/installations/business"); const result = await response.json(); expect(result).toMatchObject({ can_manage: false, items: [{ name: "future", plugin_id: "future-business" }], }); expect(result.items).toHaveLength(1); } finally { staticCapabilityCatalog.pop(); } });