feat(FN-3552): test approval request store and SSE relay behavior
Adds test coverage for the approval request system: strengthens core lifecycle assertions in the approval-request-store tests and adds SSE relay behavior coverage for approval events. Fusion-Task-Id: FN-3552
This commit is contained in:
@@ -166,6 +166,43 @@ describe("ApprovalRequestStore", () => {
|
||||
expect(store.getAuditHistory(created.id).map((e) => e.eventType)).toEqual(["created", "denied"]);
|
||||
});
|
||||
|
||||
it("persists immutable actor snapshots and decision audit metadata", () => {
|
||||
const requester = { ...REQUESTER };
|
||||
const approver = { ...APPROVER };
|
||||
const created = store.create({
|
||||
requester,
|
||||
targetAction: {
|
||||
category: "command_execution",
|
||||
action: "bash",
|
||||
summary: "Run pnpm test",
|
||||
resourceType: "command",
|
||||
resourceId: "pnpm test",
|
||||
},
|
||||
taskId: "FN-3552",
|
||||
});
|
||||
|
||||
requester.actorName = "Mutated Requester";
|
||||
const decided = store.decide(created.id, "approved", { actor: approver, note: "ship it" });
|
||||
approver.actorName = "Mutated Approver";
|
||||
|
||||
const fetched = store.get(created.id);
|
||||
expect(fetched?.requester.actorName).toBe("Executor");
|
||||
expect(decided.decidedAt).toBeTruthy();
|
||||
|
||||
const history = store.getAuditHistory(created.id);
|
||||
expect(history).toHaveLength(2);
|
||||
expect(history[0]).toMatchObject({
|
||||
eventType: "created",
|
||||
actor: { actorId: "agent-1", actorName: "Executor" },
|
||||
});
|
||||
expect(history[1]).toMatchObject({
|
||||
eventType: "approved",
|
||||
actor: { actorId: "user:dashboard", actorName: "Dashboard User" },
|
||||
note: "ship it",
|
||||
});
|
||||
expect(history[1]?.createdAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("rejects invalid transitions", () => {
|
||||
const created = createSampleRequest();
|
||||
|
||||
@@ -179,6 +216,18 @@ describe("ApprovalRequestStore", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps denied requests terminal and disallows completion", () => {
|
||||
const created = createSampleRequest();
|
||||
store.decide(created.id, "denied", { actor: APPROVER, note: "not safe" });
|
||||
|
||||
expect(() => store.markCompleted(created.id, { actor: REQUESTER, note: "should never execute" })).toThrow(
|
||||
"Invalid approval request transition: denied -> completed",
|
||||
);
|
||||
expect(() => store.decide(created.id, "approved", { actor: APPROVER })).toThrow(
|
||||
"Invalid approval request transition: denied -> approved",
|
||||
);
|
||||
});
|
||||
|
||||
it("lists and filters approval requests", () => {
|
||||
const first = createSampleRequest("FN-100");
|
||||
const second = createSampleRequest("FN-200");
|
||||
|
||||
@@ -2,7 +2,13 @@ import { EventEmitter } from "node:events";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Request, Response } from "express";
|
||||
import type { TaskStore, AutomationStore } from "@fusion/core";
|
||||
import { createSSE, disconnectSSEClient, getActiveSSEConnections, markSSEClientAlive } from "../sse.js";
|
||||
import {
|
||||
createSSE,
|
||||
disconnectSSEClient,
|
||||
emitApprovalSseEvent,
|
||||
getActiveSSEConnections,
|
||||
markSSEClientAlive,
|
||||
} from "../sse.js";
|
||||
|
||||
class MockSocket extends EventEmitter {
|
||||
destroyed = false;
|
||||
@@ -101,6 +107,45 @@ afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("approval SSE events", () => {
|
||||
it("relays approval events to connected clients", () => {
|
||||
const connection = openSseConnection("approval-relay");
|
||||
|
||||
emitApprovalSseEvent("approval:requested", { id: "apr-1", status: "pending" });
|
||||
emitApprovalSseEvent("approval:updated", { id: "apr-1", status: "pending" });
|
||||
emitApprovalSseEvent("approval:decided", { id: "apr-1", status: "approved" });
|
||||
|
||||
expect(connection.res.write).toHaveBeenCalledWith(
|
||||
`event: approval:requested\ndata: ${JSON.stringify({ id: "apr-1", status: "pending" })}\n\n`,
|
||||
);
|
||||
expect(connection.res.write).toHaveBeenCalledWith(
|
||||
`event: approval:updated\ndata: ${JSON.stringify({ id: "apr-1", status: "pending" })}\n\n`,
|
||||
);
|
||||
expect(connection.res.write).toHaveBeenCalledWith(
|
||||
`event: approval:decided\ndata: ${JSON.stringify({ id: "apr-1", status: "approved" })}\n\n`,
|
||||
);
|
||||
|
||||
connection.req.emit("close");
|
||||
});
|
||||
|
||||
it("filters project-scoped approval events to matching project connections", () => {
|
||||
const projectA = openSseConnection("approval-project", "project-a");
|
||||
const projectB = openSseConnection("approval-project", "project-b");
|
||||
|
||||
emitApprovalSseEvent("approval:requested", { id: "apr-a" }, "project-a");
|
||||
|
||||
expect(projectA.res.write).toHaveBeenCalledWith(
|
||||
`event: approval:requested\ndata: ${JSON.stringify({ id: "apr-a" })}\n\n`,
|
||||
);
|
||||
expect(projectB.res.write).not.toHaveBeenCalledWith(
|
||||
`event: approval:requested\ndata: ${JSON.stringify({ id: "apr-a" })}\n\n`,
|
||||
);
|
||||
|
||||
projectA.req.emit("close");
|
||||
projectB.req.emit("close");
|
||||
});
|
||||
});
|
||||
|
||||
describe("automation store SSE events", () => {
|
||||
it("subscribes to all automation store events", () => {
|
||||
const connection = openSseConnectionWithAutomation("automation-subscribe");
|
||||
|
||||
Reference in New Issue
Block a user