test(integration): split autonomous-chain.spec.ts into per-scenario files
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.
This commit is contained in:
1 parent
6e4fd078d9
commit
0f1b7c0b33
12 files changed
+1517
-1409
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Step 4: Hire approval + decideApproval side effects.
|
||||
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { installSharedSetup } from "./shared-setup";
|
||||
|
||||
const shared = installSharedSetup();
|
||||
const { createAgent } = shared;
|
||||
|
||||
describe("Step 4: Hire approval + decideApproval", () => {
|
||||
it("decideApproval sets hired agent to idle when approved", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
|
||||
const approvalId = shared.db.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "hire_agent",
|
||||
payloadSummary: "Hire CTO",
|
||||
impactSummary: "Test impact",
|
||||
payloadJson: JSON.stringify({ agentId: hireId }),
|
||||
});
|
||||
|
||||
shared.db.decideApproval({
|
||||
approvalId,
|
||||
state: "approved",
|
||||
decisionNote: "Approved by board",
|
||||
});
|
||||
|
||||
const updatedAgent = shared.db.getAgent(hireId);
|
||||
expect(updatedAgent.status).toBe("idle");
|
||||
});
|
||||
|
||||
it("decideApproval sets hired agent to terminated when rejected", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
|
||||
const approvalId = shared.db.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "hire_agent",
|
||||
payloadSummary: "Hire CTO",
|
||||
impactSummary: "Test impact",
|
||||
payloadJson: JSON.stringify({ agentId: hireId }),
|
||||
});
|
||||
|
||||
shared.db.decideApproval({
|
||||
approvalId,
|
||||
state: "rejected",
|
||||
decisionNote: "Not needed",
|
||||
});
|
||||
|
||||
const updatedAgent = shared.db.getAgent(hireId);
|
||||
expect(updatedAgent.status).toBe("terminated");
|
||||
});
|
||||
|
||||
it("getApprovalRecord includes payloadJson", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
const approvalId = shared.db.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "hire_agent",
|
||||
payloadSummary: "Hire CTO",
|
||||
impactSummary: "Test",
|
||||
payloadJson: JSON.stringify({ agentId: hireId, config: { connector: "codex_local" } }),
|
||||
});
|
||||
|
||||
const approval = shared.db.getApprovalRecord(approvalId);
|
||||
expect(approval.payloadJson).toBeTruthy();
|
||||
const payload = JSON.parse(approval.payloadJson!);
|
||||
expect(payload.agentId).toBe(hireId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Automation rules, agent messages, auto-assignment, and workflow CRUD.
|
||||
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { installSharedSetup } from "./shared-setup";
|
||||
|
||||
const shared = installSharedSetup();
|
||||
const { createAgent } = shared;
|
||||
|
||||
describe("Automation Rules CRUD", () => {
|
||||
it("creates, reads, toggles, and deletes an automation rule", () => {
|
||||
const ruleId = shared.db.saveAutomationRule({
|
||||
companyId: shared.companyId,
|
||||
name: "Auto-escalate blocked tasks",
|
||||
description: "When a task is blocked, notify the manager",
|
||||
trigger: "task_blocked",
|
||||
conditionsJson: JSON.stringify({ department: "engineering" }),
|
||||
action: "escalate_to_manager",
|
||||
actionConfigJson: JSON.stringify({ message: "Task blocked" }),
|
||||
sourceDepartment: "engineering",
|
||||
targetDepartment: "executive",
|
||||
priority: 80,
|
||||
status: "active",
|
||||
});
|
||||
|
||||
expect(ruleId).toBeTruthy();
|
||||
|
||||
// Rule appears in snapshot
|
||||
const snapshot = shared.db.listSnapshot();
|
||||
const rule = snapshot.automationRules.find(r => r.id === ruleId);
|
||||
expect(rule).toBeTruthy();
|
||||
expect(rule!.name).toBe("Auto-escalate blocked tasks");
|
||||
expect(rule!.trigger).toBe("task_blocked");
|
||||
expect(rule!.action).toBe("escalate_to_manager");
|
||||
expect(rule!.status).toBe("active");
|
||||
expect(rule!.priority).toBe(80);
|
||||
expect(rule!.executionCount).toBe(0);
|
||||
|
||||
// Toggle to paused
|
||||
shared.db.toggleAutomationRule(ruleId, shared.companyId, "paused");
|
||||
const updated = shared.db.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
expect(updated!.status).toBe("paused");
|
||||
|
||||
// Record execution
|
||||
shared.db.recordAutomationExecution(ruleId, shared.companyId, "Auto-escalate blocked tasks", "task_blocked", "escalate_to_manager", "Escalated task-123");
|
||||
const afterExec = shared.db.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
expect(afterExec!.executionCount).toBe(1);
|
||||
expect(afterExec!.lastExecutedAt).toBeTruthy();
|
||||
|
||||
// Automation log
|
||||
const log = shared.db.getAutomationLog(shared.companyId);
|
||||
expect(log).toHaveLength(1);
|
||||
expect(log[0].ruleName).toBe("Auto-escalate blocked tasks");
|
||||
|
||||
// Delete
|
||||
shared.db.deleteAutomationRule(ruleId, shared.companyId);
|
||||
const afterDelete = shared.db.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
expect(afterDelete).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates an existing rule", () => {
|
||||
const ruleId = shared.db.saveAutomationRule({
|
||||
companyId: shared.companyId,
|
||||
name: "Original name",
|
||||
trigger: "task_created",
|
||||
action: "assign_task",
|
||||
});
|
||||
|
||||
shared.db.saveAutomationRule({
|
||||
id: ruleId,
|
||||
companyId: shared.companyId,
|
||||
name: "Updated name",
|
||||
trigger: "task_created",
|
||||
action: "notify_agent",
|
||||
priority: 90,
|
||||
});
|
||||
|
||||
const rule = shared.db.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
expect(rule!.name).toBe("Updated name");
|
||||
expect(rule!.action).toBe("notify_agent");
|
||||
expect(rule!.priority).toBe(90);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Agent Messages", () => {
|
||||
let agentAId: string;
|
||||
let agentBId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
agentAId = shared.db.saveAgent({
|
||||
companyId: shared.companyId, name: "Alice", role: "Engineer", title: "Senior Engineer",
|
||||
department: "engineering", connectorId: "claude_local", status: "active",
|
||||
capabilities: "Backend development, API design, code review",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
agentBId = shared.db.saveAgent({
|
||||
companyId: shared.companyId, name: "Bob", role: "Designer", title: "UX Designer",
|
||||
department: "design", connectorId: "claude_local", status: "active",
|
||||
capabilities: "UI design, wireframing, user research",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it("sends and retrieves direct messages", () => {
|
||||
const msgId = shared.db.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: agentAId,
|
||||
toAgentId: agentBId,
|
||||
channel: "direct",
|
||||
subject: "Need design review",
|
||||
body: "Can you review the login page mockup?",
|
||||
priority: "normal",
|
||||
});
|
||||
|
||||
expect(msgId).toBeTruthy();
|
||||
|
||||
const messages = shared.db.listAgentMessages(shared.companyId);
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].subject).toBe("Need design review");
|
||||
expect(messages[0].fromAgentId).toBe(agentAId);
|
||||
expect(messages[0].toAgentId).toBe(agentBId);
|
||||
expect(messages[0].readAt).toBeNull();
|
||||
});
|
||||
|
||||
it("marks messages as read", () => {
|
||||
const msgId = shared.db.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: agentAId,
|
||||
toAgentId: agentBId,
|
||||
subject: "Test",
|
||||
body: "Hello",
|
||||
});
|
||||
|
||||
shared.db.markMessageReadForReader(msgId, shared.companyId);
|
||||
const messages = shared.db.listAgentMessages(shared.companyId);
|
||||
const updatedMessage = messages.find((message) => message.id === msgId);
|
||||
expect(updatedMessage?.readAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("tracks read state per agent without marking the sender unread", () => {
|
||||
const msgId = shared.db.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: agentAId,
|
||||
toAgentId: agentBId,
|
||||
subject: "Test",
|
||||
body: "Hello",
|
||||
});
|
||||
|
||||
const senderView = shared.db.listAgentMessages(shared.companyId, { agentId: agentAId });
|
||||
expect(senderView).toHaveLength(1);
|
||||
expect(senderView[0].id).toBe(msgId);
|
||||
expect(senderView[0].readAt).toBeTruthy();
|
||||
|
||||
const recipientViewBefore = shared.db.listAgentMessages(shared.companyId, { agentId: agentBId, unreadOnly: true });
|
||||
expect(recipientViewBefore).toHaveLength(1);
|
||||
expect(recipientViewBefore[0].id).toBe(msgId);
|
||||
|
||||
shared.db.markMessageReadForReader(msgId, shared.companyId, agentBId);
|
||||
|
||||
const recipientViewAfter = shared.db.listAgentMessages(shared.companyId, { agentId: agentBId, unreadOnly: true });
|
||||
expect(recipientViewAfter).toHaveLength(0);
|
||||
|
||||
const thirdAgentId = createAgent({ name: "Agent C" });
|
||||
const thirdView = shared.db.listAgentMessages(shared.companyId, { agentId: thirdAgentId });
|
||||
expect(thirdView).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("limits department and project messages to relevant members", () => {
|
||||
const engineeringLeadId = createAgent({ name: "Engineering Lead", role: "Lead", department: "engineering" });
|
||||
const productLeadId = createAgent({ name: "Product Lead", role: "Lead", department: "product" });
|
||||
|
||||
const projectId = shared.db.saveProject({
|
||||
companyId: shared.companyId,
|
||||
name: "Cross-team Project",
|
||||
description: "Scope",
|
||||
leadAgentId: engineeringLeadId,
|
||||
status: "active",
|
||||
});
|
||||
shared.db.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Project task",
|
||||
description: "Do work",
|
||||
assigneeAgentId: engineeringLeadId,
|
||||
projectId,
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
});
|
||||
|
||||
const deptMessageId = shared.db.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: engineeringLeadId,
|
||||
channel: "department",
|
||||
channelTargetId: "engineering",
|
||||
subject: "Engineering update",
|
||||
body: "Only engineering should see this",
|
||||
});
|
||||
const projectMessageId = shared.db.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: engineeringLeadId,
|
||||
channel: "project",
|
||||
channelTargetId: projectId,
|
||||
subject: "Project update",
|
||||
body: "Only project members should see this",
|
||||
});
|
||||
|
||||
const engineeringView = shared.db.listAgentMessages(shared.companyId, { agentId: engineeringLeadId });
|
||||
expect(engineeringView.some((message) => message.id === deptMessageId)).toBe(true);
|
||||
expect(engineeringView.some((message) => message.id === projectMessageId)).toBe(true);
|
||||
|
||||
const productView = shared.db.listAgentMessages(shared.companyId, { agentId: productLeadId });
|
||||
expect(productView.some((message) => message.id === deptMessageId)).toBe(false);
|
||||
expect(productView.some((message) => message.id === projectMessageId)).toBe(false);
|
||||
});
|
||||
|
||||
it("filters messages by channel", () => {
|
||||
shared.db.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, subject: "Dept update", body: "All good", channel: "department", channelTargetId: "engineering" });
|
||||
shared.db.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, toAgentId: agentBId, subject: "DM", body: "Hey", channel: "direct" });
|
||||
|
||||
const deptMsgs = shared.db.listAgentMessages(shared.companyId, { channel: "department" });
|
||||
expect(deptMsgs).toHaveLength(1);
|
||||
expect(deptMsgs[0].subject).toBe("Dept update");
|
||||
|
||||
const directMsgs = shared.db.listAgentMessages(shared.companyId, { channel: "direct" });
|
||||
expect(directMsgs).toHaveLength(1);
|
||||
expect(directMsgs[0].subject).toBe("DM");
|
||||
});
|
||||
|
||||
it("appears in snapshot", () => {
|
||||
shared.db.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, channel: "company", subject: "Snapshot test", body: "Hi" });
|
||||
const snapshot = shared.db.listSnapshot();
|
||||
expect(snapshot.agentMessages.length).toBeGreaterThanOrEqual(1);
|
||||
expect(snapshot.agentMessages[0].subject).toBe("Snapshot test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Auto Task Assignment", () => {
|
||||
let backendId: string;
|
||||
let frontendId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
backendId = shared.db.saveAgent({
|
||||
companyId: shared.companyId, name: "Backend Dev", role: "Backend Engineer", title: "Senior Backend",
|
||||
department: "engineering", connectorId: "codex_local", status: "active",
|
||||
capabilities: "API development, database design, Node.js, PostgreSQL, backend testing",
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
frontendId = shared.db.saveAgent({
|
||||
companyId: shared.companyId, name: "Frontend Dev", role: "Frontend Engineer", title: "Senior Frontend",
|
||||
department: "engineering", connectorId: "codex_local", status: "active",
|
||||
capabilities: "React, TypeScript, CSS, Tailwind, responsive design, frontend testing, UI components",
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it("assigns to the best-matching agent by capability keywords", () => {
|
||||
const taskId = shared.db.saveTask({
|
||||
companyId: shared.companyId, title: "Build React component for user dashboard",
|
||||
description: "Create a responsive React component with Tailwind CSS styling",
|
||||
priority: "medium", status: "todo",
|
||||
});
|
||||
|
||||
const assignedId = shared.db.autoAssignTask(taskId, shared.companyId);
|
||||
expect(assignedId).toBe(frontendId); // "React", "responsive", "Tailwind" match frontend
|
||||
});
|
||||
|
||||
it("assigns backend task to backend agent", () => {
|
||||
const taskId = shared.db.saveTask({
|
||||
companyId: shared.companyId, title: "Design database schema for orders",
|
||||
description: "PostgreSQL database design with API endpoints for order management",
|
||||
priority: "high", status: "todo",
|
||||
});
|
||||
|
||||
const assignedId = shared.db.autoAssignTask(taskId, shared.companyId);
|
||||
expect(assignedId).toBe(backendId); // "database", "PostgreSQL", "API" match backend
|
||||
});
|
||||
|
||||
it("returns existing assignee if task already assigned", () => {
|
||||
const taskId = shared.db.saveTask({
|
||||
companyId: shared.companyId, title: "Already assigned task",
|
||||
description: "This task has an assignee",
|
||||
assigneeAgentId: frontendId,
|
||||
priority: "medium", status: "todo",
|
||||
});
|
||||
|
||||
const result = shared.db.autoAssignTask(taskId, shared.companyId);
|
||||
expect(result).toBe(frontendId);
|
||||
});
|
||||
|
||||
it("returns null if all agents have exhausted their budget", () => {
|
||||
// Set positive budgets that are fully spent
|
||||
shared.db.saveAgent({ id: backendId, companyId: shared.companyId, name: "Backend Dev", role: "Backend", title: "Backend", connectorId: "codex_local", status: "active", capabilities: "backend", budgetMonthlyUsd: 100 });
|
||||
shared.db.saveAgent({ id: frontendId, companyId: shared.companyId, name: "Frontend Dev", role: "Frontend", title: "Frontend", connectorId: "codex_local", status: "active", capabilities: "frontend", budgetMonthlyUsd: 100 });
|
||||
// Exhaust budgets by recording spending via direct SQL (since finishRun would need a real run)
|
||||
(shared.db as any).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(backendId);
|
||||
(shared.db as any).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(frontendId);
|
||||
|
||||
const taskId = shared.db.saveTask({ companyId: shared.companyId, title: "No budget task", description: "Test", priority: "low", status: "todo" });
|
||||
const result = shared.db.autoAssignTask(taskId, shared.companyId);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("penalizes agents with many active tasks", () => {
|
||||
// Give backend agent 5 active tasks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
shared.db.saveTask({ companyId: shared.companyId, title: `Backend task ${i}`, description: "", assigneeAgentId: backendId, priority: "medium", status: "in_progress" });
|
||||
}
|
||||
|
||||
// Generic task with no strong keyword match — should go to less busy frontend agent
|
||||
const taskId = shared.db.saveTask({ companyId: shared.companyId, title: "Implement feature", description: "Build the feature", priority: "medium", status: "todo" });
|
||||
const assignedId = shared.db.autoAssignTask(taskId, shared.companyId);
|
||||
expect(assignedId).toBe(frontendId); // Frontend has 0 active tasks vs backend's 5
|
||||
});
|
||||
});
|
||||
|
||||
describe("Workflow Pipelines", () => {
|
||||
it("creates, activates, and deletes a workflow", () => {
|
||||
const wfId = shared.db.saveWorkflow({
|
||||
companyId: shared.companyId,
|
||||
name: "Code Review Pipeline",
|
||||
description: "Auto-review flow for PRs",
|
||||
triggerType: "task_status_changed",
|
||||
triggerConfigJson: JSON.stringify({ toStatus: "in_review" }),
|
||||
stepsJson: JSON.stringify([
|
||||
{ id: "s1", name: "Run tests", action: "run_task", status: "pending" },
|
||||
{ id: "s2", name: "Notify reviewer", action: "notify_agent", status: "pending" },
|
||||
]),
|
||||
status: "draft",
|
||||
});
|
||||
|
||||
expect(wfId).toBeTruthy();
|
||||
const snapshot = shared.db.listSnapshot();
|
||||
const wf = snapshot.workflows.find(w => w.id === wfId);
|
||||
expect(wf!.name).toBe("Code Review Pipeline");
|
||||
expect(wf!.status).toBe("draft");
|
||||
expect(wf!.runCount).toBe(0);
|
||||
|
||||
// Delete
|
||||
shared.db.deleteWorkflow(wfId, shared.companyId);
|
||||
expect(shared.db.listSnapshot().workflows.find(w => w.id === wfId)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Step 2: CEO phased prompts + pending approvals + credential escalation.
|
||||
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildSystemPrompt } 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("Step 2: CEO phased prompts", () => {
|
||||
it("Phase 0: shows team building instructions when no reports at all", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
directReports: [], // truly no reports
|
||||
chainOfCommand: [],
|
||||
pendingApprovals: [],
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
expect(prompt).toContain("Team Building Phase");
|
||||
expect(prompt).toContain("Design the Organization Structure");
|
||||
expect(prompt).toContain("Submit Org Strategy for Approval");
|
||||
expect(prompt).not.toContain("Awaiting Org Approval");
|
||||
expect(prompt).not.toContain("Goal Decomposition");
|
||||
});
|
||||
|
||||
it("Phase 0.5: shows awaiting hire approvals when pending_approval hires exist but no org approval", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
|
||||
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
const hire = shared.db.getAgent(hireId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
directReports: [hire],
|
||||
chainOfCommand: [],
|
||||
pendingApprovals: [],
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
expect(prompt).toContain("Awaiting Hire Approvals");
|
||||
expect(prompt).not.toContain("Team Building Phase");
|
||||
expect(prompt).not.toContain("Goal Decomposition");
|
||||
});
|
||||
|
||||
it("Phase 1: shows awaiting org approval when pending approve_ceo_strategy exists", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
directReports: [],
|
||||
chainOfCommand: [],
|
||||
pendingApprovals: [
|
||||
{
|
||||
id: "approval-1",
|
||||
companyId: shared.companyId,
|
||||
type: "approve_ceo_strategy",
|
||||
state: "pending",
|
||||
payloadSummary: "Org structure: CEO + CTO + CMO",
|
||||
impactSummary: "Team of 3",
|
||||
requestedByAgentId: ceoId,
|
||||
relatedTaskId: null,
|
||||
decisionNote: "",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as ApprovalRecord,
|
||||
],
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
expect(prompt).toContain("Awaiting Org Approval");
|
||||
expect(prompt).not.toContain("Team Building Phase");
|
||||
expect(prompt).not.toContain("Goal Decomposition");
|
||||
});
|
||||
|
||||
it("Phase 2: shows normal executive instructions when active reports and 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],
|
||||
chainOfCommand: [],
|
||||
pendingApprovals: [],
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
expect(prompt).toContain("Executive / CEO");
|
||||
expect(prompt).toContain("Goal Decomposition");
|
||||
expect(prompt).toContain("Delegation");
|
||||
expect(prompt).toContain("Unblock & Coordinate");
|
||||
expect(prompt).not.toContain("Team Building Phase");
|
||||
expect(prompt).not.toContain("Awaiting Org Approval");
|
||||
});
|
||||
|
||||
it("includes pending approvals section in prompt", () => {
|
||||
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],
|
||||
chainOfCommand: [],
|
||||
pendingApprovals: [
|
||||
{
|
||||
id: "approval-x",
|
||||
companyId: shared.companyId,
|
||||
type: "secret_access",
|
||||
state: "pending",
|
||||
payloadSummary: "Need Twitter API keys",
|
||||
impactSummary: "",
|
||||
requestedByAgentId: ceoId,
|
||||
relatedTaskId: null,
|
||||
decisionNote: "",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as ApprovalRecord,
|
||||
],
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
expect(prompt).toContain("Pending Approvals");
|
||||
expect(prompt).toContain("Need Twitter API keys");
|
||||
});
|
||||
|
||||
it("Manager prompt includes credential escalation guidance", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ceo = shared.db.getAgent(ceoId);
|
||||
const managerId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: managerId });
|
||||
const ic = shared.db.getAgent(icId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: managerId,
|
||||
directReports: [ic],
|
||||
chainOfCommand: [ceo],
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
expect(prompt).toContain("Break down work and delegate it");
|
||||
});
|
||||
|
||||
it("IC prompt includes credential escalation guidance", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ceo = shared.db.getAgent(ceoId);
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ceoId });
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: icId,
|
||||
directReports: [],
|
||||
chainOfCommand: [ceo],
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
expect(prompt).toContain("Your manager is auto-notified and will help");
|
||||
expect(prompt).toContain("explain what API key/account you need");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Step 7: Worker crash recovery (database layer).
|
||||
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AppDatabase } from "../../../src/main/database";
|
||||
import { installSharedSetup } from "./shared-setup";
|
||||
|
||||
const shared = installSharedSetup();
|
||||
const { createAgent } = shared;
|
||||
|
||||
describe("Step 7: Worker crash recovery (database layer)", () => {
|
||||
it("interruptStaleRuns clears running and queued runs on init", async () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
const taskId = shared.db.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "In-progress task",
|
||||
description: "Running when crash happened",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId: shared.workspaceId,
|
||||
priority: "medium",
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const run = shared.db.createRun({
|
||||
companyId: shared.companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId: shared.workspaceId,
|
||||
connectorId: "codex_local",
|
||||
});
|
||||
|
||||
shared.db.markRunStatus(run.id, "running", "Started");
|
||||
|
||||
// Verify it's running
|
||||
let snapshot = shared.db.listSnapshot();
|
||||
expect(snapshot.runs.find((r) => r.id === run.id)?.status).toBe("running");
|
||||
|
||||
// Simulate app restart — close and re-init
|
||||
shared.db.close();
|
||||
|
||||
const profileDir = shared.tempDirs[shared.tempDirs.length - 1]!;
|
||||
shared.db = new AppDatabase(profileDir);
|
||||
shared.db.init(); // This calls interruptStaleRuns()
|
||||
|
||||
snapshot = shared.db.listSnapshot();
|
||||
const recoveredRun = snapshot.runs.find((r) => r.id === run.id);
|
||||
expect(recoveredRun?.status).toBe("interrupted");
|
||||
});
|
||||
|
||||
it("finishRun can mark interrupted runs from crash recovery", () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
const taskId = shared.db.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Crash recovery task",
|
||||
description: "Test",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId: shared.workspaceId,
|
||||
priority: "medium",
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const run = shared.db.createRun({
|
||||
companyId: shared.companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId: shared.workspaceId,
|
||||
connectorId: "codex_local",
|
||||
});
|
||||
|
||||
shared.db.markRunStatus(run.id, "running", "Started");
|
||||
|
||||
// Simulate crash recovery - mark as interrupted
|
||||
shared.db.finishRun({
|
||||
runId: run.id,
|
||||
status: "interrupted",
|
||||
summary: "Worker process crashed.",
|
||||
errorMessage: "Worker process exited unexpectedly.",
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
model: null,
|
||||
sessionDisplayId: null,
|
||||
costUsd: null,
|
||||
unattributedCost: true,
|
||||
});
|
||||
|
||||
const snapshot = shared.db.listSnapshot();
|
||||
const recovered = snapshot.runs.find((r) => r.id === run.id);
|
||||
expect(recovered?.status).toBe("interrupted");
|
||||
expect(recovered?.errorMessage).toBe("Worker process exited unexpectedly.");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Step 1: CEO role detection fix.
|
||||
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildSystemPrompt } from "../../../src/main/prompt-builder";
|
||||
import { installSharedSetup } from "./shared-setup";
|
||||
|
||||
const shared = installSharedSetup();
|
||||
const { createAgent, buildCtx } = shared;
|
||||
|
||||
describe("Step 1: CEO role detection", () => {
|
||||
it("identifies CEO when chainOfCommand is empty, even with no direct reports", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
directReports: [], // no reports yet!
|
||||
chainOfCommand: [], // top-level
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
|
||||
// CEO should NOT get IC instructions
|
||||
expect(prompt).not.toContain("Individual Contributor");
|
||||
// CEO should get executive instructions (Phase 0: Team Building)
|
||||
expect(prompt).toContain("CEO");
|
||||
expect(prompt).toContain("Team Building");
|
||||
});
|
||||
|
||||
it("identifies manager when has reports and has chain of command", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const managerId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
const ceo = shared.db.getAgent(ceoId);
|
||||
const icId = createAgent({ name: "IC", role: "Engineer", reportsTo: managerId });
|
||||
const ic = shared.db.getAgent(icId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: managerId,
|
||||
directReports: [ic],
|
||||
chainOfCommand: [ceo], // reports to CEO
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
expect(prompt).toContain("Operating Instructions (Manager)");
|
||||
expect(prompt).not.toContain("Individual Contributor");
|
||||
expect(prompt).not.toContain("Executive / CEO");
|
||||
});
|
||||
|
||||
it("identifies IC when has chain of command and no direct reports", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ceo = shared.db.getAgent(ceoId);
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ceoId });
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: icId,
|
||||
directReports: [],
|
||||
chainOfCommand: [ceo],
|
||||
});
|
||||
|
||||
const prompt = buildSystemPrompt(ctx);
|
||||
expect(prompt).toContain("Individual Contributor");
|
||||
expect(prompt).not.toContain("Operating Instructions (Manager)");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Step 6: Run failure escalation (database layer).
|
||||
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { installSharedSetup } from "./shared-setup";
|
||||
|
||||
const shared = installSharedSetup();
|
||||
const { createAgent } = shared;
|
||||
|
||||
describe("Step 6: Run failure escalation (database layer)", () => {
|
||||
it("finishRun marks run as failed and returns outcome", () => {
|
||||
const agentId = createAgent({ name: "IC", role: "Engineer" });
|
||||
const taskId = shared.db.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Failing task",
|
||||
description: "Will fail",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId: shared.workspaceId,
|
||||
priority: "medium",
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const run = shared.db.createRun({
|
||||
companyId: shared.companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId: shared.workspaceId,
|
||||
connectorId: "codex_local",
|
||||
});
|
||||
|
||||
shared.db.markRunStatus(run.id, "running", "Started");
|
||||
|
||||
const outcome = shared.db.finishRun({
|
||||
runId: run.id,
|
||||
status: "failed",
|
||||
summary: null,
|
||||
errorMessage: "Process crashed",
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
model: "gpt-5-codex",
|
||||
sessionDisplayId: null,
|
||||
costUsd: 0.5,
|
||||
unattributedCost: false,
|
||||
});
|
||||
|
||||
expect(outcome.budgetHardStopped).toBe(false);
|
||||
|
||||
const snapshot = shared.db.listSnapshot();
|
||||
const finishedRun = snapshot.runs.find((r) => r.id === run.id);
|
||||
expect(finishedRun?.status).toBe("failed");
|
||||
expect(finishedRun?.errorMessage).toBe("Process crashed");
|
||||
});
|
||||
|
||||
it("getChainOfCommand returns manager for failure escalation", () => {
|
||||
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 });
|
||||
|
||||
const chain = shared.db.getChainOfCommand(icId);
|
||||
// First in chain is the immediate manager (CTO)
|
||||
expect(chain.length).toBeGreaterThan(0);
|
||||
expect(chain[0].name).toBe("CTO");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Shared setup for the autonomous-chain integration test suite.
|
||||
*
|
||||
* The original 50KB autonomous-chain.spec.ts shared a module-level `db`,
|
||||
* `companyId`, and `workspaceId` across every describe block via top-level
|
||||
* `beforeEach`/`afterEach` hooks. This module exports `installSharedSetup`,
|
||||
* which each split spec file calls to wire equivalent hooks into its own
|
||||
* vitest context and hand back a live handle.
|
||||
*
|
||||
* The helpers preserve the exact behavior of the original file — tests are
|
||||
* physically moved, not rewritten.
|
||||
*/
|
||||
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, beforeEach } from "vitest";
|
||||
import { AppDatabase } from "../../../src/main/database";
|
||||
import { buildSystemPrompt } from "../../../src/main/prompt-builder";
|
||||
import type { AgentRecord, ApprovalRecord, TaskRecord } from "../../../src/shared/types";
|
||||
|
||||
export interface SharedContext {
|
||||
db: AppDatabase;
|
||||
companyId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface SharedHandle {
|
||||
get db(): AppDatabase;
|
||||
set db(value: AppDatabase);
|
||||
get companyId(): string;
|
||||
get workspaceId(): string;
|
||||
createAgent(overrides?: Partial<Parameters<AppDatabase["saveAgent"]>[0]>): string;
|
||||
buildCtx(overrides?: Record<string, unknown>): Parameters<typeof buildSystemPrompt>[0];
|
||||
tempDirs: string[];
|
||||
}
|
||||
|
||||
export function installSharedSetup(): SharedHandle {
|
||||
const state = {
|
||||
db: null as AppDatabase | null,
|
||||
companyId: "",
|
||||
workspaceId: "",
|
||||
};
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const profileDir = await createTempDir("agentcompany-chain-test-");
|
||||
state.db = new AppDatabase(profileDir);
|
||||
state.db.init();
|
||||
|
||||
state.companyId = state.db.saveCompany({
|
||||
name: "Autonomous Test Corp",
|
||||
description: "Test company for autonomous chain",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
state.workspaceId = state.db.saveWorkspace({
|
||||
companyId: state.companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
repoUrl: "https://example.com/repo.git",
|
||||
repoRef: "main",
|
||||
isPrimary: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
state.db?.close();
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
function currentDb(): AppDatabase {
|
||||
if (!state.db) throw new Error("shared-setup: db accessed outside test hook");
|
||||
return state.db;
|
||||
}
|
||||
|
||||
function createAgent(overrides: Partial<Parameters<AppDatabase["saveAgent"]>[0]> = {}): string {
|
||||
return currentDb().saveAgent({
|
||||
companyId: state.companyId,
|
||||
name: "TestAgent",
|
||||
role: "CEO",
|
||||
title: "Test",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId: state.workspaceId,
|
||||
model: null,
|
||||
capabilities: "Test capabilities",
|
||||
budgetMonthlyUsd: 200,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function buildCtx(overrides: Record<string, unknown> = {}): Parameters<typeof buildSystemPrompt>[0] {
|
||||
const agent = currentDb().getAgent(overrides.agentId as string ?? createAgent());
|
||||
return {
|
||||
agent,
|
||||
apiUrl: "http://127.0.0.1:3000",
|
||||
apiKey: "test-key",
|
||||
runId: "test-run-id",
|
||||
task: null,
|
||||
companyName: "Autonomous Test Corp",
|
||||
goals: [],
|
||||
projects: [],
|
||||
directReports: overrides.directReports as AgentRecord[] ?? [],
|
||||
chainOfCommand: overrides.chainOfCommand as AgentRecord[] ?? [],
|
||||
assignedTasks: [],
|
||||
allAgents: [],
|
||||
pendingApprovals: overrides.pendingApprovals as ApprovalRecord[] ?? [],
|
||||
tasksAwaitingReview: overrides.tasksAwaitingReview as TaskRecord[] ?? [],
|
||||
wakeReason: overrides.wakeReason as string | null ?? null,
|
||||
allTasks: [],
|
||||
recentComments: [],
|
||||
companyDescription: "",
|
||||
socialAccounts: [],
|
||||
...overrides,
|
||||
} as Parameters<typeof buildSystemPrompt>[0];
|
||||
}
|
||||
|
||||
return {
|
||||
get db() { return currentDb(); },
|
||||
set db(value: AppDatabase) { state.db = value; },
|
||||
get companyId() { return state.companyId; },
|
||||
get workspaceId() { return state.workspaceId; },
|
||||
createAgent,
|
||||
buildCtx,
|
||||
tempDirs,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Step 5: Task status + comment wake propagation.
|
||||
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { installSharedSetup } from "./shared-setup";
|
||||
|
||||
const shared = installSharedSetup();
|
||||
const { createAgent } = shared;
|
||||
|
||||
describe("Step 5: Task status + comment wake propagation", () => {
|
||||
it("getChainOfCommand returns correct hierarchy", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO", reportsTo: null });
|
||||
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId });
|
||||
|
||||
const chain = shared.db.getChainOfCommand(icId);
|
||||
expect(chain).toHaveLength(2);
|
||||
expect(chain[0].id).toBe(ctoId); // immediate manager
|
||||
expect(chain[1].id).toBe(ceoId); // top level
|
||||
});
|
||||
|
||||
it("getChainOfCommand handles circular references safely", () => {
|
||||
// Create a potential cycle (shouldn't happen in practice, but guard against it)
|
||||
const agent1 = createAgent({ name: "Agent1" });
|
||||
// agent1 reports to no one (CEO), so chain should be empty
|
||||
const chain = shared.db.getChainOfCommand(agent1);
|
||||
expect(chain).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("task tree supports parent-child relationships", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
|
||||
const parentTaskId = shared.db.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Build feature X",
|
||||
description: "Top-level task",
|
||||
assigneeAgentId: ctoId,
|
||||
workspaceId: shared.workspaceId,
|
||||
priority: "high",
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const childTaskId = shared.db.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Implement module A",
|
||||
description: "Subtask",
|
||||
assigneeAgentId: ctoId,
|
||||
parentId: parentTaskId,
|
||||
workspaceId: shared.workspaceId,
|
||||
priority: "medium",
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const childTask = shared.db.getTask(childTaskId);
|
||||
expect(childTask.parentId).toBe(parentTaskId);
|
||||
|
||||
const parentTask = shared.db.getTask(parentTaskId);
|
||||
expect(parentTask.assigneeAgentId).toBe(ctoId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Step 8: Wake reason injection into task prompt.
|
||||
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildTaskPrompt } from "../../../src/main/prompt-builder";
|
||||
import { installSharedSetup } from "./shared-setup";
|
||||
|
||||
const shared = installSharedSetup();
|
||||
const { createAgent, buildCtx } = shared;
|
||||
|
||||
describe("Step 8: Wake reason injection", () => {
|
||||
it("includes wake reason section for non-timer triggers", () => {
|
||||
const agentId = createAgent({ name: "CTO", role: "CTO" });
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ceo = shared.db.getAgent(ceoId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId,
|
||||
chainOfCommand: [ceo],
|
||||
wakeReason: "subtask_completed",
|
||||
});
|
||||
|
||||
const prompt = buildTaskPrompt(ctx);
|
||||
expect(prompt).toContain("Wake Reason");
|
||||
expect(prompt).toContain("subtask you delegated has been completed");
|
||||
});
|
||||
|
||||
it("includes wake reason for timer triggers", () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId,
|
||||
wakeReason: "timer",
|
||||
});
|
||||
|
||||
const prompt = buildTaskPrompt(ctx);
|
||||
expect(prompt).toContain("Wake Reason");
|
||||
expect(prompt).toContain("Regular heartbeat check-in");
|
||||
});
|
||||
|
||||
it("includes wake reason for manual triggers", () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId,
|
||||
wakeReason: "manual",
|
||||
});
|
||||
|
||||
const prompt = buildTaskPrompt(ctx);
|
||||
expect(prompt).toContain("Wake Reason");
|
||||
expect(prompt).toContain("manually triggered by the human operator");
|
||||
});
|
||||
|
||||
it("includes correct message for each wake reason", () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
|
||||
const reasons: Record<string, string> = {
|
||||
assignment: "You have been assigned a new task",
|
||||
goal_activated: "goal you own has been activated",
|
||||
approval_resolved: "approval you requested has been decided",
|
||||
subtask_completed: "subtask you delegated has been completed",
|
||||
comment: "new comment has been posted",
|
||||
report_blocked: "reports is blocked",
|
||||
report_failed: "reports has failed",
|
||||
};
|
||||
|
||||
for (const [reason, expectedText] of Object.entries(reasons)) {
|
||||
const ctx = buildCtx({
|
||||
agentId,
|
||||
wakeReason: reason,
|
||||
});
|
||||
const prompt = buildTaskPrompt(ctx);
|
||||
expect(prompt).toContain("Wake Reason");
|
||||
expect(prompt).toContain(expectedText);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles unknown wake reason gracefully", () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId,
|
||||
wakeReason: "custom_trigger_xyz",
|
||||
});
|
||||
|
||||
const prompt = buildTaskPrompt(ctx);
|
||||
expect(prompt).toContain("Wake Reason");
|
||||
expect(prompt).toContain("Trigger: custom_trigger_xyz");
|
||||
});
|
||||
|
||||
it("includes wake reason before task description", () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
const taskId = shared.db.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Do the thing",
|
||||
description: "Test task",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId: shared.workspaceId,
|
||||
priority: "medium",
|
||||
status: "todo",
|
||||
});
|
||||
const task = shared.db.getTask(taskId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId,
|
||||
task,
|
||||
wakeReason: "assignment",
|
||||
});
|
||||
|
||||
const prompt = buildTaskPrompt(ctx);
|
||||
const wakeIndex = prompt.indexOf("Wake Reason");
|
||||
const taskIndex = prompt.indexOf("Your immediate task");
|
||||
expect(wakeIndex).toBeGreaterThan(-1);
|
||||
expect(taskIndex).toBeGreaterThan(-1);
|
||||
expect(wakeIndex).toBeLessThan(taskIndex);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Step 3: Deferred wake queue lifecycle.
|
||||
* Physically extracted from autonomous-chain.spec.ts without behavior changes.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { installSharedSetup } from "./shared-setup";
|
||||
|
||||
const shared = installSharedSetup();
|
||||
const { createAgent } = shared;
|
||||
|
||||
describe("Step 3: Wakeup request lifecycle (database)", () => {
|
||||
it("enqueue and list wakeup requests with coalescing", () => {
|
||||
const agentId = createAgent({ name: "TestAgent" });
|
||||
|
||||
// Enqueue two wakes for the same busy agent. Coalesced into one row
|
||||
// with trigger_detail JSON array containing both triggers.
|
||||
shared.db.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed");
|
||||
shared.db.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment");
|
||||
|
||||
// List returns the coalesced wake
|
||||
const wakes = shared.db.listWakeupRequests(shared.companyId, { agentId, status: "queued" });
|
||||
expect(wakes).toHaveLength(1);
|
||||
expect(wakes[0].companyId).toBe(shared.companyId);
|
||||
expect(wakes[0].coalescedCount).toBe(1);
|
||||
|
||||
const triggers = JSON.parse(wakes[0].triggerDetail!);
|
||||
expect(triggers).toHaveLength(2);
|
||||
expect(triggers[0].trigger).toBe("subtask_completed");
|
||||
expect(triggers[1].trigger).toBe("comment");
|
||||
});
|
||||
|
||||
it("returns empty array when no wakeup requests exist", () => {
|
||||
const agentId = createAgent({ name: "TestAgent" });
|
||||
const wakes = shared.db.listWakeupRequests(shared.companyId, { agentId, status: "queued" });
|
||||
expect(wakes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("does not return wakes for other agents", () => {
|
||||
const agent1 = createAgent({ name: "Agent1" });
|
||||
const agent2 = createAgent({ name: "Agent2" });
|
||||
|
||||
shared.db.enqueueWakeupRequest(shared.companyId, agent1, "comment", "comment");
|
||||
shared.db.enqueueWakeupRequest(shared.companyId, agent2, "subtask_completed", "subtask_completed");
|
||||
|
||||
const wakes1 = shared.db.listWakeupRequests(shared.companyId, { agentId: agent1, status: "queued" });
|
||||
expect(wakes1).toHaveLength(1);
|
||||
expect(wakes1[0].source).toBe("comment");
|
||||
|
||||
// Agent2's wake should still be there
|
||||
const wakes2 = shared.db.listWakeupRequests(shared.companyId, { agentId: agent2, status: "queued" });
|
||||
expect(wakes2).toHaveLength(1);
|
||||
expect(wakes2[0].source).toBe("subtask_completed");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user