The original tests/integration/autonomous-chain.spec.ts was ~1400 lines and 50 KB, covering 19 describe blocks across 10 orthogonal scenarios. Physically extract each describe (or closely related group) into its own file inside tests/integration/autonomous-chain/ so each file stays under the project's 500-line ceiling and failures point at the scenario that regressed. Shared beforeEach / afterEach / createAgent / buildCtx helpers move to autonomous-chain/shared-setup.ts, which exposes installSharedSetup() as a factory that each spec calls at module load. The tests themselves are moved byte-for-byte; only module-level references (`db`, `companyId`, `workspaceId`, `tempDirs`) are rewritten to go through the shared handle. No assertions, it() calls, or behavior are changed. Split layout: role-detection.spec.ts — Step 1: CEO role detection ceo-phased-prompts.spec.ts — Step 2: phased prompts + approvals wakeup-queue.spec.ts — Step 3: deferred wake queue approval-decisions.spec.ts — Step 4: hire approval decisions task-propagation.spec.ts — Step 5: task/comment wake propagation run-failure.spec.ts — Step 6: run failure escalation crash-recovery.spec.ts — Step 7: worker crash recovery wake-reason.spec.ts — Step 8: wake reason injection e2e-flows.spec.ts — full-chain + review-fix regressions automation-messages.spec.ts — automation/messages/assignment/workflows Test count stays at 363 passing; typecheck stays at 156 errors.
337 lines
12 KiB
TypeScript
337 lines
12 KiB
TypeScript
/**
|
|
* End-to-end chain flows + adversarial review-fix regression tests.
|
|
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import { buildSystemPrompt, buildTaskPrompt } from "../../../src/main/prompt-builder";
|
|
import type { ApprovalRecord } from "../../../src/shared/types";
|
|
import { installSharedSetup } from "./shared-setup";
|
|
|
|
const shared = installSharedSetup();
|
|
const { createAgent, buildCtx } = shared;
|
|
|
|
describe("E2E: Full autonomous CEO cold start flow", () => {
|
|
it("simulates complete chain: CEO creation → team build → org approval → delegation → completion", () => {
|
|
// 1. Create CEO agent
|
|
const ceoId = createAgent({ name: "Atlas", role: "CEO", status: "idle" });
|
|
|
|
// CEO has no team yet → Phase 0
|
|
const phase0Ctx = buildCtx({
|
|
agentId: ceoId,
|
|
directReports: [],
|
|
chainOfCommand: [],
|
|
pendingApprovals: [],
|
|
});
|
|
const phase0Prompt = buildSystemPrompt(phase0Ctx);
|
|
expect(phase0Prompt).toContain("Team Building Phase");
|
|
|
|
// 2. CEO submits hire requests
|
|
const ctoId = createAgent({ name: "Nova", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
|
const cmoId = createAgent({ name: "Aria", role: "CMO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
|
|
|
// 3. CEO submits org strategy approval
|
|
const orgApprovalId = shared.db.requestApproval({
|
|
companyId: shared.companyId,
|
|
requestedByAgentId: ceoId,
|
|
type: "approve_ceo_strategy",
|
|
payloadSummary: "Org: CEO + CTO + CMO",
|
|
impactSummary: "3-person team",
|
|
});
|
|
|
|
// CEO now in Phase 1
|
|
const ctoRecord = shared.db.getAgent(ctoId);
|
|
const cmoRecord = shared.db.getAgent(cmoId);
|
|
const phase1Ctx = buildCtx({
|
|
agentId: ceoId,
|
|
directReports: [ctoRecord, cmoRecord],
|
|
chainOfCommand: [],
|
|
pendingApprovals: [{
|
|
id: orgApprovalId,
|
|
companyId: shared.companyId,
|
|
type: "approve_ceo_strategy",
|
|
state: "pending",
|
|
payloadSummary: "Org: CEO + CTO + CMO",
|
|
impactSummary: "3-person team",
|
|
requestedByAgentId: ceoId,
|
|
relatedTaskId: null,
|
|
decisionNote: "",
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
} as ApprovalRecord],
|
|
});
|
|
const phase1Prompt = buildSystemPrompt(phase1Ctx);
|
|
expect(phase1Prompt).toContain("Awaiting Org Approval");
|
|
|
|
// 4. Board approves hires
|
|
const hireApprovalCTO = shared.db.requestApproval({
|
|
companyId: shared.companyId,
|
|
requestedByAgentId: ceoId,
|
|
type: "hire_agent",
|
|
payloadSummary: "Hire Nova as CTO",
|
|
impactSummary: "Test",
|
|
payloadJson: JSON.stringify({ agentId: ctoId }),
|
|
});
|
|
shared.db.decideApproval({ approvalId: hireApprovalCTO, state: "approved", decisionNote: "Approved" });
|
|
|
|
const hireApprovalCMO = shared.db.requestApproval({
|
|
companyId: shared.companyId,
|
|
requestedByAgentId: ceoId,
|
|
type: "hire_agent",
|
|
payloadSummary: "Hire Aria as CMO",
|
|
impactSummary: "Test",
|
|
payloadJson: JSON.stringify({ agentId: cmoId }),
|
|
});
|
|
shared.db.decideApproval({ approvalId: hireApprovalCMO, state: "approved", decisionNote: "Approved" });
|
|
|
|
// Agents now idle
|
|
expect(shared.db.getAgent(ctoId).status).toBe("idle");
|
|
expect(shared.db.getAgent(cmoId).status).toBe("idle");
|
|
|
|
// 5. Board approves org strategy
|
|
shared.db.decideApproval({ approvalId: orgApprovalId, state: "approved", decisionNote: "Go ahead" });
|
|
|
|
// 6. CEO now in Phase 2
|
|
const updatedCto = shared.db.getAgent(ctoId);
|
|
const updatedCmo = shared.db.getAgent(cmoId);
|
|
const phase2Ctx = buildCtx({
|
|
agentId: ceoId,
|
|
directReports: [updatedCto, updatedCmo],
|
|
chainOfCommand: [],
|
|
pendingApprovals: [], // all approved now
|
|
});
|
|
const phase2Prompt = buildSystemPrompt(phase2Ctx);
|
|
expect(phase2Prompt).toContain("Executive / CEO");
|
|
expect(phase2Prompt).toContain("Goal Decomposition");
|
|
expect(phase2Prompt).toContain("Delegation");
|
|
|
|
// 7. Verify hierarchical chain
|
|
const ctoChain = shared.db.getChainOfCommand(ctoId);
|
|
expect(ctoChain).toHaveLength(1);
|
|
expect(ctoChain[0].id).toBe(ceoId);
|
|
|
|
// 8. Create task tree
|
|
const projectId = shared.db.saveProject({
|
|
companyId: shared.companyId,
|
|
name: "Launch MVP",
|
|
description: "Build and launch MVP",
|
|
goalId: null,
|
|
leadAgentId: ctoId,
|
|
status: "in_progress",
|
|
});
|
|
|
|
const parentTaskId = shared.db.saveTask({
|
|
companyId: shared.companyId,
|
|
title: "Build backend",
|
|
description: "Implement API",
|
|
assigneeAgentId: ctoId,
|
|
projectId,
|
|
workspaceId: shared.workspaceId,
|
|
priority: "high",
|
|
status: "in_progress",
|
|
});
|
|
|
|
// CTO creates subtask for IC (would be done via API)
|
|
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId, status: "idle" });
|
|
const subtaskId = shared.db.saveTask({
|
|
companyId: shared.companyId,
|
|
title: "Implement auth",
|
|
description: "JWT auth",
|
|
assigneeAgentId: icId,
|
|
parentId: parentTaskId,
|
|
projectId,
|
|
workspaceId: shared.workspaceId,
|
|
priority: "medium",
|
|
status: "todo",
|
|
});
|
|
|
|
// Verify parent-child relationship
|
|
const subtask = shared.db.getTask(subtaskId);
|
|
expect(subtask.parentId).toBe(parentTaskId);
|
|
|
|
// 9. Wakeup request test: enqueue wake while IC is busy
|
|
shared.db.enqueueWakeupRequest(shared.companyId, icId, "comment", "comment");
|
|
const queuedWakes = shared.db.listWakeupRequests(shared.companyId, { agentId: icId, status: "queued" });
|
|
expect(queuedWakes).toHaveLength(1);
|
|
expect(queuedWakes[0].source).toBe("comment");
|
|
|
|
// 10. Verify IC prompt has correct wake reason
|
|
const icPromptCtx = buildCtx({
|
|
agentId: icId,
|
|
task: subtask,
|
|
chainOfCommand: [shared.db.getAgent(ctoId), shared.db.getAgent(ceoId)],
|
|
wakeReason: "assignment",
|
|
});
|
|
const icPrompt = buildTaskPrompt(icPromptCtx);
|
|
expect(icPrompt).toContain("Wake Reason");
|
|
expect(icPrompt).toContain("You have been assigned a new task");
|
|
expect(icPrompt).toContain("Implement auth");
|
|
});
|
|
});
|
|
|
|
describe("E2E: Wakeup request lifecycle with run lifecycle", () => {
|
|
it("wake enqueue + coalesce + claim + complete cycle", () => {
|
|
const agentId = createAgent({ name: "Agent" });
|
|
|
|
// Simulate: agent is running, wakes come in. All triggers are coalesced
|
|
// into a single wakeup request with JSON array of trigger details.
|
|
shared.db.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed");
|
|
shared.db.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment");
|
|
shared.db.enqueueWakeupRequest(shared.companyId, agentId, "report_blocked", "report_blocked");
|
|
|
|
// Verify coalesced into single row with all triggers
|
|
const wakes = shared.db.listWakeupRequests(shared.companyId, { agentId, status: "queued" });
|
|
expect(wakes).toHaveLength(1);
|
|
expect(wakes[0].coalescedCount).toBe(2);
|
|
|
|
const triggers = JSON.parse(wakes[0].triggerDetail!);
|
|
expect(triggers).toHaveLength(3);
|
|
expect(triggers[0].trigger).toBe("subtask_completed");
|
|
expect(triggers[1].trigger).toBe("comment");
|
|
expect(triggers[2].trigger).toBe("report_blocked");
|
|
|
|
// Wakes remain queued until consumed by the run lifecycle
|
|
expect(shared.db.listWakeupRequests(shared.companyId, { agentId, status: "queued" })).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("E2E: Blocked escalation through org hierarchy", () => {
|
|
it("getChainOfCommand provides escalation path for blocked IC", () => {
|
|
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
|
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
|
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId });
|
|
|
|
// When IC marks task blocked, we need to find its manager
|
|
const chain = shared.db.getChainOfCommand(icId);
|
|
expect(chain[0].id).toBe(ctoId); // immediate manager gets woken
|
|
|
|
// When CTO needs to escalate further
|
|
const ctoChain = shared.db.getChainOfCommand(ctoId);
|
|
expect(ctoChain[0].id).toBe(ceoId);
|
|
});
|
|
});
|
|
|
|
describe("Review Fix: CEO phase ordering edge case", () => {
|
|
it("shows awaiting hire approvals when org strategy approved first but hires still pending", () => {
|
|
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
|
// Hires submitted but not yet approved
|
|
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
|
const hire = shared.db.getAgent(hireId);
|
|
|
|
// Org strategy already approved (not in pendingApprovals), but hires still pending
|
|
const ctx = buildCtx({
|
|
agentId: ceoId,
|
|
directReports: [hire], // only pending_approval
|
|
chainOfCommand: [],
|
|
pendingApprovals: [], // org strategy already resolved
|
|
});
|
|
|
|
const prompt = buildSystemPrompt(ctx);
|
|
// Should NOT get Phase 0 (team building) — hires are already submitted
|
|
expect(prompt).not.toContain("Team Building Phase");
|
|
// Should get the awaiting hire approvals phase
|
|
expect(prompt).toContain("Awaiting Hire Approvals");
|
|
});
|
|
|
|
it("moves to Phase 2 once hires are approved (no pending org approval)", () => {
|
|
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
|
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" });
|
|
const cto = shared.db.getAgent(ctoId);
|
|
|
|
const ctx = buildCtx({
|
|
agentId: ceoId,
|
|
directReports: [cto], // active report
|
|
chainOfCommand: [],
|
|
pendingApprovals: [],
|
|
});
|
|
|
|
const prompt = buildSystemPrompt(ctx);
|
|
expect(prompt).toContain("Executive / CEO");
|
|
expect(prompt).not.toContain("Awaiting Hire Approvals");
|
|
expect(prompt).not.toContain("Team Building Phase");
|
|
});
|
|
});
|
|
|
|
describe("Review Fix: Org gate consistency (revision_requested)", () => {
|
|
it("org-strategy revision_requested blocks the CEO in Phase 1", () => {
|
|
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
|
|
|
const ctx = buildCtx({
|
|
agentId: ceoId,
|
|
directReports: [],
|
|
chainOfCommand: [],
|
|
pendingApprovals: [
|
|
{
|
|
id: "approval-rev",
|
|
companyId: shared.companyId,
|
|
type: "approve_ceo_strategy",
|
|
state: "revision_requested",
|
|
payloadSummary: "Org plan needs changes",
|
|
impactSummary: "",
|
|
requestedByAgentId: ceoId,
|
|
relatedTaskId: null,
|
|
decisionNote: "Please add a CPO",
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
} as ApprovalRecord,
|
|
],
|
|
});
|
|
|
|
const prompt = buildSystemPrompt(ctx);
|
|
expect(prompt).toContain("Awaiting Org Approval");
|
|
expect(prompt).toContain("revision_requested");
|
|
});
|
|
});
|
|
|
|
describe("Review Fix: Rejected approval wakes agent", () => {
|
|
it("decideApproval with rejected state still triggers wake (tested via DB)", () => {
|
|
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
|
|
|
const approvalId = shared.db.requestApproval({
|
|
companyId: shared.companyId,
|
|
requestedByAgentId: ceoId,
|
|
type: "approve_ceo_strategy",
|
|
payloadSummary: "Org plan",
|
|
impactSummary: "test",
|
|
});
|
|
|
|
shared.db.decideApproval({
|
|
approvalId,
|
|
state: "rejected",
|
|
decisionNote: "Not ready",
|
|
});
|
|
|
|
// Verify approval state
|
|
const approval = shared.db.getApprovalRecord(approvalId);
|
|
expect(approval.state).toBe("rejected");
|
|
// The IPC handler in index.ts now wakes on rejected too —
|
|
// we verify the DB layer works; the wakeAgentIfPossible call is in index.ts
|
|
});
|
|
});
|
|
|
|
describe("Review Fix: Root task completion notification", () => {
|
|
it("root task (no parentId) should still have org hierarchy for manager lookup", () => {
|
|
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
|
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
|
|
|
// CTO has a root-level task (no parentId)
|
|
const taskId = shared.db.saveTask({
|
|
companyId: shared.companyId,
|
|
title: "Standalone task from CEO",
|
|
description: "No parent",
|
|
assigneeAgentId: ctoId,
|
|
workspaceId: shared.workspaceId,
|
|
priority: "high",
|
|
status: "done",
|
|
});
|
|
|
|
const task = shared.db.getTask(taskId);
|
|
expect(task.parentId).toBeNull();
|
|
|
|
// CTO's chain of command includes CEO — so api-server can wake CEO on completion
|
|
const chain = shared.db.getChainOfCommand(ctoId);
|
|
expect(chain).toHaveLength(1);
|
|
expect(chain[0].id).toBe(ceoId);
|
|
});
|
|
});
|