Build the current workspace snapshot on native CI runners
This branch snapshots the in-progress desktop workspace so GitHub-hosted Windows and Linux runners can package the same source tree that already passes local verification on macOS. Constraint: Local Apple Silicon Docker/Wine packaging crashes before Windows artifacts are produced Rejected: Publish from the incomplete base repo state | remote typecheck failed because current source additions were missing Confidence: medium Scope-risk: broad Directive: Treat this branch as a build snapshot only; do not merge it without reviewing the unrelated workspace changes it contains Tested: local typecheck, lint, workspace typecheck, unit tests, integration tests, e2e smoke, macOS signed release upload Not-tested: Native Windows/Linux artifacts before this CI run
This commit is contained in:
1 parent
0e497cd892
commit
b772e1c758
271 files changed
+35047
-22879
No files matched your search
@@ -0,0 +1,174 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { createBudgetEnforcementService } from "../../src/main/budget-enforcement-service";
|
||||
import { createRunDispatch } from "../../src/main/orchestration/run-dispatch";
|
||||
import { createTaskRunner } from "../../src/main/orchestration/task-runner";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
const logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
function createHarness(db: AppDatabase) {
|
||||
const enqueueWorkerRun = vi.fn();
|
||||
const budgetService = createBudgetEnforcementService({
|
||||
db,
|
||||
logger,
|
||||
notify: vi.fn(),
|
||||
publishDomainChanged: vi.fn(),
|
||||
});
|
||||
|
||||
const deps = {
|
||||
db,
|
||||
logger,
|
||||
publishDomainChanged: vi.fn(),
|
||||
emitEvent: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
createRunToken: () => "token",
|
||||
getApiPort: () => 3210,
|
||||
dispatchAutomation: vi.fn(),
|
||||
hasWorkerReady: () => true,
|
||||
enqueueWorkerRun,
|
||||
isQuitting: () => false,
|
||||
listRuntimeSkillEntries: () => [],
|
||||
budgetService,
|
||||
checkpointEngine: { shouldPauseHeartbeat: () => false },
|
||||
};
|
||||
|
||||
const runDispatch = createRunDispatch(deps);
|
||||
const taskRunner = createTaskRunner(
|
||||
deps,
|
||||
runDispatch.resolveWorkspace,
|
||||
runDispatch.getRunEligibilityFailure,
|
||||
runDispatch.dispatchRunToWorker,
|
||||
(filter) => db.hasActiveRun(filter),
|
||||
);
|
||||
|
||||
return { enqueueWorkerRun, taskRunner };
|
||||
}
|
||||
|
||||
describe("agent model switching", () => {
|
||||
it("uses the agent's updated model for subsequent runtime dispatches", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-model-switching-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Model Switch Co",
|
||||
description: "Tests post-hire model switching",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.saveAgent({
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
title: "Frontend Engineer",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: "gpt-5.4",
|
||||
capabilities: "Build frontend features",
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
|
||||
const { enqueueWorkerRun, taskRunner } = createHarness(db);
|
||||
|
||||
const firstTaskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Initial model task",
|
||||
description: "Use the first model for this run.",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId,
|
||||
priority: "high",
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const firstRun = taskRunner.queueTaskRun(firstTaskId, "manual", "board");
|
||||
expect(firstRun.ok).toBe(true);
|
||||
expect(enqueueWorkerRun).toHaveBeenCalledTimes(1);
|
||||
expect(enqueueWorkerRun.mock.calls[0]?.[0]?.model).toBe("gpt-5.4");
|
||||
|
||||
if (!firstRun.ok) {
|
||||
throw new Error("Expected first run to queue successfully.");
|
||||
}
|
||||
|
||||
db.finishRun({
|
||||
runId: firstRun.data,
|
||||
status: "succeeded",
|
||||
summary: "Done",
|
||||
errorMessage: null,
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
model: "gpt-5.4",
|
||||
sessionDisplayId: null,
|
||||
costUsd: null,
|
||||
unattributedCost: true,
|
||||
});
|
||||
|
||||
db.saveAgent({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
title: "Frontend Engineer",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: "gpt-5.3-codex",
|
||||
capabilities: "Build frontend features",
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
|
||||
const secondTaskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Updated model task",
|
||||
description: "Use the new model for this run.",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId,
|
||||
priority: "high",
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const secondRun = taskRunner.queueTaskRun(secondTaskId, "manual", "board");
|
||||
expect(secondRun.ok).toBe(true);
|
||||
expect(enqueueWorkerRun).toHaveBeenCalledTimes(2);
|
||||
expect(enqueueWorkerRun.mock.calls[1]?.[0]?.model).toBe("gpt-5.3-codex");
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createAgentToken, initializeAgentTokenSigningKey } from "../../src/main/agent-auth";
|
||||
import { createApiServer } from "../../src/main/api-server";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
async function createHarness() {
|
||||
const profileDir = await createTempDir("agentcompany-recursive-org-");
|
||||
initializeAgentTokenSigningKey(profileDir);
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Recursive Org Co",
|
||||
description: "Tests manager-driven recursive hires",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
title: "Chief Executive Officer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const ctoId = db.saveAgent({
|
||||
companyId,
|
||||
name: "CTO",
|
||||
role: "CTO",
|
||||
title: "Chief Technology Officer",
|
||||
status: "active",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead engineering",
|
||||
budgetMonthlyUsd: 220,
|
||||
department: "engineering",
|
||||
});
|
||||
const taskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Build the org",
|
||||
description: "CTO should hire the first IC under engineering.",
|
||||
assigneeAgentId: ctoId,
|
||||
workspaceId,
|
||||
priority: "high",
|
||||
status: "in_progress",
|
||||
});
|
||||
const run = db.createRun({ companyId, taskId, agentId: ctoId, workspaceId, connectorId: "codex_local" });
|
||||
const token = createAgentToken(ctoId, companyId, run.id);
|
||||
const server = createApiServer(db, () => {}, () => null);
|
||||
const port = await server.start();
|
||||
|
||||
return {
|
||||
db,
|
||||
server,
|
||||
companyId,
|
||||
workspaceId,
|
||||
ceoId,
|
||||
ctoId,
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-Agent-Company-Run-Id": run.id,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("recursive org execution", () => {
|
||||
it("manager can request a hire under themselves and the hierarchy reflects it after approval", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
const response = await fetch(`${harness.baseUrl}/api/companies/${harness.companyId}/agents`, {
|
||||
method: "POST",
|
||||
headers: harness.headers,
|
||||
body: JSON.stringify({
|
||||
name: "Engineering IC",
|
||||
role: "Engineer",
|
||||
title: "Frontend Engineer",
|
||||
department: "engineering",
|
||||
connectorId: "codex_local",
|
||||
workspaceId: harness.workspaceId,
|
||||
model: null,
|
||||
capabilities: "React, TypeScript, Tailwind, frontend delivery",
|
||||
budgetMonthlyUsd: 180,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
const body = await response.json() as { agentId: string; approvalId: string; autoApproved: boolean };
|
||||
expect(body.autoApproved).toBe(false);
|
||||
|
||||
const pendingHire = harness.db.getAgent(body.agentId);
|
||||
expect(pendingHire.status).toBe("pending_approval");
|
||||
expect(pendingHire.reportsTo).toBe(harness.ctoId);
|
||||
|
||||
const approval = harness.db.getApprovalRecord(body.approvalId);
|
||||
expect(approval.type).toBe("hire_agent");
|
||||
expect(approval.requestedByAgentId).toBe(harness.ctoId);
|
||||
expect(approval.relatedAgentId).toBe(body.agentId);
|
||||
|
||||
harness.db.decideApproval({
|
||||
approvalId: body.approvalId,
|
||||
state: "approved",
|
||||
decisionNote: "Approved by board",
|
||||
});
|
||||
|
||||
const approvedHire = harness.db.getAgent(body.agentId);
|
||||
expect(approvedHire.status).toBe("idle");
|
||||
expect(harness.db.getDirectReports(harness.ctoId, harness.companyId).map((agent) => agent.id)).toContain(body.agentId);
|
||||
expect(harness.db.getDirectReports(harness.ceoId, harness.companyId).map((agent) => agent.id)).not.toContain(body.agentId);
|
||||
expect(harness.db.getChainOfCommand(body.agentId).map((agent) => agent.id)).toEqual([harness.ctoId, harness.ceoId]);
|
||||
|
||||
const agentResponse = await fetch(`${harness.baseUrl}/api/agents/${body.agentId}`, {
|
||||
headers: harness.headers,
|
||||
});
|
||||
expect(agentResponse.status).toBe(200);
|
||||
const agentBody = await agentResponse.json() as { manager: { id: string } | null };
|
||||
expect(agentBody.manager?.id).toBe(harness.ctoId);
|
||||
|
||||
const meResponse = await fetch(`${harness.baseUrl}/api/agents/me`, {
|
||||
headers: harness.headers,
|
||||
});
|
||||
expect(meResponse.status).toBe(200);
|
||||
const meBody = await meResponse.json() as { directReports: Array<{ id: string }> };
|
||||
expect(meBody.directReports.map((agent) => agent.id)).toContain(body.agentId);
|
||||
} finally {
|
||||
harness.server.stop();
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("manager cannot route a hire outside their reporting branch", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
const cmoId = harness.db.saveAgent({
|
||||
companyId: harness.companyId,
|
||||
name: "CMO",
|
||||
role: "CMO",
|
||||
title: "Chief Marketing Officer",
|
||||
status: "active",
|
||||
reportsTo: harness.ceoId,
|
||||
connectorId: "codex_local",
|
||||
workspaceId: harness.workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead marketing",
|
||||
budgetMonthlyUsd: 220,
|
||||
department: "marketing",
|
||||
});
|
||||
|
||||
const response = await fetch(`${harness.baseUrl}/api/companies/${harness.companyId}/agents`, {
|
||||
method: "POST",
|
||||
headers: harness.headers,
|
||||
body: JSON.stringify({
|
||||
name: "Misrouted Hire",
|
||||
role: "Engineer",
|
||||
title: "Misrouted Engineer",
|
||||
department: "engineering",
|
||||
reportsTo: cmoId,
|
||||
connectorId: "codex_local",
|
||||
workspaceId: harness.workspaceId,
|
||||
model: null,
|
||||
capabilities: "Should not report outside the CTO branch",
|
||||
budgetMonthlyUsd: 180,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
const body = await response.json() as { error: string };
|
||||
expect(body.error).toContain("must report to you or a manager in your reporting chain");
|
||||
} finally {
|
||||
harness.server.stop();
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createAgentToken, initializeAgentTokenSigningKey } from "../../src/main/agent-auth";
|
||||
import { createApiServer } from "../../src/main/api-server";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
async function createHarness() {
|
||||
const profileDir = await createTempDir("agentcompany-api-events-");
|
||||
initializeAgentTokenSigningKey(profileDir);
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.saveCompany({ name: "Orbit Foundry", description: "API test", status: "active" });
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.saveAgent({
|
||||
companyId,
|
||||
name: "Helios",
|
||||
role: "CEO",
|
||||
title: "Lead agent",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Operate the company",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Ship the API",
|
||||
description: "Capture structured run events.",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId,
|
||||
priority: "high",
|
||||
status: "todo",
|
||||
});
|
||||
const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "codex_local" });
|
||||
const token = createAgentToken(agentId, companyId, run.id);
|
||||
const server = createApiServer(db, () => {}, () => null);
|
||||
const port = await server.start();
|
||||
|
||||
return {
|
||||
db,
|
||||
server,
|
||||
runId: run.id,
|
||||
companyId,
|
||||
agentId,
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-Agent-Company-Run-Id": run.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function readUntil(reader: ReadableStreamDefaultReader<Uint8Array>, matcher: (text: string) => boolean, timeoutMs = 4_000) {
|
||||
const decoder = new TextDecoder();
|
||||
let text = "";
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
text += decoder.decode(value, { stream: true });
|
||||
if (matcher(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for stream match. Received: ${text}`);
|
||||
}
|
||||
|
||||
describe("run event API", () => {
|
||||
it("returns persisted run events for the active run token", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
harness.db.appendRunEvent({
|
||||
companyId: harness.companyId,
|
||||
runId: harness.runId,
|
||||
agentId: harness.agentId,
|
||||
eventType: "run_status",
|
||||
level: "info",
|
||||
message: "Run started",
|
||||
payload: { status: "running" },
|
||||
});
|
||||
|
||||
const response = await fetch(`${harness.baseUrl}/api/runs/${harness.runId}/events?offset=0&limit=10`, {
|
||||
headers: harness.headers,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json() as {
|
||||
events: Array<{ eventType: string; message: string | null; payload: Record<string, unknown> | null }>;
|
||||
nextOffset: number;
|
||||
};
|
||||
expect(body.events).toHaveLength(1);
|
||||
expect(body.events[0]?.eventType).toBe("run_status");
|
||||
expect(body.events[0]?.message).toBe("Run started");
|
||||
expect(body.events[0]?.payload).toMatchObject({ status: "running" });
|
||||
expect(body.nextOffset).toBe(1);
|
||||
} finally {
|
||||
harness.server.stop();
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("streams new run events over SSE for the active run token", async () => {
|
||||
const harness = await createHarness();
|
||||
const controller = new AbortController();
|
||||
try {
|
||||
const response = await fetch(`${harness.baseUrl}/api/runs/${harness.runId}/events/stream`, {
|
||||
headers: harness.headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/event-stream");
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
expect(reader).toBeDefined();
|
||||
|
||||
setTimeout(() => {
|
||||
harness.db.appendRunEvent({
|
||||
companyId: harness.companyId,
|
||||
runId: harness.runId,
|
||||
agentId: harness.agentId,
|
||||
eventType: "run_log",
|
||||
stream: "stdout",
|
||||
message: "hello from sse",
|
||||
});
|
||||
}, 100);
|
||||
|
||||
const text = await readUntil(reader!, (chunk) => chunk.includes("event: run-event") && chunk.includes("hello from sse"));
|
||||
expect(text).toContain("event: connected");
|
||||
expect(text).toContain("\"eventType\":\"run_log\"");
|
||||
expect(text).toContain("\"message\":\"hello from sse\"");
|
||||
} finally {
|
||||
controller.abort();
|
||||
harness.server.stop();
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { createBudgetEnforcementService } from "../../src/main/budget-enforcement-service";
|
||||
import { createRunDispatch } from "../../src/main/orchestration/run-dispatch";
|
||||
import { createTaskRunner } from "../../src/main/orchestration/task-runner";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
const logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
function createOrchestrationHarness(db: AppDatabase) {
|
||||
const budgetService = createBudgetEnforcementService({
|
||||
db,
|
||||
logger,
|
||||
notify: vi.fn(),
|
||||
publishDomainChanged: vi.fn(),
|
||||
});
|
||||
|
||||
const deps = {
|
||||
db,
|
||||
logger,
|
||||
publishDomainChanged: vi.fn(),
|
||||
emitEvent: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
createRunToken: () => "test-token",
|
||||
getApiPort: () => 3210,
|
||||
dispatchAutomation: vi.fn(),
|
||||
hasWorkerReady: () => true,
|
||||
enqueueWorkerRun: vi.fn(),
|
||||
isQuitting: () => false,
|
||||
listRuntimeSkillEntries: () => [],
|
||||
budgetService,
|
||||
checkpointEngine: { shouldPauseHeartbeat: () => false },
|
||||
};
|
||||
|
||||
const runDispatch = createRunDispatch(deps);
|
||||
const taskRunner = createTaskRunner(
|
||||
deps,
|
||||
runDispatch.resolveWorkspace,
|
||||
runDispatch.getRunEligibilityFailure,
|
||||
runDispatch.dispatchRunToWorker,
|
||||
(filter) => db.hasActiveRun(filter),
|
||||
);
|
||||
|
||||
return { deps, taskRunner };
|
||||
}
|
||||
|
||||
describe("connector model enforcement", () => {
|
||||
it("blocks queueTaskRun when an agent keeps a disabled explicit model", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-model-enforcement-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Runtime Enforcement Co",
|
||||
description: "Blocks runs when agent models are disabled",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
title: "Chief Executive Officer",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: "gpt-5.4",
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const taskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Run with invalid model",
|
||||
description: "This task should be blocked because the agent model is disabled.",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId,
|
||||
priority: "high",
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
db.setDisabledModels("codex_local", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
|
||||
const { taskRunner } = createOrchestrationHarness(db);
|
||||
const result = taskRunner.queueTaskRun(taskId, "manual", "board");
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) {
|
||||
throw new Error("Expected queueTaskRun to reject the disabled model.");
|
||||
}
|
||||
expect(result.error.code).toBe("CONNECTOR_MODEL_NOT_ALLOWED");
|
||||
expect(db.listSnapshot().runs).toHaveLength(0);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("fails recovered queued runs when the persisted agent model becomes invalid", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-model-recovery-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Recovered Run Co",
|
||||
description: "Rejects invalid queued runs during recovery",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
title: "Chief Executive Officer",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: "gpt-5.4",
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const taskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Recover queued run",
|
||||
description: "This queued run should fail on recovery because the model is disabled.",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId,
|
||||
priority: "high",
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const run = db.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
});
|
||||
const checkoutSuccess = db.checkoutTask(taskId, run.id);
|
||||
expect(checkoutSuccess).toBe(true);
|
||||
|
||||
db.setDisabledModels("codex_local", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
|
||||
const { taskRunner } = createOrchestrationHarness(db);
|
||||
taskRunner.resumeQueuedRuns();
|
||||
|
||||
const recoveredRun = db.listSnapshot().runs.find((entry) => entry.id === run.id);
|
||||
expect(recoveredRun?.status).toBe("failed");
|
||||
expect(recoveredRun?.errorMessage).toContain("Connector model is disabled or invalid");
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
@@ -27,7 +27,7 @@ vi.mock("electron", () => ({
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { createBudgetEnforcementService } from "../../src/main/budget-enforcement-service";
|
||||
import { createSessionManagementService } from "../../src/main/session-management-service";
|
||||
import { validateTaskCreation, checkCapabilityMatch } from "../../src/main/task-quality-gates";
|
||||
import { validateTaskCreation, checkCapabilityMatch as _checkCapabilityMatch } from "../../src/main/task-quality-gates";
|
||||
import type { ConnectorId } from "../../src/shared/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createAgentToken, initializeAgentTokenSigningKey } from "../../src/main/agent-auth";
|
||||
import { createApiServer } from "../../src/main/api-server";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { processRunCompletion } from "../../src/main/automation/automation-run-completion";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
async function createHarness() {
|
||||
const profileDir = await createTempDir("agentcompany-doc-provenance-");
|
||||
initializeAgentTokenSigningKey(profileDir);
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Document Provenance Co",
|
||||
description: "Tests document revision provenance through run completion",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.saveAgent({
|
||||
companyId,
|
||||
name: "Research Lead",
|
||||
role: "Researcher",
|
||||
title: "Research Lead",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Research and synthesis",
|
||||
budgetMonthlyUsd: 180,
|
||||
});
|
||||
const taskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Refresh the market report",
|
||||
description: "Update the report with new findings and submit it for review.",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId,
|
||||
priority: "high",
|
||||
status: "in_progress",
|
||||
});
|
||||
db.setTaskType(taskId, "research");
|
||||
|
||||
const existingDocumentId = db.saveDocument({
|
||||
companyId,
|
||||
type: "knowledge_article",
|
||||
title: "Market report",
|
||||
content: "Initial report body",
|
||||
authorAgentId: agentId,
|
||||
projectId: null,
|
||||
goalId: null,
|
||||
status: "draft",
|
||||
});
|
||||
|
||||
const run = db.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "codex_local",
|
||||
});
|
||||
const token = createAgentToken(agentId, companyId, run.id);
|
||||
const server = createApiServer(db, () => {}, () => null);
|
||||
const port = await server.start();
|
||||
|
||||
return {
|
||||
db,
|
||||
server,
|
||||
run,
|
||||
companyId,
|
||||
taskId,
|
||||
existingDocumentId,
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-Agent-Company-Run-Id": run.id,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("document run provenance", () => {
|
||||
it("keeps sourceRunId on revised documents so run completion still counts them as deliverables", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
const patchResponse = await fetch(
|
||||
`${harness.baseUrl}/api/companies/${harness.companyId}/documents/${harness.existingDocumentId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: harness.headers,
|
||||
body: JSON.stringify({
|
||||
content: "Updated report with new evidence and recommendations.",
|
||||
changeSummary: "Add latest market evidence",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(patchResponse.status).toBe(200);
|
||||
const updatedDocument = harness.db.queryAll<{ sourceRunId: string | null }>(
|
||||
"select source_run_id as sourceRunId from documents where id = ? limit 1",
|
||||
harness.existingDocumentId,
|
||||
)[0];
|
||||
expect(updatedDocument?.sourceRunId).toBe(harness.run.id);
|
||||
|
||||
const persistedRun = harness.db.listSnapshot().runs.find((entry) => entry.id === harness.run.id);
|
||||
if (!persistedRun) {
|
||||
throw new Error("Expected persisted run record for provenance test.");
|
||||
}
|
||||
|
||||
processRunCompletion({
|
||||
db: harness.db,
|
||||
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
||||
publishDomainChanged: () => {},
|
||||
notify: vi.fn(),
|
||||
wakeAgentIfPossible: vi.fn().mockReturnValue(null),
|
||||
}, {
|
||||
dispatchAutomation: vi.fn(),
|
||||
handleTaskStatusChange: vi.fn(),
|
||||
}, persistedRun, {
|
||||
status: "succeeded",
|
||||
summary: "Updated the market report",
|
||||
errorMessage: null,
|
||||
costUsd: null,
|
||||
}, () => false);
|
||||
|
||||
const task = harness.db.getTask(harness.taskId);
|
||||
expect(task.status).toBe("in_review");
|
||||
} finally {
|
||||
harness.server.stop();
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("increments document version across multiple edits and rejects invalid reviewers without partial mutation", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
const firstPatch = await fetch(
|
||||
`${harness.baseUrl}/api/companies/${harness.companyId}/documents/${harness.existingDocumentId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: harness.headers,
|
||||
body: JSON.stringify({
|
||||
content: "First revision of the report",
|
||||
changeSummary: "Initial revision",
|
||||
}),
|
||||
},
|
||||
);
|
||||
expect(firstPatch.status).toBe(200);
|
||||
|
||||
const revisionState = harness.db.queryAll<{ version: number; latestRevisionId: string | null }>(
|
||||
"select version, latest_revision_id as latestRevisionId from documents where id = ? limit 1",
|
||||
harness.existingDocumentId,
|
||||
)[0];
|
||||
expect(revisionState?.version).toBe(2);
|
||||
expect(revisionState?.latestRevisionId).not.toBeNull();
|
||||
|
||||
const invalidReviewerPatch = await fetch(
|
||||
`${harness.baseUrl}/api/companies/${harness.companyId}/documents/${harness.existingDocumentId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: harness.headers,
|
||||
body: JSON.stringify({
|
||||
reviewerAgentId: "00000000-0000-0000-0000-000000000000",
|
||||
content: "This write should be rejected before any mutation occurs",
|
||||
changeSummary: "Invalid reviewer should block this patch",
|
||||
}),
|
||||
},
|
||||
);
|
||||
expect(invalidReviewerPatch.status).toBe(400);
|
||||
|
||||
const rejectedState = harness.db.queryAll<{ version: number; content: string; revisionCount: number }>(
|
||||
`select d.version as version, d.content as content,
|
||||
(select count(*) from document_revisions where document_id = d.id and company_id = d.company_id) as revisionCount
|
||||
from documents d where d.id = ? limit 1`,
|
||||
harness.existingDocumentId,
|
||||
)[0];
|
||||
expect(rejectedState?.version).toBe(2);
|
||||
expect(rejectedState?.content).toBe("First revision of the report");
|
||||
expect(rejectedState?.revisionCount).toBe(1);
|
||||
|
||||
const secondPatch = await fetch(
|
||||
`${harness.baseUrl}/api/companies/${harness.companyId}/documents/${harness.existingDocumentId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: harness.headers,
|
||||
body: JSON.stringify({
|
||||
content: "Second revision of the report",
|
||||
changeSummary: "Follow-up revision",
|
||||
}),
|
||||
},
|
||||
);
|
||||
expect(secondPatch.status).toBe(200);
|
||||
|
||||
const finalState = harness.db.queryAll<{ version: number; revisionCount: number }>(
|
||||
`select d.version as version,
|
||||
(select count(*) from document_revisions where document_id = d.id and company_id = d.company_id) as revisionCount
|
||||
from documents d where d.id = ? limit 1`,
|
||||
harness.existingDocumentId,
|
||||
)[0];
|
||||
expect(finalState?.version).toBe(3);
|
||||
expect(finalState?.revisionCount).toBe(2);
|
||||
} finally {
|
||||
harness.server.stop();
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not stamp sourceRunId for metadata-only document patches", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
const metadataPatch = await fetch(
|
||||
`${harness.baseUrl}/api/companies/${harness.companyId}/documents/${harness.existingDocumentId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: harness.headers,
|
||||
body: JSON.stringify({
|
||||
title: "Renamed market report",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(metadataPatch.status).toBe(200);
|
||||
const updatedDocument = harness.db.queryAll<{ sourceRunId: string | null; title: string }>(
|
||||
"select source_run_id as sourceRunId, title from documents where id = ? limit 1",
|
||||
harness.existingDocumentId,
|
||||
)[0];
|
||||
expect(updatedDocument?.sourceRunId).toBeNull();
|
||||
expect(updatedDocument?.title).toBe("Renamed market report");
|
||||
} finally {
|
||||
harness.server.stop();
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("creates deliverable review approvals that stay linked to the originating task", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
harness.db.saveCompany({
|
||||
id: harness.companyId,
|
||||
name: "Document Provenance Co",
|
||||
description: "Tests document review task linkage",
|
||||
status: "active",
|
||||
reviewDeliverables: true,
|
||||
});
|
||||
|
||||
const createResponse = await fetch(
|
||||
`${harness.baseUrl}/api/companies/${harness.companyId}/documents`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: harness.headers,
|
||||
body: JSON.stringify({
|
||||
type: "prd",
|
||||
title: "Run-linked PRD",
|
||||
content: "Initial draft that should trigger deliverable review.",
|
||||
status: "draft",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(createResponse.status).toBe(201);
|
||||
const approvals = harness.db.listSnapshot().approvals.filter((approval) => approval.type === "deliverable_review");
|
||||
expect(approvals).toHaveLength(1);
|
||||
expect(approvals[0]?.relatedTaskId).toBe(harness.taskId);
|
||||
} finally {
|
||||
harness.server.stop();
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,303 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { createOnboardingService } from "../../src/main/onboarding-service";
|
||||
import { buildRunPayload } from "../../src/main/orchestration/turn-context";
|
||||
import { getConnectorDefinition } from "../../src/main/connectors";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
function createNoopLogger() {
|
||||
return {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
function createBootstrapHarness(db: AppDatabase) {
|
||||
const noop = () => {};
|
||||
return createOnboardingService({
|
||||
db,
|
||||
seedDefaultAutomationRules: noop,
|
||||
publishDomainChanged: noop,
|
||||
queueTaskRun: () => ({ ok: true, data: "run-1" }),
|
||||
});
|
||||
}
|
||||
|
||||
function buildKickoffPayload(db: AppDatabase, summary: { companyId: string; leadAgentId: string | null; taskId: string | null; workspaceId: string | null }) {
|
||||
if (!summary.leadAgentId || !summary.taskId) {
|
||||
throw new Error("Missing lead agent or task in onboarding summary.");
|
||||
}
|
||||
|
||||
const agent = db.getAgent(summary.leadAgentId);
|
||||
const task = db.getTask(summary.taskId);
|
||||
const connector = db.getConnector(agent.connectorId);
|
||||
const workspace = summary.workspaceId ? db.getWorkspace(summary.workspaceId) : null;
|
||||
const company = db.listSnapshot().companies.find((entry) => entry.id === summary.companyId);
|
||||
|
||||
if (!company) {
|
||||
throw new Error(`Company ${summary.companyId} not found in snapshot.`);
|
||||
}
|
||||
|
||||
return buildRunPayload({
|
||||
runId: "run-1",
|
||||
companyId: summary.companyId,
|
||||
agent,
|
||||
task,
|
||||
connector,
|
||||
workspace,
|
||||
workspaceMode: "exclusive",
|
||||
companyName: company.name,
|
||||
companyDescription: company.description,
|
||||
snapshot: db.listSnapshot(),
|
||||
trigger: "bootstrap",
|
||||
}, {
|
||||
db,
|
||||
logger: createNoopLogger(),
|
||||
getApiPort: () => 3210,
|
||||
createRunToken: () => "test-token",
|
||||
listRuntimeSkillEntries: () => [],
|
||||
});
|
||||
}
|
||||
|
||||
describe("onboarding model pool integration", () => {
|
||||
it("persists an explicit CEO model and excludes disabled native models from runtime prompt context", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-onboarding-models-");
|
||||
const workspaceDir = await createTempDir("agentcompany-onboarding-workspace-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const codexDefinition = getConnectorDefinition("codex_local");
|
||||
const enabledModel = codexDefinition.module.models?.[0]?.id;
|
||||
const disabledModel = codexDefinition.module.models?.[1]?.id;
|
||||
|
||||
if (!enabledModel || !disabledModel) {
|
||||
throw new Error("codex_local requires at least two models for this integration test.");
|
||||
}
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
db.setDisabledModels("codex_local", [{ id: disabledModel, label: disabledModel }]);
|
||||
|
||||
const summary = createBootstrapHarness(db).bootstrapOnboarding({
|
||||
companyName: "Model Governance Co",
|
||||
companyDescription: "Tests onboarding model governance",
|
||||
workspacePath: workspaceDir,
|
||||
autoApproveHires: true,
|
||||
reviewDeliverables: false,
|
||||
executionMode: "ai_ceo",
|
||||
goalTitle: "Launch runtime-safe company",
|
||||
goalDescription: "Ensure onboarding connector models flow into runtime decisions.",
|
||||
connectorId: "codex_local",
|
||||
connectorModel: enabledModel,
|
||||
});
|
||||
|
||||
if (!summary.leadAgentId) {
|
||||
throw new Error("Expected lead agent id from onboarding bootstrap.");
|
||||
}
|
||||
|
||||
const agent = db.getAgent(summary.leadAgentId);
|
||||
expect(agent.connectorId).toBe("codex_local");
|
||||
expect(agent.model).toBe(enabledModel);
|
||||
expect(db.getDisabledModels("codex_local")).toEqual([{ id: disabledModel, label: disabledModel }]);
|
||||
expect(db.getCurrentCompanyId()).toBe(summary.companyId);
|
||||
|
||||
const payload = buildKickoffPayload(db, summary);
|
||||
expect(payload.model).toBe(enabledModel);
|
||||
expect(payload.systemPrompt).not.toBeNull();
|
||||
const systemPrompt = payload.systemPrompt ?? "";
|
||||
expect(systemPrompt).toContain("Enabled models:");
|
||||
expect(systemPrompt).toContain(enabledModel);
|
||||
const codexConnectorLine = systemPrompt
|
||||
.split("\n")
|
||||
.find((line) => line.includes("**Codex**"));
|
||||
expect(codexConnectorLine).toBeDefined();
|
||||
expect(codexConnectorLine).not.toContain(`, ${disabledModel},`);
|
||||
expect(codexConnectorLine).not.toContain(` ${disabledModel}.`);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("uses compat connector csv models minus disabled entries in runtime prompt context", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-onboarding-compat-");
|
||||
const workspaceDir = await createTempDir("agentcompany-onboarding-compat-workspace-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.saveConnector({
|
||||
id: "claude_compatible",
|
||||
command: "claude",
|
||||
model: "model-alpha,model-beta,model-gamma",
|
||||
envBindingText: "",
|
||||
notes: "Custom provider",
|
||||
});
|
||||
db.updateConnectorHealth({
|
||||
id: "claude_compatible",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
db.setDisabledModels("claude_compatible", [{ id: "model-beta", label: "model-beta" }]);
|
||||
|
||||
const summary = createBootstrapHarness(db).bootstrapOnboarding({
|
||||
companyName: "Compat Governance Co",
|
||||
companyDescription: "Tests compat model governance",
|
||||
workspacePath: workspaceDir,
|
||||
autoApproveHires: true,
|
||||
reviewDeliverables: false,
|
||||
executionMode: "ai_ceo",
|
||||
goalTitle: "Launch compat company",
|
||||
goalDescription: "Ensure compat model pools survive onboarding and runtime prompt assembly.",
|
||||
connectorId: "claude_compatible",
|
||||
connectorModel: "model-alpha",
|
||||
});
|
||||
|
||||
const payload = buildKickoffPayload(db, summary);
|
||||
expect(payload.systemPrompt).not.toBeNull();
|
||||
const systemPrompt = payload.systemPrompt ?? "";
|
||||
expect(systemPrompt).toContain("Enabled models: model-alpha, model-gamma.");
|
||||
expect(systemPrompt).not.toContain("model-beta");
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("stores a null CEO model when onboarding leaves the connector model at default", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-onboarding-default-");
|
||||
const workspaceDir = await createTempDir("agentcompany-onboarding-default-workspace-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const summary = createBootstrapHarness(db).bootstrapOnboarding({
|
||||
companyName: "Default Model Co",
|
||||
companyDescription: "Tests default connector model behavior",
|
||||
workspacePath: workspaceDir,
|
||||
autoApproveHires: true,
|
||||
reviewDeliverables: false,
|
||||
executionMode: "ai_ceo",
|
||||
goalTitle: "Launch default-mode company",
|
||||
goalDescription: "Leave the CEO model unset so runtime falls back to connector defaults.",
|
||||
connectorId: "codex_local",
|
||||
connectorModel: "",
|
||||
});
|
||||
|
||||
if (!summary.leadAgentId) {
|
||||
throw new Error("Expected lead agent id from onboarding bootstrap.");
|
||||
}
|
||||
|
||||
const agent = db.getAgent(summary.leadAgentId);
|
||||
expect(agent.model).toBeNull();
|
||||
|
||||
const payload = buildKickoffPayload(db, summary);
|
||||
const defaultModel = getConnectorDefinition("codex_local").defaultModel;
|
||||
expect(payload.model).toBe(defaultModel);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("reroutes default runtime selection to the first enabled model when the connector default is disabled", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-onboarding-reroute-");
|
||||
const workspaceDir = await createTempDir("agentcompany-onboarding-reroute-workspace-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const codexDefinition = getConnectorDefinition("codex_local");
|
||||
const defaultModel = codexDefinition.defaultModel;
|
||||
const fallbackModel = codexDefinition.module.models?.find((model) => model.id !== defaultModel)?.id;
|
||||
|
||||
if (!defaultModel || !fallbackModel) {
|
||||
throw new Error("codex_local requires a default and fallback model for reroute testing.");
|
||||
}
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
db.setDisabledModels("codex_local", [{ id: defaultModel, label: defaultModel }]);
|
||||
|
||||
const summary = createBootstrapHarness(db).bootstrapOnboarding({
|
||||
companyName: "Default Reroute Co",
|
||||
companyDescription: "Tests runtime reroute when the connector default is disabled",
|
||||
workspacePath: workspaceDir,
|
||||
autoApproveHires: true,
|
||||
reviewDeliverables: false,
|
||||
executionMode: "ai_ceo",
|
||||
goalTitle: "Launch rerouted company",
|
||||
goalDescription: "Ensure runtime picks the first enabled model when connector default is disabled.",
|
||||
connectorId: "codex_local",
|
||||
connectorModel: "",
|
||||
});
|
||||
|
||||
const payload = buildKickoffPayload(db, summary);
|
||||
expect(payload.model).toBe(fallbackModel);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("rejects an explicit onboarding model that is disabled for the selected connector", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-onboarding-reject-");
|
||||
const workspaceDir = await createTempDir("agentcompany-onboarding-reject-workspace-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const codexDefinition = getConnectorDefinition("codex_local");
|
||||
const disabledModel = codexDefinition.module.models?.[0]?.id;
|
||||
|
||||
if (!disabledModel) {
|
||||
throw new Error("codex_local requires a model to validate onboarding rejection.");
|
||||
}
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
db.setDisabledModels("codex_local", [{ id: disabledModel, label: disabledModel }]);
|
||||
|
||||
expect(() => createBootstrapHarness(db).bootstrapOnboarding({
|
||||
companyName: "Rejected Model Co",
|
||||
companyDescription: "Tests invalid onboarding model rejection",
|
||||
workspacePath: workspaceDir,
|
||||
autoApproveHires: true,
|
||||
reviewDeliverables: false,
|
||||
executionMode: "ai_ceo",
|
||||
goalTitle: "Reject disabled model bootstrap",
|
||||
goalDescription: "Invalid explicit models should fail before the CEO is created.",
|
||||
connectorId: "codex_local",
|
||||
connectorModel: disabledModel,
|
||||
})).toThrow(`Codex does not allow model "${disabledModel}".`);
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { buildTaskPrompt } from "../../src/main/prompt-builder";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("skill-aware capability registry", () => {
|
||||
it("autoAssignTask prefers an agent whose assigned skill matches the task, even when capability text is generic", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-skill-assign-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Skill Match Co",
|
||||
description: "Tests skill-aware assignment",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
db.saveAgent({
|
||||
companyId,
|
||||
name: "Generic Engineer",
|
||||
role: "Engineer",
|
||||
title: "Engineer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "General software delivery",
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
const styledEngineerId = db.saveAgent({
|
||||
companyId,
|
||||
name: "Frontend Specialist",
|
||||
role: "Engineer",
|
||||
title: "Frontend Specialist",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "General software delivery",
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
|
||||
const skillId = randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
db.upsertCompanySkill({
|
||||
id: skillId,
|
||||
companyId,
|
||||
key: "frontend/react-ui",
|
||||
slug: "react-ui",
|
||||
name: "React UI",
|
||||
description: "Build React components with hooks, Tailwind, and accessible layouts",
|
||||
markdown: "# React UI\nUse when building React components with hooks and Tailwind.",
|
||||
sourceType: "local_path",
|
||||
sourceLocator: null,
|
||||
sourceRef: null,
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: "[]",
|
||||
metadata: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
db.assignSkillToAgent(styledEngineerId, skillId);
|
||||
|
||||
const taskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Build React dashboard widget",
|
||||
description: "Create an accessible React component with hooks and Tailwind styling.",
|
||||
workspaceId,
|
||||
priority: "high",
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const assignedId = db.autoAssignTask(taskId, companyId);
|
||||
expect(assignedId).toBe(styledEngineerId);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("injects the company capability registry into the task prompt", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-capability-prompt-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Registry Co",
|
||||
description: "Tests capability registry prompt injection",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
title: "Chief Executive Officer",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
|
||||
const skillId = randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
db.upsertCompanySkill({
|
||||
id: skillId,
|
||||
companyId,
|
||||
key: "frontend/react-ui",
|
||||
slug: "react-ui",
|
||||
name: "React UI",
|
||||
description: "Use when building React interfaces",
|
||||
markdown: "# React UI\nUse when building React interfaces.",
|
||||
sourceType: "local_path",
|
||||
sourceLocator: null,
|
||||
sourceRef: null,
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: "[]",
|
||||
metadata: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const snapshot = db.listSnapshot();
|
||||
const prompt = buildTaskPrompt({
|
||||
agent: db.getAgent(agentId),
|
||||
apiUrl: "http://127.0.0.1:3210",
|
||||
apiKey: "token",
|
||||
runId: "run-1",
|
||||
task: null,
|
||||
companyName: "Registry Co",
|
||||
companyDescription: "Tests capability registry prompt injection",
|
||||
goals: [],
|
||||
projects: [],
|
||||
directReports: [],
|
||||
chainOfCommand: [],
|
||||
assignedTasks: [],
|
||||
allAgents: snapshot.agents.filter((entry) => entry.companyId === companyId),
|
||||
pendingApprovals: [],
|
||||
tasksAwaitingReview: [],
|
||||
wakeReason: "assignment",
|
||||
allTasks: [],
|
||||
recentComments: [],
|
||||
socialAccounts: [],
|
||||
allConnectors: snapshot.connectors
|
||||
.filter((connector) => connector.id === "codex_local")
|
||||
.map((connector) => ({ id: connector.id, label: connector.label, status: connector.status, enabledModels: ["gpt-5.4"] })),
|
||||
companySkillCatalog: db.listCompanySkills(companyId).map((skill) => ({
|
||||
name: skill.name,
|
||||
slug: skill.slug,
|
||||
description: skill.description,
|
||||
})),
|
||||
companyWorkspaces: snapshot.workspaces
|
||||
.filter((workspace) => workspace.companyId === companyId)
|
||||
.map((workspace) => ({ name: workspace.name, localPath: workspace.localPath })),
|
||||
companyDocuments: [],
|
||||
skillEntries: [
|
||||
{
|
||||
name: "React Standards",
|
||||
whenToUse: "building React UI",
|
||||
allowedTools: ["read", "edit"],
|
||||
effort: "high",
|
||||
contextMode: "inline",
|
||||
},
|
||||
],
|
||||
skillNames: ["React Standards"],
|
||||
});
|
||||
|
||||
expect(prompt).toContain("# Company Capability Registry");
|
||||
expect(prompt).toContain("React UI (`react-ui`)");
|
||||
expect(prompt).toContain("Primary:");
|
||||
expect(prompt).toContain("Ready Connectors");
|
||||
expect(prompt).toContain("Your domain skills:");
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import { createThreadIntegration } from "../../src/main/thread-integration";
|
||||
import { createCheckpointEngine } from "../../src/main/checkpoint-engine";
|
||||
import { dispatchAgentMessage, HUMAN_SENDER_ID } from "../../src/main/message-dispatch";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
function createNoopLogger() {
|
||||
return {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
function createScaffold(db: AppDatabase) {
|
||||
db.updateConnectorHealth({
|
||||
id: "codex_local",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Thread Wait Co",
|
||||
description: "Tests cofounder wait states and replies",
|
||||
status: "active",
|
||||
executionMode: "ai_cofounder",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: "/tmp/thread-wait",
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
title: "Chief Executive Officer",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const engineerId = db.saveAgent({
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
title: "Lead Engineer",
|
||||
status: "idle",
|
||||
reportsTo: ceoId,
|
||||
connectorId: "codex_local",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Ship features",
|
||||
budgetMonthlyUsd: 180,
|
||||
});
|
||||
const taskId = db.saveTask({
|
||||
companyId,
|
||||
title: "Ship the feature",
|
||||
description: "Implement the approved deliverable and wait for co-founder review.",
|
||||
assigneeAgentId: engineerId,
|
||||
workspaceId,
|
||||
priority: "high",
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
return { companyId, workspaceId, ceoId, engineerId, taskId };
|
||||
}
|
||||
|
||||
describe("thread wait + heartbeat integration", () => {
|
||||
it("pauses the assignee heartbeat on successful run completion until a human reply resolves the wait state", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-thread-wait-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const { companyId, engineerId, taskId } = createScaffold(db);
|
||||
const notifications: Array<{ title: string; body: string }> = [];
|
||||
|
||||
const threadIntegration = createThreadIntegration({
|
||||
db,
|
||||
logger: createNoopLogger(),
|
||||
notify: (notification) => {
|
||||
notifications.push(notification);
|
||||
},
|
||||
});
|
||||
const checkpointEngine = createCheckpointEngine({
|
||||
db,
|
||||
logger: createNoopLogger(),
|
||||
threadIntegration,
|
||||
publishDomainChanged: () => {},
|
||||
notify: () => {},
|
||||
});
|
||||
|
||||
threadIntegration.onTaskCreated(taskId, companyId);
|
||||
threadIntegration.onRunFinished("run-1", taskId, engineerId, companyId, "succeeded", null);
|
||||
|
||||
const thread = db.listThreads({
|
||||
companyId,
|
||||
type: "task",
|
||||
relatedEntityType: "task",
|
||||
relatedEntityId: taskId,
|
||||
limit: 1,
|
||||
})[0];
|
||||
|
||||
if (!thread) {
|
||||
throw new Error("Expected a task thread to exist for the completed run.");
|
||||
}
|
||||
|
||||
let waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.threadId === thread.id);
|
||||
expect(waitStates).toHaveLength(1);
|
||||
expect(waitStates[0]?.waitingFor).toBe("user_reply");
|
||||
expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true);
|
||||
|
||||
const otherAgentId = db.saveAgent({
|
||||
companyId,
|
||||
name: "Marketer",
|
||||
role: "CMO",
|
||||
title: "Chief Marketing Officer",
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "codex_local",
|
||||
workspaceId: db.getTask(taskId).workspaceId ?? null,
|
||||
model: null,
|
||||
capabilities: "Drive marketing work",
|
||||
budgetMonthlyUsd: 180,
|
||||
});
|
||||
expect(checkpointEngine.shouldPauseHeartbeat(companyId, otherAgentId)).toBe(false);
|
||||
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0]?.body).toContain("@Board review needed");
|
||||
|
||||
dispatchAgentMessage({
|
||||
db,
|
||||
onHumanMessage: (currentCompanyId, toAgentId, threadId, waitStateId) =>
|
||||
threadIntegration.onHumanMessage(currentCompanyId, toAgentId, threadId, waitStateId),
|
||||
}, {
|
||||
companyId,
|
||||
fromAgentId: HUMAN_SENDER_ID,
|
||||
fromHumanUser: true,
|
||||
channel: "direct",
|
||||
toAgentId: engineerId,
|
||||
threadId: thread.id,
|
||||
body: "Board reviewed this deliverable. Continue.",
|
||||
});
|
||||
|
||||
waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.threadId === thread.id);
|
||||
expect(waitStates).toHaveLength(0);
|
||||
expect(threadIntegration.hasUnresolvedWaitStatesForAgent(companyId, engineerId)).toBe(false);
|
||||
expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(false);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("resolves approval-linked wait states only when the approval is actually decided", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-approval-wait-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const { companyId, engineerId, taskId } = createScaffold(db);
|
||||
const threadIntegration = createThreadIntegration({
|
||||
db,
|
||||
logger: createNoopLogger(),
|
||||
});
|
||||
const checkpointEngine = createCheckpointEngine({
|
||||
db,
|
||||
logger: createNoopLogger(),
|
||||
threadIntegration,
|
||||
publishDomainChanged: () => {},
|
||||
notify: () => {},
|
||||
});
|
||||
|
||||
threadIntegration.onTaskCreated(taskId, companyId);
|
||||
const thread = db.listThreads({
|
||||
companyId,
|
||||
type: "task",
|
||||
relatedEntityType: "task",
|
||||
relatedEntityId: taskId,
|
||||
limit: 1,
|
||||
})[0];
|
||||
|
||||
if (!thread) {
|
||||
throw new Error("Expected a task thread to exist for approval resolution testing.");
|
||||
}
|
||||
|
||||
const approvalId = db.requestApproval({
|
||||
companyId,
|
||||
relatedTaskId: taskId,
|
||||
requestedByAgentId: engineerId,
|
||||
type: "draft_review",
|
||||
payloadSummary: "Review the draft",
|
||||
impactSummary: "Board approval is required before this work can continue.",
|
||||
payloadJson: "{}",
|
||||
});
|
||||
|
||||
threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "draft_review", "Draft review needed");
|
||||
let waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId);
|
||||
expect(waitStates).toHaveLength(1);
|
||||
expect(waitStates[0]?.waitingFor).toBe("user_approval");
|
||||
expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true);
|
||||
|
||||
dispatchAgentMessage({
|
||||
db,
|
||||
onHumanMessage: (currentCompanyId, toAgentId, threadId, waitStateId) =>
|
||||
threadIntegration.onHumanMessage(currentCompanyId, toAgentId, threadId, waitStateId),
|
||||
}, {
|
||||
companyId,
|
||||
fromAgentId: HUMAN_SENDER_ID,
|
||||
fromHumanUser: true,
|
||||
channel: "direct",
|
||||
toAgentId: engineerId,
|
||||
threadId: thread.id,
|
||||
body: "Board left a comment but did not decide approval yet.",
|
||||
});
|
||||
|
||||
waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId);
|
||||
expect(waitStates).toHaveLength(1);
|
||||
expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true);
|
||||
|
||||
db.decideApproval({
|
||||
approvalId,
|
||||
state: "approved",
|
||||
decisionNote: "Approved by board",
|
||||
});
|
||||
threadIntegration.onApprovalResolved(approvalId, companyId, "approved");
|
||||
waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId);
|
||||
expect(waitStates).toHaveLength(0);
|
||||
expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(false);
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user