Files
next-tjwater-frontend/src/features/agent/frontend-action/index.test.ts
T
jiang c0e4231b83
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s
feat: expand intelligent investigation workspace
2026-10-30 17:30:19 +08:00

96 lines
4.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { FrontendActionExecutor } from "./executor";
import { parseFrontendActionRegistry, parseFrontendActionRequest, type FrontendActionResult } from ".";
const storage = new Map<string, string>();
beforeEach(() => {
storage.clear();
vi.stubGlobal("sessionStorage", {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => storage.set(key, value)
});
});
describe("SCADA frontend actions", () => {
it("accepts registered view actions and rejects unknown action names", () => {
expect(parseFrontendActionRegistry({
schema_version: "frontend-action-registry@2",
actions: [
{ id: "render_scada_analysis", version: "frontend-action@2" },
{ id: "clear_scada_analysis", version: "frontend-action@2" },
{ id: "zoom_to_map", version: "frontend-action@2" }
]
})?.actions.map((action) => action.id)).toEqual([
"render_scada_analysis",
"clear_scada_analysis",
"zoom_to_map"
]);
expect(parseFrontendActionRequest(createRequest("execute_script"))).toBeNull();
expect(parseFrontendActionRequest(createRequest("render_scada_analysis"))).not.toBeNull();
});
it("submits the real browser output and replays the terminal receipt idempotently", async () => {
const execute = vi.fn(async () => ({
rendered_ids: ["MP01"],
missing_ids: [],
level_counts: { high: 1, medium: 0, low: 0, unrated: 0 },
fitted: true
}));
const submit = vi.fn(async (
_sessionId: string,
_actionId: string,
_result: FrontendActionResult
) => undefined);
const executor = new FrontendActionExecutor(execute, submit);
const request = createRequest("render_scada_analysis");
await executor.handle(request, "session-1");
await executor.handle(request, "session-1");
expect(execute).toHaveBeenCalledTimes(1);
expect(submit).toHaveBeenCalledTimes(2);
expect(submit.mock.calls[0][2]).toMatchObject({
version: "frontend-action-result@2",
status: "succeeded",
output: { rendered_ids: ["MP01"], fitted: true }
});
expect(submit.mock.calls[1][2]).toEqual(submit.mock.calls[0][2]);
});
});
function createRequest(name: string) {
return {
version: "frontend-action@2",
actionId: "action-1",
toolCallId: "call-1",
sessionId: "session-1",
command: { action: name, items: [{ device_id: "MP01", level: "high" }] },
issuedAt: Date.now(),
expiresAt: Date.now() + 15_000
};
}
describe("ordered v2 actions", () => {
it("rejects v1 envelopes, legacy name/params and malformed canonical commands", () => {
const request = createRequest("render_scada_analysis");
expect(parseFrontendActionRequest({ ...request, version: "frontend-action@1" })).toBeNull();
expect(parseFrontendActionRequest({ ...request, name: "render_scada_analysis", params: {} })).toBeNull();
expect(parseFrontendActionRequest({ ...request, command: { action: "zoom_to_map", center: [181, 31], zoom: 14 } })).toBeNull();
});
it("keeps commands in stream order and cancels queued work when a session changes", async () => {
let release!: () => void;
const first = new Promise<void>((resolve) => { release = resolve; });
const execute = vi.fn(async (request, signal) => { await first; signal.throwIfAborted(); return { action: request.command.action }; });
const submit = vi.fn(async () => {});
const executor = new FrontendActionExecutor(execute, submit);
const one = executor.handle(createRequest("render_scada_analysis"), "session-1");
const two = executor.handle({ ...createRequest("render_scada_analysis"), actionId: "action-2" }, "session-1");
await Promise.resolve();
expect(execute).toHaveBeenCalledTimes(1);
executor.cancelAll(); release(); await Promise.all([one, two]);
expect(execute).toHaveBeenCalledTimes(1);
expect(submit.mock.calls.map((call) => (call as unknown[])[2])).toEqual([
expect.objectContaining({ status: "cancelled" }), expect.objectContaining({ status: "cancelled" })
]);
});
});