test(FN-4689): complete Step 2 — add routine sandbox audit tests
Fusion-Task-Id: FN-4689 Fusion-Task-Lineage: 3b856c6f-27b2-4a1c-bd6e-41e716bffd4d
This commit is contained in:
committed by
gsxdsm
parent
2fc8d6d3fa
commit
423041a14a
@@ -0,0 +1,192 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import type { AutomationStep, RunAuditEventInput, Routine, RoutineStore, TaskStore } from "@fusion/core";
|
||||
import type { HeartbeatMonitor } from "../agent-heartbeat.js";
|
||||
import { RoutineRunner } from "../routine-runner.js";
|
||||
import { __resetSandboxBackendForTests, __setSandboxBackendForTests } from "../sandbox/index.js";
|
||||
import * as runAudit from "../run-audit.js";
|
||||
import type { SandboxBackend, SandboxRunResult } from "../sandbox/types.js";
|
||||
|
||||
class AuditStoreStub {
|
||||
events: RunAuditEventInput[] = [];
|
||||
recordRunAuditEvent(event: RunAuditEventInput): void {
|
||||
this.events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
function createRoutine(overrides: Partial<Routine> = {}): Routine {
|
||||
return {
|
||||
id: "routine-1",
|
||||
agentId: "agent-1",
|
||||
name: "Routine 1",
|
||||
enabled: true,
|
||||
executionPolicy: "parallel",
|
||||
catchUpPolicy: "skip",
|
||||
runCount: 0,
|
||||
runHistory: [],
|
||||
trigger: { type: "cron", cronExpression: "* * * * *" },
|
||||
cronExpression: "* * * * *",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
} as Routine;
|
||||
}
|
||||
|
||||
function createRoutineStore(routine: Routine): RoutineStore {
|
||||
return {
|
||||
getRoutine: vi.fn().mockResolvedValue(routine),
|
||||
startRoutineExecution: vi.fn().mockResolvedValue(undefined),
|
||||
completeRoutineExecution: vi.fn().mockResolvedValue(undefined),
|
||||
getDueRoutines: vi.fn().mockResolvedValue([]),
|
||||
listRoutines: vi.fn().mockResolvedValue([routine]),
|
||||
updateRoutine: vi.fn(),
|
||||
recordRun: vi.fn(),
|
||||
cancelRoutineExecution: vi.fn(),
|
||||
init: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
} as unknown as RoutineStore;
|
||||
}
|
||||
|
||||
function createHeartbeatMonitor(): HeartbeatMonitor {
|
||||
return {
|
||||
executeHeartbeat: vi.fn(),
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
trackAgent: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
} as unknown as HeartbeatMonitor;
|
||||
}
|
||||
|
||||
function createBackend(result: SandboxRunResult): SandboxBackend {
|
||||
return {
|
||||
capabilities: () => ({
|
||||
id: "native",
|
||||
supportsNetworkPolicy: false,
|
||||
supportsFilesystemPolicy: false,
|
||||
supportsStreaming: true,
|
||||
platform: "any",
|
||||
}),
|
||||
prepare: vi.fn().mockResolvedValue(undefined),
|
||||
run: vi.fn().mockResolvedValue(result),
|
||||
runStreaming: vi.fn(),
|
||||
dispose: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
}
|
||||
|
||||
describe("RoutineRunner sandbox audit", () => {
|
||||
beforeEach(() => {
|
||||
__resetSandboxBackendForTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSandboxBackendForTests();
|
||||
});
|
||||
|
||||
it("emits sandbox:prepare and sandbox:run for successful command routines", async () => {
|
||||
__setSandboxBackendForTests(createBackend({ stdout: "ok", stderr: "", exitCode: 0, signal: null, timedOut: false, bufferExceeded: false }));
|
||||
const auditStore = new AuditStoreStub();
|
||||
const taskStore = auditStore as unknown as TaskStore;
|
||||
|
||||
const routine = createRoutine({ id: "routine-success", agentId: "agent-success", command: "echo ok" });
|
||||
const runner = new RoutineRunner({
|
||||
routineStore: createRoutineStore(routine),
|
||||
heartbeatMonitor: createHeartbeatMonitor(),
|
||||
rootDir: process.cwd(),
|
||||
taskStore,
|
||||
});
|
||||
|
||||
const result = await runner.executeRoutine(routine.id, "cron");
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const prepareEvent = auditStore.events.find((event) => event.domain === "sandbox" && event.mutationType === "sandbox:prepare");
|
||||
const runEvent = auditStore.events.find((event) => event.domain === "sandbox" && event.mutationType === "sandbox:run");
|
||||
|
||||
expect(prepareEvent).toBeTruthy();
|
||||
expect(runEvent).toBeTruthy();
|
||||
expect(runEvent?.target).toBe("native");
|
||||
expect(runEvent?.runId).toContain("routine-routine-success");
|
||||
expect(runEvent?.agentId).toBe("agent-success");
|
||||
});
|
||||
|
||||
it("emits sandbox:failure for failed command routines", async () => {
|
||||
__setSandboxBackendForTests(createBackend({ stdout: "", stderr: "boom", exitCode: 7, signal: null, timedOut: false, bufferExceeded: false }));
|
||||
const auditStore = new AuditStoreStub();
|
||||
const taskStore = auditStore as unknown as TaskStore;
|
||||
|
||||
const routine = createRoutine({ id: "routine-failure", command: "false" });
|
||||
const runner = new RoutineRunner({
|
||||
routineStore: createRoutineStore(routine),
|
||||
heartbeatMonitor: createHeartbeatMonitor(),
|
||||
rootDir: process.cwd(),
|
||||
taskStore,
|
||||
});
|
||||
|
||||
const result = await runner.executeRoutine(routine.id, "cron");
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
const failureEvent = auditStore.events.find((event) => event.domain === "sandbox" && event.mutationType === "sandbox:failure");
|
||||
expect(failureEvent).toBeTruthy();
|
||||
expect(failureEvent?.runId).toContain("routine-routine-failure");
|
||||
});
|
||||
|
||||
it("falls back to routine-runner agentId in audit context when routine has no agent", async () => {
|
||||
__setSandboxBackendForTests(createBackend({ stdout: "ok", stderr: "", exitCode: 0, signal: null, timedOut: false, bufferExceeded: false }));
|
||||
const auditStore = new AuditStoreStub();
|
||||
const taskStore = auditStore as unknown as TaskStore;
|
||||
|
||||
const routine = createRoutine({ id: "routine-no-agent", agentId: undefined, command: "echo ok" });
|
||||
const runner = new RoutineRunner({
|
||||
routineStore: createRoutineStore(routine),
|
||||
heartbeatMonitor: createHeartbeatMonitor(),
|
||||
rootDir: process.cwd(),
|
||||
taskStore,
|
||||
});
|
||||
|
||||
await runner.executeRoutine(routine.id, "cron");
|
||||
|
||||
const runEvent = auditStore.events.find((event) => event.domain === "sandbox" && event.mutationType === "sandbox:run");
|
||||
expect(runEvent?.agentId).toBe("routine-runner");
|
||||
});
|
||||
|
||||
it("emits no sandbox events when taskStore is absent", async () => {
|
||||
__setSandboxBackendForTests(createBackend({ stdout: "ok", stderr: "", exitCode: 0, signal: null, timedOut: false, bufferExceeded: false }));
|
||||
const createRunAuditorSpy = vi.spyOn(runAudit, "createRunAuditor");
|
||||
|
||||
const routine = createRoutine({ id: "routine-no-store", command: "echo ok" });
|
||||
const runner = new RoutineRunner({
|
||||
routineStore: createRoutineStore(routine),
|
||||
heartbeatMonitor: createHeartbeatMonitor(),
|
||||
rootDir: process.cwd(),
|
||||
});
|
||||
|
||||
const result = await runner.executeRoutine(routine.id, "cron");
|
||||
expect(result.success).toBe(true);
|
||||
expect(createRunAuditorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits sandbox:run for command steps", async () => {
|
||||
__setSandboxBackendForTests(createBackend({ stdout: "ok", stderr: "", exitCode: 0, signal: null, timedOut: false, bufferExceeded: false }));
|
||||
const auditStore = new AuditStoreStub();
|
||||
const taskStore = auditStore as unknown as TaskStore;
|
||||
|
||||
const routine = createRoutine({
|
||||
id: "routine-steps",
|
||||
command: undefined,
|
||||
steps: [{ id: "step-1", name: "step one", type: "command", command: "echo ok" } satisfies AutomationStep],
|
||||
});
|
||||
|
||||
const runner = new RoutineRunner({
|
||||
routineStore: createRoutineStore(routine),
|
||||
heartbeatMonitor: createHeartbeatMonitor(),
|
||||
rootDir: process.cwd(),
|
||||
taskStore,
|
||||
});
|
||||
|
||||
const result = await runner.executeRoutine(routine.id, "cron");
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const runEvents = auditStore.events.filter((event) => event.domain === "sandbox" && event.mutationType === "sandbox:run");
|
||||
expect(runEvents).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user