update
This commit is contained in:
1 parent
806200b3d1
commit
5547c29491
216 files changed
+5043
-3352
No files matched your search
@@ -1,111 +0,0 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { HumanApprovalHook } from "../../src/main/agent-runtime/approval-hook";
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true";
|
||||
});
|
||||
|
||||
const noopLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
||||
|
||||
describe("HumanApprovalHook — autonomous contract", () => {
|
||||
let tempDir: string;
|
||||
let db: AppDatabase;
|
||||
const emit = vi.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "agentcompany-approval-hook-"));
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
emit.mockClear();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
db.close();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function scaffoldIds() {
|
||||
const companyId = db.saveCompany({
|
||||
name: "Approval Co",
|
||||
description: "fixture",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: tempDir,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.saveAgent({
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
title: "Eng",
|
||||
status: "running",
|
||||
reportsTo: null,
|
||||
connectorId: "claude-pro",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Build",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Autonomy fixture",
|
||||
description: "no gate",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId,
|
||||
priority: "medium",
|
||||
status: "in_progress",
|
||||
});
|
||||
return { companyId, agentId, taskId };
|
||||
}
|
||||
|
||||
it("approves every tool call synchronously without writing an approval row", async () => {
|
||||
const { companyId, agentId, taskId } = scaffoldIds();
|
||||
const hook = new HumanApprovalHook({ db, emitEvent: emit, logger: noopLogger });
|
||||
const verdict = await hook.before("run-1", {
|
||||
agentId,
|
||||
companyId,
|
||||
taskId,
|
||||
toolCallId: "tc-1",
|
||||
toolName: "bash",
|
||||
args: { command: "rm -rf /" },
|
||||
});
|
||||
expect(verdict).toEqual({});
|
||||
const approvals = db.listSnapshot({ companyId }).approvals.filter((a) => a.state === "pending");
|
||||
expect(approvals).toHaveLength(0);
|
||||
expect(emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("approves non-destructive tool calls with the same auto-approve contract", async () => {
|
||||
const { companyId, agentId, taskId } = scaffoldIds();
|
||||
const hook = new HumanApprovalHook({ db, emitEvent: emit, logger: noopLogger });
|
||||
const verdict = await hook.before("run-2", {
|
||||
agentId,
|
||||
companyId,
|
||||
taskId,
|
||||
toolCallId: "tc-2",
|
||||
toolName: "edit",
|
||||
args: { path: "src/foo.ts" },
|
||||
});
|
||||
expect(verdict).toEqual({});
|
||||
});
|
||||
|
||||
it("cancelAll is a no-op because no approvals are queued", async () => {
|
||||
const { companyId, agentId, taskId } = scaffoldIds();
|
||||
const hook = new HumanApprovalHook({ db, emitEvent: emit, logger: noopLogger });
|
||||
const p1 = hook.before("run-x", { agentId, companyId, taskId, toolCallId: "a", toolName: "bash", args: { command: "ls" } });
|
||||
const p2 = hook.before("run-x", { agentId, companyId, taskId, toolCallId: "b", toolName: "write", args: { path: "/tmp/x" } });
|
||||
hook.cancelAll("Run cancelled.");
|
||||
const [v1, v2] = await Promise.all([p1, p2]);
|
||||
expect(v1).toEqual({});
|
||||
expect(v2).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -48,7 +48,7 @@ describe("CONNECTORS catalog", () => {
|
||||
});
|
||||
|
||||
it("OAuth connectors carry one of the five pi OAuth provider ids", () => {
|
||||
const validOauthIds = new Set(["anthropic", "openai-codex", "github-copilot", "gemini-cli", "antigravity"]);
|
||||
const validOauthIds = new Set(["anthropic", "openai-codex", "github-copilot", "google-gemini-cli", "google-antigravity"]);
|
||||
const oauthConnectors = CONNECTORS.filter((c) => c.auth?.type === "oauth");
|
||||
for (const connector of oauthConnectors) {
|
||||
if (connector.auth?.type !== "oauth") throw new Error("unreachable");
|
||||
|
||||
@@ -40,22 +40,22 @@ describe("CredentialResolver", () => {
|
||||
});
|
||||
|
||||
it("resolves an api_key connector backed by an env variable", async () => {
|
||||
process.env.MY_TEST_OPENAI_KEY = "sk-from-env-1234";
|
||||
process.env.OPENAI_API_KEY = "sk-from-env-1234";
|
||||
store.save("openai", {
|
||||
kind: "api_key",
|
||||
value: { source: "env", value: "MY_TEST_OPENAI_KEY" },
|
||||
value: { source: "env", value: "OPENAI_API_KEY" },
|
||||
});
|
||||
try {
|
||||
const resolved = await resolver.resolve("openai");
|
||||
expect(resolved.apiKey).toBe("sk-from-env-1234");
|
||||
} finally {
|
||||
delete process.env.MY_TEST_OPENAI_KEY;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
}
|
||||
});
|
||||
|
||||
it("throws CREDENTIAL_MISSING when the env-backed key is unset", async () => {
|
||||
delete process.env.UNSET_TEST_KEY;
|
||||
store.save("xai", { kind: "api_key", value: { source: "env", value: "UNSET_TEST_KEY" } });
|
||||
delete process.env.XAI_API_KEY;
|
||||
store.save("xai", { kind: "api_key", value: { source: "env", value: "XAI_API_KEY" } });
|
||||
await expect(resolver.resolve("xai")).rejects.toBeInstanceOf(CredentialError);
|
||||
});
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ function createHeartbeatTask(
|
||||
status: string,
|
||||
agentName = "Agent",
|
||||
): string {
|
||||
return db.saveTask({
|
||||
const id = db.saveTask({
|
||||
companyId,
|
||||
title: `[Heartbeat] ${agentName} — Check for work`,
|
||||
description: `Automated heartbeat run for ${agentName}.`,
|
||||
@@ -78,6 +78,11 @@ function createHeartbeatTask(
|
||||
status,
|
||||
priority: "medium",
|
||||
});
|
||||
// saveTask is a generic user-task write path and does not set the
|
||||
// heartbeat flag. Flip it directly so this fixture matches what
|
||||
// acquireHeartbeatTask would produce at runtime.
|
||||
db.queryAll("UPDATE tasks SET is_heartbeat = 1 WHERE id = ?", id);
|
||||
return id;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("notification-service", () => {
|
||||
let service: ReturnType<typeof createNotificationService>;
|
||||
let mockGetMainWindow: ReturnType<typeof vi.fn>;
|
||||
let mockEmitEvent: ReturnType<typeof vi.fn>;
|
||||
let mockLogger: { info: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
|
||||
let mockLogger: { info: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
@@ -59,13 +59,14 @@ describe("notification-service", () => {
|
||||
mockEmitEvent = vi.fn();
|
||||
mockLogger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
service = createNotificationService({
|
||||
getMainWindow: mockGetMainWindow as unknown as () => BrowserWindow | null,
|
||||
emitEvent: mockEmitEvent as unknown as (event: DesktopEvent) => void,
|
||||
logger: mockLogger as { info: (...args: unknown[]) => void; error: (...args: unknown[]) => void },
|
||||
logger: mockLogger as { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
parseFrontmatterMarkdown,
|
||||
classifyInventoryKind,
|
||||
deriveTrustLevel,
|
||||
estimateTokens,
|
||||
} from "@main/skill-format";
|
||||
import type { FileInventoryEntry } from "@main/skill-types";
|
||||
|
||||
@@ -177,24 +176,4 @@ describe("skill-format", () => {
|
||||
expect(deriveTrustLevel([])).toBe("markdown_only");
|
||||
});
|
||||
});
|
||||
|
||||
describe("estimateTokens", () => {
|
||||
it("returns 1 for a 4-character string", () => {
|
||||
expect(estimateTokens("abcd")).toBe(1);
|
||||
});
|
||||
|
||||
it("returns 0 for empty string", () => {
|
||||
expect(estimateTokens("")).toBe(0);
|
||||
});
|
||||
|
||||
it("rounds up with Math.ceil", () => {
|
||||
expect(estimateTokens("ab")).toBe(1);
|
||||
expect(estimateTokens("abcde")).toBe(2);
|
||||
});
|
||||
|
||||
it("handles longer strings", () => {
|
||||
const text = "a".repeat(100);
|
||||
expect(estimateTokens(text)).toBe(25);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,54 +1,16 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
TOKEN_BUDGETS,
|
||||
resolveTokenBudget,
|
||||
} from "@main/skill-types";
|
||||
import type {
|
||||
SkillSourceType,
|
||||
TrustLevel,
|
||||
FileInventoryEntry,
|
||||
SkillRecord,
|
||||
SkillImportResult,
|
||||
RuntimeSkillEntry,
|
||||
ManagedSkillRef,
|
||||
ProjectedSkill,
|
||||
SkillAssignment,
|
||||
SkillUpdateStatus,
|
||||
} from "@main/skill-types";
|
||||
|
||||
describe("skill-types", () => {
|
||||
describe("TOKEN_BUDGETS", () => {
|
||||
it("has entries for all supported connector types", () => {
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("claude-pro");
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("anthropic");
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("gemini-cli");
|
||||
expect(TOKEN_BUDGETS).toHaveProperty("google");
|
||||
});
|
||||
|
||||
it("has numeric token caps for each connector", () => {
|
||||
expect(TOKEN_BUDGETS["claude-pro"]).toBe(50000);
|
||||
expect(TOKEN_BUDGETS["anthropic"]).toBe(50000);
|
||||
expect(TOKEN_BUDGETS["gemini-cli"]).toBe(50000);
|
||||
expect(TOKEN_BUDGETS["google"]).toBe(50000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveTokenBudget", () => {
|
||||
it("returns exact match for known connector types", () => {
|
||||
expect(resolveTokenBudget("claude-pro")).toBe(50000);
|
||||
expect(resolveTokenBudget("anthropic")).toBe(50000);
|
||||
expect(resolveTokenBudget("gemini-cli")).toBe(50000);
|
||||
});
|
||||
|
||||
it("resolves anthropic-family connectors to the larger injection budget", () => {
|
||||
expect(resolveTokenBudget("amazon-bedrock")).toBe(50000);
|
||||
expect(resolveTokenBudget("google-vertex")).toBe(50000);
|
||||
});
|
||||
|
||||
it("returns 30000 default for unknown connector types", () => {
|
||||
expect(resolveTokenBudget("unknown_connector")).toBe(30000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("type shape checks", () => {
|
||||
it("SkillSourceType accepts all five source kinds", () => {
|
||||
const sources: SkillSourceType[] = [
|
||||
@@ -121,31 +83,15 @@ describe("skill-types", () => {
|
||||
expect(result.isUpdate).toBe(false);
|
||||
});
|
||||
|
||||
it("RuntimeSkillEntry has runtimeName, sourcePath, name, tokenEstimate and semantics", () => {
|
||||
const entry: RuntimeSkillEntry = {
|
||||
runtimeName: "claude",
|
||||
sourcePath: "/path/to/skill",
|
||||
it("ProjectedSkill matches pi-coding-agent Skill projection", () => {
|
||||
const entry: ProjectedSkill = {
|
||||
name: "test-skill",
|
||||
tokenEstimate: 1000,
|
||||
allowedTools: ["read", "write"],
|
||||
hooks: ["on_task_start"],
|
||||
contextMode: "inline",
|
||||
isAgent: false,
|
||||
effort: "medium",
|
||||
whenToUse: "when writing code",
|
||||
description: "What this skill does and when to use it.",
|
||||
filePath: "/abs/path/to/test-skill/SKILL.md",
|
||||
baseDir: "/abs/path/to/test-skill",
|
||||
};
|
||||
expect(entry.tokenEstimate).toBe(1000);
|
||||
expect(entry.allowedTools).toEqual(["read", "write"]);
|
||||
expect(entry.contextMode).toBe("inline");
|
||||
});
|
||||
|
||||
it("ManagedSkillRef has runtimeName and sourcePath only", () => {
|
||||
const ref: ManagedSkillRef = {
|
||||
runtimeName: "claude",
|
||||
sourcePath: "/path/to/skill",
|
||||
};
|
||||
expect(ref.runtimeName).toBe("claude");
|
||||
expect(ref.sourcePath).toBe("/path/to/skill");
|
||||
expect(entry.name).toBe("test-skill");
|
||||
expect(entry.filePath.endsWith("SKILL.md")).toBe(true);
|
||||
});
|
||||
|
||||
it("SkillAssignment has agentId and skillId", () => {
|
||||
|
||||
@@ -272,6 +272,7 @@ const fakeTask = (id: string, title = "Task"): TaskRecord => ({
|
||||
sessionDisplayId: null,
|
||||
costUsd: 0,
|
||||
requiresUserReview: false,
|
||||
isHeartbeat: false,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
cancelledAt: null,
|
||||
|
||||
@@ -14,7 +14,8 @@ describe("sweepStaleReviews", () => {
|
||||
function makeSweepDeps(overrides: Partial<SweepStaleReviewsDeps> = {}): SweepStaleReviewsDeps {
|
||||
return {
|
||||
db: {
|
||||
queryAll: vi.fn().mockReturnValue([]),
|
||||
listReviewTasksForSweep: vi.fn().mockReturnValue([]),
|
||||
findPendingApprovalForTask: vi.fn().mockReturnValue(null),
|
||||
getManagerForAgent: vi.fn().mockReturnValue(null),
|
||||
getChainOfCommand: vi.fn().mockReturnValue([]),
|
||||
advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_review" }),
|
||||
@@ -36,12 +37,13 @@ describe("sweepStaleReviews", () => {
|
||||
it("escalates to manager's manager when task in_review for 35 minutes", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as {
|
||||
queryAll: ReturnType<typeof vi.fn>;
|
||||
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
|
||||
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
|
||||
getManagerForAgent: ReturnType<typeof vi.fn>;
|
||||
getChainOfCommand: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
dbMock.queryAll.mockReturnValueOnce([
|
||||
dbMock.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
]);
|
||||
dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager One", companyId: "company-1" });
|
||||
@@ -64,9 +66,9 @@ describe("sweepStaleReviews", () => {
|
||||
|
||||
it("does not escalate when task in_review for only 25 minutes", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as { queryAll: ReturnType<typeof vi.fn> };
|
||||
const dbMock = deps.db as unknown as { listReviewTasksForSweep: ReturnType<typeof vi.fn> };
|
||||
|
||||
dbMock.queryAll.mockReturnValueOnce([
|
||||
dbMock.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: TWENTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
]);
|
||||
|
||||
@@ -79,16 +81,16 @@ describe("sweepStaleReviews", () => {
|
||||
it("notifies the human when the reviewer has no higher manager", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as {
|
||||
queryAll: ReturnType<typeof vi.fn>;
|
||||
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
|
||||
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
|
||||
getManagerForAgent: ReturnType<typeof vi.fn>;
|
||||
getChainOfCommand: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
dbMock.queryAll
|
||||
.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
])
|
||||
.mockReturnValueOnce([]);
|
||||
dbMock.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
]);
|
||||
dbMock.findPendingApprovalForTask.mockReturnValueOnce(null);
|
||||
dbMock.getManagerForAgent.mockReturnValueOnce({ id: "ceo-1", name: "CEO", companyId: "company-1" });
|
||||
dbMock.getChainOfCommand.mockReturnValueOnce([]);
|
||||
|
||||
@@ -108,17 +110,18 @@ describe("sweepStaleReviews", () => {
|
||||
it("auto-approves pending approval linked to a stale review task", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as {
|
||||
queryAll: ReturnType<typeof vi.fn>;
|
||||
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
|
||||
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
|
||||
getManagerForAgent: ReturnType<typeof vi.fn>;
|
||||
getChainOfCommand: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
dbMock.queryAll.mockReturnValueOnce([
|
||||
dbMock.listReviewTasksForSweep.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", type: "deliverable_review" }]);
|
||||
dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
|
||||
|
||||
sweepStaleReviews("company-1", deps, NOW);
|
||||
|
||||
@@ -137,17 +140,18 @@ describe("sweepStaleReviews", () => {
|
||||
it("swallows resolveApprovalDecision races gracefully", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as {
|
||||
queryAll: ReturnType<typeof vi.fn>;
|
||||
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
|
||||
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
|
||||
getManagerForAgent: ReturnType<typeof vi.fn>;
|
||||
getChainOfCommand: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
dbMock.queryAll.mockReturnValueOnce([
|
||||
dbMock.listReviewTasksForSweep.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", type: "deliverable_review" }]);
|
||||
dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
|
||||
(deps.resolveApprovalDecision as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
||||
throw new Error("Approval already decided");
|
||||
});
|
||||
@@ -158,18 +162,19 @@ describe("sweepStaleReviews", () => {
|
||||
it("advances task status to done after escalation", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as {
|
||||
queryAll: ReturnType<typeof vi.fn>;
|
||||
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
|
||||
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
|
||||
getManagerForAgent: ReturnType<typeof vi.fn>;
|
||||
getChainOfCommand: ReturnType<typeof vi.fn>;
|
||||
advanceTaskStatus: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
dbMock.queryAll.mockReturnValueOnce([
|
||||
dbMock.listReviewTasksForSweep.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([]);
|
||||
dbMock.findPendingApprovalForTask.mockReturnValueOnce(null);
|
||||
|
||||
sweepStaleReviews("company-1", deps, NOW);
|
||||
|
||||
@@ -183,9 +188,9 @@ describe("sweepStaleReviews", () => {
|
||||
|
||||
it("skips tasks without an assignee", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as { queryAll: ReturnType<typeof vi.fn> };
|
||||
const dbMock = deps.db as unknown as { listReviewTasksForSweep: ReturnType<typeof vi.fn> };
|
||||
|
||||
dbMock.queryAll.mockReturnValueOnce([
|
||||
dbMock.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: null, updatedAt: THIRTY_FIVE_MIN_AGO, title: "Orphan task" },
|
||||
]);
|
||||
|
||||
@@ -203,9 +208,9 @@ describe("sweepStaleReviews", () => {
|
||||
const deps = makeSweepDeps({
|
||||
getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"),
|
||||
});
|
||||
const dbMock = deps.db as unknown as { queryAll: ReturnType<typeof vi.fn> };
|
||||
const dbMock = deps.db as unknown as { listReviewTasksForSweep: ReturnType<typeof vi.fn> };
|
||||
|
||||
dbMock.queryAll.mockReturnValueOnce([
|
||||
dbMock.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: TWENTY_FIVE_MIN_AGO, title: "Review me" },
|
||||
]);
|
||||
|
||||
@@ -222,15 +227,15 @@ describe("sweepStaleReviews", () => {
|
||||
getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"),
|
||||
});
|
||||
const dbMock = deps.db as unknown as {
|
||||
queryAll: ReturnType<typeof vi.fn>;
|
||||
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
|
||||
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
|
||||
getManagerForAgent: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
dbMock.queryAll
|
||||
.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Review me" },
|
||||
])
|
||||
.mockReturnValueOnce([{ id: "approval-1", type: "deliverable_review" }]);
|
||||
dbMock.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Review me" },
|
||||
]);
|
||||
dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
|
||||
dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" });
|
||||
|
||||
const count = sweepStaleReviews("company-1", deps, NOW);
|
||||
|
||||
Reference in new issue
Block a user