v0.2.0: Git-native architecture (#1)

This commit was merged in pull request #1.
This commit is contained in:
2026-05-29 12:59:45 +00:00
parent 9cf5c000d9
commit 6e637e4af0
50 changed files with 5852 additions and 135 deletions
+119
View File
@@ -0,0 +1,119 @@
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_CI_CONFIG } from "../types/config.js";
describe("EscalationProtocol", () => {
let tempDir: string;
let protocol: EscalationProtocol;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-escalation-test-"));
const noAutoCommitConfig = {
...DEFAULT_CI_CONFIG,
git: { ...DEFAULT_CI_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");
});
});
});