import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { EscalationProtocol, EscalationInput } from "../core/escalation.js"; import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js"; describe("EscalationProtocol", () => { let tempDir: string; let protocol: EscalationProtocol; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-escalation-test-")); const noAutoCommitConfig = { ...DEFAULT_CIAGENT_CONFIG, git: { ...DEFAULT_CIAGENT_CONFIG.git, auto_commit: false }, }; protocol = new EscalationProtocol(noAutoCommitConfig, tempDir); }); afterEach(() => { protocol.clearAllTimers(); fs.rmSync(tempDir, { recursive: true, force: true }); }); const baseInput: EscalationInput = { type: "irreversible_action", phase: "1", description: "Deploy to staging environment", context: "Phase 1 backend is complete, all tests pass", options: [ { id: "A", label: "Deploy to staging", description: "Deploy to staging for integration testing", recommended: true }, { id: "B", label: "Skip deployment", description: "Continue locally", recommended: false }, { id: "C", label: "Abort phase", description: "Await manual deployment", recommended: false }, ], default_option_id: "A", }; describe("escalate", () => { it("creates an escalation with a generated ID", () => { const escalation = protocol.escalate(baseInput); expect(escalation.id).toMatch(/^E-\d{3}$/); expect(escalation.type).toBe("irreversible_action"); expect(escalation.description).toBe("Deploy to staging environment"); expect(escalation.resolution).toBe("pending"); }); it("increments escalation IDs sequentially", () => { const e1 = protocol.escalate(baseInput); const e2 = protocol.escalate(baseInput); expect(e1.id).toBe("E-001"); expect(e2.id).toBe("E-002"); }); }); describe("resolveEscalation", () => { it("resolves a pending escalation", () => { const escalation = protocol.escalate(baseInput); const resolved = protocol.resolveEscalation(escalation.id, "A"); expect(resolved).not.toBeNull(); expect(resolved!.resolution).toBe("approved"); expect(resolved!.resolution_detail).toContain("A"); }); it("returns null for unknown escalation ID", () => { const result = protocol.resolveEscalation("E-999", "A"); expect(result).toBeNull(); }); it("supports different resolution types", () => { const escalation = protocol.escalate(baseInput); const rejected = protocol.resolveEscalation(escalation.id, "C", "rejected"); expect(rejected!.resolution).toBe("rejected"); }); }); describe("getPendingEscalations", () => { it("returns pending escalations", () => { protocol.escalate(baseInput); protocol.escalate(baseInput); const pending = protocol.getPendingEscalations(); expect(pending).toHaveLength(2); }); it("returns empty list when no pending escalations", () => { expect(protocol.getPendingEscalations()).toHaveLength(0); }); it("removes resolved escalations from pending", () => { const e1 = protocol.escalate(baseInput); protocol.escalate(baseInput); protocol.resolveEscalation(e1.id, "A"); expect(protocol.getPendingEscalations()).toHaveLength(1); }); }); describe("hasPending", () => { it("returns true when there are pending escalations", () => { protocol.escalate(baseInput); expect(protocol.hasPending()).toBe(true); }); it("returns false when no pending escalations", () => { expect(protocol.hasPending()).toBe(false); }); }); describe("formatEscalation", () => { it("formats escalation for display", () => { const escalation = protocol.escalate(baseInput); const formatted = protocol.formatEscalation(escalation); expect(formatted).toContain("ESCALATION"); expect(formatted).toContain("Irreversible Action"); expect(formatted).toContain("Deploy to staging environment"); expect(formatted).toContain("Options:"); expect(formatted).toContain("recommended"); expect(formatted).toContain("auto-proceed"); }); }); });