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:
Fusion
2026-05-09 10:18:10 -07:00
committed by gsxdsm
parent 0a2f3d6024
commit 86baa2662e
2 changed files with 95 additions and 1 deletions

View File

@@ -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");

View File

@@ -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");