feat: expand intelligent investigation workspace
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent ddd03dbff6
commit c0e4231b83
223 changed files with 13155 additions and 5304 deletions
@@ -0,0 +1,51 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createAgentApiClient } from "./client";
import type { AnswerViewStateWrite } from "./answer-view-state";
const remote = { version: "answer-view-state@1", message_id: "message", block_id: "block", revision: 2,
schema_digest: "digest", nodes: {}, block: { evidenceFilter: "all" }, updated_at: null };
const write: AnswerViewStateWrite = { message_id: "message", block_id: "block", expected_revision: 2, schema_digest: "digest", request_id: "intent", nodes: {}, block: { evidenceFilter: "all" } };
const token = (subject: string) => `header.${btoa(JSON.stringify({ iss: "https://identity.local", sub: subject }))}.signature`;
afterEach(() => vi.unstubAllGlobals());
describe("authorized answer view state API", () => {
it("keeps the message and block identity and uses the configured auth/project context", async () => {
const fetcher = vi.fn().mockImplementation(async () => Response.json(remote)); vi.stubGlobal("fetch", fetcher);
const api = createAgentApiClient("https://agent.local", { getAccessToken: async () => token("alice"), getProjectId: async () => "project" });
await expect(api.getAnswerViewState("session/one", "message", "block")).resolves.toEqual(remote);
await api.putAnswerViewState("session/one", write);
expect(fetcher.mock.calls[0][0]).toBe("https://agent.local/api/v1/session/session%2Fone/ui-state?message_id=message&block_id=block");
expect(fetcher.mock.calls[1][0]).toBe("https://agent.local/api/v1/session/session%2Fone/ui-state");
expect(fetcher.mock.calls[1][1].body).toBe(JSON.stringify(write));
for (const [, init] of fetcher.mock.calls) {
expect(new Headers(init.headers).get("Authorization")).toBe(`Bearer ${token("alice")}`);
expect(new Headers(init.headers).get("X-Project-Id")).toBe("project");
}
});
it("retains structured conflict details for the explicit user choices", async () => {
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ code: "ANSWER_VIEW_STATE_CONFLICT", detail: "new revision", current: remote }, { status: 409 })));
await expect(createAgentApiClient("https://agent.local").putAnswerViewState("session", write)).rejects.toMatchObject({
status: 409, code: "ANSWER_VIEW_STATE_CONFLICT", current: remote,
});
});
it("rejects writes if a mutable token provider changes subject in the same project", async () => {
const fetcher = vi.fn(async () => Response.json(remote)); vi.stubGlobal("fetch", fetcher);
let subject = "alice";
const api = createAgentApiClient("https://agent.local", { getAccessToken: async () => token(subject), getProjectId: async () => "same-project" });
await api.getAnswerViewState("session", "message", "block"); subject = "bob";
await expect(api.putAnswerViewState("session", write)).rejects.toMatchObject({ code: "ANSWER_VIEW_IDENTITY_CHANGED" });
expect(fetcher).toHaveBeenCalledTimes(1);
});
it("rejects a late read if identity changes while the request is in flight", async () => {
let finish!: (response: Response) => void;
const fetcher = vi.fn(() => new Promise<Response>((resolve) => { finish = resolve; })); vi.stubGlobal("fetch", fetcher);
let subject = "alice";
const api = createAgentApiClient("https://agent.local", { getAccessToken: async () => token(subject) });
const read = api.getAnswerViewState("session", "message", "block");
await vi.waitFor(() => expect(fetcher).toHaveBeenCalledTimes(1)); subject = "bob"; finish(Response.json(remote));
await expect(read).rejects.toMatchObject({ code: "ANSWER_VIEW_IDENTITY_CHANGED" });
});
it("rejects remote state containing unknown executable fields", async () => {
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ ...remote, nodes: { x: { type: "Evidence", open: true, script: "bad" } } })));
await expect(createAgentApiClient("https://agent.local").getAnswerViewState("session", "message", "block")).rejects.toThrow();
});
});