Files
fusion/packages/dashboard/app/api/__tests__/client-identity-header.test.ts
gsxdsm cb57093d03 refactor: domain folder layout (types, API, core, engine) (#2398)
## Summary

Wave 17 organizes Fusion into **domain folders** (stacks on #2397).

### Layout
- **core/types/** — board, task, agents, settings, merge, workflow,
mesh, …
- **core/src/** — agents, ai, async-stores, workflows, tasks, config,
db, …
- **dashboard/app/api/** — client, tasks, agents, git, missions,
planning, …
- **engine/src/** — agents, auth, execution, merge, missions, overseer,
worktree, …

Root keepers retained for large entrypoints (`store.ts`, `executor.ts`,
`merger.ts`, …).

Public barrels (`@fusion/core`, `@fusion/engine`, `app/api.ts` → legacy)
stay stable.

## Test plan
- [x] `@fusion/core` typecheck
- [x] `@fusion/engine` typecheck (pre-existing playwright-core noise
only)
- [ ] CI merge gate

**Stack:** #2394 → #2397 → **this PR**
2026-08-03 00:20:53 -07:00

74 lines
2.8 KiB
TypeScript

/*
FNXC:TaskDeleteAttribution 2026-07-26-14:30:
Client half of the delete-attribution invariant. The server can only label a request `operator-ui`
if the dashboard actually sends `x-fusion-client: dashboard-ui`; without this the UI's own Delete
click would report as `api-unattributed` and the four-delete incident would still be unattributable.
Asserted centrally (on `api()`, not on `deleteTask`) because that is where the header is set —
covering the authenticated and unauthenticated branches, the with-caller-headers case, and the
task-delete call itself so no future route has to remember the header.
*/
import { afterEach, describe, expect, it, vi } from "vitest";
import { FUSION_CLIENT_HEADER, FUSION_DASHBOARD_UI_CLIENT } from "@fusion/core";
import { api } from "../client";
import { deleteTask } from "../tasks/tasks-lifecycle";
import * as auth from "../../auth";
function mockFetch() {
return vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "content-type": "application/json" } }),
);
}
function sentHeaders(spy: ReturnType<typeof mockFetch>): Headers {
const init = spy.mock.calls[0]?.[1] as RequestInit | undefined;
return new Headers(init?.headers ?? {});
}
describe("dashboard API client identity header", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("stamps the dashboard-ui client header on unauthenticated requests", async () => {
vi.spyOn(auth, "getAuthToken").mockReturnValue(undefined);
const spy = mockFetch();
await api("/tasks");
expect(sentHeaders(spy).get(FUSION_CLIENT_HEADER)).toBe(FUSION_DASHBOARD_UI_CLIENT);
});
it("stamps the client header on authenticated requests too", async () => {
vi.spyOn(auth, "getAuthToken").mockReturnValue("token-1");
const spy = mockFetch();
await api("/tasks");
expect(sentHeaders(spy).get(FUSION_CLIENT_HEADER)).toBe(FUSION_DASHBOARD_UI_CLIENT);
});
it("keeps the client header when the caller supplies its own headers", async () => {
vi.spyOn(auth, "getAuthToken").mockReturnValue(undefined);
const spy = mockFetch();
await api("/tasks", { method: "POST", headers: { "X-Custom": "1" } });
const headers = sentHeaders(spy);
expect(headers.get(FUSION_CLIENT_HEADER)).toBe(FUSION_DASHBOARD_UI_CLIENT);
expect(headers.get("X-Custom")).toBe("1");
});
it("sends the client header on the task delete call (the surface that was unattributable)", async () => {
vi.spyOn(auth, "getAuthToken").mockReturnValue(undefined);
const spy = mockFetch();
await deleteTask("FN-8600");
const init = spy.mock.calls[0]?.[1] as RequestInit | undefined;
expect(init?.method).toBe("DELETE");
expect(sentHeaders(spy).get(FUSION_CLIENT_HEADER)).toBe(FUSION_DASHBOARD_UI_CLIENT);
});
});