This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 14:03:56 -04:00
1 parent 806200b3d1
commit 5547c29491
216 files changed
+5043 -3352

No files matched your search

-111
View File
@@ -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({});
});
});
+1 -1
View File
@@ -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");
+5 -5
View File
@@ -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);
});
+6 -1
View File
@@ -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;
}
// ---------------------------------------------------------------------------
+3 -2
View File
@@ -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 },
});
});
-21
View File
@@ -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);
});
});
});
+8 -62
View File
@@ -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", () => {
+1
View File
@@ -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,
+35 -30
View File
@@ -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);