From e1f8a3c9ab69b9943311dd8d6d09318d9b5b623f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 27 Mar 2026 00:38:57 -0400 Subject: [PATCH] test(07-02): add failing tests for sweepStaleReviews escalation - 8 tests covering TASK-04 stale review escalation logic - Escalation to manager's manager, CEO fallback notification - Auto-approve pending approvals, race condition handling - Task status advancement, no-assignee skip, timeout constant Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/unit/task-quality-gates.test.ts | 192 +++++++++++++++++++++++++- 1 file changed, 190 insertions(+), 2 deletions(-) diff --git a/tests/unit/task-quality-gates.test.ts b/tests/unit/task-quality-gates.test.ts index da7787e9..68255855 100644 --- a/tests/unit/task-quality-gates.test.ts +++ b/tests/unit/task-quality-gates.test.ts @@ -1,11 +1,16 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { validateTaskCreation, checkAcceptanceCriteria, checkCapabilityMatch, ACCEPTANCE_CRITERIA_KEYWORDS, + sweepStaleReviews, + REVIEW_TIMEOUT_MS, +} from "../../src/main/task-quality-gates"; +import type { + TaskValidationResult, + SweepStaleReviewsDeps, } from "../../src/main/task-quality-gates"; -import type { TaskValidationResult } from "../../src/main/task-quality-gates"; // --- TASK-01: validateTaskCreation tests --- @@ -201,3 +206,186 @@ describe("checkCapabilityMatch", () => { expect(result.matched).toBe(true); }); }); + +// --- TASK-04: sweepStaleReviews tests --- + +describe("sweepStaleReviews", () => { + const NOW = Date.now(); + const THIRTY_FIVE_MIN_AGO = new Date(NOW - 35 * 60 * 1000).toISOString(); + const TWENTY_FIVE_MIN_AGO = new Date(NOW - 25 * 60 * 1000).toISOString(); + + function makeSweepDeps(overrides: Partial = {}): SweepStaleReviewsDeps { + return { + db: { + queryAll: vi.fn().mockReturnValue([]), + getManagerForAgent: vi.fn().mockReturnValue(null), + getChainOfCommand: vi.fn().mockReturnValue([]), + advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_review" }), + }, + dispatchAgentMessage: vi.fn().mockReturnValue({ ok: true }), + resolveApprovalDecision: vi.fn(), + notify: vi.fn(), + handleTaskStatusChange: vi.fn(), + handleApprovalResolved: vi.fn(), + emitEvent: vi.fn(), + publishDomainChanged: vi.fn(), + wakeAgentIfPossible: vi.fn(), + logger: { info: vi.fn(), warn: vi.fn() }, + ...overrides, + }; + } + + it("escalates to manager's manager when task in_review for 35 minutes", () => { + const deps = makeSweepDeps(); + const dbMock = deps.db as { queryAll: ReturnType; getManagerForAgent: ReturnType; getChainOfCommand: ReturnType }; + + // Task in review for 35 min + dbMock.queryAll.mockReturnValueOnce([ + { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, + ]); + // Direct manager of the assignee (the reviewer) + dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager One", companyId: "company-1" }); + // Chain of command for the reviewer — the reviewer's manager (escalation target) + dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-manager-1", name: "Higher Manager" }]); + + const count = sweepStaleReviews("company-1", deps, NOW); + expect(count).toBe(1); + expect(deps.dispatchAgentMessage).toHaveBeenCalledWith( + expect.objectContaining({ db: deps.db }), + expect.objectContaining({ + companyId: "company-1", + fromAgentId: "manager-1", + toAgentId: "higher-manager-1", + channel: "direct", + priority: "urgent", + }), + "subtask_completed", + ); + }); + + it("does not escalate when task in_review for only 25 minutes (under threshold)", () => { + const deps = makeSweepDeps(); + const dbMock = deps.db as { queryAll: ReturnType }; + + dbMock.queryAll.mockReturnValueOnce([ + { id: "task-1", assigneeAgentId: "agent-1", updatedAt: TWENTY_FIVE_MIN_AGO, title: "Build login" }, + ]); + + const count = sweepStaleReviews("company-1", deps, NOW); + expect(count).toBe(0); + expect(deps.dispatchAgentMessage).not.toHaveBeenCalled(); + expect(deps.notify).not.toHaveBeenCalled(); + }); + + it("notifies human when no higher manager (reviewer is CEO)", () => { + const deps = makeSweepDeps(); + const dbMock = deps.db as { queryAll: ReturnType; getManagerForAgent: ReturnType; getChainOfCommand: ReturnType }; + + dbMock.queryAll + .mockReturnValueOnce([ + { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, + ]) + .mockReturnValueOnce([]); // no pending approvals + dbMock.getManagerForAgent.mockReturnValueOnce({ id: "ceo-1", name: "CEO", companyId: "company-1" }); + // Empty chain means reviewer is the top of hierarchy + dbMock.getChainOfCommand.mockReturnValueOnce([]); + + const count = sweepStaleReviews("company-1", deps, NOW); + expect(count).toBe(1); + expect(deps.notify).toHaveBeenCalledWith( + expect.objectContaining({ + title: "Review Timeout", + urgency: "critical", + batchKey: "review_escalation", + navigation: expect.objectContaining({ section: "tasks", entityId: "task-1" }), + }), + ); + expect(deps.dispatchAgentMessage).not.toHaveBeenCalled(); + }); + + it("auto-approves pending approval linked to stale review task", () => { + const deps = makeSweepDeps(); + const dbMock = deps.db as { queryAll: ReturnType; getManagerForAgent: ReturnType; getChainOfCommand: ReturnType }; + + // First queryAll: stale task + dbMock.queryAll.mockReturnValueOnce([ + { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, + ]); + dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); + dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); + // Second queryAll: pending approval + dbMock.queryAll.mockReturnValueOnce([{ id: "approval-1" }]); + + sweepStaleReviews("company-1", deps, NOW); + + expect(deps.resolveApprovalDecision).toHaveBeenCalledWith( + expect.objectContaining({ + db: deps.db, + companyId: "company-1", + approvalId: "approval-1", + state: "approved", + decisionNote: "Auto-approved: review timeout exceeded 30 minutes.", + actor: "system", + }), + ); + }); + + it("catches resolveApprovalDecision error gracefully (race condition)", () => { + const deps = makeSweepDeps(); + const dbMock = deps.db as { queryAll: ReturnType; getManagerForAgent: ReturnType; getChainOfCommand: ReturnType }; + + dbMock.queryAll.mockReturnValueOnce([ + { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, + ]); + dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); + dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); + dbMock.queryAll.mockReturnValueOnce([{ id: "approval-1" }]); + + // resolveApprovalDecision throws (already decided) + (deps.resolveApprovalDecision as ReturnType).mockImplementationOnce(() => { + throw new Error("Approval already decided"); + }); + + // Should not throw + expect(() => sweepStaleReviews("company-1", deps, NOW)).not.toThrow(); + }); + + it("advances task status to done after escalation", () => { + const deps = makeSweepDeps(); + const dbMock = deps.db as { queryAll: ReturnType; getManagerForAgent: ReturnType; getChainOfCommand: ReturnType; advanceTaskStatus: ReturnType }; + + dbMock.queryAll.mockReturnValueOnce([ + { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, + ]); + dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); + dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); + dbMock.queryAll.mockReturnValueOnce([]); // no approvals + + sweepStaleReviews("company-1", deps, NOW); + + expect(dbMock.advanceTaskStatus).toHaveBeenCalledWith( + "task-1", + "done", + "Auto-advanced: review timeout exceeded 30 minutes.", + ); + expect(deps.handleTaskStatusChange).toHaveBeenCalledWith("task-1", "company-1", "in_review", "done"); + }); + + it("skips task with no assigneeAgentId", () => { + const deps = makeSweepDeps(); + const dbMock = deps.db as { queryAll: ReturnType }; + + dbMock.queryAll.mockReturnValueOnce([ + { id: "task-1", assigneeAgentId: null, updatedAt: THIRTY_FIVE_MIN_AGO, title: "Orphan task" }, + ]); + + const count = sweepStaleReviews("company-1", deps, NOW); + expect(count).toBe(0); + expect(deps.dispatchAgentMessage).not.toHaveBeenCalled(); + expect(deps.notify).not.toHaveBeenCalled(); + }); + + it("exports REVIEW_TIMEOUT_MS as 30 minutes in milliseconds", () => { + expect(REVIEW_TIMEOUT_MS).toBe(30 * 60 * 1000); + }); +});