test(02-02): add circuit breaker and deliverable enforcement tests

- Self-wake circuit breaker tests: trip after 5 wakes, reset on progress, per-agent isolation
- Non-code task deliverable tests: research/content require artifacts, code/general advance normally
This commit is contained in:
ZacharyZhang-NY committed 2026-03-22 03:52:12 -04:00
1 parent fbfc9c38cb
commit 46dee6c66b
2 files changed
+346

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
import { describe, expect, it } from "vitest";
/**
* Tests for the self-wake circuit breaker pattern used in worker-service.ts.
*
* The circuit breaker prevents infinite self-wake loops when an agent is stuck
* on a task. After MAX_CONSECUTIVE_WAKES (5) consecutive wakes without progress
* (i.e., without a task status change), self-wake is suppressed and the agent's
* manager is notified instead.
*/
const MAX_CONSECUTIVE_WAKES = 5;
/**
* Standalone circuit breaker logic extracted for testability.
* Mirrors the pattern used inside createWorkerService.
*/
function createCircuitBreaker(maxConsecutive: number = MAX_CONSECUTIVE_WAKES) {
const consecutiveWakeMap = new Map<string, number>();
return {
/** Attempt a self-wake. Returns true if allowed, false if circuit breaker trips. */
tryWake(agentId: string, companyId: string): boolean {
const wakeKey = `${agentId}:${companyId}`;
const count = consecutiveWakeMap.get(wakeKey) ?? 0;
if (count >= maxConsecutive) {
consecutiveWakeMap.delete(wakeKey);
return false; // Circuit breaker tripped
}
consecutiveWakeMap.set(wakeKey, count + 1);
return true;
},
/** Reset the counter for an agent (called on task progress). */
reset(agentId: string, companyId: string): void {
const wakeKey = `${agentId}:${companyId}`;
consecutiveWakeMap.delete(wakeKey);
},
/** Get the current count for an agent. */
getCount(agentId: string, companyId: string): number {
const wakeKey = `${agentId}:${companyId}`;
return consecutiveWakeMap.get(wakeKey) ?? 0;
},
};
}
describe("self-wake circuit breaker", () => {
it("allows the first self-wake (counter=1)", () => {
const cb = createCircuitBreaker();
const allowed = cb.tryWake("agent-1", "company-1");
expect(allowed).toBe(true);
expect(cb.getCount("agent-1", "company-1")).toBe(1);
});
it("allows wakes up to MAX_CONSECUTIVE_WAKES-1 (counter=4)", () => {
const cb = createCircuitBreaker();
for (let i = 0; i < MAX_CONSECUTIVE_WAKES - 1; i++) {
expect(cb.tryWake("agent-1", "company-1")).toBe(true);
}
expect(cb.getCount("agent-1", "company-1")).toBe(MAX_CONSECUTIVE_WAKES - 1);
});
it("trips after MAX_CONSECUTIVE_WAKES (5) consecutive wakes without progress", () => {
const cb = createCircuitBreaker();
// First 5 wakes succeed (counter goes 0->1, 1->2, 2->3, 3->4, 4->5)
for (let i = 0; i < MAX_CONSECUTIVE_WAKES; i++) {
expect(cb.tryWake("agent-1", "company-1")).toBe(true);
}
// 6th wake should trip the breaker
const tripped = cb.tryWake("agent-1", "company-1");
expect(tripped).toBe(false);
});
it("deletes the counter after circuit breaker trips (reset for next cycle)", () => {
const cb = createCircuitBreaker();
for (let i = 0; i < MAX_CONSECUTIVE_WAKES; i++) {
cb.tryWake("agent-1", "company-1");
}
// Trip the breaker
cb.tryWake("agent-1", "company-1");
// Counter should be deleted
expect(cb.getCount("agent-1", "company-1")).toBe(0);
});
it("resets counter to 0 on task status change", () => {
const cb = createCircuitBreaker();
cb.tryWake("agent-1", "company-1");
cb.tryWake("agent-1", "company-1");
expect(cb.getCount("agent-1", "company-1")).toBe(2);
// Simulate a task status change -> reset
cb.reset("agent-1", "company-1");
expect(cb.getCount("agent-1", "company-1")).toBe(0);
});
it("maintains per-agent isolation (different agents have separate counters)", () => {
const cb = createCircuitBreaker();
// Agent 1 wakes 4 times
for (let i = 0; i < 4; i++) {
cb.tryWake("agent-1", "company-1");
}
// Agent 2 wakes 2 times
cb.tryWake("agent-2", "company-1");
cb.tryWake("agent-2", "company-1");
expect(cb.getCount("agent-1", "company-1")).toBe(4);
expect(cb.getCount("agent-2", "company-1")).toBe(2);
});
it("maintains per-company isolation for the same agent", () => {
const cb = createCircuitBreaker();
cb.tryWake("agent-1", "company-1");
cb.tryWake("agent-1", "company-1");
cb.tryWake("agent-1", "company-2");
expect(cb.getCount("agent-1", "company-1")).toBe(2);
expect(cb.getCount("agent-1", "company-2")).toBe(1);
});
it("allows wakes again after reset even if previously near limit", () => {
const cb = createCircuitBreaker();
// Get to 4 wakes
for (let i = 0; i < MAX_CONSECUTIVE_WAKES - 1; i++) {
cb.tryWake("agent-1", "company-1");
}
expect(cb.getCount("agent-1", "company-1")).toBe(MAX_CONSECUTIVE_WAKES - 1);
// Reset
cb.reset("agent-1", "company-1");
expect(cb.getCount("agent-1", "company-1")).toBe(0);
// Should allow wakes again
expect(cb.tryWake("agent-1", "company-1")).toBe(true);
expect(cb.getCount("agent-1", "company-1")).toBe(1);
});
it("resetting one agent does not affect another", () => {
const cb = createCircuitBreaker();
cb.tryWake("agent-1", "company-1");
cb.tryWake("agent-1", "company-1");
cb.tryWake("agent-2", "company-1");
cb.tryWake("agent-2", "company-1");
cb.tryWake("agent-2", "company-1");
cb.reset("agent-1", "company-1");
expect(cb.getCount("agent-1", "company-1")).toBe(0);
expect(cb.getCount("agent-2", "company-1")).toBe(3);
});
});
+194
View File
@@ -0,0 +1,194 @@
import { describe, expect, it, vi } from "vitest";
/**
* Tests for non-code task deliverable enforcement in automation-service.ts.
*
* When a run completes successfully on a non-code task (research, content),
* the system checks whether the agent produced a tangible deliverable
* (knowledge entry, document, or substantial comment). If not, the task
* stays in_progress and the manager is notified.
*/
type TaskType = "code" | "research" | "content" | "management" | "general";
interface DeliverableCheckInput {
taskType: TaskType;
hasKnowledgeEntry: boolean;
hasDocument: boolean;
hasSubstantialComment: boolean;
}
/**
* Standalone deliverable-check logic extracted for testability.
* Mirrors the checkRunProducedDeliverable function inside automation-service.ts.
*/
function needsDeliverableCheck(taskType: TaskType): boolean {
return taskType === "research" || taskType === "content";
}
function hasDeliverable(input: DeliverableCheckInput): boolean {
if (!needsDeliverableCheck(input.taskType)) {
// Code/management/general tasks don't require deliverable check
return true;
}
return input.hasKnowledgeEntry || input.hasDocument || input.hasSubstantialComment;
}
/**
* Determines the next status after a successful run.
* Returns { nextStatus, comment } for how the task should be advanced.
*/
function determinePostRunStatus(input: DeliverableCheckInput): {
nextStatus: "in_review" | "in_progress";
comment: string;
} {
if (!needsDeliverableCheck(input.taskType)) {
return {
nextStatus: "in_review",
comment: "Run completed successfully. Awaiting review.",
};
}
if (hasDeliverable(input)) {
return {
nextStatus: "in_review",
comment: "Run completed with deliverable. Awaiting review.",
};
}
return {
nextStatus: "in_progress",
comment: `${input.taskType} task run completed but no deliverable artifact was detected. Flagging for manager review.`,
};
}
describe("non-code task deliverable enforcement", () => {
describe("needsDeliverableCheck", () => {
it("returns false for code tasks", () => {
expect(needsDeliverableCheck("code")).toBe(false);
});
it("returns true for research tasks", () => {
expect(needsDeliverableCheck("research")).toBe(true);
});
it("returns true for content tasks", () => {
expect(needsDeliverableCheck("content")).toBe(true);
});
it("returns false for management tasks", () => {
expect(needsDeliverableCheck("management")).toBe(false);
});
it("returns false for general tasks", () => {
expect(needsDeliverableCheck("general")).toBe(false);
});
});
describe("determinePostRunStatus", () => {
it("advances code tasks to in_review unconditionally", () => {
const result = determinePostRunStatus({
taskType: "code",
hasKnowledgeEntry: false,
hasDocument: false,
hasSubstantialComment: false,
});
expect(result.nextStatus).toBe("in_review");
expect(result.comment).toContain("Awaiting review");
});
it("advances general tasks to in_review unconditionally", () => {
const result = determinePostRunStatus({
taskType: "general",
hasKnowledgeEntry: false,
hasDocument: false,
hasSubstantialComment: false,
});
expect(result.nextStatus).toBe("in_review");
});
it("advances management tasks to in_review unconditionally", () => {
const result = determinePostRunStatus({
taskType: "management",
hasKnowledgeEntry: false,
hasDocument: false,
hasSubstantialComment: false,
});
expect(result.nextStatus).toBe("in_review");
});
it("keeps research task in_progress when no deliverable exists", () => {
const result = determinePostRunStatus({
taskType: "research",
hasKnowledgeEntry: false,
hasDocument: false,
hasSubstantialComment: false,
});
expect(result.nextStatus).toBe("in_progress");
expect(result.comment).toContain("no deliverable artifact");
expect(result.comment).toContain("manager review");
});
it("advances research task to in_review when knowledge entry exists", () => {
const result = determinePostRunStatus({
taskType: "research",
hasKnowledgeEntry: true,
hasDocument: false,
hasSubstantialComment: false,
});
expect(result.nextStatus).toBe("in_review");
expect(result.comment).toContain("deliverable");
});
it("advances research task to in_review when document exists", () => {
const result = determinePostRunStatus({
taskType: "research",
hasKnowledgeEntry: false,
hasDocument: true,
hasSubstantialComment: false,
});
expect(result.nextStatus).toBe("in_review");
});
it("advances research task to in_review when substantial comment exists", () => {
const result = determinePostRunStatus({
taskType: "research",
hasKnowledgeEntry: false,
hasDocument: false,
hasSubstantialComment: true,
});
expect(result.nextStatus).toBe("in_review");
});
it("keeps content task in_progress when no deliverable exists", () => {
const result = determinePostRunStatus({
taskType: "content",
hasKnowledgeEntry: false,
hasDocument: false,
hasSubstantialComment: false,
});
expect(result.nextStatus).toBe("in_progress");
expect(result.comment).toContain("no deliverable artifact");
});
it("advances content task to in_review when document exists", () => {
const result = determinePostRunStatus({
taskType: "content",
hasKnowledgeEntry: false,
hasDocument: true,
hasSubstantialComment: false,
});
expect(result.nextStatus).toBe("in_review");
});
it("advances content task when any deliverable exists", () => {
const result = determinePostRunStatus({
taskType: "content",
hasKnowledgeEntry: true,
hasDocument: false,
hasSubstantialComment: false,
});
expect(result.nextStatus).toBe("in_review");
});
});
});