refactor(db): group database domain modules
This commit is contained in:
1 parent
e43a89096d
commit
6da0a58769
160 files changed
+2124
-1999
No files matched your search
@@ -58,7 +58,7 @@ function createHarness(db: AppDatabase) {
|
||||
runDispatch.resolveWorkspace,
|
||||
runDispatch.getRunEligibilityFailure,
|
||||
runDispatch.dispatchRunToWorker,
|
||||
(filter) => db.runs.hasActiveRun(filter),
|
||||
(filter) => db.core.runs.hasActiveRun(filter),
|
||||
);
|
||||
|
||||
return { enqueueWorkerRun, taskRunner };
|
||||
@@ -70,7 +70,7 @@ describe("agent model switching", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -78,12 +78,12 @@ describe("agent model switching", () => {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Model Switch Co",
|
||||
description: "Tests post-hire model switching",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -91,7 +91,7 @@ describe("agent model switching", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
@@ -107,7 +107,7 @@ describe("agent model switching", () => {
|
||||
|
||||
const { enqueueWorkerRun, taskRunner } = createHarness(db);
|
||||
|
||||
const firstTaskId = db.tasks.saveTask({
|
||||
const firstTaskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Initial model task",
|
||||
description: "Use the first model for this run.",
|
||||
@@ -126,7 +126,7 @@ describe("agent model switching", () => {
|
||||
throw new Error("Expected first run to queue successfully.");
|
||||
}
|
||||
|
||||
db.runs.finishRun({
|
||||
db.core.runs.finishRun({
|
||||
runId: firstRun.data,
|
||||
status: "succeeded",
|
||||
summary: "Done",
|
||||
@@ -137,7 +137,7 @@ describe("agent model switching", () => {
|
||||
unattributedCost: true,
|
||||
});
|
||||
|
||||
db.agents.saveAgent({
|
||||
db.core.agents.saveAgent({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
@@ -152,7 +152,7 @@ describe("agent model switching", () => {
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
|
||||
const secondTaskId = db.tasks.saveTask({
|
||||
const secondTaskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Updated model task",
|
||||
description: "Use the new model for this run.",
|
||||
|
||||
@@ -25,7 +25,7 @@ async function createHarness() {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -33,12 +33,12 @@ async function createHarness() {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Recursive Org Co",
|
||||
description: "Tests manager-driven recursive hires",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -46,7 +46,7 @@ async function createHarness() {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -59,7 +59,7 @@ async function createHarness() {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const ctoId = db.agents.saveAgent({
|
||||
const ctoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CTO",
|
||||
role: "CTO",
|
||||
@@ -73,7 +73,7 @@ async function createHarness() {
|
||||
budgetMonthlyUsd: 220,
|
||||
department: "engineering",
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Build the org",
|
||||
description: "CTO should hire the first IC under engineering.",
|
||||
@@ -82,7 +82,7 @@ async function createHarness() {
|
||||
priority: "high",
|
||||
status: "in_progress",
|
||||
});
|
||||
const run = db.runs.createRun({ companyId, taskId, agentId: ctoId, workspaceId, connectorId: "openai-codex" });
|
||||
const run = db.core.runs.createRun({ companyId, taskId, agentId: ctoId, workspaceId, connectorId: "openai-codex" });
|
||||
const token = createAgentToken(ctoId, companyId, run.id);
|
||||
const server = createApiServer(db, createTestApiServerCallbacks());
|
||||
const port = await server.start();
|
||||
@@ -127,26 +127,26 @@ describe("recursive org execution", () => {
|
||||
const body = await response.json() as { agentId: string; approvalId: string; autoApproved: boolean };
|
||||
expect(body.autoApproved).toBe(false);
|
||||
|
||||
const pendingHire = harness.db.entityCrud.getAgent(body.agentId);
|
||||
const pendingHire = harness.db.core.entity.getAgent(body.agentId);
|
||||
expect(pendingHire.status).toBe("pending_approval");
|
||||
expect(pendingHire.reportsTo).toBe(harness.ctoId);
|
||||
|
||||
const approval = harness.db.approvalsMod.getApprovalRecord(body.approvalId);
|
||||
const approval = harness.db.core.approvals.getApprovalRecord(body.approvalId);
|
||||
expect(approval.type).toBe("hire_agent");
|
||||
expect(approval.requestedByAgentId).toBe(harness.ctoId);
|
||||
expect(approval.relatedAgentId).toBe(body.agentId);
|
||||
|
||||
harness.db.approvalsMod.decideApproval({
|
||||
harness.db.core.approvals.decideApproval({
|
||||
approvalId: body.approvalId,
|
||||
state: "approved",
|
||||
decisionNote: "Approved by board",
|
||||
});
|
||||
|
||||
const approvedHire = harness.db.entityCrud.getAgent(body.agentId);
|
||||
const approvedHire = harness.db.core.entity.getAgent(body.agentId);
|
||||
expect(approvedHire.status).toBe("idle");
|
||||
expect(harness.db.agents.getDirectReports(harness.ctoId, harness.companyId).map((agent) => agent.id)).toContain(body.agentId);
|
||||
expect(harness.db.agents.getDirectReports(harness.ceoId, harness.companyId).map((agent) => agent.id)).not.toContain(body.agentId);
|
||||
expect(harness.db.agents.getChainOfCommand(body.agentId).map((agent) => agent.id)).toEqual([harness.ctoId, harness.ceoId]);
|
||||
expect(harness.db.core.agents.getDirectReports(harness.ctoId, harness.companyId).map((agent) => agent.id)).toContain(body.agentId);
|
||||
expect(harness.db.core.agents.getDirectReports(harness.ceoId, harness.companyId).map((agent) => agent.id)).not.toContain(body.agentId);
|
||||
expect(harness.db.core.agents.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,
|
||||
@@ -170,7 +170,7 @@ describe("recursive org execution", () => {
|
||||
it("routes cross-department hires to the relevant department lead when reportsTo is omitted", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
const hrId = harness.db.agents.saveAgent({
|
||||
const hrId = harness.db.core.agents.saveAgent({
|
||||
companyId: harness.companyId,
|
||||
name: "HR Director",
|
||||
role: "HR Director",
|
||||
@@ -184,7 +184,7 @@ describe("recursive org execution", () => {
|
||||
budgetMonthlyUsd: 180,
|
||||
department: "hr",
|
||||
});
|
||||
const hrTaskId = harness.db.tasks.saveTask({
|
||||
const hrTaskId = harness.db.core.tasks.saveTask({
|
||||
companyId: harness.companyId,
|
||||
title: "Fill engineering gap",
|
||||
description: "Request a backend engineer for the engineering team.",
|
||||
@@ -193,7 +193,7 @@ describe("recursive org execution", () => {
|
||||
priority: "high",
|
||||
status: "in_progress",
|
||||
});
|
||||
const hrRun = harness.db.runs.createRun({
|
||||
const hrRun = harness.db.core.runs.createRun({
|
||||
companyId: harness.companyId,
|
||||
taskId: hrTaskId,
|
||||
agentId: hrId,
|
||||
@@ -224,7 +224,7 @@ describe("recursive org execution", () => {
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
const body = await response.json() as { agentId: string };
|
||||
const pendingHire = harness.db.entityCrud.getAgent(body.agentId);
|
||||
const pendingHire = harness.db.core.entity.getAgent(body.agentId);
|
||||
expect(pendingHire.department).toBe("engineering");
|
||||
expect(pendingHire.reportsTo).toBe(harness.ctoId);
|
||||
} finally {
|
||||
@@ -264,7 +264,7 @@ describe("recursive org execution", () => {
|
||||
it("manager cannot route a hire outside their reporting branch", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
const cmoId = harness.db.agents.saveAgent({
|
||||
const cmoId = harness.db.core.agents.saveAgent({
|
||||
companyId: harness.companyId,
|
||||
name: "CMO",
|
||||
role: "CMO",
|
||||
|
||||
@@ -25,8 +25,8 @@ async function createHarness() {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.company.saveCompany({ name: "Orbit Foundry", description: "API test", status: "active" });
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const companyId = db.core.company.saveCompany({ name: "Orbit Foundry", description: "API test", status: "active" });
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -34,7 +34,7 @@ async function createHarness() {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Helios",
|
||||
role: "CEO",
|
||||
@@ -47,7 +47,7 @@ async function createHarness() {
|
||||
capabilities: "Operate the company",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Ship the API",
|
||||
description: "Capture structured run events.",
|
||||
@@ -56,7 +56,7 @@ async function createHarness() {
|
||||
priority: "high",
|
||||
status: "todo",
|
||||
});
|
||||
const run = db.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "openai-codex" });
|
||||
const run = db.core.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "openai-codex" });
|
||||
const token = createAgentToken(agentId, companyId, run.id);
|
||||
const server = createApiServer(db, createTestApiServerCallbacks());
|
||||
const port = await server.start();
|
||||
@@ -96,7 +96,7 @@ describe("run event API", () => {
|
||||
it("returns persisted run events for the active run token", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
harness.db.runs.appendRunEvent({
|
||||
harness.db.core.runs.appendRunEvent({
|
||||
companyId: harness.companyId,
|
||||
runId: harness.runId,
|
||||
agentId: harness.agentId,
|
||||
@@ -142,7 +142,7 @@ describe("run event API", () => {
|
||||
expect(reader).toBeDefined();
|
||||
|
||||
setTimeout(() => {
|
||||
harness.db.runs.appendRunEvent({
|
||||
harness.db.core.runs.appendRunEvent({
|
||||
companyId: harness.companyId,
|
||||
runId: harness.runId,
|
||||
agentId: harness.agentId,
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("Step 4: Hire approval + decideApproval", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
|
||||
const approvalId = shared.db.approvalsMod.requestApproval({
|
||||
const approvalId = shared.db.core.approvals.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "hire_agent",
|
||||
@@ -23,13 +23,13 @@ describe("Step 4: Hire approval + decideApproval", () => {
|
||||
payloadJson: JSON.stringify({ agentId: hireId }),
|
||||
});
|
||||
|
||||
shared.db.approvalsMod.decideApproval({
|
||||
shared.db.core.approvals.decideApproval({
|
||||
approvalId,
|
||||
state: "approved",
|
||||
decisionNote: "Approved by board",
|
||||
});
|
||||
|
||||
const updatedAgent = shared.db.entityCrud.getAgent(hireId);
|
||||
const updatedAgent = shared.db.core.entity.getAgent(hireId);
|
||||
expect(updatedAgent.status).toBe("idle");
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("Step 4: Hire approval + decideApproval", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
|
||||
const approvalId = shared.db.approvalsMod.requestApproval({
|
||||
const approvalId = shared.db.core.approvals.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "hire_agent",
|
||||
@@ -46,20 +46,20 @@ describe("Step 4: Hire approval + decideApproval", () => {
|
||||
payloadJson: JSON.stringify({ agentId: hireId }),
|
||||
});
|
||||
|
||||
shared.db.approvalsMod.decideApproval({
|
||||
shared.db.core.approvals.decideApproval({
|
||||
approvalId,
|
||||
state: "rejected",
|
||||
decisionNote: "Not needed",
|
||||
});
|
||||
|
||||
const updatedAgent = shared.db.entityCrud.getAgent(hireId);
|
||||
const updatedAgent = shared.db.core.entity.getAgent(hireId);
|
||||
expect(updatedAgent.status).toBe("terminated");
|
||||
});
|
||||
|
||||
it("getApprovalRecord includes payloadJson", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
const approvalId = shared.db.approvalsMod.requestApproval({
|
||||
const approvalId = shared.db.core.approvals.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "hire_agent",
|
||||
@@ -68,7 +68,7 @@ describe("Step 4: Hire approval + decideApproval", () => {
|
||||
payloadJson: JSON.stringify({ agentId: hireId, config: { connector: "openai-codex" } }),
|
||||
});
|
||||
|
||||
const approval = shared.db.approvalsMod.getApprovalRecord(approvalId);
|
||||
const approval = shared.db.core.approvals.getApprovalRecord(approvalId);
|
||||
expect(approval.payloadJson).toBeTruthy();
|
||||
const payload = JSON.parse(approval.payloadJson!);
|
||||
expect(payload.agentId).toBe(hireId);
|
||||
|
||||
@@ -11,7 +11,7 @@ const { createAgent } = shared;
|
||||
|
||||
describe("Automation Rules CRUD", () => {
|
||||
it("creates, reads, toggles, and deletes an automation rule", () => {
|
||||
const ruleId = shared.db.automationMod.saveAutomationRule({
|
||||
const ruleId = shared.db.automation.rules.saveAutomationRule({
|
||||
companyId: shared.companyId,
|
||||
name: "Auto-escalate blocked tasks",
|
||||
description: "When a task is blocked, notify the manager",
|
||||
@@ -28,7 +28,7 @@ describe("Automation Rules CRUD", () => {
|
||||
expect(ruleId).toBeTruthy();
|
||||
|
||||
// Rule appears in snapshot
|
||||
const snapshot = shared.db.snap.listSnapshot();
|
||||
const snapshot = shared.db.projection.snapshot.listSnapshot();
|
||||
const rule = snapshot.automationRules.find(r => r.id === ruleId);
|
||||
expect(rule).toBeTruthy();
|
||||
expect(rule!.name).toBe("Auto-escalate blocked tasks");
|
||||
@@ -39,36 +39,36 @@ describe("Automation Rules CRUD", () => {
|
||||
expect(rule!.executionCount).toBe(0);
|
||||
|
||||
// Toggle to paused
|
||||
shared.db.automationMod.toggleAutomationRule(ruleId, shared.companyId, "paused");
|
||||
const updated = shared.db.snap.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
shared.db.automation.rules.toggleAutomationRule(ruleId, shared.companyId, "paused");
|
||||
const updated = shared.db.projection.snapshot.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
expect(updated!.status).toBe("paused");
|
||||
|
||||
// Record execution
|
||||
shared.db.automationMod.recordAutomationExecution(ruleId, shared.companyId, "Auto-escalate blocked tasks", "task_blocked", "escalate_to_manager", "Escalated task-123");
|
||||
const afterExec = shared.db.snap.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
shared.db.automation.rules.recordAutomationExecution(ruleId, shared.companyId, "Auto-escalate blocked tasks", "task_blocked", "escalate_to_manager", "Escalated task-123");
|
||||
const afterExec = shared.db.projection.snapshot.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
expect(afterExec!.executionCount).toBe(1);
|
||||
expect(afterExec!.lastExecutedAt).toBeTruthy();
|
||||
|
||||
// Automation log
|
||||
const log = shared.db.automationMod.getAutomationLog(shared.companyId);
|
||||
const log = shared.db.automation.rules.getAutomationLog(shared.companyId);
|
||||
expect(log).toHaveLength(1);
|
||||
expect(log[0].ruleName).toBe("Auto-escalate blocked tasks");
|
||||
|
||||
// Delete
|
||||
shared.db.automationMod.deleteAutomationRule(ruleId, shared.companyId);
|
||||
const afterDelete = shared.db.snap.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
shared.db.automation.rules.deleteAutomationRule(ruleId, shared.companyId);
|
||||
const afterDelete = shared.db.projection.snapshot.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
expect(afterDelete).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates an existing rule", () => {
|
||||
const ruleId = shared.db.automationMod.saveAutomationRule({
|
||||
const ruleId = shared.db.automation.rules.saveAutomationRule({
|
||||
companyId: shared.companyId,
|
||||
name: "Original name",
|
||||
trigger: "task_created",
|
||||
action: "assign_task",
|
||||
});
|
||||
|
||||
shared.db.automationMod.saveAutomationRule({
|
||||
shared.db.automation.rules.saveAutomationRule({
|
||||
id: ruleId,
|
||||
companyId: shared.companyId,
|
||||
name: "Updated name",
|
||||
@@ -77,7 +77,7 @@ describe("Automation Rules CRUD", () => {
|
||||
priority: 90,
|
||||
});
|
||||
|
||||
const rule = shared.db.snap.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
const rule = shared.db.projection.snapshot.listSnapshot().automationRules.find(r => r.id === ruleId);
|
||||
expect(rule!.name).toBe("Updated name");
|
||||
expect(rule!.action).toBe("notify_agent");
|
||||
expect(rule!.priority).toBe(90);
|
||||
@@ -89,13 +89,13 @@ describe("Agent Messages", () => {
|
||||
let agentBId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
agentAId = shared.db.agents.saveAgent({
|
||||
agentAId = shared.db.core.agents.saveAgent({
|
||||
companyId: shared.companyId, name: "Alice", role: "Engineer", title: "Senior Engineer",
|
||||
department: "engineering", connectorId: "claude-pro", status: "active",
|
||||
capabilities: "Backend development, API design, code review",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
agentBId = shared.db.agents.saveAgent({
|
||||
agentBId = shared.db.core.agents.saveAgent({
|
||||
companyId: shared.companyId, name: "Bob", role: "Designer", title: "UX Designer",
|
||||
department: "design", connectorId: "claude-pro", status: "active",
|
||||
capabilities: "UI design, wireframing, user research",
|
||||
@@ -104,7 +104,7 @@ describe("Agent Messages", () => {
|
||||
});
|
||||
|
||||
it("sends and retrieves direct messages", () => {
|
||||
const msgId = shared.db.messages.sendAgentMessage({
|
||||
const msgId = shared.db.core.messages.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: agentAId,
|
||||
toAgentId: agentBId,
|
||||
@@ -116,7 +116,7 @@ describe("Agent Messages", () => {
|
||||
|
||||
expect(msgId).toBeTruthy();
|
||||
|
||||
const messages = shared.db.messages.listAgentMessages(shared.companyId);
|
||||
const messages = shared.db.core.messages.listAgentMessages(shared.companyId);
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].subject).toBe("Need design review");
|
||||
expect(messages[0].fromAgentId).toBe(agentAId);
|
||||
@@ -125,7 +125,7 @@ describe("Agent Messages", () => {
|
||||
});
|
||||
|
||||
it("marks messages as read", () => {
|
||||
const msgId = shared.db.messages.sendAgentMessage({
|
||||
const msgId = shared.db.core.messages.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: agentAId,
|
||||
toAgentId: agentBId,
|
||||
@@ -133,14 +133,14 @@ describe("Agent Messages", () => {
|
||||
body: "Hello",
|
||||
});
|
||||
|
||||
shared.db.messages.markMessageReadForReader(msgId, shared.companyId);
|
||||
const messages = shared.db.messages.listAgentMessages(shared.companyId);
|
||||
shared.db.core.messages.markMessageReadForReader(msgId, shared.companyId);
|
||||
const messages = shared.db.core.messages.listAgentMessages(shared.companyId);
|
||||
const updatedMessage = messages.find((message) => message.id === msgId);
|
||||
expect(updatedMessage?.readAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("tracks read state per agent without marking the sender unread", () => {
|
||||
const msgId = shared.db.messages.sendAgentMessage({
|
||||
const msgId = shared.db.core.messages.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: agentAId,
|
||||
toAgentId: agentBId,
|
||||
@@ -148,22 +148,22 @@ describe("Agent Messages", () => {
|
||||
body: "Hello",
|
||||
});
|
||||
|
||||
const senderView = shared.db.messages.listAgentMessages(shared.companyId, { agentId: agentAId });
|
||||
const senderView = shared.db.core.messages.listAgentMessages(shared.companyId, { agentId: agentAId });
|
||||
expect(senderView).toHaveLength(1);
|
||||
expect(senderView[0].id).toBe(msgId);
|
||||
expect(senderView[0].readAt).toBeTruthy();
|
||||
|
||||
const recipientViewBefore = shared.db.messages.listAgentMessages(shared.companyId, { agentId: agentBId, unreadOnly: true });
|
||||
const recipientViewBefore = shared.db.core.messages.listAgentMessages(shared.companyId, { agentId: agentBId, unreadOnly: true });
|
||||
expect(recipientViewBefore).toHaveLength(1);
|
||||
expect(recipientViewBefore[0].id).toBe(msgId);
|
||||
|
||||
shared.db.messages.markMessageReadForReader(msgId, shared.companyId, agentBId);
|
||||
shared.db.core.messages.markMessageReadForReader(msgId, shared.companyId, agentBId);
|
||||
|
||||
const recipientViewAfter = shared.db.messages.listAgentMessages(shared.companyId, { agentId: agentBId, unreadOnly: true });
|
||||
const recipientViewAfter = shared.db.core.messages.listAgentMessages(shared.companyId, { agentId: agentBId, unreadOnly: true });
|
||||
expect(recipientViewAfter).toHaveLength(0);
|
||||
|
||||
const thirdAgentId = createAgent({ name: "Agent C" });
|
||||
const thirdView = shared.db.messages.listAgentMessages(shared.companyId, { agentId: thirdAgentId });
|
||||
const thirdView = shared.db.core.messages.listAgentMessages(shared.companyId, { agentId: thirdAgentId });
|
||||
expect(thirdView).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -171,14 +171,14 @@ describe("Agent Messages", () => {
|
||||
const engineeringLeadId = createAgent({ name: "Engineering Lead", role: "Lead", department: "engineering" });
|
||||
const productLeadId = createAgent({ name: "Product Lead", role: "Lead", department: "product" });
|
||||
|
||||
const projectId = shared.db.entityCrud.saveProject({
|
||||
const projectId = shared.db.core.entity.saveProject({
|
||||
companyId: shared.companyId,
|
||||
name: "Cross-team Project",
|
||||
description: "Scope",
|
||||
leadAgentId: engineeringLeadId,
|
||||
status: "active",
|
||||
});
|
||||
shared.db.tasks.saveTask({
|
||||
shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Project task",
|
||||
description: "Do work",
|
||||
@@ -188,7 +188,7 @@ describe("Agent Messages", () => {
|
||||
priority: "medium",
|
||||
});
|
||||
|
||||
const deptMessageId = shared.db.messages.sendAgentMessage({
|
||||
const deptMessageId = shared.db.core.messages.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: engineeringLeadId,
|
||||
channel: "department",
|
||||
@@ -196,7 +196,7 @@ describe("Agent Messages", () => {
|
||||
subject: "Engineering update",
|
||||
body: "Only engineering should see this",
|
||||
});
|
||||
const projectMessageId = shared.db.messages.sendAgentMessage({
|
||||
const projectMessageId = shared.db.core.messages.sendAgentMessage({
|
||||
companyId: shared.companyId,
|
||||
fromAgentId: engineeringLeadId,
|
||||
channel: "project",
|
||||
@@ -205,31 +205,31 @@ describe("Agent Messages", () => {
|
||||
body: "Only project members should see this",
|
||||
});
|
||||
|
||||
const engineeringView = shared.db.messages.listAgentMessages(shared.companyId, { agentId: engineeringLeadId });
|
||||
const engineeringView = shared.db.core.messages.listAgentMessages(shared.companyId, { agentId: engineeringLeadId });
|
||||
expect(engineeringView.some((message) => message.id === deptMessageId)).toBe(true);
|
||||
expect(engineeringView.some((message) => message.id === projectMessageId)).toBe(true);
|
||||
|
||||
const productView = shared.db.messages.listAgentMessages(shared.companyId, { agentId: productLeadId });
|
||||
const productView = shared.db.core.messages.listAgentMessages(shared.companyId, { agentId: productLeadId });
|
||||
expect(productView.some((message) => message.id === deptMessageId)).toBe(false);
|
||||
expect(productView.some((message) => message.id === projectMessageId)).toBe(false);
|
||||
});
|
||||
|
||||
it("filters messages by channel", () => {
|
||||
shared.db.messages.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, subject: "Dept update", body: "All good", channel: "department", channelTargetId: "engineering" });
|
||||
shared.db.messages.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, toAgentId: agentBId, subject: "DM", body: "Hey", channel: "direct" });
|
||||
shared.db.core.messages.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, subject: "Dept update", body: "All good", channel: "department", channelTargetId: "engineering" });
|
||||
shared.db.core.messages.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, toAgentId: agentBId, subject: "DM", body: "Hey", channel: "direct" });
|
||||
|
||||
const deptMsgs = shared.db.messages.listAgentMessages(shared.companyId, { channel: "department" });
|
||||
const deptMsgs = shared.db.core.messages.listAgentMessages(shared.companyId, { channel: "department" });
|
||||
expect(deptMsgs).toHaveLength(1);
|
||||
expect(deptMsgs[0].subject).toBe("Dept update");
|
||||
|
||||
const directMsgs = shared.db.messages.listAgentMessages(shared.companyId, { channel: "direct" });
|
||||
const directMsgs = shared.db.core.messages.listAgentMessages(shared.companyId, { channel: "direct" });
|
||||
expect(directMsgs).toHaveLength(1);
|
||||
expect(directMsgs[0].subject).toBe("DM");
|
||||
});
|
||||
|
||||
it("appears in snapshot", () => {
|
||||
shared.db.messages.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, channel: "company", subject: "Snapshot test", body: "Hi" });
|
||||
const snapshot = shared.db.snap.listSnapshot();
|
||||
shared.db.core.messages.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, channel: "company", subject: "Snapshot test", body: "Hi" });
|
||||
const snapshot = shared.db.projection.snapshot.listSnapshot();
|
||||
expect(snapshot.agentMessages.length).toBeGreaterThanOrEqual(1);
|
||||
expect(snapshot.agentMessages[0].subject).toBe("Snapshot test");
|
||||
});
|
||||
@@ -240,13 +240,13 @@ describe("Auto Task Assignment", () => {
|
||||
let frontendId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
backendId = shared.db.agents.saveAgent({
|
||||
backendId = shared.db.core.agents.saveAgent({
|
||||
companyId: shared.companyId, name: "Backend Dev", role: "Backend Engineer", title: "Senior Backend",
|
||||
department: "engineering", connectorId: "openai-codex", status: "active",
|
||||
capabilities: "API development, database design, Node.js, PostgreSQL, backend testing",
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
frontendId = shared.db.agents.saveAgent({
|
||||
frontendId = shared.db.core.agents.saveAgent({
|
||||
companyId: shared.companyId, name: "Frontend Dev", role: "Frontend Engineer", title: "Senior Frontend",
|
||||
department: "engineering", connectorId: "openai-codex", status: "active",
|
||||
capabilities: "React, TypeScript, CSS, Tailwind, responsive design, frontend testing, UI components",
|
||||
@@ -255,68 +255,68 @@ describe("Auto Task Assignment", () => {
|
||||
});
|
||||
|
||||
it("assigns to the best-matching agent by capability keywords", () => {
|
||||
const taskId = shared.db.tasks.saveTask({
|
||||
const taskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId, title: "Build React component for user dashboard",
|
||||
description: "Create a responsive React component with Tailwind CSS styling",
|
||||
priority: "medium", status: "todo",
|
||||
});
|
||||
|
||||
const assignedId = shared.db.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
const assignedId = shared.db.core.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
expect(assignedId).toBe(frontendId); // "React", "responsive", "Tailwind" match frontend
|
||||
});
|
||||
|
||||
it("assigns backend task to backend agent", () => {
|
||||
const taskId = shared.db.tasks.saveTask({
|
||||
const taskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId, title: "Design database schema for orders",
|
||||
description: "PostgreSQL database design with API endpoints for order management",
|
||||
priority: "high", status: "todo",
|
||||
});
|
||||
|
||||
const assignedId = shared.db.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
const assignedId = shared.db.core.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
expect(assignedId).toBe(backendId); // "database", "PostgreSQL", "API" match backend
|
||||
});
|
||||
|
||||
it("returns existing assignee if task already assigned", () => {
|
||||
const taskId = shared.db.tasks.saveTask({
|
||||
const taskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId, title: "Already assigned task",
|
||||
description: "This task has an assignee",
|
||||
assigneeAgentId: frontendId,
|
||||
priority: "medium", status: "todo",
|
||||
});
|
||||
|
||||
const result = shared.db.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
const result = shared.db.core.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
expect(result).toBe(frontendId);
|
||||
});
|
||||
|
||||
it("returns null if all agents have exhausted their budget", () => {
|
||||
// Set positive budgets that are fully spent
|
||||
shared.db.agents.saveAgent({ id: backendId, companyId: shared.companyId, name: "Backend Dev", role: "Backend", title: "Backend", connectorId: "openai-codex", status: "active", capabilities: "backend", budgetMonthlyUsd: 100 });
|
||||
shared.db.agents.saveAgent({ id: frontendId, companyId: shared.companyId, name: "Frontend Dev", role: "Frontend", title: "Frontend", connectorId: "openai-codex", status: "active", capabilities: "frontend", budgetMonthlyUsd: 100 });
|
||||
shared.db.core.agents.saveAgent({ id: backendId, companyId: shared.companyId, name: "Backend Dev", role: "Backend", title: "Backend", connectorId: "openai-codex", status: "active", capabilities: "backend", budgetMonthlyUsd: 100 });
|
||||
shared.db.core.agents.saveAgent({ id: frontendId, companyId: shared.companyId, name: "Frontend Dev", role: "Frontend", title: "Frontend", connectorId: "openai-codex", status: "active", capabilities: "frontend", budgetMonthlyUsd: 100 });
|
||||
// Exhaust budgets by recording spending via direct SQL (since finishRun would need a real run)
|
||||
(shared.db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(backendId);
|
||||
(shared.db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(frontendId);
|
||||
|
||||
const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "No budget task", description: "Test", priority: "low", status: "todo" });
|
||||
const result = shared.db.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
const taskId = shared.db.core.tasks.saveTask({ companyId: shared.companyId, title: "No budget task", description: "Test", priority: "low", status: "todo" });
|
||||
const result = shared.db.core.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("penalizes agents with many active tasks", () => {
|
||||
// Give backend agent 5 active tasks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
shared.db.tasks.saveTask({ companyId: shared.companyId, title: `Backend task ${i}`, description: "", assigneeAgentId: backendId, priority: "medium", status: "in_progress" });
|
||||
shared.db.core.tasks.saveTask({ companyId: shared.companyId, title: `Backend task ${i}`, description: "", assigneeAgentId: backendId, priority: "medium", status: "in_progress" });
|
||||
}
|
||||
|
||||
// Generic task with no strong keyword match — should go to less busy frontend agent
|
||||
const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Implement feature", description: "Build the feature", priority: "medium", status: "todo" });
|
||||
const assignedId = shared.db.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
const taskId = shared.db.core.tasks.saveTask({ companyId: shared.companyId, title: "Implement feature", description: "Build the feature", priority: "medium", status: "todo" });
|
||||
const assignedId = shared.db.core.tasks.autoAssignTask(taskId, shared.companyId);
|
||||
expect(assignedId).toBe(frontendId); // Frontend has 0 active tasks vs backend's 5
|
||||
});
|
||||
});
|
||||
|
||||
describe("Workflow Pipelines", () => {
|
||||
it("creates, activates, and deletes a workflow", () => {
|
||||
const wfId = shared.db.automationMod.saveWorkflow({
|
||||
const wfId = shared.db.automation.rules.saveWorkflow({
|
||||
companyId: shared.companyId,
|
||||
name: "Code Review Pipeline",
|
||||
description: "Auto-review flow for PRs",
|
||||
@@ -330,14 +330,14 @@ describe("Workflow Pipelines", () => {
|
||||
});
|
||||
|
||||
expect(wfId).toBeTruthy();
|
||||
const snapshot = shared.db.snap.listSnapshot();
|
||||
const snapshot = shared.db.projection.snapshot.listSnapshot();
|
||||
const wf = snapshot.workflows.find(w => w.id === wfId);
|
||||
expect(wf!.name).toBe("Code Review Pipeline");
|
||||
expect(wf!.status).toBe("draft");
|
||||
expect(wf!.runCount).toBe(0);
|
||||
|
||||
// Delete
|
||||
shared.db.automationMod.deleteWorkflow(wfId, shared.companyId);
|
||||
expect(shared.db.snap.listSnapshot().workflows.find(w => w.id === wfId)).toBeUndefined();
|
||||
shared.db.automation.rules.deleteWorkflow(wfId, shared.companyId);
|
||||
expect(shared.db.projection.snapshot.listSnapshot().workflows.find(w => w.id === wfId)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe("Step 2: CEO phased prompts", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
|
||||
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
const hire = shared.db.entityCrud.getAgent(hireId);
|
||||
const hire = shared.db.core.entity.getAgent(hireId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
@@ -86,7 +86,7 @@ describe("Step 2: CEO phased prompts", () => {
|
||||
it("Phase 2: shows normal executive instructions when active reports and no pending org approval", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" });
|
||||
const cto = shared.db.entityCrud.getAgent(ctoId);
|
||||
const cto = shared.db.core.entity.getAgent(ctoId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
@@ -107,7 +107,7 @@ describe("Step 2: CEO phased prompts", () => {
|
||||
it("includes pending approvals section in prompt", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" });
|
||||
const cto = shared.db.entityCrud.getAgent(ctoId);
|
||||
const cto = shared.db.core.entity.getAgent(ctoId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
@@ -137,10 +137,10 @@ describe("Step 2: CEO phased prompts", () => {
|
||||
|
||||
it("Manager prompt includes credential escalation guidance", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ceo = shared.db.entityCrud.getAgent(ceoId);
|
||||
const ceo = shared.db.core.entity.getAgent(ceoId);
|
||||
const managerId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: managerId });
|
||||
const ic = shared.db.entityCrud.getAgent(icId);
|
||||
const ic = shared.db.core.entity.getAgent(icId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: managerId,
|
||||
@@ -154,7 +154,7 @@ describe("Step 2: CEO phased prompts", () => {
|
||||
|
||||
it("IC prompt includes credential escalation guidance", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ceo = shared.db.entityCrud.getAgent(ceoId);
|
||||
const ceo = shared.db.core.entity.getAgent(ceoId);
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ceoId });
|
||||
|
||||
const ctx = buildCtx({
|
||||
|
||||
@@ -13,7 +13,7 @@ const { createAgent } = shared;
|
||||
describe("Step 7: Worker crash recovery (database layer)", () => {
|
||||
it("interruptStaleRuns clears running and queued runs on init", async () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
const taskId = shared.db.tasks.saveTask({
|
||||
const taskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "In-progress task",
|
||||
description: "Running when crash happened",
|
||||
@@ -23,7 +23,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => {
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const run = shared.db.runs.createRun({
|
||||
const run = shared.db.core.runs.createRun({
|
||||
companyId: shared.companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -31,10 +31,10 @@ describe("Step 7: Worker crash recovery (database layer)", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
shared.db.runs.markRunStatus(run.id, "running", "Started");
|
||||
shared.db.core.runs.markRunStatus(run.id, "running", "Started");
|
||||
|
||||
// Verify it's running
|
||||
let snapshot = shared.db.snap.listSnapshot();
|
||||
let snapshot = shared.db.projection.snapshot.listSnapshot();
|
||||
expect(snapshot.runs.find((r) => r.id === run.id)?.status).toBe("running");
|
||||
|
||||
// Simulate app restart — close and re-init
|
||||
@@ -44,14 +44,14 @@ describe("Step 7: Worker crash recovery (database layer)", () => {
|
||||
shared.db = new AppDatabase(profileDir);
|
||||
shared.db.init(); // This calls interruptStaleRuns()
|
||||
|
||||
snapshot = shared.db.snap.listSnapshot();
|
||||
snapshot = shared.db.projection.snapshot.listSnapshot();
|
||||
const recoveredRun = snapshot.runs.find((r) => r.id === run.id);
|
||||
expect(recoveredRun?.status).toBe("interrupted");
|
||||
});
|
||||
|
||||
it("finishRun can mark interrupted runs from crash recovery", () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
const taskId = shared.db.tasks.saveTask({
|
||||
const taskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Crash recovery task",
|
||||
description: "Test",
|
||||
@@ -61,7 +61,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => {
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const run = shared.db.runs.createRun({
|
||||
const run = shared.db.core.runs.createRun({
|
||||
companyId: shared.companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -69,10 +69,10 @@ describe("Step 7: Worker crash recovery (database layer)", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
shared.db.runs.markRunStatus(run.id, "running", "Started");
|
||||
shared.db.core.runs.markRunStatus(run.id, "running", "Started");
|
||||
|
||||
// Simulate crash recovery - mark as interrupted
|
||||
shared.db.runs.finishRun({
|
||||
shared.db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "interrupted",
|
||||
summary: "Worker process crashed.",
|
||||
@@ -83,7 +83,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => {
|
||||
unattributedCost: true,
|
||||
});
|
||||
|
||||
const snapshot = shared.db.snap.listSnapshot();
|
||||
const snapshot = shared.db.projection.snapshot.listSnapshot();
|
||||
const recovered = snapshot.runs.find((r) => r.id === run.id);
|
||||
expect(recovered?.status).toBe("interrupted");
|
||||
expect(recovered?.errorMessage).toBe("Worker process exited unexpectedly.");
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
const cmoId = createAgent({ name: "Aria", role: "CMO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
|
||||
// 3. CEO submits org strategy approval
|
||||
const orgApprovalId = shared.db.approvalsMod.requestApproval({
|
||||
const orgApprovalId = shared.db.core.approvals.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "approve_ceo_strategy",
|
||||
@@ -40,8 +40,8 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
});
|
||||
|
||||
// CEO now in Phase 1
|
||||
const ctoRecord = shared.db.entityCrud.getAgent(ctoId);
|
||||
const cmoRecord = shared.db.entityCrud.getAgent(cmoId);
|
||||
const ctoRecord = shared.db.core.entity.getAgent(ctoId);
|
||||
const cmoRecord = shared.db.core.entity.getAgent(cmoId);
|
||||
const phase1Ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
directReports: [ctoRecord, cmoRecord],
|
||||
@@ -64,7 +64,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
expect(phase1Prompt).toContain("Awaiting Org Strategy Approval");
|
||||
|
||||
// 4. Board approves hires
|
||||
const hireApprovalCTO = shared.db.approvalsMod.requestApproval({
|
||||
const hireApprovalCTO = shared.db.core.approvals.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "hire_agent",
|
||||
@@ -72,9 +72,9 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
impactSummary: "Test",
|
||||
payloadJson: JSON.stringify({ agentId: ctoId }),
|
||||
});
|
||||
shared.db.approvalsMod.decideApproval({ approvalId: hireApprovalCTO, state: "approved", decisionNote: "Approved" });
|
||||
shared.db.core.approvals.decideApproval({ approvalId: hireApprovalCTO, state: "approved", decisionNote: "Approved" });
|
||||
|
||||
const hireApprovalCMO = shared.db.approvalsMod.requestApproval({
|
||||
const hireApprovalCMO = shared.db.core.approvals.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "hire_agent",
|
||||
@@ -82,18 +82,18 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
impactSummary: "Test",
|
||||
payloadJson: JSON.stringify({ agentId: cmoId }),
|
||||
});
|
||||
shared.db.approvalsMod.decideApproval({ approvalId: hireApprovalCMO, state: "approved", decisionNote: "Approved" });
|
||||
shared.db.core.approvals.decideApproval({ approvalId: hireApprovalCMO, state: "approved", decisionNote: "Approved" });
|
||||
|
||||
// Agents now idle
|
||||
expect(shared.db.entityCrud.getAgent(ctoId).status).toBe("idle");
|
||||
expect(shared.db.entityCrud.getAgent(cmoId).status).toBe("idle");
|
||||
expect(shared.db.core.entity.getAgent(ctoId).status).toBe("idle");
|
||||
expect(shared.db.core.entity.getAgent(cmoId).status).toBe("idle");
|
||||
|
||||
// 5. Board approves org strategy
|
||||
shared.db.approvalsMod.decideApproval({ approvalId: orgApprovalId, state: "approved", decisionNote: "Go ahead" });
|
||||
shared.db.core.approvals.decideApproval({ approvalId: orgApprovalId, state: "approved", decisionNote: "Go ahead" });
|
||||
|
||||
// 6. CEO now in Phase 2
|
||||
const updatedCto = shared.db.entityCrud.getAgent(ctoId);
|
||||
const updatedCmo = shared.db.entityCrud.getAgent(cmoId);
|
||||
const updatedCto = shared.db.core.entity.getAgent(ctoId);
|
||||
const updatedCmo = shared.db.core.entity.getAgent(cmoId);
|
||||
const phase2Ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
directReports: [updatedCto, updatedCmo],
|
||||
@@ -106,12 +106,12 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
expect(phase2Prompt).toContain("Delegation");
|
||||
|
||||
// 7. Verify hierarchical chain
|
||||
const ctoChain = shared.db.agents.getChainOfCommand(ctoId);
|
||||
const ctoChain = shared.db.core.agents.getChainOfCommand(ctoId);
|
||||
expect(ctoChain).toHaveLength(1);
|
||||
expect(ctoChain[0].id).toBe(ceoId);
|
||||
|
||||
// 8. Create task tree
|
||||
const projectId = shared.db.entityCrud.saveProject({
|
||||
const projectId = shared.db.core.entity.saveProject({
|
||||
companyId: shared.companyId,
|
||||
name: "Launch MVP",
|
||||
description: "Build and launch MVP",
|
||||
@@ -120,7 +120,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const parentTaskId = shared.db.tasks.saveTask({
|
||||
const parentTaskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Build backend",
|
||||
description: "Implement API",
|
||||
@@ -133,7 +133,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
|
||||
// CTO creates subtask for IC (would be done via API)
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId, status: "idle" });
|
||||
const subtaskId = shared.db.tasks.saveTask({
|
||||
const subtaskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Implement auth",
|
||||
description: "JWT auth",
|
||||
@@ -146,12 +146,12 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
});
|
||||
|
||||
// Verify parent-child relationship
|
||||
const subtask = shared.db.entityCrud.getTask(subtaskId);
|
||||
const subtask = shared.db.core.entity.getTask(subtaskId);
|
||||
expect(subtask.parentId).toBe(parentTaskId);
|
||||
|
||||
// 9. Wakeup request test: enqueue wake while IC is busy
|
||||
shared.db.wakeup.enqueueWakeupRequest(shared.companyId, icId, "comment", "comment");
|
||||
const queuedWakes = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId: icId, status: "queued" });
|
||||
shared.db.automation.wakeup.enqueueWakeupRequest(shared.companyId, icId, "comment", "comment");
|
||||
const queuedWakes = shared.db.automation.wakeup.listWakeupRequests(shared.companyId, { agentId: icId, status: "queued" });
|
||||
expect(queuedWakes).toHaveLength(1);
|
||||
expect(queuedWakes[0].source).toBe("comment");
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
|
||||
const icPromptCtx = buildCtx({
|
||||
agentId: icId,
|
||||
task: subtask,
|
||||
chainOfCommand: [shared.db.entityCrud.getAgent(ctoId), shared.db.entityCrud.getAgent(ceoId)],
|
||||
chainOfCommand: [shared.db.core.entity.getAgent(ctoId), shared.db.core.entity.getAgent(ceoId)],
|
||||
wakeReason: "assignment",
|
||||
});
|
||||
const icPrompt = buildTaskPrompt(icPromptCtx);
|
||||
@@ -175,12 +175,12 @@ describe("E2E: Wakeup request lifecycle with run lifecycle", () => {
|
||||
|
||||
// Simulate: agent is running, wakes come in. All triggers are coalesced
|
||||
// into a single wakeup request with JSON array of trigger details.
|
||||
shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed");
|
||||
shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment");
|
||||
shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "report_blocked", "report_blocked");
|
||||
shared.db.automation.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed");
|
||||
shared.db.automation.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment");
|
||||
shared.db.automation.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "report_blocked", "report_blocked");
|
||||
|
||||
// Verify coalesced into single row with all triggers
|
||||
const wakes = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" });
|
||||
const wakes = shared.db.automation.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" });
|
||||
expect(wakes).toHaveLength(1);
|
||||
expect(wakes[0].coalescedCount).toBe(2);
|
||||
|
||||
@@ -191,7 +191,7 @@ describe("E2E: Wakeup request lifecycle with run lifecycle", () => {
|
||||
expect(triggers[2].trigger).toBe("report_blocked");
|
||||
|
||||
// Wakes remain queued until consumed by the run lifecycle
|
||||
expect(shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" })).toHaveLength(1);
|
||||
expect(shared.db.automation.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" })).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -202,11 +202,11 @@ describe("E2E: Blocked escalation through org hierarchy", () => {
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId });
|
||||
|
||||
// When IC marks task blocked, we need to find its manager
|
||||
const chain = shared.db.agents.getChainOfCommand(icId);
|
||||
const chain = shared.db.core.agents.getChainOfCommand(icId);
|
||||
expect(chain[0].id).toBe(ctoId); // immediate manager gets woken
|
||||
|
||||
// When CTO needs to escalate further
|
||||
const ctoChain = shared.db.agents.getChainOfCommand(ctoId);
|
||||
const ctoChain = shared.db.core.agents.getChainOfCommand(ctoId);
|
||||
expect(ctoChain[0].id).toBe(ceoId);
|
||||
});
|
||||
});
|
||||
@@ -216,7 +216,7 @@ describe("Review Fix: CEO phase ordering edge case", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
// Hires submitted but not yet approved
|
||||
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" });
|
||||
const hire = shared.db.entityCrud.getAgent(hireId);
|
||||
const hire = shared.db.core.entity.getAgent(hireId);
|
||||
|
||||
// Org strategy already approved (not in pendingApprovals), but hires still pending
|
||||
const ctx = buildCtx({
|
||||
@@ -236,7 +236,7 @@ describe("Review Fix: CEO phase ordering edge case", () => {
|
||||
it("moves to Phase 2 once hires are approved (no pending org approval)", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" });
|
||||
const cto = shared.db.entityCrud.getAgent(ctoId);
|
||||
const cto = shared.db.core.entity.getAgent(ctoId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: ceoId,
|
||||
@@ -287,7 +287,7 @@ describe("Review Fix: Rejected approval wakes agent", () => {
|
||||
it("decideApproval with rejected state still triggers wake (tested via DB)", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
|
||||
const approvalId = shared.db.approvalsMod.requestApproval({
|
||||
const approvalId = shared.db.core.approvals.requestApproval({
|
||||
companyId: shared.companyId,
|
||||
requestedByAgentId: ceoId,
|
||||
type: "approve_ceo_strategy",
|
||||
@@ -295,14 +295,14 @@ describe("Review Fix: Rejected approval wakes agent", () => {
|
||||
impactSummary: "test",
|
||||
});
|
||||
|
||||
shared.db.approvalsMod.decideApproval({
|
||||
shared.db.core.approvals.decideApproval({
|
||||
approvalId,
|
||||
state: "rejected",
|
||||
decisionNote: "Not ready",
|
||||
});
|
||||
|
||||
// Verify approval state
|
||||
const approval = shared.db.approvalsMod.getApprovalRecord(approvalId);
|
||||
const approval = shared.db.core.approvals.getApprovalRecord(approvalId);
|
||||
expect(approval.state).toBe("rejected");
|
||||
// The IPC handler in index.ts now wakes on rejected too —
|
||||
// we verify the DB layer works; the wakeAgentIfPossible call is in index.ts
|
||||
@@ -315,7 +315,7 @@ describe("Review Fix: Root task completion notification", () => {
|
||||
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
|
||||
// CTO has a root-level task (no parentId)
|
||||
const taskId = shared.db.tasks.saveTask({
|
||||
const taskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Standalone task from CEO",
|
||||
description: "No parent",
|
||||
@@ -325,11 +325,11 @@ describe("Review Fix: Root task completion notification", () => {
|
||||
status: "done",
|
||||
});
|
||||
|
||||
const task = shared.db.entityCrud.getTask(taskId);
|
||||
const task = shared.db.core.entity.getTask(taskId);
|
||||
expect(task.parentId).toBeNull();
|
||||
|
||||
// CTO's chain of command includes CEO — so api-server can wake CEO on completion
|
||||
const chain = shared.db.agents.getChainOfCommand(ctoId);
|
||||
const chain = shared.db.core.agents.getChainOfCommand(ctoId);
|
||||
expect(chain).toHaveLength(1);
|
||||
expect(chain[0].id).toBe(ceoId);
|
||||
});
|
||||
|
||||
@@ -31,9 +31,9 @@ describe("Step 1: CEO role detection", () => {
|
||||
it("identifies manager when has reports and has chain of command", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const managerId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
const ceo = shared.db.entityCrud.getAgent(ceoId);
|
||||
const ceo = shared.db.core.entity.getAgent(ceoId);
|
||||
const icId = createAgent({ name: "IC", role: "Engineer", reportsTo: managerId });
|
||||
const ic = shared.db.entityCrud.getAgent(icId);
|
||||
const ic = shared.db.core.entity.getAgent(icId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId: managerId,
|
||||
@@ -49,7 +49,7 @@ describe("Step 1: CEO role detection", () => {
|
||||
|
||||
it("identifies IC when has chain of command and no direct reports", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ceo = shared.db.entityCrud.getAgent(ceoId);
|
||||
const ceo = shared.db.core.entity.getAgent(ceoId);
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ceoId });
|
||||
|
||||
const ctx = buildCtx({
|
||||
|
||||
@@ -12,7 +12,7 @@ const { createAgent } = shared;
|
||||
describe("Step 6: Run failure escalation (database layer)", () => {
|
||||
it("finishRun marks run as failed and returns outcome", () => {
|
||||
const agentId = createAgent({ name: "IC", role: "Engineer" });
|
||||
const taskId = shared.db.tasks.saveTask({
|
||||
const taskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Failing task",
|
||||
description: "Will fail",
|
||||
@@ -22,7 +22,7 @@ describe("Step 6: Run failure escalation (database layer)", () => {
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const run = shared.db.runs.createRun({
|
||||
const run = shared.db.core.runs.createRun({
|
||||
companyId: shared.companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -30,9 +30,9 @@ describe("Step 6: Run failure escalation (database layer)", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
shared.db.runs.markRunStatus(run.id, "running", "Started");
|
||||
shared.db.core.runs.markRunStatus(run.id, "running", "Started");
|
||||
|
||||
const outcome = shared.db.runs.finishRun({
|
||||
const outcome = shared.db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "failed",
|
||||
summary: null,
|
||||
@@ -45,7 +45,7 @@ describe("Step 6: Run failure escalation (database layer)", () => {
|
||||
|
||||
expect(outcome.budgetHardStopped).toBe(false);
|
||||
|
||||
const snapshot = shared.db.snap.listSnapshot();
|
||||
const snapshot = shared.db.projection.snapshot.listSnapshot();
|
||||
const finishedRun = snapshot.runs.find((r) => r.id === run.id);
|
||||
expect(finishedRun?.status).toBe("failed");
|
||||
expect(finishedRun?.errorMessage).toBe("Process crashed");
|
||||
@@ -56,7 +56,7 @@ describe("Step 6: Run failure escalation (database layer)", () => {
|
||||
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId });
|
||||
|
||||
const chain = shared.db.agents.getChainOfCommand(icId);
|
||||
const chain = shared.db.core.agents.getChainOfCommand(icId);
|
||||
// First in chain is the immediate manager (CTO)
|
||||
expect(chain.length).toBeGreaterThan(0);
|
||||
expect(chain[0].name).toBe("CTO");
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface SharedHandle {
|
||||
set db(value: AppDatabase);
|
||||
get companyId(): string;
|
||||
get workspaceId(): string;
|
||||
createAgent(overrides?: Partial<Parameters<AppDatabase["agents"]["saveAgent"]>[0]>): string;
|
||||
createAgent(overrides?: Partial<Parameters<AppDatabase["core"]["agents"]["saveAgent"]>[0]>): string;
|
||||
buildCtx(overrides?: Record<string, unknown>): Parameters<typeof buildSystemPrompt>[0];
|
||||
tempDirs: string[];
|
||||
}
|
||||
@@ -54,13 +54,13 @@ export function installSharedSetup(): SharedHandle {
|
||||
state.db = new AppDatabase(profileDir);
|
||||
state.db.init();
|
||||
|
||||
state.companyId = state.db.company.saveCompany({
|
||||
state.companyId = state.db.core.company.saveCompany({
|
||||
name: "Autonomous Test Corp",
|
||||
description: "Test company for autonomous chain",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
state.workspaceId = state.db.workspacesMod.saveWorkspace({
|
||||
state.workspaceId = state.db.core.workspaces.saveWorkspace({
|
||||
companyId: state.companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -80,8 +80,8 @@ export function installSharedSetup(): SharedHandle {
|
||||
return state.db;
|
||||
}
|
||||
|
||||
function createAgent(overrides: Partial<Parameters<AppDatabase["agents"]["saveAgent"]>[0]> = {}): string {
|
||||
return currentDb().agents.saveAgent({
|
||||
function createAgent(overrides: Partial<Parameters<AppDatabase["core"]["agents"]["saveAgent"]>[0]> = {}): string {
|
||||
return currentDb().core.agents.saveAgent({
|
||||
companyId: state.companyId,
|
||||
name: "TestAgent",
|
||||
role: "CEO",
|
||||
@@ -98,7 +98,7 @@ export function installSharedSetup(): SharedHandle {
|
||||
}
|
||||
|
||||
function buildCtx(overrides: Record<string, unknown> = {}): Parameters<typeof buildSystemPrompt>[0] {
|
||||
const agent = currentDb().entityCrud.getAgent(overrides.agentId as string ?? createAgent());
|
||||
const agent = currentDb().core.entity.getAgent(overrides.agentId as string ?? createAgent());
|
||||
return {
|
||||
agent,
|
||||
apiUrl: "http://127.0.0.1:3000",
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("Step 5: Task status + comment wake propagation", () => {
|
||||
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId });
|
||||
|
||||
const chain = shared.db.agents.getChainOfCommand(icId);
|
||||
const chain = shared.db.core.agents.getChainOfCommand(icId);
|
||||
expect(chain).toHaveLength(2);
|
||||
expect(chain[0].id).toBe(ctoId); // immediate manager
|
||||
expect(chain[1].id).toBe(ceoId); // top level
|
||||
@@ -25,7 +25,7 @@ describe("Step 5: Task status + comment wake propagation", () => {
|
||||
// Create a potential cycle (shouldn't happen in practice, but guard against it)
|
||||
const agent1 = createAgent({ name: "Agent1" });
|
||||
// agent1 reports to no one (CEO), so chain should be empty
|
||||
const chain = shared.db.agents.getChainOfCommand(agent1);
|
||||
const chain = shared.db.core.agents.getChainOfCommand(agent1);
|
||||
expect(chain).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("Step 5: Task status + comment wake propagation", () => {
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
|
||||
|
||||
const parentTaskId = shared.db.tasks.saveTask({
|
||||
const parentTaskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Build feature X",
|
||||
description: "Top-level task",
|
||||
@@ -43,7 +43,7 @@ describe("Step 5: Task status + comment wake propagation", () => {
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const childTaskId = shared.db.tasks.saveTask({
|
||||
const childTaskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Implement module A",
|
||||
description: "Subtask",
|
||||
@@ -54,10 +54,10 @@ describe("Step 5: Task status + comment wake propagation", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const childTask = shared.db.entityCrud.getTask(childTaskId);
|
||||
const childTask = shared.db.core.entity.getTask(childTaskId);
|
||||
expect(childTask.parentId).toBe(parentTaskId);
|
||||
|
||||
const parentTask = shared.db.entityCrud.getTask(parentTaskId);
|
||||
const parentTask = shared.db.core.entity.getTask(parentTaskId);
|
||||
expect(parentTask.assigneeAgentId).toBe(ctoId);
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ describe("Step 8: Wake reason injection", () => {
|
||||
it("includes wake reason section for non-timer triggers", () => {
|
||||
const agentId = createAgent({ name: "CTO", role: "CTO" });
|
||||
const ceoId = createAgent({ name: "CEO", role: "CEO" });
|
||||
const ceo = shared.db.entityCrud.getAgent(ceoId);
|
||||
const ceo = shared.db.core.entity.getAgent(ceoId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId,
|
||||
@@ -92,7 +92,7 @@ describe("Step 8: Wake reason injection", () => {
|
||||
|
||||
it("includes wake reason before task description", () => {
|
||||
const agentId = createAgent({ name: "Agent" });
|
||||
const taskId = shared.db.tasks.saveTask({
|
||||
const taskId = shared.db.core.tasks.saveTask({
|
||||
companyId: shared.companyId,
|
||||
title: "Do the thing",
|
||||
description: "Test task",
|
||||
@@ -101,7 +101,7 @@ describe("Step 8: Wake reason injection", () => {
|
||||
priority: "medium",
|
||||
status: "todo",
|
||||
});
|
||||
const task = shared.db.entityCrud.getTask(taskId);
|
||||
const task = shared.db.core.entity.getTask(taskId);
|
||||
|
||||
const ctx = buildCtx({
|
||||
agentId,
|
||||
|
||||
@@ -15,11 +15,11 @@ describe("Step 3: Wakeup request lifecycle (database)", () => {
|
||||
|
||||
// Enqueue two wakes for the same busy agent. Coalesced into one row
|
||||
// with trigger_detail JSON array containing both triggers.
|
||||
shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed");
|
||||
shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment");
|
||||
shared.db.automation.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed");
|
||||
shared.db.automation.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment");
|
||||
|
||||
// List returns the coalesced wake
|
||||
const wakes = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" });
|
||||
const wakes = shared.db.automation.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" });
|
||||
expect(wakes).toHaveLength(1);
|
||||
expect(wakes[0].companyId).toBe(shared.companyId);
|
||||
expect(wakes[0].coalescedCount).toBe(1);
|
||||
@@ -32,7 +32,7 @@ describe("Step 3: Wakeup request lifecycle (database)", () => {
|
||||
|
||||
it("returns empty array when no wakeup requests exist", () => {
|
||||
const agentId = createAgent({ name: "TestAgent" });
|
||||
const wakes = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" });
|
||||
const wakes = shared.db.automation.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" });
|
||||
expect(wakes).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -40,15 +40,15 @@ describe("Step 3: Wakeup request lifecycle (database)", () => {
|
||||
const agent1 = createAgent({ name: "Agent1" });
|
||||
const agent2 = createAgent({ name: "Agent2" });
|
||||
|
||||
shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agent1, "comment", "comment");
|
||||
shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agent2, "subtask_completed", "subtask_completed");
|
||||
shared.db.automation.wakeup.enqueueWakeupRequest(shared.companyId, agent1, "comment", "comment");
|
||||
shared.db.automation.wakeup.enqueueWakeupRequest(shared.companyId, agent2, "subtask_completed", "subtask_completed");
|
||||
|
||||
const wakes1 = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId: agent1, status: "queued" });
|
||||
const wakes1 = shared.db.automation.wakeup.listWakeupRequests(shared.companyId, { agentId: agent1, status: "queued" });
|
||||
expect(wakes1).toHaveLength(1);
|
||||
expect(wakes1[0].source).toBe("comment");
|
||||
|
||||
// Agent2's wake should still be there
|
||||
const wakes2 = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId: agent2, status: "queued" });
|
||||
const wakes2 = shared.db.automation.wakeup.listWakeupRequests(shared.companyId, { agentId: agent2, status: "queued" });
|
||||
expect(wakes2).toHaveLength(1);
|
||||
expect(wakes2[0].source).toBe("subtask_completed");
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ function createOrchestrationHarness(db: AppDatabase) {
|
||||
runDispatch.resolveWorkspace,
|
||||
runDispatch.getRunEligibilityFailure,
|
||||
runDispatch.dispatchRunToWorker,
|
||||
(filter) => db.runs.hasActiveRun(filter),
|
||||
(filter) => db.core.runs.hasActiveRun(filter),
|
||||
);
|
||||
|
||||
return { deps, taskRunner };
|
||||
@@ -69,7 +69,7 @@ describe("connector model enforcement", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -77,12 +77,12 @@ describe("connector model enforcement", () => {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Runtime Enforcement Co",
|
||||
description: "Blocks runs when agent models are disabled",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -90,7 +90,7 @@ describe("connector model enforcement", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -103,7 +103,7 @@ describe("connector model enforcement", () => {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Run with invalid model",
|
||||
description: "This task should be blocked because the agent model is disabled.",
|
||||
@@ -113,7 +113,7 @@ describe("connector model enforcement", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
db.settings.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
db.core.settings.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
|
||||
const { taskRunner } = createOrchestrationHarness(db);
|
||||
const result = taskRunner.queueTaskRun(taskId, "manual", "board");
|
||||
@@ -123,7 +123,7 @@ describe("connector model enforcement", () => {
|
||||
throw new Error("Expected queueTaskRun to reject the disabled model.");
|
||||
}
|
||||
expect(result.error.code).toBe("CONNECTOR_MODEL_NOT_ALLOWED");
|
||||
expect(db.snap.listSnapshot().runs).toHaveLength(0);
|
||||
expect(db.projection.snapshot.listSnapshot().runs).toHaveLength(0);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -133,7 +133,7 @@ describe("connector model enforcement", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -141,12 +141,12 @@ describe("connector model enforcement", () => {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Recovered Run Co",
|
||||
description: "Rejects invalid queued runs during recovery",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -154,7 +154,7 @@ describe("connector model enforcement", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -167,7 +167,7 @@ describe("connector model enforcement", () => {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Recover queued run",
|
||||
description: "This queued run should fail on recovery because the model is disabled.",
|
||||
@@ -177,22 +177,22 @@ describe("connector model enforcement", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
const checkoutSuccess = db.tasks.checkoutTask(taskId, run.id);
|
||||
const checkoutSuccess = db.core.tasks.checkoutTask(taskId, run.id);
|
||||
expect(checkoutSuccess).toBe(true);
|
||||
|
||||
db.settings.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
db.core.settings.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
|
||||
const { taskRunner } = createOrchestrationHarness(db);
|
||||
taskRunner.resumeQueuedRuns();
|
||||
|
||||
const recoveredRun = db.snap.listSnapshot().runs.find((entry) => entry.id === run.id);
|
||||
const recoveredRun = db.projection.snapshot.listSnapshot().runs.find((entry) => entry.id === run.id);
|
||||
expect(recoveredRun?.status).toBe("failed");
|
||||
expect(recoveredRun?.errorMessage).toContain("Connector model is disabled or invalid");
|
||||
|
||||
|
||||
@@ -57,20 +57,20 @@ const mockLogger = {
|
||||
function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult {
|
||||
const connectorId: ConnectorId = "gemini-cli";
|
||||
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: connectorId,
|
||||
model: "gemini-2.5-pro",
|
||||
envBindingText: "",
|
||||
notes: "Non-exempt connector for session rotation tests",
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Pipeline Test Corp",
|
||||
description: "Cross-subsystem pipeline integration tests",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary Workspace",
|
||||
localPath: "/tmp/pipeline-test",
|
||||
@@ -79,7 +79,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult {
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "PipelineAgent",
|
||||
role: "IC",
|
||||
@@ -93,7 +93,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult {
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
|
||||
const goalId = db.entityCrud.saveGoal({
|
||||
const goalId = db.core.entity.saveGoal({
|
||||
companyId,
|
||||
title: "Implement Authentication",
|
||||
description: "Build full authentication system",
|
||||
@@ -101,7 +101,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult {
|
||||
status: "active",
|
||||
});
|
||||
|
||||
const projectId = db.entityCrud.saveProject({
|
||||
const projectId = db.core.entity.saveProject({
|
||||
companyId,
|
||||
goalId,
|
||||
name: "Auth Project",
|
||||
@@ -112,7 +112,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult {
|
||||
});
|
||||
|
||||
// Budget policy: company-level with hardStop
|
||||
const policyId = db.budget.saveBudgetPolicy({
|
||||
const policyId = db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "company",
|
||||
scopeId: companyId,
|
||||
@@ -175,7 +175,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
expect(validation.errors).toHaveLength(0);
|
||||
|
||||
// Create the task in the DB
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId: s.companyId,
|
||||
title: "Build login page",
|
||||
description:
|
||||
@@ -187,14 +187,14 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
});
|
||||
|
||||
// 2. CAS checkout: create run and checkout task
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId: s.companyId,
|
||||
taskId,
|
||||
agentId: s.agentId,
|
||||
workspaceId: s.workspaceId,
|
||||
connectorId: s.connectorId,
|
||||
});
|
||||
const checkoutResult = db.tasks.checkoutTask(taskId, runId);
|
||||
const checkoutResult = db.core.tasks.checkoutTask(taskId, runId);
|
||||
expect(checkoutResult).toBe(true);
|
||||
|
||||
// 3. Budget gate: verify run is allowed
|
||||
@@ -207,9 +207,9 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
expect(budgetResult.blockedBy).toHaveLength(0);
|
||||
|
||||
// 4. Execute run
|
||||
db.runs.markRunStatus(runId, "running", "Starting");
|
||||
db.runs.recordCostEntry({ runId, companyId: s.companyId, agentId: s.agentId, taskId, connectorId: "gemini-cli", amountUsd: 0.05, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.runs.markRunStatus(runId, "running", "Starting");
|
||||
db.core.runs.recordCostEntry({ runId, companyId: s.companyId, agentId: s.agentId, taskId, connectorId: "gemini-cli", amountUsd: 0.05, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId,
|
||||
status: "succeeded",
|
||||
summary: "Login page built with form validation",
|
||||
@@ -221,7 +221,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
});
|
||||
|
||||
// 5. Session management: create session and track usage
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId: s.companyId,
|
||||
agentId: s.agentId,
|
||||
adapterType: "gemini-cli",
|
||||
@@ -229,13 +229,13 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
sessionService.accumulateTokenUsage(sessionId, 5000, 2000, runId);
|
||||
|
||||
// 6. Verify final state
|
||||
const run = db.runs.getRun(runId);
|
||||
const run = db.core.runs.getRun(runId);
|
||||
expect(run.status).toBe("succeeded");
|
||||
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.costUsd).toBeGreaterThan(0);
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session).not.toBeNull();
|
||||
expect(session!.totalInputTokens).toBe(5000);
|
||||
expect(session!.totalOutputTokens).toBe(2000);
|
||||
@@ -254,7 +254,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
it("CAS prevents double checkout within pipeline", () => {
|
||||
const s = scaffold(db);
|
||||
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId: s.companyId,
|
||||
title: "Concurrent Task",
|
||||
description:
|
||||
@@ -266,17 +266,17 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
});
|
||||
|
||||
// First agent checks out
|
||||
const { id: runId1 } = db.runs.createRun({
|
||||
const { id: runId1 } = db.core.runs.createRun({
|
||||
companyId: s.companyId,
|
||||
taskId,
|
||||
agentId: s.agentId,
|
||||
workspaceId: s.workspaceId,
|
||||
connectorId: s.connectorId,
|
||||
});
|
||||
expect(db.tasks.checkoutTask(taskId, runId1)).toBe(true);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runId1)).toBe(true);
|
||||
|
||||
// Second agent creates a run but cannot check out
|
||||
const agent2Id = db.agents.saveAgent({
|
||||
const agent2Id = db.core.agents.saveAgent({
|
||||
companyId: s.companyId,
|
||||
name: "Agent2",
|
||||
role: "IC",
|
||||
@@ -289,14 +289,14 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
capabilities: "Backend development",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const { id: runId2 } = db.runs.createRun({
|
||||
const { id: runId2 } = db.core.runs.createRun({
|
||||
companyId: s.companyId,
|
||||
taskId,
|
||||
agentId: agent2Id,
|
||||
workspaceId: s.workspaceId,
|
||||
connectorId: s.connectorId,
|
||||
});
|
||||
expect(db.tasks.checkoutTask(taskId, runId2)).toBe(false);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runId2)).toBe(false);
|
||||
});
|
||||
|
||||
it("budget gate blocks run when policy exhausted", () => {
|
||||
@@ -311,7 +311,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
});
|
||||
|
||||
// Create and complete a run that exceeds the budget
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId: s.companyId,
|
||||
title: "Expensive Task",
|
||||
description:
|
||||
@@ -322,7 +322,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId: s.companyId,
|
||||
taskId,
|
||||
agentId: s.agentId,
|
||||
@@ -330,10 +330,10 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
connectorId: s.connectorId,
|
||||
});
|
||||
|
||||
db.tasks.checkoutTask(taskId, runId);
|
||||
db.runs.markRunStatus(runId, "running", "Starting");
|
||||
db.runs.recordCostEntry({ runId, companyId: s.companyId, agentId: s.agentId, taskId, connectorId: s.connectorId, amountUsd: 1.5, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId, runId);
|
||||
db.core.runs.markRunStatus(runId, "running", "Starting");
|
||||
db.core.runs.recordCostEntry({ runId, companyId: s.companyId, agentId: s.agentId, taskId, connectorId: s.connectorId, amountUsd: 1.5, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId,
|
||||
status: "succeeded",
|
||||
summary: "Done",
|
||||
@@ -363,7 +363,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
logger: mockLogger,
|
||||
});
|
||||
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId: s.companyId,
|
||||
title: "Multi-Run Task",
|
||||
description:
|
||||
@@ -375,14 +375,14 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
});
|
||||
|
||||
// Create session
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId: s.companyId,
|
||||
agentId: s.agentId,
|
||||
adapterType: "gemini-cli",
|
||||
});
|
||||
|
||||
// Run 1: create run and accumulate tokens
|
||||
const { id: runId1 } = db.runs.createRun({
|
||||
const { id: runId1 } = db.core.runs.createRun({
|
||||
companyId: s.companyId,
|
||||
taskId,
|
||||
agentId: s.agentId,
|
||||
@@ -392,7 +392,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
sessionService.accumulateTokenUsage(sessionId, 5000, 2000, runId1);
|
||||
|
||||
// Run 2: create another run and accumulate more tokens
|
||||
const { id: runId2 } = db.runs.createRun({
|
||||
const { id: runId2 } = db.core.runs.createRun({
|
||||
companyId: s.companyId,
|
||||
taskId,
|
||||
agentId: s.agentId,
|
||||
@@ -402,7 +402,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
sessionService.accumulateTokenUsage(sessionId, 3000, 1000, runId2);
|
||||
|
||||
// Verify accumulated session state
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session).not.toBeNull();
|
||||
expect(session!.runCount).toBe(2);
|
||||
expect(session!.totalInputTokens).toBe(8000);
|
||||
@@ -421,7 +421,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
});
|
||||
|
||||
// --- First run: costs $3, budget allows ---
|
||||
const taskId1 = db.tasks.saveTask({
|
||||
const taskId1 = db.core.tasks.saveTask({
|
||||
companyId: s.companyId,
|
||||
title: "First Pipeline Task",
|
||||
description:
|
||||
@@ -432,17 +432,17 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const { id: runId1 } = db.runs.createRun({
|
||||
const { id: runId1 } = db.core.runs.createRun({
|
||||
companyId: s.companyId,
|
||||
taskId: taskId1,
|
||||
agentId: s.agentId,
|
||||
workspaceId: s.workspaceId,
|
||||
connectorId: s.connectorId,
|
||||
});
|
||||
db.tasks.checkoutTask(taskId1, runId1);
|
||||
db.runs.markRunStatus(runId1, "running", "Starting");
|
||||
db.runs.recordCostEntry({ runId: runId1, companyId: s.companyId, agentId: s.agentId, taskId: taskId1, connectorId: s.connectorId, amountUsd: 3.0, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId1, runId1);
|
||||
db.core.runs.markRunStatus(runId1, "running", "Starting");
|
||||
db.core.runs.recordCostEntry({ runId: runId1, companyId: s.companyId, agentId: s.agentId, taskId: taskId1, connectorId: s.connectorId, amountUsd: 3.0, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId: runId1,
|
||||
status: "succeeded",
|
||||
summary: "First task done",
|
||||
@@ -462,7 +462,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
expect(afterFirst.allowed).toBe(true);
|
||||
|
||||
// --- Second run: costs $3, now total = $6 > $5 budget ---
|
||||
const taskId2 = db.tasks.saveTask({
|
||||
const taskId2 = db.core.tasks.saveTask({
|
||||
companyId: s.companyId,
|
||||
title: "Second Pipeline Task",
|
||||
description:
|
||||
@@ -473,17 +473,17 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const { id: runId2 } = db.runs.createRun({
|
||||
const { id: runId2 } = db.core.runs.createRun({
|
||||
companyId: s.companyId,
|
||||
taskId: taskId2,
|
||||
agentId: s.agentId,
|
||||
workspaceId: s.workspaceId,
|
||||
connectorId: s.connectorId,
|
||||
});
|
||||
db.tasks.checkoutTask(taskId2, runId2);
|
||||
db.runs.markRunStatus(runId2, "running", "Starting");
|
||||
db.runs.recordCostEntry({ runId: runId2, companyId: s.companyId, agentId: s.agentId, taskId: taskId2, connectorId: s.connectorId, amountUsd: 3.0, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId2, runId2);
|
||||
db.core.runs.markRunStatus(runId2, "running", "Starting");
|
||||
db.core.runs.recordCostEntry({ runId: runId2, companyId: s.companyId, agentId: s.agentId, taskId: taskId2, connectorId: s.connectorId, amountUsd: 3.0, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId: runId2,
|
||||
status: "succeeded",
|
||||
summary: "Second task done",
|
||||
|
||||
@@ -37,13 +37,13 @@ describe("AppDatabase", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Orbit Foundry",
|
||||
description: "Test company",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -52,7 +52,7 @@ describe("AppDatabase", () => {
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Helios",
|
||||
role: "CEO",
|
||||
@@ -66,7 +66,7 @@ describe("AppDatabase", () => {
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
|
||||
const goalId = db.entityCrud.saveGoal({
|
||||
const goalId = db.core.entity.saveGoal({
|
||||
companyId,
|
||||
title: "Ship the desktop app",
|
||||
description: "Reach packaged desktop parity.",
|
||||
@@ -74,7 +74,7 @@ describe("AppDatabase", () => {
|
||||
status: "active",
|
||||
});
|
||||
|
||||
const projectId = db.entityCrud.saveProject({
|
||||
const projectId = db.core.entity.saveProject({
|
||||
companyId,
|
||||
goalId,
|
||||
name: "Desktop rebuild",
|
||||
@@ -83,7 +83,7 @@ describe("AppDatabase", () => {
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
projectId,
|
||||
goalId,
|
||||
@@ -95,25 +95,25 @@ describe("AppDatabase", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
db.entityCrud.saveSecret({
|
||||
db.core.entity.saveSecret({
|
||||
companyId,
|
||||
name: "vault-openai",
|
||||
description: "Test key",
|
||||
value: "sk-test-123",
|
||||
});
|
||||
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "openai-codex",
|
||||
model: null,
|
||||
envBindingText: "OPENAI_API_KEY=vault-openai",
|
||||
notes: "Connector note",
|
||||
});
|
||||
|
||||
expect(db.entityCrud.resolveSecretEnv("OPENAI_API_KEY=vault-openai", companyId)).toEqual({
|
||||
expect(db.core.entity.resolveSecretEnv("OPENAI_API_KEY=vault-openai", companyId)).toEqual({
|
||||
OPENAI_API_KEY: "sk-test-123",
|
||||
});
|
||||
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -121,11 +121,11 @@ describe("AppDatabase", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
await db.runs.appendRunLog(run.id, "stdout", "hello from run\n");
|
||||
expect(db.runs.readRunLog(run.id)).toContain("hello from run");
|
||||
await db.core.runs.appendRunLog(run.id, "stdout", "hello from run\n");
|
||||
expect(db.core.runs.readRunLog(run.id)).toContain("hello from run");
|
||||
|
||||
db.runs.markRunStatus(run.id, "running", "Connector launched");
|
||||
db.runs.finishRun({
|
||||
db.core.runs.markRunStatus(run.id, "running", "Connector launched");
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Completed successfully",
|
||||
@@ -136,22 +136,22 @@ describe("AppDatabase", () => {
|
||||
unattributedCost: false,
|
||||
});
|
||||
|
||||
const snapshotBeforeBackup = db.snap.listSnapshot();
|
||||
const snapshotBeforeBackup = db.projection.snapshot.listSnapshot();
|
||||
expect(snapshotBeforeBackup.companies).toHaveLength(1);
|
||||
expect(snapshotBeforeBackup.runs[0]?.status).toBe("succeeded");
|
||||
expect(snapshotBeforeBackup.tasks[0]?.activeRunId).toBeNull();
|
||||
|
||||
await db.backupMod.exportBackup(backupDir);
|
||||
await db.core.backup.exportBackup(backupDir);
|
||||
|
||||
db.company.saveCompany({
|
||||
db.core.company.saveCompany({
|
||||
name: "Temporary company",
|
||||
description: "Should disappear after restore",
|
||||
status: "active",
|
||||
});
|
||||
expect(db.snap.listSnapshot().companies).toHaveLength(2);
|
||||
expect(db.projection.snapshot.listSnapshot().companies).toHaveLength(2);
|
||||
|
||||
await db.backupMod.restoreFromBackup(backupDir);
|
||||
const restoredSnapshot = db.snap.listSnapshot();
|
||||
await db.core.backup.restoreFromBackup(backupDir);
|
||||
const restoredSnapshot = db.projection.snapshot.listSnapshot();
|
||||
expect(restoredSnapshot.companies).toHaveLength(1);
|
||||
expect(restoredSnapshot.companies[0]?.name).toBe("Orbit Foundry");
|
||||
expect(restoredSnapshot.runs[0]?.summary).toBe("Completed successfully");
|
||||
@@ -164,19 +164,19 @@ describe("AppDatabase", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const alphaCompanyId = db.company.saveCompany({
|
||||
const alphaCompanyId = db.core.company.saveCompany({
|
||||
name: "Alpha",
|
||||
description: "Alpha company",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
const betaCompanyId = db.company.saveCompany({
|
||||
const betaCompanyId = db.core.company.saveCompany({
|
||||
name: "Beta",
|
||||
description: "Beta company",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
const alphaWorkspaceId = db.workspacesMod.saveWorkspace({
|
||||
const alphaWorkspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId: alphaCompanyId,
|
||||
name: "Alpha Workspace",
|
||||
localPath: profileDir,
|
||||
@@ -185,7 +185,7 @@ describe("AppDatabase", () => {
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
const betaWorkspaceId = db.workspacesMod.saveWorkspace({
|
||||
const betaWorkspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId: betaCompanyId,
|
||||
name: "Beta Workspace",
|
||||
localPath: profileDir,
|
||||
@@ -194,7 +194,7 @@ describe("AppDatabase", () => {
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
const alphaAgentId = db.agents.saveAgent({
|
||||
const alphaAgentId = db.core.agents.saveAgent({
|
||||
companyId: alphaCompanyId,
|
||||
name: "Alpha CEO",
|
||||
role: "CEO",
|
||||
@@ -208,7 +208,7 @@ describe("AppDatabase", () => {
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
|
||||
db.agents.saveAgent({
|
||||
db.core.agents.saveAgent({
|
||||
companyId: betaCompanyId,
|
||||
name: "Beta CEO",
|
||||
role: "CEO",
|
||||
@@ -222,7 +222,7 @@ describe("AppDatabase", () => {
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
|
||||
db.tasks.saveTask({
|
||||
db.core.tasks.saveTask({
|
||||
companyId: alphaCompanyId,
|
||||
title: "Alpha task",
|
||||
description: "Alpha work",
|
||||
@@ -232,7 +232,7 @@ describe("AppDatabase", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
db.tasks.saveTask({
|
||||
db.core.tasks.saveTask({
|
||||
companyId: betaCompanyId,
|
||||
title: "Beta task",
|
||||
description: "Beta work",
|
||||
@@ -242,9 +242,9 @@ describe("AppDatabase", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
db.settings.setCurrentCompany(alphaCompanyId);
|
||||
db.core.settings.setCurrentCompany(alphaCompanyId);
|
||||
|
||||
const rendererSnapshot = db.snap.listRendererSnapshot();
|
||||
const rendererSnapshot = db.projection.snapshot.listRendererSnapshot();
|
||||
expect(rendererSnapshot.currentCompanyId).toBe(alphaCompanyId);
|
||||
expect(rendererSnapshot.companies).toHaveLength(2);
|
||||
expect(rendererSnapshot.connectors.length).toBeGreaterThan(0);
|
||||
|
||||
@@ -27,7 +27,7 @@ async function createHarness() {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -35,12 +35,12 @@ async function createHarness() {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Document Provenance Co",
|
||||
description: "Tests document revision provenance through run completion",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -48,7 +48,7 @@ async function createHarness() {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Research Lead",
|
||||
role: "Researcher",
|
||||
@@ -61,7 +61,7 @@ async function createHarness() {
|
||||
capabilities: "Research and synthesis",
|
||||
budgetMonthlyUsd: 180,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Refresh the market report",
|
||||
description: "Update the report with new findings and submit it for review.",
|
||||
@@ -70,9 +70,9 @@ async function createHarness() {
|
||||
priority: "high",
|
||||
status: "in_progress",
|
||||
});
|
||||
db.tasks.setTaskType(taskId, "research");
|
||||
db.core.tasks.setTaskType(taskId, "research");
|
||||
|
||||
const existingDocumentId = db.documentsMod.saveDocument({
|
||||
const existingDocumentId = db.core.documents.saveDocument({
|
||||
companyId,
|
||||
type: "knowledge_article",
|
||||
title: "Market report",
|
||||
@@ -83,7 +83,7 @@ async function createHarness() {
|
||||
status: "draft",
|
||||
});
|
||||
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -150,7 +150,7 @@ describe("document run provenance", () => {
|
||||
)[0];
|
||||
expect(updatedDocument?.sourceRunId).toBe(harness.run.id);
|
||||
|
||||
const persistedRun = harness.db.snap.listSnapshot().runs.find((entry) => entry.id === harness.run.id);
|
||||
const persistedRun = harness.db.projection.snapshot.listSnapshot().runs.find((entry) => entry.id === harness.run.id);
|
||||
if (!persistedRun) {
|
||||
throw new Error("Expected persisted run record for provenance test.");
|
||||
}
|
||||
@@ -173,7 +173,7 @@ describe("document run provenance", () => {
|
||||
// ai_ceo + top-level CEO without `reviewDeliverables` advances
|
||||
// straight to `done` — there is no reviewer in that configuration
|
||||
// so the prior `in_review` state was a dead letter.
|
||||
const task = harness.db.entityCrud.getTask(harness.taskId);
|
||||
const task = harness.db.core.entity.getTask(harness.taskId);
|
||||
expect(task.status).toBe("done");
|
||||
} finally {
|
||||
harness.server.stop();
|
||||
@@ -285,7 +285,7 @@ describe("document run provenance", () => {
|
||||
it("creates deliverable review approvals that stay linked to the originating task", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
harness.db.company.saveCompany({
|
||||
harness.db.core.company.saveCompany({
|
||||
id: harness.companyId,
|
||||
name: "Document Provenance Co",
|
||||
description: "Tests document review task linkage",
|
||||
@@ -308,7 +308,7 @@ describe("document run provenance", () => {
|
||||
);
|
||||
|
||||
expect(createResponse.status).toBe(201);
|
||||
const approvals = harness.db.snap.listSnapshot().approvals.filter((approval) => approval.type === "deliverable_review");
|
||||
const approvals = harness.db.projection.snapshot.listSnapshot().approvals.filter((approval) => approval.type === "deliverable_review");
|
||||
expect(approvals).toHaveLength(1);
|
||||
expect(approvals[0]?.relatedTaskId).toBe(harness.taskId);
|
||||
} finally {
|
||||
|
||||
@@ -52,11 +52,11 @@ function buildKickoffPayload(db: AppDatabase, summary: { companyId: string; lead
|
||||
throw new Error("Missing lead agent or task in onboarding summary.");
|
||||
}
|
||||
|
||||
const agent = db.entityCrud.getAgent(summary.leadAgentId);
|
||||
const task = db.entityCrud.getTask(summary.taskId);
|
||||
const connector = db.connectorsCrud.getConnector(agent.connectorId);
|
||||
const workspace = summary.workspaceId ? db.workspacesMod.getWorkspace(summary.workspaceId) : null;
|
||||
const company = db.snap.listSnapshot().companies.find((entry) => entry.id === summary.companyId);
|
||||
const agent = db.core.entity.getAgent(summary.leadAgentId);
|
||||
const task = db.core.entity.getTask(summary.taskId);
|
||||
const connector = db.core.connectors.getConnector(agent.connectorId);
|
||||
const workspace = summary.workspaceId ? db.core.workspaces.getWorkspace(summary.workspaceId) : null;
|
||||
const company = db.projection.snapshot.listSnapshot().companies.find((entry) => entry.id === summary.companyId);
|
||||
|
||||
if (!company) {
|
||||
throw new Error(`Company ${summary.companyId} not found in snapshot.`);
|
||||
@@ -72,7 +72,7 @@ function buildKickoffPayload(db: AppDatabase, summary: { companyId: string; lead
|
||||
workspaceMode: "exclusive",
|
||||
companyName: company.name,
|
||||
companyDescription: company.description,
|
||||
snapshot: db.snap.listSnapshot(),
|
||||
snapshot: db.projection.snapshot.listSnapshot(),
|
||||
trigger: "bootstrap",
|
||||
}, {
|
||||
db,
|
||||
@@ -98,7 +98,7 @@ describe("onboarding model pool integration", () => {
|
||||
throw new Error("openai-codex requires at least two models for this integration test.");
|
||||
}
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -125,11 +125,11 @@ describe("onboarding model pool integration", () => {
|
||||
throw new Error("Expected lead agent id from onboarding bootstrap.");
|
||||
}
|
||||
|
||||
const agent = db.entityCrud.getAgent(summary.leadAgentId);
|
||||
const agent = db.core.entity.getAgent(summary.leadAgentId);
|
||||
expect(agent.connectorId).toBe("openai-codex");
|
||||
expect(agent.model).toBe(enabledModel);
|
||||
expect(db.settings.getDisabledModels(summary.companyId, "openai-codex")).toEqual([{ id: disabledModel, label: disabledModel }]);
|
||||
expect(db.settings.getCurrentCompanyId()).toBe(summary.companyId);
|
||||
expect(db.core.settings.getDisabledModels(summary.companyId, "openai-codex")).toEqual([{ id: disabledModel, label: disabledModel }]);
|
||||
expect(db.core.settings.getCurrentCompanyId()).toBe(summary.companyId);
|
||||
|
||||
const payload = buildKickoffPayload(db, summary);
|
||||
expect(payload.model).toBe(enabledModel);
|
||||
@@ -161,7 +161,7 @@ describe("onboarding model pool integration", () => {
|
||||
throw new Error("anthropic provider must expose at least two models from pi-ai.");
|
||||
}
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "anthropic",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -205,7 +205,7 @@ describe("onboarding model pool integration", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -230,7 +230,7 @@ describe("onboarding model pool integration", () => {
|
||||
throw new Error("Expected lead agent id from onboarding bootstrap.");
|
||||
}
|
||||
|
||||
const agent = db.entityCrud.getAgent(summary.leadAgentId);
|
||||
const agent = db.core.entity.getAgent(summary.leadAgentId);
|
||||
expect(agent.model).toBeNull();
|
||||
|
||||
const payload = buildKickoffPayload(db, summary);
|
||||
@@ -254,7 +254,7 @@ describe("onboarding model pool integration", () => {
|
||||
throw new Error("openai-codex requires a default and fallback model for reroute testing.");
|
||||
}
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -296,7 +296,7 @@ describe("onboarding model pool integration", () => {
|
||||
throw new Error("openai-codex requires a model to validate onboarding rejection.");
|
||||
}
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
|
||||
@@ -35,13 +35,13 @@ describe("pi-mono migrations 27 + 28", () => {
|
||||
const db = new AppDatabase(dir);
|
||||
db.init();
|
||||
|
||||
db.sessionEntries.append({
|
||||
db.projection.sessionEntries.append({
|
||||
taskSessionId: "test-session",
|
||||
kind: "message",
|
||||
role: "user",
|
||||
payloadJson: JSON.stringify({ role: "user", content: "hello" }),
|
||||
});
|
||||
const rows = db.sessionEntries.listActive("test-session");
|
||||
const rows = db.projection.sessionEntries.listActive("test-session");
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]?.kind).toBe("message");
|
||||
db.close();
|
||||
@@ -53,7 +53,7 @@ describe("pi-mono migrations 27 + 28", () => {
|
||||
db.init();
|
||||
|
||||
const cipher = db.vault.encrypt(JSON.stringify({ source: "literal", value: "sk-test-1234567890" }));
|
||||
db.credentialsTable.upsert({
|
||||
db.core.credentials.upsert({
|
||||
connectorId: "test-anthropic",
|
||||
kind: "api_key",
|
||||
backend: cipher.backend,
|
||||
@@ -65,7 +65,7 @@ describe("pi-mono migrations 27 + 28", () => {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const rows = db.credentialsTable.list();
|
||||
const rows = db.core.credentials.list();
|
||||
expect(rows.some((row) => row.connectorId === "test-anthropic")).toBe(true);
|
||||
db.close();
|
||||
});
|
||||
@@ -74,14 +74,14 @@ describe("pi-mono migrations 27 + 28", () => {
|
||||
const dir = await freshProfile("agentcompany-migration-idempotent-");
|
||||
const dbA = new AppDatabase(dir);
|
||||
dbA.init();
|
||||
dbA.sessionEntries.append({
|
||||
dbA.projection.sessionEntries.append({
|
||||
taskSessionId: "session-x",
|
||||
kind: "message",
|
||||
role: "user",
|
||||
payloadJson: JSON.stringify({ role: "user", content: "first" }),
|
||||
});
|
||||
const cipher = dbA.vault.encrypt(JSON.stringify({ source: "literal", value: "sk-keep-me" }));
|
||||
dbA.credentialsTable.upsert({
|
||||
dbA.core.credentials.upsert({
|
||||
connectorId: "test-keep",
|
||||
kind: "api_key",
|
||||
backend: cipher.backend,
|
||||
@@ -96,8 +96,8 @@ describe("pi-mono migrations 27 + 28", () => {
|
||||
|
||||
const dbB = new AppDatabase(dir);
|
||||
dbB.init();
|
||||
expect(dbB.sessionEntries.listActive("session-x")).toHaveLength(1);
|
||||
expect(dbB.credentialsTable.list().filter((row) => row.connectorId === "test-keep")).toHaveLength(1);
|
||||
expect(dbB.projection.sessionEntries.listActive("session-x")).toHaveLength(1);
|
||||
expect(dbB.core.credentials.list().filter((row) => row.connectorId === "test-keep")).toHaveLength(1);
|
||||
dbB.close();
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("pi-mono migrations 27 + 28", () => {
|
||||
const db = new AppDatabase(dir);
|
||||
db.init();
|
||||
|
||||
const rows = db.snap.listSnapshot().connectors.map((entry) => entry.id);
|
||||
const rows = db.projection.snapshot.listSnapshot().connectors.map((entry) => entry.id);
|
||||
expect(rows.length).toBe(26);
|
||||
expect(rows).toContain("claude-pro");
|
||||
expect(rows).toContain("amazon-bedrock");
|
||||
|
||||
@@ -54,7 +54,7 @@ function scaffold(db: AppDatabase): ScaffoldResult {
|
||||
const connectorId: ConnectorId = "gemini-cli";
|
||||
|
||||
// Connector
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: connectorId,
|
||||
model: "gemini-2.5-pro",
|
||||
envBindingText: "",
|
||||
@@ -62,14 +62,14 @@ function scaffold(db: AppDatabase): ScaffoldResult {
|
||||
});
|
||||
|
||||
// Company
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Recovery Test Corp",
|
||||
description: "Process recovery integration tests",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
// Workspace
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary Workspace",
|
||||
localPath: "/tmp/recovery-test",
|
||||
@@ -79,7 +79,7 @@ function scaffold(db: AppDatabase): ScaffoldResult {
|
||||
});
|
||||
|
||||
// Agent
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "RecoveryAgent",
|
||||
role: "IC",
|
||||
@@ -94,7 +94,7 @@ function scaffold(db: AppDatabase): ScaffoldResult {
|
||||
});
|
||||
|
||||
// Goal
|
||||
const goalId = db.entityCrud.saveGoal({
|
||||
const goalId = db.core.entity.saveGoal({
|
||||
companyId,
|
||||
title: "Build login page",
|
||||
description: "Implement login page with authentication",
|
||||
@@ -103,7 +103,7 @@ function scaffold(db: AppDatabase): ScaffoldResult {
|
||||
});
|
||||
|
||||
// Project
|
||||
const projectId = db.entityCrud.saveProject({
|
||||
const projectId = db.core.entity.saveProject({
|
||||
companyId,
|
||||
goalId,
|
||||
name: "Auth Module",
|
||||
@@ -145,7 +145,7 @@ function createAndStartRun(
|
||||
): { runId: string; taskId: string } {
|
||||
const taskId =
|
||||
opts.taskId ??
|
||||
db.tasks.saveTask({
|
||||
db.core.tasks.saveTask({
|
||||
companyId: opts.companyId,
|
||||
title: opts.taskTitle ?? "Test Task",
|
||||
description:
|
||||
@@ -157,7 +157,7 @@ function createAndStartRun(
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId: opts.companyId,
|
||||
taskId,
|
||||
agentId: opts.agentId,
|
||||
@@ -165,11 +165,11 @@ function createAndStartRun(
|
||||
connectorId: opts.connectorId,
|
||||
});
|
||||
|
||||
db.tasks.checkoutTask(taskId, runId);
|
||||
db.runs.markRunStatus(runId, "running", "Starting execution");
|
||||
db.core.tasks.checkoutTask(taskId, runId);
|
||||
db.core.runs.markRunStatus(runId, "running", "Starting execution");
|
||||
|
||||
if (opts.claimWorkspace !== false) {
|
||||
db.workspacesMod.claimWorkspaceLock(opts.workspaceId, runId);
|
||||
db.core.workspaces.claimWorkspaceLock(opts.workspaceId, runId);
|
||||
}
|
||||
|
||||
// Set agent status to "running" to simulate active execution
|
||||
@@ -243,7 +243,7 @@ describe("Process Recovery -- Worker Crash Detection", () => {
|
||||
const s = scaffold(db);
|
||||
|
||||
// Create a second agent for a second run
|
||||
const agent2Id = db.agents.saveAgent({
|
||||
const agent2Id = db.core.agents.saveAgent({
|
||||
companyId: s.companyId,
|
||||
name: "Agent2",
|
||||
role: "IC",
|
||||
@@ -278,8 +278,8 @@ describe("Process Recovery -- Worker Crash Detection", () => {
|
||||
const interruptedCount = simulateWorkerCrash(db);
|
||||
|
||||
expect(interruptedCount).toBe(2);
|
||||
expect(db.runs.getRun(run1.runId).status).toBe("interrupted");
|
||||
expect(db.runs.getRun(run2.runId).status).toBe("interrupted");
|
||||
expect(db.core.runs.getRun(run1.runId).status).toBe("interrupted");
|
||||
expect(db.core.runs.getRun(run2.runId).status).toBe("interrupted");
|
||||
});
|
||||
|
||||
it("releases task locks after crash", () => {
|
||||
@@ -306,7 +306,7 @@ describe("Process Recovery -- Worker Crash Detection", () => {
|
||||
// However, after crash, the task is set to "blocked" and a subsequent
|
||||
// stale lock recovery pass handles the checkout_run_id.
|
||||
// For this test, verify the task status is blocked (the recovery mechanism).
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.status).toBe("blocked");
|
||||
});
|
||||
|
||||
@@ -320,11 +320,11 @@ describe("Process Recovery -- Worker Crash Detection", () => {
|
||||
});
|
||||
|
||||
// Verify agent is running before crash
|
||||
expect(db.entityCrud.getAgent(s.agentId).status).toBe("running");
|
||||
expect(db.core.entity.getAgent(s.agentId).status).toBe("running");
|
||||
|
||||
simulateWorkerCrash(db);
|
||||
|
||||
expect(db.entityCrud.getAgent(s.agentId).status).toBe("idle");
|
||||
expect(db.core.entity.getAgent(s.agentId).status).toBe("idle");
|
||||
});
|
||||
|
||||
it("releases workspace locks after crash", () => {
|
||||
@@ -356,7 +356,7 @@ describe("Process Recovery -- Worker Crash Detection", () => {
|
||||
expect(wsAfter.lock_holder_run_id).toBeNull();
|
||||
|
||||
// Verify we can re-claim the workspace (lock is truly released)
|
||||
const agent2Id = db.agents.saveAgent({
|
||||
const agent2Id = db.core.agents.saveAgent({
|
||||
companyId: s.companyId,
|
||||
name: "Agent2",
|
||||
role: "IC",
|
||||
@@ -369,7 +369,7 @@ describe("Process Recovery -- Worker Crash Detection", () => {
|
||||
capabilities: "Testing",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId2 = db.tasks.saveTask({
|
||||
const taskId2 = db.core.tasks.saveTask({
|
||||
companyId: s.companyId,
|
||||
title: "Recovery Task",
|
||||
description: "A recovery task after crash to verify workspace lock is released properly.",
|
||||
@@ -378,14 +378,14 @@ describe("Process Recovery -- Worker Crash Detection", () => {
|
||||
priority: "medium",
|
||||
status: "todo",
|
||||
});
|
||||
const { id: newRunId } = db.runs.createRun({
|
||||
const { id: newRunId } = db.core.runs.createRun({
|
||||
companyId: s.companyId,
|
||||
taskId: taskId2,
|
||||
agentId: agent2Id,
|
||||
workspaceId: s.workspaceId,
|
||||
connectorId: s.connectorId,
|
||||
});
|
||||
expect(db.workspacesMod.claimWorkspaceLock(s.workspaceId, newRunId)).toBe(true);
|
||||
expect(db.core.workspaces.claimWorkspaceLock(s.workspaceId, newRunId)).toBe(true);
|
||||
});
|
||||
|
||||
it("tasks set to blocked after crash", () => {
|
||||
@@ -398,12 +398,12 @@ describe("Process Recovery -- Worker Crash Detection", () => {
|
||||
});
|
||||
|
||||
// Verify task is in_progress before crash
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("in_progress");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("in_progress");
|
||||
|
||||
simulateWorkerCrash(db);
|
||||
|
||||
// Task should be blocked (prevents heartbeat re-queuing)
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("blocked");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("blocked");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -422,12 +422,12 @@ describe("Process Recovery -- Context Preservation (D-02)", () => {
|
||||
});
|
||||
|
||||
// Verify assignment before crash
|
||||
expect(db.entityCrud.getTask(taskId).assigneeAgentId).toBe(s.agentId);
|
||||
expect(db.core.entity.getTask(taskId).assigneeAgentId).toBe(s.agentId);
|
||||
|
||||
simulateWorkerCrash(db);
|
||||
|
||||
// Agent assignment must be preserved for retry context
|
||||
expect(db.entityCrud.getTask(taskId).assigneeAgentId).toBe(s.agentId);
|
||||
expect(db.core.entity.getTask(taskId).assigneeAgentId).toBe(s.agentId);
|
||||
});
|
||||
|
||||
it("session handoff markdown generated on crash", () => {
|
||||
@@ -440,7 +440,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => {
|
||||
});
|
||||
|
||||
// Create a task session (simulating what happens during normal execution)
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId: s.companyId,
|
||||
agentId: s.agentId,
|
||||
adapterType: "gemini-cli",
|
||||
@@ -448,7 +448,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => {
|
||||
|
||||
// Drive the session past the maxRuns=20 threshold so rotation fires.
|
||||
for (let i = 0; i < 20; i++) {
|
||||
db.tasks.incrementSessionUsage(sessionId, 1000, 500, `${runId}-${i}`);
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 1000, 500, `${runId}-${i}`);
|
||||
}
|
||||
|
||||
const sessionService = createSessionManagementService({
|
||||
@@ -456,7 +456,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => {
|
||||
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
});
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session).not.toBeNull();
|
||||
|
||||
const rotationResult = sessionService.evaluateRotation(session!);
|
||||
@@ -499,7 +499,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => {
|
||||
const agents = [s.agentId];
|
||||
for (let i = 2; i <= 3; i++) {
|
||||
agents.push(
|
||||
db.agents.saveAgent({
|
||||
db.core.agents.saveAgent({
|
||||
companyId: s.companyId,
|
||||
name: `Agent${i}`,
|
||||
role: "IC",
|
||||
@@ -530,7 +530,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => {
|
||||
|
||||
expect(interruptedCount).toBe(3);
|
||||
for (const run of runs) {
|
||||
expect(db.runs.getRun(run.runId).status).toBe("interrupted");
|
||||
expect(db.core.runs.getRun(run.runId).status).toBe("interrupted");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -540,7 +540,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => {
|
||||
const agents = [s.agentId];
|
||||
for (let i = 2; i <= 3; i++) {
|
||||
agents.push(
|
||||
db.agents.saveAgent({
|
||||
db.core.agents.saveAgent({
|
||||
companyId: s.companyId,
|
||||
name: `Agent${i}`,
|
||||
role: "IC",
|
||||
@@ -571,7 +571,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => {
|
||||
|
||||
// All tasks should be blocked (crash recovery sets them to blocked)
|
||||
for (const run of runs) {
|
||||
const task = db.entityCrud.getTask(run.taskId);
|
||||
const task = db.core.entity.getTask(run.taskId);
|
||||
expect(task.status).toBe("blocked");
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("skill-aware capability registry", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -32,12 +32,12 @@ describe("skill-aware capability registry", () => {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Skill Match Co",
|
||||
description: "Tests skill-aware assignment",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -46,7 +46,7 @@ describe("skill-aware capability registry", () => {
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
db.agents.saveAgent({
|
||||
db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Generic Engineer",
|
||||
role: "Engineer",
|
||||
@@ -59,7 +59,7 @@ describe("skill-aware capability registry", () => {
|
||||
capabilities: "General software delivery",
|
||||
budgetMonthlyUsd: 200,
|
||||
});
|
||||
const styledEngineerId = db.agents.saveAgent({
|
||||
const styledEngineerId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Frontend Specialist",
|
||||
role: "Engineer",
|
||||
@@ -75,7 +75,7 @@ describe("skill-aware capability registry", () => {
|
||||
|
||||
const skillId = randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
db.skills.upsertCompanySkill({
|
||||
db.core.skills.upsertCompanySkill({
|
||||
id: skillId,
|
||||
companyId,
|
||||
key: "frontend/react-ui",
|
||||
@@ -92,9 +92,9 @@ describe("skill-aware capability registry", () => {
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
db.skills.assignSkillToAgent(styledEngineerId, skillId);
|
||||
db.core.skills.assignSkillToAgent(styledEngineerId, skillId);
|
||||
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Build React dashboard widget",
|
||||
description: "Create an accessible React component with hooks and Tailwind styling.",
|
||||
@@ -103,7 +103,7 @@ describe("skill-aware capability registry", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const assignedId = db.tasks.autoAssignTask(taskId, companyId);
|
||||
const assignedId = db.core.tasks.autoAssignTask(taskId, companyId);
|
||||
expect(assignedId).toBe(styledEngineerId);
|
||||
|
||||
db.close();
|
||||
@@ -114,7 +114,7 @@ describe("skill-aware capability registry", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -122,12 +122,12 @@ describe("skill-aware capability registry", () => {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Registry Co",
|
||||
description: "Tests capability registry prompt injection",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -135,7 +135,7 @@ describe("skill-aware capability registry", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -151,7 +151,7 @@ describe("skill-aware capability registry", () => {
|
||||
|
||||
const skillId = randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
db.skills.upsertCompanySkill({
|
||||
db.core.skills.upsertCompanySkill({
|
||||
id: skillId,
|
||||
companyId,
|
||||
key: "frontend/react-ui",
|
||||
@@ -169,9 +169,9 @@ describe("skill-aware capability registry", () => {
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const snapshot = db.snap.listSnapshot();
|
||||
const snapshot = db.projection.snapshot.listSnapshot();
|
||||
const prompt = buildTaskPrompt({
|
||||
agent: db.entityCrud.getAgent(agentId),
|
||||
agent: db.core.entity.getAgent(agentId),
|
||||
apiUrl: "http://127.0.0.1:3210",
|
||||
apiKey: "token",
|
||||
runId: "run-1",
|
||||
@@ -193,7 +193,7 @@ describe("skill-aware capability registry", () => {
|
||||
allConnectors: snapshot.connectors
|
||||
.filter((connector) => connector.id === "openai-codex")
|
||||
.map((connector) => ({ id: connector.id, label: connector.label, status: connector.status, enabledModels: ["gpt-5.4"] })),
|
||||
companySkillCatalog: db.skills.listCompanySkills(companyId).map((skill) => ({
|
||||
companySkillCatalog: db.core.skills.listCompanySkills(companyId).map((skill) => ({
|
||||
name: skill.name,
|
||||
slug: skill.slug,
|
||||
description: skill.description,
|
||||
|
||||
@@ -28,7 +28,7 @@ function createNoopLogger() {
|
||||
}
|
||||
|
||||
function createScaffold(db: AppDatabase) {
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -36,13 +36,13 @@ function createScaffold(db: AppDatabase) {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Thread Wait Co",
|
||||
description: "Tests cofounder wait states and replies",
|
||||
status: "active",
|
||||
executionMode: "ai_cofounder",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: "/tmp/thread-wait",
|
||||
@@ -50,7 +50,7 @@ function createScaffold(db: AppDatabase) {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -63,7 +63,7 @@ function createScaffold(db: AppDatabase) {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const engineerId = db.agents.saveAgent({
|
||||
const engineerId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
@@ -76,7 +76,7 @@ function createScaffold(db: AppDatabase) {
|
||||
capabilities: "Ship features",
|
||||
budgetMonthlyUsd: 180,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Ship the feature",
|
||||
description: "Implement the approved deliverable and wait for co-founder review.",
|
||||
@@ -116,7 +116,7 @@ describe("thread wait + heartbeat integration", () => {
|
||||
threadIntegration.onTaskCreated(taskId, companyId);
|
||||
threadIntegration.onRunFinished("run-1", taskId, engineerId, companyId, "succeeded", null);
|
||||
|
||||
const thread = db.threads.listThreads({
|
||||
const thread = db.core.threads.listThreads({
|
||||
companyId,
|
||||
type: "task",
|
||||
relatedEntityType: "task",
|
||||
@@ -128,12 +128,12 @@ describe("thread wait + heartbeat integration", () => {
|
||||
throw new Error("Expected a task thread to exist for the completed run.");
|
||||
}
|
||||
|
||||
let waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.threadId === thread.id);
|
||||
let waitStates = db.core.threads.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.agents.saveAgent({
|
||||
const otherAgentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Marketer",
|
||||
role: "CMO",
|
||||
@@ -141,7 +141,7 @@ describe("thread wait + heartbeat integration", () => {
|
||||
status: "idle",
|
||||
reportsTo: null,
|
||||
connectorId: "openai-codex",
|
||||
workspaceId: db.entityCrud.getTask(taskId).workspaceId ?? null,
|
||||
workspaceId: db.core.entity.getTask(taskId).workspaceId ?? null,
|
||||
model: null,
|
||||
capabilities: "Drive marketing work",
|
||||
budgetMonthlyUsd: 180,
|
||||
@@ -165,7 +165,7 @@ describe("thread wait + heartbeat integration", () => {
|
||||
body: "Board reviewed this deliverable. Continue.",
|
||||
});
|
||||
|
||||
waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.threadId === thread.id);
|
||||
waitStates = db.core.threads.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);
|
||||
@@ -192,7 +192,7 @@ describe("thread wait + heartbeat integration", () => {
|
||||
});
|
||||
|
||||
threadIntegration.onTaskCreated(taskId, companyId);
|
||||
const thread = db.threads.listThreads({
|
||||
const thread = db.core.threads.listThreads({
|
||||
companyId,
|
||||
type: "task",
|
||||
relatedEntityType: "task",
|
||||
@@ -204,7 +204,7 @@ describe("thread wait + heartbeat integration", () => {
|
||||
throw new Error("Expected a task thread to exist for approval resolution testing.");
|
||||
}
|
||||
|
||||
const approvalId = db.approvalsMod.requestApproval({
|
||||
const approvalId = db.core.approvals.requestApproval({
|
||||
companyId,
|
||||
relatedTaskId: taskId,
|
||||
requestedByAgentId: engineerId,
|
||||
@@ -215,7 +215,7 @@ describe("thread wait + heartbeat integration", () => {
|
||||
});
|
||||
|
||||
threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "draft_review", "Draft review needed");
|
||||
let waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId);
|
||||
let waitStates = db.core.threads.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);
|
||||
@@ -234,17 +234,17 @@ describe("thread wait + heartbeat integration", () => {
|
||||
body: "Board left a comment but did not decide approval yet.",
|
||||
});
|
||||
|
||||
waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId);
|
||||
waitStates = db.core.threads.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId);
|
||||
expect(waitStates).toHaveLength(1);
|
||||
expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true);
|
||||
|
||||
db.approvalsMod.decideApproval({
|
||||
db.core.approvals.decideApproval({
|
||||
approvalId,
|
||||
state: "approved",
|
||||
decisionNote: "Approved by board",
|
||||
});
|
||||
threadIntegration.onApprovalResolved(approvalId, companyId, "approved");
|
||||
waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId);
|
||||
waitStates = db.core.threads.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId);
|
||||
expect(waitStates).toHaveLength(0);
|
||||
expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(false);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ async function createHarness(): Promise<Harness> {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -53,12 +53,12 @@ async function createHarness(): Promise<Harness> {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Work Products Co",
|
||||
description: "Tests archived visibility, bulk retirement, deliverable signal",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: workspacePath,
|
||||
@@ -66,7 +66,7 @@ async function createHarness(): Promise<Harness> {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Artifact Engineer",
|
||||
role: "Engineer",
|
||||
@@ -79,7 +79,7 @@ async function createHarness(): Promise<Harness> {
|
||||
capabilities: "Writes files",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Produce artifact",
|
||||
description: "",
|
||||
@@ -88,7 +88,7 @@ async function createHarness(): Promise<Harness> {
|
||||
priority: "medium",
|
||||
status: "in_progress",
|
||||
});
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -107,7 +107,7 @@ async function createHarness(): Promise<Harness> {
|
||||
}
|
||||
|
||||
function registerArtifact(harness: Harness, relativePath: string): string {
|
||||
return harness.db.documentsMod.createWorkProduct({
|
||||
return harness.db.core.documents.createWorkProduct({
|
||||
companyId: harness.companyId,
|
||||
taskId: harness.taskId,
|
||||
workspaceId: harness.workspaceId,
|
||||
@@ -126,12 +126,12 @@ describe("work_products archived visibility", () => {
|
||||
try {
|
||||
const activeId = registerArtifact(harness, "docs/active.md");
|
||||
const archivedId = registerArtifact(harness, "docs/retired.md");
|
||||
harness.db.documentsMod.updateWorkProduct(archivedId, harness.companyId, {
|
||||
harness.db.core.documents.updateWorkProduct(archivedId, harness.companyId, {
|
||||
status: "archived",
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
const snapshot = harness.db.snap.listSnapshot();
|
||||
const snapshot = harness.db.projection.snapshot.listSnapshot();
|
||||
const visibleIds = snapshot.workProducts.map((wp) => wp.id);
|
||||
expect(visibleIds).toContain(activeId);
|
||||
expect(visibleIds).not.toContain(archivedId);
|
||||
@@ -145,12 +145,12 @@ describe("work_products archived visibility", () => {
|
||||
try {
|
||||
const activeId = registerArtifact(harness, "src/feature.ts");
|
||||
const archivedId = registerArtifact(harness, "src/removed.ts");
|
||||
harness.db.documentsMod.updateWorkProduct(archivedId, harness.companyId, {
|
||||
harness.db.core.documents.updateWorkProduct(archivedId, harness.companyId, {
|
||||
status: "archived",
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
const rows = harness.db.documentsMod.listWorkProductsForTask(harness.taskId, harness.companyId);
|
||||
const rows = harness.db.core.documents.listWorkProductsForTask(harness.taskId, harness.companyId);
|
||||
const ids = rows.map((r) => r.id);
|
||||
expect(ids).toContain(activeId);
|
||||
expect(ids).not.toContain(archivedId);
|
||||
@@ -164,16 +164,16 @@ describe("work_products archived visibility", () => {
|
||||
try {
|
||||
const id = registerArtifact(harness, "notes/ghost.md");
|
||||
expect(
|
||||
harness.db.documentsMod.resolveWorkProductTarget(id, harness.companyId),
|
||||
harness.db.core.documents.resolveWorkProductTarget(id, harness.companyId),
|
||||
).not.toBeNull();
|
||||
|
||||
harness.db.documentsMod.updateWorkProduct(id, harness.companyId, {
|
||||
harness.db.core.documents.updateWorkProduct(id, harness.companyId, {
|
||||
status: "archived",
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
expect(
|
||||
harness.db.documentsMod.resolveWorkProductTarget(id, harness.companyId),
|
||||
harness.db.core.documents.resolveWorkProductTarget(id, harness.companyId),
|
||||
).toMatchObject({ kind: "error", reason: "archived" });
|
||||
} finally {
|
||||
harness.db.close();
|
||||
@@ -193,7 +193,7 @@ describe("work_products bulk retirement", () => {
|
||||
ids.push(registerArtifact(harness, rel));
|
||||
}
|
||||
|
||||
const archived = harness.db.documentsMod.archiveWorkProductsByExternalPaths({
|
||||
const archived = harness.db.core.documents.archiveWorkProductsByExternalPaths({
|
||||
companyId: harness.companyId,
|
||||
workspaceId: harness.workspaceId,
|
||||
provider: "filesystem",
|
||||
@@ -206,7 +206,7 @@ describe("work_products bulk retirement", () => {
|
||||
for (const id of ids) {
|
||||
expect(archivedIdSet.has(id)).toBe(true);
|
||||
}
|
||||
const visible = harness.db.snap.listSnapshot().workProducts;
|
||||
const visible = harness.db.projection.snapshot.listSnapshot().workProducts;
|
||||
expect(visible.length).toBe(0);
|
||||
} finally {
|
||||
harness.db.close();
|
||||
@@ -217,7 +217,7 @@ describe("work_products bulk retirement", () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
const id = registerArtifact(harness, "tmp/scratch.log");
|
||||
harness.db.documentsMod.archiveWorkProductsByExternalPaths({
|
||||
harness.db.core.documents.archiveWorkProductsByExternalPaths({
|
||||
companyId: harness.companyId,
|
||||
workspaceId: harness.workspaceId,
|
||||
provider: "filesystem",
|
||||
@@ -225,7 +225,7 @@ describe("work_products bulk retirement", () => {
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
const second = harness.db.documentsMod.archiveWorkProductsByExternalPaths({
|
||||
const second = harness.db.core.documents.archiveWorkProductsByExternalPaths({
|
||||
companyId: harness.companyId,
|
||||
workspaceId: harness.workspaceId,
|
||||
provider: "filesystem",
|
||||
@@ -235,7 +235,7 @@ describe("work_products bulk retirement", () => {
|
||||
|
||||
expect(second).toHaveLength(0);
|
||||
expect(
|
||||
harness.db.documentsMod.resolveWorkProductTarget(id, harness.companyId),
|
||||
harness.db.core.documents.resolveWorkProductTarget(id, harness.companyId),
|
||||
).toMatchObject({ kind: "error", reason: "archived" });
|
||||
} finally {
|
||||
harness.db.close();
|
||||
@@ -252,7 +252,7 @@ describe("work_products bulk retirement", () => {
|
||||
|
||||
const workProductId = registerArtifact(harness, relPath);
|
||||
expect(
|
||||
harness.db.documentsMod.resolveWorkProductTarget(workProductId, harness.companyId),
|
||||
harness.db.core.documents.resolveWorkProductTarget(workProductId, harness.companyId),
|
||||
).not.toBeNull();
|
||||
|
||||
const baseline = {
|
||||
@@ -277,7 +277,7 @@ describe("work_products bulk retirement", () => {
|
||||
expect(retired.map((o) => (o as { relativePath: string }).relativePath)).toContain(relPath);
|
||||
|
||||
expect(
|
||||
harness.db.documentsMod.resolveWorkProductTarget(workProductId, harness.companyId),
|
||||
harness.db.core.documents.resolveWorkProductTarget(workProductId, harness.companyId),
|
||||
).toMatchObject({ kind: "error", reason: "archived" });
|
||||
} finally {
|
||||
harness.db.close();
|
||||
@@ -290,19 +290,19 @@ describe("work_products deliverable signal", () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
const id = registerArtifact(harness, "deliverables/final.md");
|
||||
const beforeArchive = harness.db.knowledgeMod.collectRunDeliverableSignals(
|
||||
const beforeArchive = harness.db.core.knowledge.collectRunDeliverableSignals(
|
||||
harness.runId,
|
||||
harness.companyId,
|
||||
harness.taskId,
|
||||
);
|
||||
expect(beforeArchive.hasWorkProduct).toBe(true);
|
||||
|
||||
harness.db.documentsMod.updateWorkProduct(id, harness.companyId, {
|
||||
harness.db.core.documents.updateWorkProduct(id, harness.companyId, {
|
||||
status: "archived",
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
const afterArchive = harness.db.knowledgeMod.collectRunDeliverableSignals(
|
||||
const afterArchive = harness.db.core.knowledge.collectRunDeliverableSignals(
|
||||
harness.runId,
|
||||
harness.companyId,
|
||||
harness.taskId,
|
||||
|
||||
@@ -22,19 +22,19 @@ describe("agent budget policy synchronization", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Budget Sync Co",
|
||||
description: "Budget sync regression coverage",
|
||||
status: "active",
|
||||
});
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -42,7 +42,7 @@ describe("agent budget policy synchronization", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
@@ -57,7 +57,7 @@ describe("agent budget policy synchronization", () => {
|
||||
department: "engineering",
|
||||
});
|
||||
|
||||
const firstPolicyId = db.budget.saveBudgetPolicy({
|
||||
const firstPolicyId = db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "agent",
|
||||
scopeId: agentId,
|
||||
@@ -70,7 +70,7 @@ describe("agent budget policy synchronization", () => {
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
db.agents.saveAgent({
|
||||
db.core.agents.saveAgent({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
@@ -86,7 +86,7 @@ describe("agent budget policy synchronization", () => {
|
||||
department: "engineering",
|
||||
});
|
||||
|
||||
const secondPolicyId = db.budget.saveBudgetPolicy({
|
||||
const secondPolicyId = db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "agent",
|
||||
scopeId: agentId,
|
||||
@@ -99,13 +99,13 @@ describe("agent budget policy synchronization", () => {
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const agentPolicies = db.budget.listBudgetPolicies(companyId).filter(
|
||||
const agentPolicies = db.core.budget.listBudgetPolicies(companyId).filter(
|
||||
(policy) => policy.scopeType === "agent" && policy.scopeId === agentId,
|
||||
);
|
||||
expect(secondPolicyId).toBe(firstPolicyId);
|
||||
expect(agentPolicies).toHaveLength(1);
|
||||
expect(agentPolicies[0]?.amount).toBe(25_000);
|
||||
expect(db.budget.getActivePoliciesForRun(companyId, agentId, null).map((policy) => policy.amount)).toEqual([25_000]);
|
||||
expect(db.core.budget.getActivePoliciesForRun(companyId, agentId, null).map((policy) => policy.amount)).toEqual([25_000]);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("agent hierarchy cycle guard", () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "agentcompany-hierarchy-"));
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
companyId = db.company.saveCompany({
|
||||
companyId = db.core.company.saveCompany({
|
||||
name: "Hierarchy Guard Co",
|
||||
description: "Tests reporting relationships.",
|
||||
status: "active",
|
||||
@@ -57,11 +57,11 @@ describe("agent hierarchy cycle guard", () => {
|
||||
|
||||
saveAgent("engineer", "ceo");
|
||||
|
||||
expect(db.entityCrud.getAgent("engineer").reportsTo).toBe("ceo");
|
||||
expect(db.core.entity.getAgent("engineer").reportsTo).toBe("ceo");
|
||||
});
|
||||
|
||||
function saveAgent(id: string, reportsTo: string | null): string {
|
||||
return db.agents.saveAgent({
|
||||
return db.core.agents.saveAgent({
|
||||
id,
|
||||
companyId,
|
||||
name: id,
|
||||
|
||||
@@ -23,19 +23,19 @@ describe("hire routing guards", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Routing Co",
|
||||
description: "Hire routing regression coverage",
|
||||
status: "active",
|
||||
});
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -43,7 +43,7 @@ describe("hire routing guards", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -57,7 +57,7 @@ describe("hire routing guards", () => {
|
||||
budgetMonthlyUsd: 250,
|
||||
department: "executive",
|
||||
});
|
||||
const hrId = db.agents.saveAgent({
|
||||
const hrId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "HR Lead",
|
||||
role: "HR Lead",
|
||||
@@ -71,7 +71,7 @@ describe("hire routing guards", () => {
|
||||
budgetMonthlyUsd: 180,
|
||||
department: "hr",
|
||||
});
|
||||
const ctoId = db.agents.saveAgent({
|
||||
const ctoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CTO",
|
||||
role: "CTO",
|
||||
|
||||
@@ -18,12 +18,12 @@ async function setupCompany(prefix: string) {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "CEO Strategy Co",
|
||||
description: "Tests canFileCeoStrategy orthogonality",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -43,7 +43,7 @@ describe("canFileCeoStrategy", () => {
|
||||
it("returns true for a genuine CEO who reports to the board", async () => {
|
||||
const { db, companyId, workspaceId } = await setupCompany("agentcompany-ceo-strategy-real-");
|
||||
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Ada",
|
||||
role: "CEO",
|
||||
@@ -65,7 +65,7 @@ describe("canFileCeoStrategy", () => {
|
||||
it("returns false for a CEO-titled agent with a virtual superior", async () => {
|
||||
const { db, companyId, workspaceId } = await setupCompany("agentcompany-ceo-strategy-virtual-");
|
||||
|
||||
const boardId = db.agents.saveAgent({
|
||||
const boardId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Board Proxy",
|
||||
role: "Board",
|
||||
@@ -78,7 +78,7 @@ describe("canFileCeoStrategy", () => {
|
||||
capabilities: "Hold CEO accountable",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Ada",
|
||||
role: "CEO",
|
||||
@@ -100,7 +100,7 @@ describe("canFileCeoStrategy", () => {
|
||||
it("returns false for a VP with no manager (role is not CEO)", async () => {
|
||||
const { db, companyId, workspaceId } = await setupCompany("agentcompany-ceo-strategy-vp-");
|
||||
|
||||
const vpId = db.agents.saveAgent({
|
||||
const vpId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Kai",
|
||||
role: "VP Product",
|
||||
@@ -124,7 +124,7 @@ describe("canFileCeoStrategy", () => {
|
||||
it("returns false for a plain IC", async () => {
|
||||
const { db, companyId, workspaceId } = await setupCompany("agentcompany-ceo-strategy-ic-");
|
||||
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Ada",
|
||||
role: "CEO",
|
||||
@@ -137,7 +137,7 @@ describe("canFileCeoStrategy", () => {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const icId = db.agents.saveAgent({
|
||||
const icId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Eve",
|
||||
role: "Engineer",
|
||||
|
||||
@@ -30,12 +30,12 @@ describe("canManageOrganization", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Org Permissions Co",
|
||||
description: "Tests recursive org authority",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -44,7 +44,7 @@ describe("canManageOrganization", () => {
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -57,7 +57,7 @@ describe("canManageOrganization", () => {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const ctoId = db.agents.saveAgent({
|
||||
const ctoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CTO",
|
||||
role: "CTO",
|
||||
@@ -71,7 +71,7 @@ describe("canManageOrganization", () => {
|
||||
budgetMonthlyUsd: 220,
|
||||
department: "engineering",
|
||||
});
|
||||
const engineerId = db.agents.saveAgent({
|
||||
const engineerId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
@@ -100,12 +100,12 @@ describe("canManageOrganization", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Org Permission Override Co",
|
||||
description: "Tests permission-based org authority",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -113,7 +113,7 @@ describe("canManageOrganization", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -126,7 +126,7 @@ describe("canManageOrganization", () => {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Operations Lead",
|
||||
role: "Specialist",
|
||||
@@ -142,7 +142,7 @@ describe("canManageOrganization", () => {
|
||||
});
|
||||
|
||||
expect(canManageOrganization(db, companyId, agentId)).toBe(false);
|
||||
db.agents.setAgentPermission(agentId, companyId, "organization_manage", true);
|
||||
db.core.agents.setAgentPermission(agentId, companyId, "organization_manage", true);
|
||||
expect(canManageOrganization(db, companyId, agentId)).toBe(true);
|
||||
expect(canRequestHire(db, companyId, agentId)).toBe(true);
|
||||
|
||||
@@ -154,12 +154,12 @@ describe("canManageOrganization", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Scoped Authority Co",
|
||||
description: "Tests that branch growth does not unlock org-wide admin powers",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -167,7 +167,7 @@ describe("canManageOrganization", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -180,7 +180,7 @@ describe("canManageOrganization", () => {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const ctoId = db.agents.saveAgent({
|
||||
const ctoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CTO",
|
||||
role: "CTO",
|
||||
@@ -194,7 +194,7 @@ describe("canManageOrganization", () => {
|
||||
budgetMonthlyUsd: 220,
|
||||
department: "engineering",
|
||||
});
|
||||
const cmoId = db.agents.saveAgent({
|
||||
const cmoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CMO",
|
||||
role: "CMO",
|
||||
@@ -208,7 +208,7 @@ describe("canManageOrganization", () => {
|
||||
budgetMonthlyUsd: 220,
|
||||
department: "marketing",
|
||||
});
|
||||
const ctoReportId = db.agents.saveAgent({
|
||||
const ctoReportId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
@@ -223,7 +223,7 @@ describe("canManageOrganization", () => {
|
||||
department: "engineering",
|
||||
});
|
||||
|
||||
const cmoTaskId = db.tasks.saveTask({
|
||||
const cmoTaskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Launch campaign",
|
||||
description: "Owned by the marketing branch",
|
||||
@@ -242,7 +242,7 @@ describe("canManageOrganization", () => {
|
||||
expect(canManageProject(db, companyId, ctoId, cmoId)).toBe(false);
|
||||
expect(canManageGoal(db, companyId, ceoId, cmoId)).toBe(true);
|
||||
expect(canManageProject(db, companyId, ceoId, cmoId)).toBe(true);
|
||||
expect(db.entityCrud.getTask(cmoTaskId).assigneeAgentId).toBe(cmoId);
|
||||
expect(db.core.entity.getTask(cmoTaskId).assigneeAgentId).toBe(cmoId);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("hire approval routing", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -31,12 +31,12 @@ describe("hire approval routing", () => {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Hire Routing Co",
|
||||
description: "Tests recursive hire wake routing",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -44,7 +44,7 @@ describe("hire approval routing", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -57,7 +57,7 @@ describe("hire approval routing", () => {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const ctoId = db.agents.saveAgent({
|
||||
const ctoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CTO",
|
||||
role: "CTO",
|
||||
@@ -91,7 +91,7 @@ describe("hire approval routing", () => {
|
||||
payloadJson: "{}",
|
||||
});
|
||||
|
||||
db.approvalsMod.decideApproval({
|
||||
db.core.approvals.decideApproval({
|
||||
approvalId,
|
||||
state: "approved",
|
||||
decisionNote: "Approved by board",
|
||||
@@ -116,7 +116,7 @@ describe("hire approval routing", () => {
|
||||
trackTimer: vi.fn(),
|
||||
}, approvalId, companyId, "approved");
|
||||
|
||||
expect(db.entityCrud.getAgent(hireId).status).toBe("idle");
|
||||
expect(db.core.entity.getAgent(hireId).status).toBe("idle");
|
||||
expect(dispatchAutomation).toHaveBeenCalledWith("approval_resolved", expect.objectContaining({ companyId, agentId: ctoId }));
|
||||
expect(dispatchAutomation).toHaveBeenCalledWith("hire_approved", expect.objectContaining({ companyId, agentId: hireId }));
|
||||
expect(wakeAgentIfPossible).toHaveBeenCalledWith(hireId, companyId, "assignment");
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("deliverable review feedback writeback", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -31,12 +31,12 @@ describe("deliverable review feedback writeback", () => {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Review Feedback Co",
|
||||
description: "Tests review feedback knowledge writeback",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -44,7 +44,7 @@ describe("deliverable review feedback writeback", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Research Lead",
|
||||
role: "Researcher",
|
||||
@@ -58,7 +58,7 @@ describe("deliverable review feedback writeback", () => {
|
||||
budgetMonthlyUsd: 180,
|
||||
department: "research",
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Draft market report",
|
||||
description: "Produce a market report with concrete evidence and business insight.",
|
||||
@@ -67,7 +67,7 @@ describe("deliverable review feedback writeback", () => {
|
||||
priority: "high",
|
||||
status: "in_review",
|
||||
});
|
||||
const documentId = db.documentsMod.saveDocument({
|
||||
const documentId = db.core.documents.saveDocument({
|
||||
companyId,
|
||||
type: "knowledge_article",
|
||||
title: "Market report",
|
||||
@@ -75,7 +75,7 @@ describe("deliverable review feedback writeback", () => {
|
||||
authorAgentId: agentId,
|
||||
status: "in_review",
|
||||
});
|
||||
const approvalId = db.approvalsMod.requestApproval({
|
||||
const approvalId = db.core.approvals.requestApproval({
|
||||
companyId,
|
||||
relatedTaskId: taskId,
|
||||
requestedByAgentId: agentId,
|
||||
@@ -85,7 +85,7 @@ describe("deliverable review feedback writeback", () => {
|
||||
payloadJson: JSON.stringify({ documentIds: [documentId] }),
|
||||
});
|
||||
|
||||
db.approvalsMod.decideApproval({
|
||||
db.core.approvals.decideApproval({
|
||||
approvalId,
|
||||
state: "revision_requested",
|
||||
decisionNote: "Add stronger competitor evidence and a clearer recommendation.",
|
||||
@@ -107,7 +107,7 @@ describe("deliverable review feedback writeback", () => {
|
||||
trackTimer: vi.fn(),
|
||||
}, approvalId, companyId, "revision_requested");
|
||||
|
||||
const knowledgeEntries = db.snap.listSnapshot().knowledgeBase.filter((entry) => entry.companyId === companyId);
|
||||
const knowledgeEntries = db.projection.snapshot.listSnapshot().knowledgeBase.filter((entry) => entry.companyId === companyId);
|
||||
expect(knowledgeEntries).toHaveLength(1);
|
||||
expect(knowledgeEntries[0]?.category).toBe("lesson_learned");
|
||||
expect(knowledgeEntries[0]?.importance).toBe("high");
|
||||
@@ -116,8 +116,8 @@ describe("deliverable review feedback writeback", () => {
|
||||
expect(knowledgeEntries[0]?.content).toContain("revision_requested");
|
||||
expect(knowledgeEntries[0]?.content).toContain("stronger competitor evidence");
|
||||
expect(wakeAgentIfPossible).toHaveBeenCalledWith(agentId, companyId, "revision_needed");
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("in_progress");
|
||||
const document = db.snap.listSnapshot().documents.find((entry) => entry.id === documentId);
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("in_progress");
|
||||
const document = db.projection.snapshot.listSnapshot().documents.find((entry) => entry.id === documentId);
|
||||
expect(document?.status).toBe("draft");
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -29,7 +29,7 @@ type RawDb = {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createTestCompany(db: AppDatabase): string {
|
||||
return db.company.saveCompany({
|
||||
return db.core.company.saveCompany({
|
||||
name: "Test Company",
|
||||
description: "Atomic checkout tests",
|
||||
status: "active",
|
||||
@@ -37,7 +37,7 @@ function createTestCompany(db: AppDatabase): string {
|
||||
}
|
||||
|
||||
function createTestConnector(db: AppDatabase): void {
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "openai-codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
@@ -46,7 +46,7 @@ function createTestConnector(db: AppDatabase): void {
|
||||
}
|
||||
|
||||
function createTestWorkspace(db: AppDatabase, companyId: string): string {
|
||||
return db.workspacesMod.saveWorkspace({
|
||||
return db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Test Workspace",
|
||||
localPath: "/tmp/test-workspace",
|
||||
@@ -57,7 +57,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string {
|
||||
}
|
||||
|
||||
function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string {
|
||||
return db.agents.saveAgent({
|
||||
return db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name,
|
||||
role: "IC",
|
||||
@@ -73,7 +73,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string
|
||||
}
|
||||
|
||||
function createTestTask(db: AppDatabase, companyId: string, agentId: string | null, workspaceId: string): string {
|
||||
return db.tasks.saveTask({
|
||||
return db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Test Task",
|
||||
description: "A test task for checkout",
|
||||
@@ -110,7 +110,7 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -118,10 +118,10 @@ describe("CAS checkout", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
const result = db.tasks.checkoutTask(taskId, runId);
|
||||
const result = db.core.tasks.checkoutTask(taskId, runId);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.status).toBe("in_progress");
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
db.runs.createRun({
|
||||
db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -140,7 +140,7 @@ describe("CAS checkout", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.activeRunId).toBeNull();
|
||||
expect(task.status).toBe("todo");
|
||||
});
|
||||
@@ -152,9 +152,9 @@ describe("CAS checkout", () => {
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
const missingRunId = randomUUID();
|
||||
|
||||
expect(() => db.tasks.checkoutTask(taskId, missingRunId)).toThrow(/Run not found/);
|
||||
expect(() => db.core.tasks.checkoutTask(taskId, missingRunId)).toThrow(/Run not found/);
|
||||
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.activeRunId).toBeNull();
|
||||
expect(task.status).toBe("todo");
|
||||
const lock = (db as unknown as RawDb).db
|
||||
@@ -171,7 +171,7 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const run = db.runs.createTaskRun({
|
||||
const run = db.core.runs.createTaskRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -180,7 +180,7 @@ describe("CAS checkout", () => {
|
||||
});
|
||||
|
||||
expect(run).not.toBeNull();
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.status).toBe("in_progress");
|
||||
expect(task.activeRunId).toBe(run?.id);
|
||||
const lock = (db as unknown as RawDb).db
|
||||
@@ -196,7 +196,7 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const firstRun = db.runs.createTaskRun({
|
||||
const firstRun = db.core.runs.createTaskRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -206,7 +206,7 @@ describe("CAS checkout", () => {
|
||||
expect(firstRun).not.toBeNull();
|
||||
|
||||
const losingRunId = randomUUID();
|
||||
const secondRun = db.runs.createTaskRun({
|
||||
const secondRun = db.core.runs.createTaskRun({
|
||||
id: losingRunId,
|
||||
companyId,
|
||||
taskId,
|
||||
@@ -229,14 +229,14 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: runId1 } = db.runs.createRun({
|
||||
const { id: runId1 } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
const { id: runId2 } = db.runs.createRun({
|
||||
const { id: runId2 } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -244,8 +244,8 @@ describe("CAS checkout", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
expect(db.tasks.checkoutTask(taskId, runId1)).toBe(true);
|
||||
expect(db.tasks.checkoutTask(taskId, runId2)).toBe(false);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runId1)).toBe(true);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runId2)).toBe(false);
|
||||
});
|
||||
|
||||
it("CAS checkout rejects already-in-progress tasks", () => {
|
||||
@@ -258,7 +258,7 @@ describe("CAS checkout", () => {
|
||||
// Manually set task to in_progress
|
||||
(db as unknown as RawDb).db.prepare("UPDATE tasks SET status = 'in_progress' WHERE id = ?").run(taskId);
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -266,7 +266,7 @@ describe("CAS checkout", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
expect(db.tasks.checkoutTask(taskId, runId)).toBe(false);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runId)).toBe(false);
|
||||
});
|
||||
|
||||
it("lock release in finishRun clears checkout fields", () => {
|
||||
@@ -276,7 +276,7 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -284,9 +284,9 @@ describe("CAS checkout", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
db.tasks.checkoutTask(taskId, runId);
|
||||
db.runs.markRunStatus(runId, "running", "");
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId, runId);
|
||||
db.core.runs.markRunStatus(runId, "running", "");
|
||||
db.core.runs.finishRun({
|
||||
runId,
|
||||
status: "succeeded",
|
||||
summary: "Done",
|
||||
@@ -312,7 +312,7 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -320,11 +320,11 @@ describe("CAS checkout", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
db.tasks.checkoutTask(taskId, runId);
|
||||
db.runs.markRunStatus(runId, "running", "");
|
||||
db.core.tasks.checkoutTask(taskId, runId);
|
||||
db.core.runs.markRunStatus(runId, "running", "");
|
||||
|
||||
// Deleting the agent triggers cancelRunForDeletion for active runs
|
||||
db.agents.deleteAgent(agentId, companyId);
|
||||
db.core.agents.deleteAgent(agentId, companyId);
|
||||
|
||||
const row = (db as unknown as RawDb).db
|
||||
.prepare("SELECT checkout_run_id, execution_locked_at FROM tasks WHERE id = ?")
|
||||
@@ -341,7 +341,7 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -349,7 +349,7 @@ describe("CAS checkout", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
db.tasks.checkoutTask(taskId, runId);
|
||||
db.core.tasks.checkoutTask(taskId, runId);
|
||||
|
||||
// Backdate execution_locked_at to 20 minutes ago
|
||||
const backdated = new Date(Date.now() - 20 * 60 * 1000).toISOString();
|
||||
@@ -363,7 +363,7 @@ describe("CAS checkout", () => {
|
||||
expect(recovered[0].taskId).toBe(taskId);
|
||||
expect(recovered[0].companyId).toBe(companyId);
|
||||
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.status).toBe("todo");
|
||||
|
||||
const row = (db as unknown as RawDb).db
|
||||
@@ -381,7 +381,7 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -389,12 +389,12 @@ describe("CAS checkout", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
db.tasks.checkoutTask(taskId, runId);
|
||||
db.core.tasks.checkoutTask(taskId, runId);
|
||||
|
||||
const recovered = db.recoverStaleLocks();
|
||||
expect(recovered.length).toBe(0);
|
||||
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.status).toBe("in_progress");
|
||||
});
|
||||
|
||||
@@ -414,19 +414,19 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, runId);
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("in_progress");
|
||||
db.core.tasks.checkoutTask(taskId, runId);
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("in_progress");
|
||||
|
||||
// Never call markRunStatus('running', ...) — this simulates the
|
||||
// "workspace already locked" bail-out inside dispatchRunToWorker.
|
||||
db.runs.finishRun({
|
||||
db.core.runs.finishRun({
|
||||
runId,
|
||||
status: "failed",
|
||||
summary: null,
|
||||
@@ -437,18 +437,18 @@ describe("CAS checkout", () => {
|
||||
unattributedCost: true,
|
||||
});
|
||||
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("todo");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("todo");
|
||||
|
||||
// And the next CAS checkout must succeed — proving the heartbeat
|
||||
// retry path is unblocked.
|
||||
const { id: retryRunId } = db.runs.createRun({
|
||||
const { id: retryRunId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
expect(db.tasks.checkoutTask(taskId, retryRunId)).toBe(true);
|
||||
expect(db.core.tasks.checkoutTask(taskId, retryRunId)).toBe(true);
|
||||
});
|
||||
|
||||
it("cancelled run that never started also rolls task back to 'todo'", () => {
|
||||
@@ -462,15 +462,15 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, runId);
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId, runId);
|
||||
db.core.runs.finishRun({
|
||||
runId,
|
||||
status: "cancelled",
|
||||
summary: null,
|
||||
@@ -481,7 +481,7 @@ describe("CAS checkout", () => {
|
||||
unattributedCost: true,
|
||||
});
|
||||
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("todo");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("todo");
|
||||
});
|
||||
|
||||
it("failed run that did start stays 'in_progress' (human retry path)", () => {
|
||||
@@ -495,16 +495,16 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: runId } = db.runs.createRun({
|
||||
const { id: runId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, runId);
|
||||
db.runs.markRunStatus(runId, "running", "");
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId, runId);
|
||||
db.core.runs.markRunStatus(runId, "running", "");
|
||||
db.core.runs.finishRun({
|
||||
runId,
|
||||
status: "failed",
|
||||
summary: null,
|
||||
@@ -515,7 +515,7 @@ describe("CAS checkout", () => {
|
||||
unattributedCost: false,
|
||||
});
|
||||
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("in_progress");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("in_progress");
|
||||
});
|
||||
|
||||
it("advanceTaskStatus('in_review') rejects a CEO self-review that has no reviewer", () => {
|
||||
@@ -531,9 +531,9 @@ describe("CAS checkout", () => {
|
||||
const ceoId = createTestAgent(db, companyId, workspaceId, "CEO");
|
||||
const taskId = createTestTask(db, companyId, ceoId, workspaceId);
|
||||
|
||||
expect(() => db.tasks.advanceTaskStatus(taskId, "in_review")).toThrow(/no reviewer/);
|
||||
expect(() => db.core.tasks.advanceTaskStatus(taskId, "in_review")).toThrow(/no reviewer/);
|
||||
// Task status must remain unchanged after the rejected transition.
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("todo");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("todo");
|
||||
});
|
||||
|
||||
it("advanceTaskStatus('in_review') accepts a CEO assignee flagged requires_user_review=1", () => {
|
||||
@@ -543,10 +543,10 @@ describe("CAS checkout", () => {
|
||||
const workspaceId = createTestWorkspace(db, companyId);
|
||||
const ceoId = createTestAgent(db, companyId, workspaceId, "CEO");
|
||||
const taskId = createTestTask(db, companyId, ceoId, workspaceId);
|
||||
db.tasks.setTaskRequiresUserReview(taskId, true);
|
||||
db.core.tasks.setTaskRequiresUserReview(taskId, true);
|
||||
|
||||
db.tasks.advanceTaskStatus(taskId, "in_review");
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("in_review");
|
||||
db.core.tasks.advanceTaskStatus(taskId, "in_review");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("in_review");
|
||||
});
|
||||
|
||||
it("interruptStaleRuns surfaces shutdown-era running runs without startup task rewrites", () => {
|
||||
@@ -563,21 +563,21 @@ describe("CAS checkout", () => {
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
const taskId = createTestTask(db, companyId, null, workspaceId);
|
||||
|
||||
const { id: liveRunId } = db.runs.createRun({
|
||||
const { id: liveRunId } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, liveRunId);
|
||||
db.runs.markRunStatus(liveRunId, "running", "");
|
||||
db.core.tasks.checkoutTask(taskId, liveRunId);
|
||||
db.core.runs.markRunStatus(liveRunId, "running", "");
|
||||
|
||||
db.close();
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
|
||||
expect(db.lastInterruptedCount).toBe(1);
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("blocked");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("blocked");
|
||||
});
|
||||
});
|
||||
@@ -147,7 +147,7 @@ function createMockDb() {
|
||||
return computeSpentCents(filtered);
|
||||
},
|
||||
};
|
||||
return { policies, costEntries, budget };
|
||||
return { policies, costEntries, core: { budget } };
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
@@ -162,7 +162,7 @@ describe("Budget Policy CRUD", () => {
|
||||
});
|
||||
|
||||
it("saveBudgetPolicy creates new policy and returns its ID", () => {
|
||||
const id = db.budget.saveBudgetPolicy({
|
||||
const id = db.core.budget.saveBudgetPolicy({
|
||||
companyId: "c1",
|
||||
scopeType: "company",
|
||||
scopeId: "c1",
|
||||
@@ -175,13 +175,13 @@ describe("Budget Policy CRUD", () => {
|
||||
});
|
||||
|
||||
it("saveBudgetPolicy updates existing policy when ID provided", () => {
|
||||
const id = db.budget.saveBudgetPolicy({
|
||||
const id = db.core.budget.saveBudgetPolicy({
|
||||
companyId: "c1",
|
||||
scopeType: "company",
|
||||
scopeId: "c1",
|
||||
amount: 10000,
|
||||
});
|
||||
db.budget.saveBudgetPolicy({
|
||||
db.core.budget.saveBudgetPolicy({
|
||||
id,
|
||||
companyId: "c1",
|
||||
scopeType: "company",
|
||||
@@ -193,14 +193,14 @@ describe("Budget Policy CRUD", () => {
|
||||
});
|
||||
|
||||
it("deleteBudgetPolicy removes policy by ID", () => {
|
||||
const id = db.budget.saveBudgetPolicy({
|
||||
const id = db.core.budget.saveBudgetPolicy({
|
||||
companyId: "c1",
|
||||
scopeType: "company",
|
||||
scopeId: "c1",
|
||||
amount: 10000,
|
||||
});
|
||||
expect(db.policies).toHaveLength(1);
|
||||
db.budget.deleteBudgetPolicy("c1", id);
|
||||
db.core.budget.deleteBudgetPolicy("c1", id);
|
||||
expect(db.policies).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -213,27 +213,27 @@ describe("getActivePoliciesForRun", () => {
|
||||
});
|
||||
|
||||
it("returns all active policies matching company+agent+project scopes", () => {
|
||||
db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 50000 });
|
||||
db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "agent", scopeId: "a1", amount: 10000 });
|
||||
db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "project", scopeId: "p1", amount: 20000 });
|
||||
db.core.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 50000 });
|
||||
db.core.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "agent", scopeId: "a1", amount: 10000 });
|
||||
db.core.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "project", scopeId: "p1", amount: 20000 });
|
||||
// Different company — should not match
|
||||
db.budget.saveBudgetPolicy({ companyId: "c2", scopeType: "company", scopeId: "c2", amount: 99999 });
|
||||
db.core.budget.saveBudgetPolicy({ companyId: "c2", scopeType: "company", scopeId: "c2", amount: 99999 });
|
||||
|
||||
const result = db.budget.getActivePoliciesForRun("c1", "a1", "p1");
|
||||
const result = db.core.budget.getActivePoliciesForRun("c1", "a1", "p1");
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((p) => p.scopeType).sort()).toEqual(["agent", "company", "project"]);
|
||||
});
|
||||
|
||||
it("returns empty array when no policies exist (D-02: no policy = unlimited)", () => {
|
||||
const result = db.budget.getActivePoliciesForRun("c1", "a1", null);
|
||||
const result = db.core.budget.getActivePoliciesForRun("c1", "a1", null);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("excludes project scope when projectId is null", () => {
|
||||
db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 50000 });
|
||||
db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "project", scopeId: "p1", amount: 20000 });
|
||||
db.core.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 50000 });
|
||||
db.core.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "project", scopeId: "p1", amount: 20000 });
|
||||
|
||||
const result = db.budget.getActivePoliciesForRun("c1", "a1", null);
|
||||
const result = db.core.budget.getActivePoliciesForRun("c1", "a1", null);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.scopeType).toBe("company");
|
||||
});
|
||||
@@ -259,7 +259,7 @@ describe("getSpentInWindow", () => {
|
||||
amount: 100000, warnPercent: 80, hardStopEnabled: true, notifyEnabled: true,
|
||||
isActive: true, createdAt: now.toISOString(), updatedAt: now.toISOString(),
|
||||
};
|
||||
const spent = db.budget.getSpentInWindow(policy);
|
||||
const spent = db.core.budget.getSpentInWindow(policy);
|
||||
expect(spent).toBe(380); // (1.50 + 2.30) * 100 = 380 cents
|
||||
});
|
||||
|
||||
@@ -276,7 +276,7 @@ describe("getSpentInWindow", () => {
|
||||
amount: 100000, warnPercent: 80, hardStopEnabled: true, notifyEnabled: true,
|
||||
isActive: true, createdAt: now.toISOString(), updatedAt: now.toISOString(),
|
||||
};
|
||||
const spent = db.budget.getSpentInWindow(policy);
|
||||
const spent = db.core.budget.getSpentInWindow(policy);
|
||||
expect(spent).toBe(500); // Only a1's $5.00 = 500 cents
|
||||
});
|
||||
|
||||
@@ -293,7 +293,7 @@ describe("getSpentInWindow", () => {
|
||||
amount: 100000, warnPercent: 80, hardStopEnabled: true, notifyEnabled: true,
|
||||
isActive: true, createdAt: now.toISOString(), updatedAt: now.toISOString(),
|
||||
};
|
||||
const spent = db.budget.getSpentInWindow(policy);
|
||||
const spent = db.core.budget.getSpentInWindow(policy);
|
||||
expect(spent).toBe(750); // Only p1's $7.50 = 750 cents
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ function createMockIncidentDb() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
return { incidents, budget };
|
||||
return { incidents, core: { budget } };
|
||||
}
|
||||
|
||||
function makePolicy(overrides: Partial<BudgetPolicyRecord> = {}): BudgetPolicyRecord {
|
||||
@@ -166,7 +166,7 @@ describe("upsertBudgetIncident", () => {
|
||||
|
||||
it("creates new incident when none exists for policy+window+threshold_type", () => {
|
||||
const policy = makePolicy();
|
||||
const id = db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
const id = db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
expect(id).toBeTruthy();
|
||||
expect(db.incidents).toHaveLength(1);
|
||||
expect(db.incidents[0]!.thresholdType).toBe("warn_threshold");
|
||||
@@ -176,8 +176,8 @@ describe("upsertBudgetIncident", () => {
|
||||
|
||||
it("updates amount_observed when open incident already exists (D-08 dedup)", () => {
|
||||
const policy = makePolicy();
|
||||
const id1 = db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
const id2 = db.budget.upsertBudgetIncident(policy, "warn_threshold", 9200);
|
||||
const id1 = db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
const id2 = db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 9200);
|
||||
expect(id1).toBe(id2); // Same incident, not a new one
|
||||
expect(db.incidents).toHaveLength(1); // Still just one
|
||||
expect(db.incidents[0]!.amountObserved).toBe(9200); // Updated
|
||||
@@ -185,8 +185,8 @@ describe("upsertBudgetIncident", () => {
|
||||
|
||||
it("creates separate incidents for different threshold types", () => {
|
||||
const policy = makePolicy();
|
||||
db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
expect(db.incidents).toHaveLength(2);
|
||||
expect(db.incidents[0]!.thresholdType).toBe("warn_threshold");
|
||||
expect(db.incidents[1]!.thresholdType).toBe("hard_stop");
|
||||
@@ -202,10 +202,10 @@ describe("resolveIncidentsForPolicy", () => {
|
||||
|
||||
it("sets status to provided resolution reason for all open incidents", () => {
|
||||
const policy = makePolicy();
|
||||
db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
|
||||
const count = db.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase");
|
||||
const count = db.core.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase");
|
||||
expect(count).toBe(2);
|
||||
expect(db.incidents[0]!.status).toBe("auto_resolved_limit_increase");
|
||||
expect(db.incidents[1]!.status).toBe("auto_resolved_limit_increase");
|
||||
@@ -214,10 +214,10 @@ describe("resolveIncidentsForPolicy", () => {
|
||||
|
||||
it("does not affect already-resolved incidents", () => {
|
||||
const policy = makePolicy();
|
||||
db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.budget.resolveIncidentsForPolicy("pol-1", "resolved");
|
||||
db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.core.budget.resolveIncidentsForPolicy("pol-1", "resolved");
|
||||
// Now try to resolve again
|
||||
const count = db.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase");
|
||||
const count = db.core.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase");
|
||||
expect(count).toBe(0);
|
||||
expect(db.incidents[0]!.status).toBe("resolved"); // Unchanged
|
||||
});
|
||||
@@ -252,7 +252,7 @@ describe("autoCloseExpiredIncidents", () => {
|
||||
updatedAt: "2025-01-15T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const count = db.budget.autoCloseExpiredIncidents("c1");
|
||||
const count = db.core.budget.autoCloseExpiredIncidents("c1");
|
||||
expect(count).toBe(1);
|
||||
expect(db.incidents[0]!.status).toBe("auto_closed");
|
||||
expect(db.incidents[0]!.resolvedAt).toBeTruthy();
|
||||
@@ -260,10 +260,10 @@ describe("autoCloseExpiredIncidents", () => {
|
||||
|
||||
it("does not close incidents whose window_end is in the future", () => {
|
||||
const policy = makePolicy();
|
||||
db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
// The auto-created incident has window_end in the future (end of current month)
|
||||
|
||||
const count = db.budget.autoCloseExpiredIncidents("c1");
|
||||
const count = db.core.budget.autoCloseExpiredIncidents("c1");
|
||||
expect(count).toBe(0);
|
||||
expect(db.incidents[0]!.status).toBe("open");
|
||||
});
|
||||
@@ -278,22 +278,22 @@ describe("listBudgetIncidents", () => {
|
||||
|
||||
it("returns all incidents for a company when status is 'all'", () => {
|
||||
const policy = makePolicy();
|
||||
db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
db.budget.resolveIncidentsForPolicy("pol-1", "resolved");
|
||||
db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
db.core.budget.resolveIncidentsForPolicy("pol-1", "resolved");
|
||||
|
||||
const all = db.budget.listBudgetIncidents("c1", "all");
|
||||
const all = db.core.budget.listBudgetIncidents("c1", "all");
|
||||
expect(all).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("filters by status when specified", () => {
|
||||
const policy = makePolicy();
|
||||
db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
// Resolve only warn
|
||||
db.budget.resolveWarnIncidentsForPolicy("pol-1", "resolved");
|
||||
db.core.budget.resolveWarnIncidentsForPolicy("pol-1", "resolved");
|
||||
|
||||
const open = db.budget.listBudgetIncidents("c1", "open");
|
||||
const open = db.core.budget.listBudgetIncidents("c1", "open");
|
||||
expect(open).toHaveLength(1);
|
||||
expect(open[0]!.thresholdType).toBe("hard_stop");
|
||||
});
|
||||
@@ -308,7 +308,7 @@ describe("checkAutoResolve (D-06)", () => {
|
||||
|
||||
it("auto-resolves incidents when policy limit increases above observed amount", () => {
|
||||
const policy = makePolicy({ amount: 10000 });
|
||||
db.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
expect(db.incidents[0]!.status).toBe("open");
|
||||
|
||||
// Simulate limit increase: now checking if spent < new limit
|
||||
@@ -316,7 +316,7 @@ describe("checkAutoResolve (D-06)", () => {
|
||||
const spent = 10500;
|
||||
const newAmount = 20000;
|
||||
if (spent < newAmount) {
|
||||
db.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase");
|
||||
db.core.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase");
|
||||
}
|
||||
|
||||
expect(db.incidents[0]!.status).toBe("auto_resolved_limit_increase");
|
||||
@@ -324,14 +324,14 @@ describe("checkAutoResolve (D-06)", () => {
|
||||
|
||||
it("auto-resolves only warn incidents when below warn threshold but above hard stop", () => {
|
||||
const policy = makePolicy({ amount: 10000, warnPercent: 80 });
|
||||
db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "warn_threshold", 8500);
|
||||
db.core.budget.upsertBudgetIncident(policy, "hard_stop", 10500);
|
||||
|
||||
// Spent is now 7500 (below warn threshold of 8000 = 10000*80/100)
|
||||
const spent = 7500;
|
||||
const _warnAmount = Math.floor((policy.amount * policy.warnPercent) / 100);
|
||||
if (spent < policy.amount) {
|
||||
db.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase");
|
||||
db.core.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase");
|
||||
}
|
||||
// Both resolved since spent < policy.amount
|
||||
expect(db.incidents.every((i) => i.status === "auto_resolved_limit_increase")).toBe(true);
|
||||
|
||||
@@ -19,19 +19,19 @@ import {
|
||||
} from "../../src/main/agent-runtime/hook-handlers";
|
||||
|
||||
function seedCofounderRun(db: AppDatabase) {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Gate Co",
|
||||
description: "Cofounder runtime gate test",
|
||||
status: "active",
|
||||
executionMode: "ai_cofounder",
|
||||
});
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "openai-codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
notes: "fixture",
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Researcher",
|
||||
role: "Research Lead",
|
||||
@@ -45,7 +45,7 @@ function seedCofounderRun(db: AppDatabase) {
|
||||
capabilities: "Research",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Draft market plan",
|
||||
description: "Prepare the market plan after approval.",
|
||||
@@ -87,7 +87,7 @@ describe("cofounder approval runtime gate", () => {
|
||||
|
||||
it("blocks destructive tools while a cofounder plan review is pending", () => {
|
||||
const { companyId, agentId, taskId, runContext } = seedCofounderRun(db);
|
||||
const approvalId = db.approvalsMod.requestApproval({
|
||||
const approvalId = db.core.approvals.requestApproval({
|
||||
companyId,
|
||||
relatedTaskId: taskId,
|
||||
requestedByAgentId: agentId,
|
||||
@@ -97,7 +97,7 @@ describe("cofounder approval runtime gate", () => {
|
||||
payloadJson: "{}",
|
||||
});
|
||||
|
||||
const blocking = db.approvalsMod.getBlockingApproval(companyId, agentId, taskId);
|
||||
const blocking = db.core.approvals.getBlockingApproval(companyId, agentId, taskId);
|
||||
expect(blocking?.id).toBe(approvalId);
|
||||
|
||||
const result = handleBeforeToolCall(
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("company mode switching", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -40,7 +40,7 @@ describe("company mode switching", () => {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Mode Switch Co",
|
||||
description: "Execution mode cleanup",
|
||||
status: "active",
|
||||
@@ -48,7 +48,7 @@ describe("company mode switching", () => {
|
||||
autoApproveHires: false,
|
||||
reviewDeliverables: true,
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -56,7 +56,7 @@ describe("company mode switching", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -69,7 +69,7 @@ describe("company mode switching", () => {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const engineerId = db.agents.saveAgent({
|
||||
const engineerId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Engineer",
|
||||
role: "Engineer",
|
||||
@@ -83,7 +83,7 @@ describe("company mode switching", () => {
|
||||
budgetMonthlyUsd: 180,
|
||||
department: "engineering",
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Ship feature",
|
||||
description: "Waits for cofounder review.",
|
||||
@@ -100,8 +100,8 @@ describe("company mode switching", () => {
|
||||
|
||||
threadIntegration.onTaskCreated(taskId, companyId);
|
||||
threadIntegration.onRunFinished("run-1", taskId, engineerId, companyId, "succeeded", null);
|
||||
db.tasks.setTaskRequiresUserReview(taskId, true);
|
||||
const approvalId = db.approvalsMod.requestApproval({
|
||||
db.core.tasks.setTaskRequiresUserReview(taskId, true);
|
||||
const approvalId = db.core.approvals.requestApproval({
|
||||
companyId,
|
||||
relatedTaskId: taskId,
|
||||
requestedByAgentId: engineerId,
|
||||
@@ -112,9 +112,9 @@ describe("company mode switching", () => {
|
||||
});
|
||||
threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "deliverable_review", "Review deliverable");
|
||||
|
||||
expect(db.threads.listThreadWaitStates(companyId).length).toBeGreaterThan(0);
|
||||
expect(db.entityCrud.getTask(taskId).requiresUserReview).toBe(true);
|
||||
expect(db.approvalsMod.getApprovalRecord(approvalId).state).toBe("pending");
|
||||
expect(db.core.threads.listThreadWaitStates(companyId).length).toBeGreaterThan(0);
|
||||
expect(db.core.entity.getTask(taskId).requiresUserReview).toBe(true);
|
||||
expect(db.core.approvals.getApprovalRecord(approvalId).state).toBe("pending");
|
||||
|
||||
const handlers = new Map<string, IpcHandler>();
|
||||
registerCoreAdminHandlers({
|
||||
@@ -122,7 +122,7 @@ describe("company mode switching", () => {
|
||||
handlers.set(channel, handler);
|
||||
},
|
||||
db,
|
||||
ensureCompanyExists: (id) => (db.settings.hasCompany(id) ? null : { ok: false, error: { code: "COMPANY_NOT_FOUND", message: "missing" } }),
|
||||
ensureCompanyExists: (id) => (db.core.settings.hasCompany(id) ? null : { ok: false, error: { code: "COMPANY_NOT_FOUND", message: "missing" } }),
|
||||
publishDomainChanged: vi.fn(),
|
||||
handleApprovalResolved: (resolvedApprovalId, resolvedCompanyId, decision) => {
|
||||
threadIntegration.onApprovalResolved(resolvedApprovalId, resolvedCompanyId, decision);
|
||||
@@ -147,10 +147,10 @@ describe("company mode switching", () => {
|
||||
}) as { ok: boolean };
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(db.company.getCompanyExecutionMode(companyId)).toBe("ai_ceo");
|
||||
expect(db.threads.listThreadWaitStates(companyId)).toHaveLength(0);
|
||||
expect(db.entityCrud.getTask(taskId).requiresUserReview).toBe(false);
|
||||
expect(db.approvalsMod.getApprovalRecord(approvalId).state).toBe("approved");
|
||||
expect(db.core.company.getCompanyExecutionMode(companyId)).toBe("ai_ceo");
|
||||
expect(db.core.threads.listThreadWaitStates(companyId)).toHaveLength(0);
|
||||
expect(db.core.entity.getTask(taskId).requiresUserReview).toBe(false);
|
||||
expect(db.core.approvals.getApprovalRecord(approvalId).state).toBe("approved");
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
|
||||
const FLAT_MODULE_KEYS = [
|
||||
"threads",
|
||||
"company",
|
||||
"agents",
|
||||
"tasks",
|
||||
"messages",
|
||||
"runs",
|
||||
"snap",
|
||||
"settings",
|
||||
"connectorsCrud",
|
||||
"workspacesMod",
|
||||
"approvalsMod",
|
||||
"social",
|
||||
"activityMod",
|
||||
"skills",
|
||||
"budget",
|
||||
"entityCrud",
|
||||
"documentsMod",
|
||||
"automationMod",
|
||||
"knowledgeMod",
|
||||
"wakeup",
|
||||
"backupMod",
|
||||
"sessionEntries",
|
||||
"inboxProjections",
|
||||
"documentLifecycleEvents",
|
||||
"credentialsTable",
|
||||
"modelCapabilities",
|
||||
];
|
||||
|
||||
describe("AppDatabase module groups", () => {
|
||||
it("exposes domain modules through grouped namespaces", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "agentcompany-db-groups-"));
|
||||
const db = new AppDatabase(dir);
|
||||
try {
|
||||
expect(Object.keys(db.core).sort()).toEqual([
|
||||
"activity",
|
||||
"agents",
|
||||
"approvals",
|
||||
"backup",
|
||||
"budget",
|
||||
"company",
|
||||
"connectors",
|
||||
"credentials",
|
||||
"documents",
|
||||
"entity",
|
||||
"knowledge",
|
||||
"messages",
|
||||
"modelCapabilities",
|
||||
"runs",
|
||||
"settings",
|
||||
"skills",
|
||||
"social",
|
||||
"tasks",
|
||||
"threads",
|
||||
"workspaces",
|
||||
]);
|
||||
expect(Object.keys(db.automation).sort()).toEqual(["rules", "wakeup"]);
|
||||
expect(Object.keys(db.projection).sort()).toEqual([
|
||||
"documentLifecycleEvents",
|
||||
"inbox",
|
||||
"sessionEntries",
|
||||
"snapshot",
|
||||
]);
|
||||
for (const key of FLAT_MODULE_KEYS) {
|
||||
expect(Object.prototype.hasOwnProperty.call(db, key)).toBe(false);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
+94
-94
@@ -49,12 +49,12 @@ describe("AppDatabase", () => {
|
||||
});
|
||||
|
||||
it("persists company-scoped entities and resolves secret env bindings", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Northline Systems",
|
||||
description: "Operator company",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary Workspace",
|
||||
localPath: "/tmp/northline",
|
||||
@@ -62,7 +62,7 @@ describe("AppDatabase", () => {
|
||||
repoRef: "main",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Chief Operator",
|
||||
role: "CEO",
|
||||
@@ -75,14 +75,14 @@ describe("AppDatabase", () => {
|
||||
capabilities: "Directs execution",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const goalId = db.entityCrud.saveGoal({
|
||||
const goalId = db.core.entity.saveGoal({
|
||||
companyId,
|
||||
title: "Ship the desktop product",
|
||||
description: "Stabilize the local operator console.",
|
||||
ownerAgentId: agentId,
|
||||
status: "active",
|
||||
});
|
||||
const projectId = db.entityCrud.saveProject({
|
||||
const projectId = db.core.entity.saveProject({
|
||||
companyId,
|
||||
goalId,
|
||||
name: "Desktop Rebuild",
|
||||
@@ -91,7 +91,7 @@ describe("AppDatabase", () => {
|
||||
status: "in_progress",
|
||||
targetDate: null,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
projectId,
|
||||
goalId,
|
||||
@@ -103,20 +103,20 @@ describe("AppDatabase", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
db.entityCrud.saveSecret({
|
||||
db.core.entity.saveSecret({
|
||||
companyId,
|
||||
name: "vault-openai",
|
||||
description: "OpenAI API token",
|
||||
value: "sk-test-secret",
|
||||
});
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "openai-codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "OPENAI_API_KEY=vault-openai",
|
||||
notes: "Primary coding connector",
|
||||
});
|
||||
|
||||
const snapshot = db.snap.listSnapshot();
|
||||
const snapshot = db.projection.snapshot.listSnapshot();
|
||||
|
||||
expect(snapshot.currentCompanyId).toBe(companyId);
|
||||
expect(snapshot.workspaces[0]?.id).toBe(workspaceId);
|
||||
@@ -124,71 +124,71 @@ describe("AppDatabase", () => {
|
||||
expect(snapshot.goals[0]?.id).toBe(goalId);
|
||||
expect(snapshot.projects[0]?.id).toBe(projectId);
|
||||
expect(snapshot.tasks[0]?.id).toBe(taskId);
|
||||
expect(db.entityCrud.resolveSecretEnv("OPENAI_API_KEY=vault-openai", companyId)).toEqual({
|
||||
expect(db.core.entity.resolveSecretEnv("OPENAI_API_KEY=vault-openai", companyId)).toEqual({
|
||||
OPENAI_API_KEY: "sk-test-secret",
|
||||
});
|
||||
});
|
||||
|
||||
it("backs up and restores the profile", async () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Backup Test Co",
|
||||
description: "Backup validation company",
|
||||
status: "active",
|
||||
});
|
||||
const backupDir = await mkdtemp(join(tmpdir(), "agentcompany-backup-"));
|
||||
|
||||
await db.backupMod.exportBackup(backupDir);
|
||||
db.company.saveCompany({
|
||||
await db.core.backup.exportBackup(backupDir);
|
||||
db.core.company.saveCompany({
|
||||
id: companyId,
|
||||
name: "Mutated Company",
|
||||
description: "Changed after backup",
|
||||
status: "paused",
|
||||
});
|
||||
|
||||
await db.backupMod.restoreFromBackup(backupDir);
|
||||
await db.core.backup.restoreFromBackup(backupDir);
|
||||
|
||||
const snapshot = db.snap.listSnapshot();
|
||||
const snapshot = db.projection.snapshot.listSnapshot();
|
||||
expect(snapshot.companies[0]?.name).toBe("Backup Test Co");
|
||||
|
||||
await rm(backupDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("scopes secret resolution to the owning company", () => {
|
||||
const alphaCompanyId = db.company.saveCompany({
|
||||
const alphaCompanyId = db.core.company.saveCompany({
|
||||
name: "Alpha Company",
|
||||
description: "Alpha",
|
||||
status: "active",
|
||||
});
|
||||
const betaCompanyId = db.company.saveCompany({
|
||||
const betaCompanyId = db.core.company.saveCompany({
|
||||
name: "Beta Company",
|
||||
description: "Beta",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
db.entityCrud.saveSecret({
|
||||
db.core.entity.saveSecret({
|
||||
companyId: alphaCompanyId,
|
||||
name: "shared-secret-name",
|
||||
description: "Alpha secret",
|
||||
value: "alpha-secret",
|
||||
});
|
||||
|
||||
expect(db.entityCrud.resolveSecretEnv("API_KEY=shared-secret-name", alphaCompanyId)).toEqual({
|
||||
expect(db.core.entity.resolveSecretEnv("API_KEY=shared-secret-name", alphaCompanyId)).toEqual({
|
||||
API_KEY: "alpha-secret",
|
||||
});
|
||||
// Beta company has no secret named "shared-secret-name", so resolveSecretEnv
|
||||
// falls back to using the raw value literally (allows direct env bindings).
|
||||
expect(db.entityCrud.resolveSecretEnv("API_KEY=shared-secret-name", betaCompanyId)).toEqual({
|
||||
expect(db.core.entity.resolveSecretEnv("API_KEY=shared-secret-name", betaCompanyId)).toEqual({
|
||||
API_KEY: "shared-secret-name",
|
||||
});
|
||||
});
|
||||
|
||||
it("atomically checks out unclaimed work via CAS with run ID", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Checkout Company",
|
||||
description: "Atomic checkout validation",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Execution Workspace",
|
||||
localPath: "/tmp/checkout",
|
||||
@@ -196,7 +196,7 @@ describe("AppDatabase", () => {
|
||||
repoRef: "main",
|
||||
isPrimary: true,
|
||||
});
|
||||
const leadId = db.agents.saveAgent({
|
||||
const leadId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Lead",
|
||||
role: "CEO",
|
||||
@@ -209,13 +209,13 @@ describe("AppDatabase", () => {
|
||||
capabilities: "Leads",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "openai-codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
notes: "Test connector",
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Claim me",
|
||||
description: "First agent wins the checkout.",
|
||||
@@ -225,14 +225,14 @@ describe("AppDatabase", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const { id: runId1 } = db.runs.createRun({
|
||||
const { id: runId1 } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: leadId,
|
||||
workspaceId,
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
const { id: runId2 } = db.runs.createRun({
|
||||
const { id: runId2 } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: leadId,
|
||||
@@ -240,20 +240,20 @@ describe("AppDatabase", () => {
|
||||
connectorId: "openai-codex",
|
||||
});
|
||||
|
||||
expect(db.tasks.checkoutTask(taskId, runId1)).toBe(true);
|
||||
expect(db.tasks.checkoutTask(taskId, runId2)).toBe(false);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runId1)).toBe(true);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runId2)).toBe(false);
|
||||
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.status).toBe("in_progress");
|
||||
});
|
||||
|
||||
it("auto-pauses an agent when a run exhausts the monthly budget", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Budget Company",
|
||||
description: "Budget hard stop validation",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Budget Workspace",
|
||||
localPath: "/tmp/budget",
|
||||
@@ -261,7 +261,7 @@ describe("AppDatabase", () => {
|
||||
repoRef: "main",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Budget Owner",
|
||||
role: "CTO",
|
||||
@@ -274,7 +274,7 @@ describe("AppDatabase", () => {
|
||||
capabilities: "Ships work",
|
||||
budgetMonthlyUsd: 10,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Spend budget",
|
||||
description: "One run should exhaust the allowance.",
|
||||
@@ -284,7 +284,7 @@ describe("AppDatabase", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
@@ -293,7 +293,7 @@ describe("AppDatabase", () => {
|
||||
});
|
||||
|
||||
// Simulate a per-message cost entry (written by CostWriter in prod).
|
||||
db.runs.recordCostEntry({
|
||||
db.core.runs.recordCostEntry({
|
||||
runId: run.id,
|
||||
companyId,
|
||||
agentId,
|
||||
@@ -303,7 +303,7 @@ describe("AppDatabase", () => {
|
||||
unattributed: false,
|
||||
});
|
||||
|
||||
const outcome = db.runs.finishRun({
|
||||
const outcome = db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Budget consumed",
|
||||
@@ -315,17 +315,17 @@ describe("AppDatabase", () => {
|
||||
});
|
||||
|
||||
expect(outcome.budgetHardStopped).toBe(true);
|
||||
expect(db.entityCrud.getAgent(agentId).status).toBe("paused");
|
||||
expect(db.entityCrud.getAgent(agentId).spentMonthlyUsd).toBe(12);
|
||||
expect(db.core.entity.getAgent(agentId).status).toBe("paused");
|
||||
expect(db.core.entity.getAgent(agentId).spentMonthlyUsd).toBe(12);
|
||||
});
|
||||
|
||||
it("persists explicit run usage status", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Usage Status Company",
|
||||
description: "Tracks run usage status",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Usage Workspace",
|
||||
localPath: "/tmp/usage-status",
|
||||
@@ -333,7 +333,7 @@ describe("AppDatabase", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Usage Agent",
|
||||
role: "Engineer",
|
||||
@@ -346,7 +346,7 @@ describe("AppDatabase", () => {
|
||||
capabilities: "Run tasks",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Usage task",
|
||||
description: "Exercise explicit usage status.",
|
||||
@@ -355,9 +355,9 @@ describe("AppDatabase", () => {
|
||||
priority: "medium",
|
||||
status: "in_progress",
|
||||
});
|
||||
const run = db.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "openai-codex" });
|
||||
const run = db.core.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "openai-codex" });
|
||||
|
||||
db.runs.finishRun({
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "failed",
|
||||
summary: null,
|
||||
@@ -369,17 +369,17 @@ describe("AppDatabase", () => {
|
||||
usageStatus: "error_stop",
|
||||
});
|
||||
|
||||
expect(db.runs.getRun(run.id).usageStatus).toBe("error_stop");
|
||||
expect(db.snap.listSnapshot().runs.find((record) => record.id === run.id)?.usageStatus).toBe("error_stop");
|
||||
expect(db.core.runs.getRun(run.id).usageStatus).toBe("error_stop");
|
||||
expect(db.projection.snapshot.listSnapshot().runs.find((record) => record.id === run.id)?.usageStatus).toBe("error_stop");
|
||||
});
|
||||
|
||||
it("activates pending hires when the corresponding approval is granted", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Hiring Company",
|
||||
description: "Approval-driven hiring",
|
||||
status: "active",
|
||||
});
|
||||
const managerId = db.agents.saveAgent({
|
||||
const managerId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Manager",
|
||||
role: "CEO",
|
||||
@@ -392,7 +392,7 @@ describe("AppDatabase", () => {
|
||||
capabilities: "Manages",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const pendingHireId = db.agents.saveAgent({
|
||||
const pendingHireId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Pending Hire",
|
||||
role: "IC",
|
||||
@@ -406,7 +406,7 @@ describe("AppDatabase", () => {
|
||||
budgetMonthlyUsd: 50,
|
||||
});
|
||||
|
||||
const approvalId = db.approvalsMod.requestApproval({
|
||||
const approvalId = db.core.approvals.requestApproval({
|
||||
companyId,
|
||||
relatedTaskId: null,
|
||||
requestedByAgentId: managerId,
|
||||
@@ -416,22 +416,22 @@ describe("AppDatabase", () => {
|
||||
payloadJson: JSON.stringify({ agentId: pendingHireId }),
|
||||
});
|
||||
|
||||
db.approvalsMod.decideApproval({
|
||||
db.core.approvals.decideApproval({
|
||||
approvalId,
|
||||
state: "approved",
|
||||
decisionNote: "Approved",
|
||||
});
|
||||
|
||||
expect(db.entityCrud.getAgent(pendingHireId).status).toBe("idle");
|
||||
expect(db.core.entity.getAgent(pendingHireId).status).toBe("idle");
|
||||
});
|
||||
|
||||
it("detects pending-approval agents via isAgentPendingApproval", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Gate Test Co",
|
||||
description: "Approval gate validation",
|
||||
status: "active",
|
||||
});
|
||||
const activeAgentId = db.agents.saveAgent({
|
||||
const activeAgentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Active Agent",
|
||||
role: "CTO",
|
||||
@@ -444,7 +444,7 @@ describe("AppDatabase", () => {
|
||||
capabilities: "Ships",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const pendingAgentId = db.agents.saveAgent({
|
||||
const pendingAgentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Pending Agent",
|
||||
role: "IC",
|
||||
@@ -458,13 +458,13 @@ describe("AppDatabase", () => {
|
||||
budgetMonthlyUsd: 50,
|
||||
});
|
||||
|
||||
expect(db.agents.isAgentPendingApproval(activeAgentId)).toBe(false);
|
||||
expect(db.agents.isAgentPendingApproval(pendingAgentId)).toBe(true);
|
||||
expect(db.core.agents.isAgentPendingApproval(activeAgentId)).toBe(false);
|
||||
expect(db.core.agents.isAgentPendingApproval(pendingAgentId)).toBe(true);
|
||||
});
|
||||
|
||||
it("runs forward-only migrations and creates schema_migrations table", () => {
|
||||
// The migration system runs during init() — verify the tracking table exists
|
||||
const snapshot = db.snap.listSnapshot();
|
||||
const snapshot = db.projection.snapshot.listSnapshot();
|
||||
expect(snapshot).toBeTruthy();
|
||||
|
||||
// Verify we can re-init without errors (migrations are idempotent)
|
||||
@@ -472,17 +472,17 @@ describe("AppDatabase", () => {
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
|
||||
const snapshot2 = db.snap.listSnapshot();
|
||||
const snapshot2 = db.projection.snapshot.listSnapshot();
|
||||
expect(snapshot2).toBeTruthy();
|
||||
});
|
||||
|
||||
it("acquires and releases workspace locks", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Lock Test Co",
|
||||
description: "Workspace lock test",
|
||||
status: "active",
|
||||
});
|
||||
const wsId = db.workspacesMod.saveWorkspace({
|
||||
const wsId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Lockable WS",
|
||||
localPath: "/tmp/lock-test",
|
||||
@@ -492,33 +492,33 @@ describe("AppDatabase", () => {
|
||||
});
|
||||
|
||||
// Acquire lock for run-1
|
||||
expect(db.workspacesMod.acquireWorkspaceLock(wsId, "run-1")).toBe(true);
|
||||
let ws = db.workspacesMod.getWorkspace(wsId);
|
||||
expect(db.core.workspaces.acquireWorkspaceLock(wsId, "run-1")).toBe(true);
|
||||
let ws = db.core.workspaces.getWorkspace(wsId);
|
||||
expect(ws?.state).toBe("busy");
|
||||
|
||||
// Same run can re-acquire (idempotent)
|
||||
expect(db.workspacesMod.acquireWorkspaceLock(wsId, "run-1")).toBe(true);
|
||||
expect(db.core.workspaces.acquireWorkspaceLock(wsId, "run-1")).toBe(true);
|
||||
|
||||
// Different run cannot acquire while run-1 holds the lock
|
||||
expect(db.workspacesMod.acquireWorkspaceLock(wsId, "run-2")).toBe(false);
|
||||
expect(db.core.workspaces.acquireWorkspaceLock(wsId, "run-2")).toBe(false);
|
||||
|
||||
// Release lock
|
||||
db.workspacesMod.releaseWorkspaceLock(wsId, "run-1");
|
||||
ws = db.workspacesMod.getWorkspace(wsId);
|
||||
db.core.workspaces.releaseWorkspaceLock(wsId, "run-1");
|
||||
ws = db.core.workspaces.getWorkspace(wsId);
|
||||
expect(ws?.state).toBe("ready");
|
||||
|
||||
// Now run-2 can acquire
|
||||
expect(db.workspacesMod.acquireWorkspaceLock(wsId, "run-2")).toBe(true);
|
||||
db.workspacesMod.releaseWorkspaceLock(wsId, "run-2");
|
||||
expect(db.core.workspaces.acquireWorkspaceLock(wsId, "run-2")).toBe(true);
|
||||
db.core.workspaces.releaseWorkspaceLock(wsId, "run-2");
|
||||
});
|
||||
|
||||
it("releases all workspace locks on crash recovery", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Recovery Co",
|
||||
description: "Lock recovery test",
|
||||
status: "active",
|
||||
});
|
||||
const wsId = db.workspacesMod.saveWorkspace({
|
||||
const wsId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Recovery WS",
|
||||
localPath: "/tmp/recovery-test",
|
||||
@@ -527,20 +527,20 @@ describe("AppDatabase", () => {
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
db.workspacesMod.acquireWorkspaceLock(wsId, "stale-run");
|
||||
expect(db.workspacesMod.getWorkspace(wsId)?.state).toBe("busy");
|
||||
db.core.workspaces.acquireWorkspaceLock(wsId, "stale-run");
|
||||
expect(db.core.workspaces.getWorkspace(wsId)?.state).toBe("busy");
|
||||
|
||||
db.workspacesMod.releaseAllWorkspaceLocks();
|
||||
expect(db.workspacesMod.getWorkspace(wsId)?.state).toBe("ready");
|
||||
db.core.workspaces.releaseAllWorkspaceLocks();
|
||||
expect(db.core.workspaces.getWorkspace(wsId)?.state).toBe("ready");
|
||||
});
|
||||
|
||||
it("purges expired run logs based on retention setting", async () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Purge Co",
|
||||
description: "Log purge test",
|
||||
status: "active",
|
||||
});
|
||||
const wsId = db.workspacesMod.saveWorkspace({
|
||||
const wsId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "PurgeWS",
|
||||
localPath: "/tmp/purge",
|
||||
@@ -548,7 +548,7 @@ describe("AppDatabase", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Purge Agent",
|
||||
role: "IC",
|
||||
@@ -561,7 +561,7 @@ describe("AppDatabase", () => {
|
||||
capabilities: "test",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Purge task",
|
||||
description: "test",
|
||||
@@ -571,11 +571,11 @@ describe("AppDatabase", () => {
|
||||
status: "todo",
|
||||
});
|
||||
|
||||
const run = db.runs.createRun({ companyId, taskId, agentId, workspaceId: wsId, connectorId: "openai-codex" });
|
||||
await db.runs.appendRunLog(run.id, "stdout", "test log line\n");
|
||||
const run = db.core.runs.createRun({ companyId, taskId, agentId, workspaceId: wsId, connectorId: "openai-codex" });
|
||||
await db.core.runs.appendRunLog(run.id, "stdout", "test log line\n");
|
||||
|
||||
// Finish the run with a very old timestamp
|
||||
db.runs.finishRun({
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "done",
|
||||
@@ -587,10 +587,10 @@ describe("AppDatabase", () => {
|
||||
});
|
||||
|
||||
// Log should exist before purge
|
||||
expect(db.runs.readRunLog(run.id)).toContain("test log line");
|
||||
expect(db.core.runs.readRunLog(run.id)).toContain("test log line");
|
||||
|
||||
// Set retention to 1 day and manually backdate the run's finished_at
|
||||
db.settings.updateSettings({ logRetentionDays: 1 });
|
||||
db.core.settings.updateSettings({ logRetentionDays: 1 });
|
||||
// Backdate finished_at to 10 days ago
|
||||
const tenDaysAgo = new Date(Date.now() - 10 * 86_400_000).toISOString();
|
||||
// Direct SQL update for test only
|
||||
@@ -598,20 +598,20 @@ describe("AppDatabase", () => {
|
||||
.prepare("update runs set finished_at = ? where id = ?")
|
||||
.run(tenDaysAgo, run.id);
|
||||
|
||||
const purged = db.settings.purgeExpiredRunLogs();
|
||||
const purged = db.core.settings.purgeExpiredRunLogs();
|
||||
expect(purged).toBe(1);
|
||||
|
||||
// Log should be gone after purge
|
||||
expect(db.runs.readRunLog(run.id)).toBe("");
|
||||
expect(db.core.runs.readRunLog(run.id)).toBe("");
|
||||
});
|
||||
|
||||
it("populates related_agent_id on hire approvals", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Agent Approval Co",
|
||||
description: "test",
|
||||
status: "active",
|
||||
});
|
||||
const managerId = db.agents.saveAgent({
|
||||
const managerId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Manager",
|
||||
role: "CEO",
|
||||
@@ -624,7 +624,7 @@ describe("AppDatabase", () => {
|
||||
capabilities: "manage",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const hireId = db.agents.saveAgent({
|
||||
const hireId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "New Hire",
|
||||
role: "IC",
|
||||
@@ -638,7 +638,7 @@ describe("AppDatabase", () => {
|
||||
budgetMonthlyUsd: 50,
|
||||
});
|
||||
|
||||
const approvalId = db.approvalsMod.requestApproval({
|
||||
const approvalId = db.core.approvals.requestApproval({
|
||||
companyId,
|
||||
requestedByAgentId: managerId,
|
||||
type: "hire_agent",
|
||||
@@ -647,12 +647,12 @@ describe("AppDatabase", () => {
|
||||
payloadJson: JSON.stringify({ agentId: hireId }),
|
||||
});
|
||||
|
||||
const approval = db.approvalsMod.getApprovalRecord(approvalId);
|
||||
const approval = db.core.approvals.getApprovalRecord(approvalId);
|
||||
// related_agent_id should be populated from payloadJson extraction
|
||||
expect(approval.relatedAgentId).toBe(hireId);
|
||||
|
||||
// Explicit relatedAgentId param should also work
|
||||
const approvalId2 = db.approvalsMod.requestApproval({
|
||||
const approvalId2 = db.core.approvals.requestApproval({
|
||||
companyId,
|
||||
requestedByAgentId: managerId,
|
||||
relatedAgentId: hireId,
|
||||
@@ -661,7 +661,7 @@ describe("AppDatabase", () => {
|
||||
impactSummary: "test",
|
||||
});
|
||||
|
||||
const approval2 = db.approvalsMod.getApprovalRecord(approvalId2);
|
||||
const approval2 = db.core.approvals.getApprovalRecord(approvalId2);
|
||||
expect(approval2.relatedAgentId).toBe(hireId);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,18 +20,18 @@ import { createBudgetEnforcementService } from "../../src/main/budget-enforcemen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function scaffold(db: AppDatabase) {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Budget Edge Case Co",
|
||||
description: "Budget exhaustion mid-run edge case tests (D-05.2)",
|
||||
status: "active",
|
||||
});
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "gemini-cli",
|
||||
model: "gemini-pro",
|
||||
envBindingText: "",
|
||||
notes: "Test connector",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: "/tmp/budget-edge-test",
|
||||
@@ -39,7 +39,7 @@ function scaffold(db: AppDatabase) {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Worker Agent",
|
||||
role: "developer",
|
||||
@@ -52,14 +52,14 @@ function scaffold(db: AppDatabase) {
|
||||
capabilities: "Executes tasks",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const goalId = db.entityCrud.saveGoal({
|
||||
const goalId = db.core.entity.saveGoal({
|
||||
companyId,
|
||||
title: "Ship feature",
|
||||
description: "Build budget edge case test",
|
||||
ownerAgentId: agentId,
|
||||
status: "active",
|
||||
});
|
||||
const projectId = db.entityCrud.saveProject({
|
||||
const projectId = db.core.entity.saveProject({
|
||||
companyId,
|
||||
goalId,
|
||||
name: "Budget Project",
|
||||
@@ -68,7 +68,7 @@ function scaffold(db: AppDatabase) {
|
||||
status: "in_progress",
|
||||
targetDate: null,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
projectId,
|
||||
goalId,
|
||||
@@ -119,7 +119,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Create company budget policy: 500 cents ($5), hard stop enabled
|
||||
db.budget.saveBudgetPolicy({
|
||||
db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "company",
|
||||
scopeId: companyId,
|
||||
@@ -128,18 +128,18 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Start Run-1 (the in-flight run)
|
||||
const run1 = db.runs.createRun({
|
||||
const run1 = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, run1.id);
|
||||
db.runs.markRunStatus(run1.id, "running", "");
|
||||
db.core.tasks.checkoutTask(taskId, run1.id);
|
||||
db.core.runs.markRunStatus(run1.id, "running", "");
|
||||
|
||||
// Create a second task and run that exhausts budget WHILE Run-1 is in-flight
|
||||
const taskId2 = db.tasks.saveTask({
|
||||
const taskId2 = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Other Task",
|
||||
description: "Another task that uses up budget",
|
||||
@@ -148,18 +148,18 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
priority: "medium",
|
||||
status: "todo",
|
||||
});
|
||||
const run0 = db.runs.createRun({
|
||||
const run0 = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId: taskId2,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId2, run0.id);
|
||||
db.runs.markRunStatus(run0.id, "running", "");
|
||||
db.core.tasks.checkoutTask(taskId2, run0.id);
|
||||
db.core.runs.markRunStatus(run0.id, "running", "");
|
||||
// Complete Run-0 with $6 cost (exceeds $5 budget)
|
||||
db.runs.recordCostEntry({ runId: run0.id, companyId, agentId, taskId: taskId2, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.runs.recordCostEntry({ runId: run0.id, companyId, agentId, taskId: taskId2, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId: run0.id,
|
||||
status: "succeeded",
|
||||
summary: "Expensive run",
|
||||
@@ -172,8 +172,8 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
|
||||
// Now complete Run-1 (the in-flight run) -- it should succeed normally
|
||||
// Per D-04/D-05: in-flight runs are unaffected by budget exhaustion
|
||||
db.runs.recordCostEntry({ runId: run1.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 0.5, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.runs.recordCostEntry({ runId: run1.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 0.5, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId: run1.id,
|
||||
status: "succeeded",
|
||||
summary: "In-flight run completed",
|
||||
@@ -185,7 +185,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Verify Run-1 completed successfully
|
||||
const finishedRun = db.runs.getRun(run1.id);
|
||||
const finishedRun = db.core.runs.getRun(run1.id);
|
||||
expect(finishedRun.status).toBe("succeeded");
|
||||
|
||||
// Verify Run-1's cost was recorded (total spend now $6.50)
|
||||
@@ -205,7 +205,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Create company budget policy: 500 cents ($5)
|
||||
db.budget.saveBudgetPolicy({
|
||||
db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "company",
|
||||
scopeId: companyId,
|
||||
@@ -214,17 +214,17 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Complete a run that exceeds budget
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, run.id);
|
||||
db.runs.markRunStatus(run.id, "running", "");
|
||||
db.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId, run.id);
|
||||
db.core.runs.markRunStatus(run.id, "running", "");
|
||||
db.core.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Done",
|
||||
@@ -253,7 +253,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Create company budget policy with known ID
|
||||
const policyId = db.budget.saveBudgetPolicy({
|
||||
const policyId = db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "company",
|
||||
scopeId: companyId,
|
||||
@@ -262,17 +262,17 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Exhaust budget
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, run.id);
|
||||
db.runs.markRunStatus(run.id, "running", "");
|
||||
db.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId, run.id);
|
||||
db.core.runs.markRunStatus(run.id, "running", "");
|
||||
db.core.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Done",
|
||||
@@ -301,7 +301,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Company-level: plenty of room (10000 cents = $100)
|
||||
db.budget.saveBudgetPolicy({
|
||||
db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "company",
|
||||
scopeId: companyId,
|
||||
@@ -310,7 +310,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Agent-level: tight budget (100 cents = $1)
|
||||
db.budget.saveBudgetPolicy({
|
||||
db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "agent",
|
||||
scopeId: agentId,
|
||||
@@ -319,17 +319,17 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Complete a run costing $1.50 (150 cents > 100 agent limit)
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, run.id);
|
||||
db.runs.markRunStatus(run.id, "running", "");
|
||||
db.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 1.5, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId, run.id);
|
||||
db.core.runs.markRunStatus(run.id, "running", "");
|
||||
db.core.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 1.5, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Done",
|
||||
@@ -358,7 +358,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Create policy with 300 cents ($3) budget
|
||||
db.budget.saveBudgetPolicy({
|
||||
db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "company",
|
||||
scopeId: companyId,
|
||||
@@ -371,17 +371,17 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
expect(before.allowed).toBe(true);
|
||||
|
||||
// Complete a run costing $4 (400 cents > 300 limit)
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, run.id);
|
||||
db.runs.markRunStatus(run.id, "running", "");
|
||||
db.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 4.0, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.tasks.checkoutTask(taskId, run.id);
|
||||
db.core.runs.markRunStatus(run.id, "running", "");
|
||||
db.core.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 4.0, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Done",
|
||||
@@ -408,7 +408,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Create tight budget
|
||||
db.budget.saveBudgetPolicy({
|
||||
db.core.budget.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType: "company",
|
||||
scopeId: companyId,
|
||||
@@ -417,19 +417,19 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
|
||||
});
|
||||
|
||||
// Run that will exceed budget on completion
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
db.tasks.checkoutTask(taskId, run.id);
|
||||
db.runs.markRunStatus(run.id, "running", "");
|
||||
db.core.tasks.checkoutTask(taskId, run.id);
|
||||
db.core.runs.markRunStatus(run.id, "running", "");
|
||||
|
||||
// Complete with cost that exceeds budget -- cost entry is still recorded
|
||||
db.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 5.0, unattributed: false });
|
||||
db.runs.finishRun({
|
||||
db.core.runs.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 5.0, unattributed: false });
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Done",
|
||||
|
||||
@@ -28,7 +28,7 @@ type RawDb = {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createTestCompany(db: AppDatabase): string {
|
||||
return db.company.saveCompany({
|
||||
return db.core.company.saveCompany({
|
||||
name: "Edge Case Company",
|
||||
description: "Concurrent checkout edge case tests (D-05.1)",
|
||||
status: "active",
|
||||
@@ -36,7 +36,7 @@ function createTestCompany(db: AppDatabase): string {
|
||||
}
|
||||
|
||||
function createTestConnector(db: AppDatabase): void {
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "gemini-cli",
|
||||
model: "gemini-pro",
|
||||
envBindingText: "",
|
||||
@@ -45,7 +45,7 @@ function createTestConnector(db: AppDatabase): void {
|
||||
}
|
||||
|
||||
function createTestWorkspace(db: AppDatabase, companyId: string): string {
|
||||
return db.workspacesMod.saveWorkspace({
|
||||
return db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Test Workspace",
|
||||
localPath: "/tmp/concurrent-test",
|
||||
@@ -56,7 +56,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string {
|
||||
}
|
||||
|
||||
function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string {
|
||||
return db.agents.saveAgent({
|
||||
return db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name,
|
||||
role: "IC",
|
||||
@@ -72,7 +72,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string
|
||||
}
|
||||
|
||||
function createTestTask(db: AppDatabase, companyId: string, workspaceId: string): string {
|
||||
return db.tasks.saveTask({
|
||||
return db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Concurrent Test Task",
|
||||
description: "A task for testing concurrent checkout edge cases",
|
||||
@@ -110,14 +110,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
const agentB = createTestAgent(db, companyId, workspaceId, "Agent-B");
|
||||
const taskId = createTestTask(db, companyId, workspaceId);
|
||||
|
||||
const { id: runIdA } = db.runs.createRun({
|
||||
const { id: runIdA } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const { id: runIdB } = db.runs.createRun({
|
||||
const { id: runIdB } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentB,
|
||||
@@ -126,15 +126,15 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
});
|
||||
|
||||
// Agent-A claims first
|
||||
const resultA = db.tasks.checkoutTask(taskId, runIdA);
|
||||
const resultA = db.core.tasks.checkoutTask(taskId, runIdA);
|
||||
expect(resultA).toBe(true);
|
||||
|
||||
// Agent-B tries to claim the same task
|
||||
const resultB = db.tasks.checkoutTask(taskId, runIdB);
|
||||
const resultB = db.core.tasks.checkoutTask(taskId, runIdB);
|
||||
expect(resultB).toBe(false);
|
||||
|
||||
// Task status must be in_progress (set by first successful checkout)
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.status).toBe("in_progress");
|
||||
});
|
||||
|
||||
@@ -146,14 +146,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
const agentB = createTestAgent(db, companyId, workspaceId, "Agent-B");
|
||||
const taskId1 = createTestTask(db, companyId, workspaceId);
|
||||
|
||||
const { id: runIdA } = db.runs.createRun({
|
||||
const { id: runIdA } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId: taskId1,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const { id: runIdB1 } = db.runs.createRun({
|
||||
const { id: runIdB1 } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId: taskId1,
|
||||
agentId: agentB,
|
||||
@@ -162,24 +162,24 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
});
|
||||
|
||||
// Agent-A wins first task
|
||||
expect(db.tasks.checkoutTask(taskId1, runIdA)).toBe(true);
|
||||
expect(db.core.tasks.checkoutTask(taskId1, runIdA)).toBe(true);
|
||||
// Agent-B loses
|
||||
expect(db.tasks.checkoutTask(taskId1, runIdB1)).toBe(false);
|
||||
expect(db.core.tasks.checkoutTask(taskId1, runIdB1)).toBe(false);
|
||||
|
||||
// Agent-B can still checkout a DIFFERENT task
|
||||
const taskId2 = createTestTask(db, companyId, workspaceId);
|
||||
const { id: runIdB2 } = db.runs.createRun({
|
||||
const { id: runIdB2 } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId: taskId2,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const resultB2 = db.tasks.checkoutTask(taskId2, runIdB2);
|
||||
const resultB2 = db.core.tasks.checkoutTask(taskId2, runIdB2);
|
||||
expect(resultB2).toBe(true);
|
||||
|
||||
// Verify second task is now in_progress
|
||||
const task2 = db.entityCrud.getTask(taskId2);
|
||||
const task2 = db.core.entity.getTask(taskId2);
|
||||
expect(task2.status).toBe("in_progress");
|
||||
});
|
||||
|
||||
@@ -192,28 +192,28 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
const taskId = createTestTask(db, companyId, workspaceId);
|
||||
|
||||
// Agent-A checks out successfully
|
||||
const { id: runIdA } = db.runs.createRun({
|
||||
const { id: runIdA } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
expect(db.tasks.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
|
||||
// Task is now in_progress. A new run from Agent-B tries to checkout
|
||||
const { id: runIdB } = db.runs.createRun({
|
||||
const { id: runIdB } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const result = db.tasks.checkoutTask(taskId, runIdB);
|
||||
const result = db.core.tasks.checkoutTask(taskId, runIdB);
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Confirm status is still in_progress
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("in_progress");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("in_progress");
|
||||
});
|
||||
|
||||
it("after lock release, CAS guard still checks task status", () => {
|
||||
@@ -225,17 +225,17 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
const taskId = createTestTask(db, companyId, workspaceId);
|
||||
|
||||
// Agent-A checks out task
|
||||
const { id: runIdA } = db.runs.createRun({
|
||||
const { id: runIdA } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
expect(db.tasks.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
|
||||
// Release the lock (sets checkout_run_id = NULL)
|
||||
db.tasks.releaseTaskLock(taskId, runIdA);
|
||||
db.core.tasks.releaseTaskLock(taskId, runIdA);
|
||||
|
||||
// Verify lock fields are cleared
|
||||
const row = (db as unknown as RawDb).db
|
||||
@@ -246,14 +246,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
|
||||
// Task is still in_progress though, so a new checkout fails
|
||||
// (CAS checks status IN ('todo', 'backlog'))
|
||||
const { id: runIdB } = db.runs.createRun({
|
||||
const { id: runIdB } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const result = db.tasks.checkoutTask(taskId, runIdB);
|
||||
const result = db.core.tasks.checkoutTask(taskId, runIdB);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
@@ -264,14 +264,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
const agentA = createTestAgent(db, companyId, workspaceId, "Agent-A");
|
||||
const taskId = createTestTask(db, companyId, workspaceId);
|
||||
|
||||
const { id: runIdA } = db.runs.createRun({
|
||||
const { id: runIdA } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
expect(db.tasks.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
|
||||
// Backdate execution_locked_at to well past the stale timeout
|
||||
(db as unknown as RawDb).db
|
||||
@@ -300,14 +300,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
const taskId = createTestTask(db, companyId, workspaceId);
|
||||
|
||||
// Agent-A checks out, then lock goes stale
|
||||
const { id: runIdA } = db.runs.createRun({
|
||||
const { id: runIdA } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentA,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
expect(db.tasks.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
expect(db.core.tasks.checkoutTask(taskId, runIdA)).toBe(true);
|
||||
|
||||
// Backdate to make stale
|
||||
(db as unknown as RawDb).db
|
||||
@@ -319,19 +319,19 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
|
||||
expect(recovered.length).toBe(1);
|
||||
|
||||
// Verify task is back to "todo"
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("todo");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("todo");
|
||||
|
||||
// Agent-B can now checkout the recovered task
|
||||
const { id: runIdB } = db.runs.createRun({
|
||||
const { id: runIdB } = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId: agentB,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli",
|
||||
});
|
||||
const result = db.tasks.checkoutTask(taskId, runIdB);
|
||||
const result = db.core.tasks.checkoutTask(taskId, runIdB);
|
||||
expect(result).toBe(true);
|
||||
expect(db.entityCrud.getTask(taskId).status).toBe("in_progress");
|
||||
expect(db.core.entity.getTask(taskId).status).toBe("in_progress");
|
||||
});
|
||||
|
||||
it("STALE_LOCK_TIMEOUT_MS matches expected 15 minute value", () => {
|
||||
|
||||
@@ -21,21 +21,21 @@ import type { ConnectorId } from "@shared/types";
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Session Edge Case Co",
|
||||
description: "Session rotation edge case tests (D-05.3)",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
// Always save gemini-cli (needed for most tests)
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "gemini-cli",
|
||||
model: "gemini-pro",
|
||||
envBindingText: "",
|
||||
notes: "Test connector - no native context management",
|
||||
});
|
||||
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: "/tmp/session-edge-test",
|
||||
@@ -43,7 +43,7 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Worker Agent",
|
||||
role: "developer",
|
||||
@@ -56,14 +56,14 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") {
|
||||
capabilities: "Executes tasks",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const goalId = db.entityCrud.saveGoal({
|
||||
const goalId = db.core.entity.saveGoal({
|
||||
companyId,
|
||||
title: "Ship feature",
|
||||
description: "Build session rotation edge case test",
|
||||
ownerAgentId: agentId,
|
||||
status: "active",
|
||||
});
|
||||
const projectId = db.entityCrud.saveProject({
|
||||
const projectId = db.core.entity.saveProject({
|
||||
companyId,
|
||||
goalId,
|
||||
name: "Session Project",
|
||||
@@ -72,7 +72,7 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") {
|
||||
status: "in_progress",
|
||||
targetDate: null,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
projectId,
|
||||
goalId,
|
||||
@@ -114,7 +114,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
const sessionService = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
// Create a session
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
@@ -122,11 +122,11 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
|
||||
// Simulate runs until we reach the maxRuns threshold (20)
|
||||
for (let i = 0; i < 20; i++) {
|
||||
db.tasks.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`);
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`);
|
||||
}
|
||||
|
||||
// Get the updated session from DB
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session).not.toBeNull();
|
||||
expect(session!.runCount).toBe(20); // matches maxRuns threshold in session-management-service
|
||||
|
||||
@@ -140,16 +140,16 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
const { companyId, agentId, taskId } = scaffold(db);
|
||||
const sessionService = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
});
|
||||
|
||||
// Accumulate tokens past the 500_000 maxTokens threshold in one big call
|
||||
db.tasks.incrementSessionUsage(sessionId, 300000, 250000, "big-run");
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 300000, 250000, "big-run");
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session).not.toBeNull();
|
||||
const totalTokens = session!.totalInputTokens + session!.totalOutputTokens;
|
||||
expect(totalTokens).toBe(550000);
|
||||
@@ -163,7 +163,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
const { companyId, agentId, taskId, workspaceId } = scaffold(db);
|
||||
const sessionService = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
@@ -171,18 +171,18 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
|
||||
// Accumulate enough to trigger rotation
|
||||
for (let i = 0; i < 20; i++) {
|
||||
db.tasks.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`);
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`);
|
||||
}
|
||||
|
||||
// Create a completed run for history (so generateHandoff has run data)
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli" as ConnectorId,
|
||||
});
|
||||
db.runs.finishRun({
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Completed initial implementation",
|
||||
@@ -193,7 +193,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
unattributedCost: false,
|
||||
});
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId)!;
|
||||
const session = db.core.tasks.getSessionById(sessionId)!;
|
||||
// generateHandoff is agent-scoped: it walks the agent's runs across
|
||||
// all tasks + taskless heartbeats, not a single task.
|
||||
const handoff = sessionService.generateHandoff(session);
|
||||
@@ -203,7 +203,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
expect(handoff).toContain("## Recent Run History");
|
||||
|
||||
// Archive the old session with handoff
|
||||
db.tasks.archiveSession(sessionId, "max_runs", handoff);
|
||||
db.core.tasks.archiveSession(sessionId, "max_runs", handoff);
|
||||
|
||||
// Verify archived session has handoff stored
|
||||
const archivedRows = db.queryAll<{ handoffMarkdown: string | null }>(
|
||||
@@ -217,27 +217,27 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
const { companyId, agentId, taskId } = scaffold(db);
|
||||
|
||||
// Create initial session
|
||||
const oldSessionId = db.tasks.createNewSession({
|
||||
const oldSessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
});
|
||||
|
||||
// Add usage to the old session
|
||||
db.tasks.incrementSessionUsage(oldSessionId, 300000, 250000, "run-final");
|
||||
db.core.tasks.incrementSessionUsage(oldSessionId, 300000, 250000, "run-final");
|
||||
|
||||
// Archive the old session
|
||||
db.tasks.archiveSession(oldSessionId, "max_tokens", "# Session Handoff\nPrevious work summary.");
|
||||
db.core.tasks.archiveSession(oldSessionId, "max_tokens", "# Session Handoff\nPrevious work summary.");
|
||||
|
||||
// Create new session
|
||||
const newSessionId = db.tasks.createNewSession({
|
||||
const newSessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
});
|
||||
|
||||
// Get the new active session
|
||||
const newSession = db.tasks.getSessionById(newSessionId);
|
||||
const newSession = db.core.tasks.getSessionById(newSessionId);
|
||||
expect(newSession).not.toBeNull();
|
||||
expect(newSession!.id).toBe(newSessionId);
|
||||
expect(newSession!.id).not.toBe(oldSessionId);
|
||||
@@ -252,7 +252,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
const { companyId, agentId, taskId } = scaffold(db, "gemini-cli");
|
||||
const sessionService = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude-pro",
|
||||
@@ -260,10 +260,10 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
|
||||
// Accumulate way past all thresholds
|
||||
for (let i = 0; i < 25; i++) {
|
||||
db.tasks.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`);
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`);
|
||||
}
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId)!;
|
||||
const session = db.core.tasks.getSessionById(sessionId)!;
|
||||
// Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly.
|
||||
const result = sessionService.evaluateRotation(session);
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
@@ -274,7 +274,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
const { companyId, agentId, taskId } = scaffold(db, "gemini-cli");
|
||||
const sessionService = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "openai-codex",
|
||||
@@ -282,10 +282,10 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
|
||||
// Accumulate way past all thresholds
|
||||
for (let i = 0; i < 25; i++) {
|
||||
db.tasks.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`);
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`);
|
||||
}
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId)!;
|
||||
const session = db.core.tasks.getSessionById(sessionId)!;
|
||||
// Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly.
|
||||
const result = sessionService.evaluateRotation(session);
|
||||
expect(result.shouldRotate).toBe(true);
|
||||
@@ -297,22 +297,22 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
const sessionService = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
// Create session and add usage
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
});
|
||||
db.tasks.incrementSessionUsage(sessionId, 300000, 200000, "run-1");
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 300000, 200000, "run-1");
|
||||
|
||||
// Create a completed run for run history
|
||||
const run = db.runs.createRun({
|
||||
const run = db.core.runs.createRun({
|
||||
companyId,
|
||||
taskId,
|
||||
agentId,
|
||||
workspaceId,
|
||||
connectorId: "gemini-cli" as ConnectorId,
|
||||
});
|
||||
db.runs.finishRun({
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Previous work done",
|
||||
@@ -324,7 +324,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
|
||||
});
|
||||
|
||||
// Rotate the session (archives old, creates new)
|
||||
const oldSession = db.tasks.getSessionById(sessionId)!;
|
||||
const oldSession = db.core.tasks.getSessionById(sessionId)!;
|
||||
sessionService.rotateSession(oldSession);
|
||||
|
||||
// After rotation, the handoff should be retrievable for the next run
|
||||
|
||||
@@ -22,15 +22,17 @@ const RETRY_COOLDOWN_MS = 30_000;
|
||||
function createMockDeps(): EscalationDeps {
|
||||
return {
|
||||
db: {
|
||||
tasks: {
|
||||
getTaskRetryCount: vi.fn().mockReturnValue(0),
|
||||
incrementTaskRetryCount: vi.fn(),
|
||||
resetTaskRetryCount: vi.fn(),
|
||||
advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_progress" }),
|
||||
},
|
||||
agents: {
|
||||
getChainOfCommand: vi.fn().mockReturnValue([]),
|
||||
getCeoForCompany: vi.fn().mockReturnValue(null),
|
||||
core: {
|
||||
tasks: {
|
||||
getTaskRetryCount: vi.fn().mockReturnValue(0),
|
||||
incrementTaskRetryCount: vi.fn(),
|
||||
resetTaskRetryCount: vi.fn(),
|
||||
advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_progress" }),
|
||||
},
|
||||
agents: {
|
||||
getChainOfCommand: vi.fn().mockReturnValue([]),
|
||||
getCeoForCompany: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
},
|
||||
},
|
||||
wakeAgentIfPossible: vi.fn().mockReturnValue("wake-id"),
|
||||
@@ -62,15 +64,15 @@ describe("Failure Escalation Pipeline", () => {
|
||||
|
||||
describe("Retry path (count < MAX_RETRY_ATTEMPTS)", () => {
|
||||
it("retries on first failure (retry_count=0)", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0);
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0);
|
||||
|
||||
const result = handleFailureEscalation(
|
||||
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
|
||||
);
|
||||
|
||||
expect(result.action).toBe("retry");
|
||||
expect(deps.db.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.db.tasks.advanceTaskStatus).toHaveBeenCalledWith(
|
||||
expect(deps.db.core.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.db.core.tasks.advanceTaskStatus).toHaveBeenCalledWith(
|
||||
"task-1",
|
||||
"todo",
|
||||
expect.stringContaining("attempt 1/2"),
|
||||
@@ -79,15 +81,15 @@ describe("Failure Escalation Pipeline", () => {
|
||||
});
|
||||
|
||||
it("retries on second failure (retry_count=1)", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(1);
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(1);
|
||||
|
||||
const result = handleFailureEscalation(
|
||||
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
|
||||
);
|
||||
|
||||
expect(result.action).toBe("retry");
|
||||
expect(deps.db.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.db.tasks.advanceTaskStatus).toHaveBeenCalledWith(
|
||||
expect(deps.db.core.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.db.core.tasks.advanceTaskStatus).toHaveBeenCalledWith(
|
||||
"task-1",
|
||||
"todo",
|
||||
expect.stringContaining("attempt 2/2"),
|
||||
@@ -96,7 +98,7 @@ describe("Failure Escalation Pipeline", () => {
|
||||
});
|
||||
|
||||
it("calls wakeAgentIfPossible via scheduleRetry callback", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0);
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0);
|
||||
|
||||
handleFailureEscalation(
|
||||
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
|
||||
@@ -111,8 +113,8 @@ describe("Failure Escalation Pipeline", () => {
|
||||
|
||||
describe("Escalation path (count >= MAX_RETRY_ATTEMPTS)", () => {
|
||||
it("escalates to chain[0] manager after max retries", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.core.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([
|
||||
{ id: "manager-1", name: "Manager Alpha" },
|
||||
]);
|
||||
|
||||
@@ -121,32 +123,32 @@ describe("Failure Escalation Pipeline", () => {
|
||||
);
|
||||
|
||||
expect(result.action).toBe("escalated");
|
||||
expect(deps.db.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.db.core.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.wakeAgentIfPossible).toHaveBeenCalledWith("manager-1", "company-1", "report_failed");
|
||||
expect(deps.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls through to CEO when chain is empty", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]);
|
||||
(deps.db.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue({ id: "ceo-1", name: "CEO Boss" });
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.core.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]);
|
||||
(deps.db.core.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue({ id: "ceo-1", name: "CEO Boss" });
|
||||
|
||||
const result = handleFailureEscalation(
|
||||
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
|
||||
);
|
||||
|
||||
expect(result.action).toBe("escalated");
|
||||
expect(deps.db.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.db.core.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.wakeAgentIfPossible).toHaveBeenCalledWith("ceo-1", "company-1", "report_failed");
|
||||
expect(deps.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls through to CEO when chain lookup throws", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.core.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
||||
throw new Error("Agent deleted");
|
||||
});
|
||||
(deps.db.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue({ id: "ceo-1", name: "CEO Boss" });
|
||||
(deps.db.core.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue({ id: "ceo-1", name: "CEO Boss" });
|
||||
|
||||
const result = handleFailureEscalation(
|
||||
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
|
||||
@@ -157,9 +159,9 @@ describe("Failure Escalation Pipeline", () => {
|
||||
});
|
||||
|
||||
it("notifies user when both chain and CEO are unavailable", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]);
|
||||
(deps.db.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue(null);
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.core.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]);
|
||||
(deps.db.core.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue(null);
|
||||
|
||||
const result = handleFailureEscalation(
|
||||
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
|
||||
@@ -177,9 +179,9 @@ describe("Failure Escalation Pipeline", () => {
|
||||
});
|
||||
|
||||
it("uses taskId as fallback when task is null", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]);
|
||||
(deps.db.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue(null);
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.core.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]);
|
||||
(deps.db.core.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue(null);
|
||||
|
||||
const result = handleFailureEscalation(
|
||||
deps, RUN, null, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
|
||||
@@ -195,19 +197,19 @@ describe("Failure Escalation Pipeline", () => {
|
||||
|
||||
describe("Timed out runs follow same path", () => {
|
||||
it("retries timed_out runs the same as failed runs", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0);
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0);
|
||||
|
||||
const result = handleFailureEscalation(
|
||||
deps, RUN, TASK, "timed_out", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
|
||||
);
|
||||
|
||||
expect(result.action).toBe("retry");
|
||||
expect(deps.db.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.db.core.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
});
|
||||
|
||||
it("escalates timed_out runs after max retries", () => {
|
||||
(deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([
|
||||
(deps.db.core.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
||||
(deps.db.core.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([
|
||||
{ id: "manager-1", name: "Manager Alpha" },
|
||||
]);
|
||||
|
||||
@@ -216,18 +218,18 @@ describe("Failure Escalation Pipeline", () => {
|
||||
);
|
||||
|
||||
expect(result.action).toBe("escalated");
|
||||
expect(deps.db.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.db.core.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success resets retry count", () => {
|
||||
it("resets retry_count on successful completion", () => {
|
||||
handleSuccessReset(deps.db, "task-1");
|
||||
expect(deps.db.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
expect(deps.db.core.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1");
|
||||
});
|
||||
|
||||
it("handles reset errors gracefully", () => {
|
||||
(deps.db.tasks.resetTaskRetryCount as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
||||
(deps.db.core.tasks.resetTaskRetryCount as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
||||
throw new Error("DB error");
|
||||
});
|
||||
|
||||
|
||||
@@ -36,20 +36,22 @@ function makeDb(
|
||||
approvals: Record<string, Array<{ id: string; state: string }>> = {},
|
||||
): AppDatabase {
|
||||
return {
|
||||
entityCrud: {
|
||||
getAgent: vi.fn((agentId: string) => {
|
||||
const found = [agent, ...reports].find((candidate) => candidate.id === agentId);
|
||||
if (!found) throw new Error(`Agent not found: ${agentId}`);
|
||||
return found;
|
||||
}),
|
||||
},
|
||||
agents: {
|
||||
hasAgentPermission: vi.fn(() => false),
|
||||
getDirectReports: vi.fn(() => reports),
|
||||
getChainOfCommand: vi.fn(() => []),
|
||||
},
|
||||
approvalsMod: {
|
||||
listCompanyApprovalsByType: vi.fn((_companyId: string, type: string) => approvals[type] ?? []),
|
||||
core: {
|
||||
entity: {
|
||||
getAgent: vi.fn((agentId: string) => {
|
||||
const found = [agent, ...reports].find((candidate) => candidate.id === agentId);
|
||||
if (!found) throw new Error(`Agent not found: ${agentId}`);
|
||||
return found;
|
||||
}),
|
||||
},
|
||||
agents: {
|
||||
hasAgentPermission: vi.fn(() => false),
|
||||
getDirectReports: vi.fn(() => reports),
|
||||
getChainOfCommand: vi.fn(() => []),
|
||||
},
|
||||
approvals: {
|
||||
listCompanyApprovalsByType: vi.fn((_companyId: string, type: string) => approvals[type] ?? []),
|
||||
},
|
||||
},
|
||||
} as unknown as AppDatabase;
|
||||
}
|
||||
|
||||
@@ -31,20 +31,20 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Handler Test Co",
|
||||
description: "Tests saveAgent model validation",
|
||||
status: "active",
|
||||
});
|
||||
db.settings.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
db.core.settings.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -52,7 +52,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -72,7 +72,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => {
|
||||
handlers.set(channel, handler);
|
||||
},
|
||||
db,
|
||||
ensureCompanyExists: (id) => (db.settings.hasCompany(id) ? null : { ok: false, error: { code: "COMPANY_NOT_FOUND", message: "missing" } }),
|
||||
ensureCompanyExists: (id) => (db.core.settings.hasCompany(id) ? null : { ok: false, error: { code: "COMPANY_NOT_FOUND", message: "missing" } }),
|
||||
findInvalidReference: () => null,
|
||||
publishDomainChanged: vi.fn(),
|
||||
handleApprovalResolved: vi.fn(),
|
||||
|
||||
@@ -63,7 +63,7 @@ describe("ipc-core-admin-handlers connector saves", () => {
|
||||
}) as { ok: boolean; error?: { code: string; message: string } };
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(db.connectorsCrud.getConnector("anthropic").model).toBe(anthropicModel);
|
||||
expect(db.core.connectors.getConnector("anthropic").model).toBe(anthropicModel);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
@@ -22,13 +22,13 @@ describe("knowledge provenance", () => {
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Knowledge Provenance Co",
|
||||
description: "Tests sourceRunId updates on knowledge entry edits",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
const knowledgeId = db.knowledgeMod.saveKnowledgeEntry({
|
||||
const knowledgeId = db.core.knowledge.saveKnowledgeEntry({
|
||||
companyId,
|
||||
category: "lesson_learned",
|
||||
topic: "Original lesson",
|
||||
@@ -37,7 +37,7 @@ describe("knowledge provenance", () => {
|
||||
sourceRunId: null,
|
||||
});
|
||||
|
||||
db.knowledgeMod.saveKnowledgeEntry({
|
||||
db.core.knowledge.saveKnowledgeEntry({
|
||||
id: knowledgeId,
|
||||
companyId,
|
||||
category: "lesson_learned",
|
||||
|
||||
@@ -19,7 +19,7 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
function createScaffold(db: AppDatabase) {
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
@@ -27,13 +27,13 @@ function createScaffold(db: AppDatabase) {
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Message Dispatch Co",
|
||||
description: "Tests board wake and wait-state resolution",
|
||||
status: "active",
|
||||
executionMode: "ai_cofounder",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: "/tmp/message-dispatch",
|
||||
@@ -41,7 +41,7 @@ function createScaffold(db: AppDatabase) {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const ceoId = db.agents.saveAgent({
|
||||
const ceoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CEO",
|
||||
role: "CEO",
|
||||
@@ -54,7 +54,7 @@ function createScaffold(db: AppDatabase) {
|
||||
capabilities: "Lead the company",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const ctoId = db.agents.saveAgent({
|
||||
const ctoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CTO",
|
||||
role: "CTO",
|
||||
@@ -68,7 +68,7 @@ function createScaffold(db: AppDatabase) {
|
||||
budgetMonthlyUsd: 200,
|
||||
department: "engineering",
|
||||
});
|
||||
const cmoId = db.agents.saveAgent({
|
||||
const cmoId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "CMO",
|
||||
role: "CMO",
|
||||
@@ -82,7 +82,7 @@ function createScaffold(db: AppDatabase) {
|
||||
budgetMonthlyUsd: 200,
|
||||
department: "marketing",
|
||||
});
|
||||
const pendingId = db.agents.saveAgent({
|
||||
const pendingId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Pending Recruit",
|
||||
role: "IC",
|
||||
@@ -96,7 +96,7 @@ function createScaffold(db: AppDatabase) {
|
||||
budgetMonthlyUsd: 100,
|
||||
department: "engineering",
|
||||
});
|
||||
const terminatedId = db.agents.saveAgent({
|
||||
const terminatedId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Former COO",
|
||||
role: "COO",
|
||||
@@ -111,7 +111,7 @@ function createScaffold(db: AppDatabase) {
|
||||
department: "operations",
|
||||
});
|
||||
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Review architecture plan",
|
||||
description: "Need board feedback in the thread.",
|
||||
@@ -191,9 +191,9 @@ describe("message dispatch", () => {
|
||||
if (!result.ok) throw new Error("Expected dispatch to succeed.");
|
||||
expect(new Set(result.data.wakeTargets)).toEqual(new Set([ceoId, ctoId]));
|
||||
|
||||
const ceoInbox = db.messages.listAgentInboxUnread(companyId, ceoId);
|
||||
const ceoInbox = db.core.messages.listAgentInboxUnread(companyId, ceoId);
|
||||
expect(ceoInbox.map((entry) => entry.id)).toContain(result.data.id);
|
||||
const ctoInbox = db.messages.listAgentInboxUnread(companyId, ctoId);
|
||||
const ctoInbox = db.core.messages.listAgentInboxUnread(companyId, ctoId);
|
||||
expect(ctoInbox.map((entry) => entry.id)).toContain(result.data.id);
|
||||
|
||||
db.close();
|
||||
@@ -230,9 +230,9 @@ describe("message dispatch", () => {
|
||||
|
||||
// The message is still visible to CTO's inbox — they were the
|
||||
// addressed recipient — but CTO is not required to answer.
|
||||
const ctoInbox = db.messages.listAgentInboxUnread(companyId, ctoId);
|
||||
const ctoInbox = db.core.messages.listAgentInboxUnread(companyId, ctoId);
|
||||
expect(ctoInbox.map((entry) => entry.id)).toContain(result.data.id);
|
||||
const ceoInbox = db.messages.listAgentInboxUnread(companyId, ceoId);
|
||||
const ceoInbox = db.core.messages.listAgentInboxUnread(companyId, ceoId);
|
||||
expect(ceoInbox.map((entry) => entry.id)).toContain(result.data.id);
|
||||
|
||||
db.close();
|
||||
@@ -282,7 +282,7 @@ describe("message dispatch", () => {
|
||||
});
|
||||
|
||||
threadIntegration.onTaskCreated(taskId, companyId);
|
||||
const thread = db.threads.listThreads({
|
||||
const thread = db.core.threads.listThreads({
|
||||
companyId,
|
||||
type: "task",
|
||||
relatedEntityType: "task",
|
||||
@@ -294,21 +294,21 @@ describe("message dispatch", () => {
|
||||
throw new Error("Expected task thread to exist.");
|
||||
}
|
||||
|
||||
const replyWait = db.threads.setThreadWaitState({
|
||||
const replyWait = db.core.threads.setThreadWaitState({
|
||||
threadId: thread.id,
|
||||
companyId,
|
||||
waitingFor: "user_reply",
|
||||
relatedApprovalId: null,
|
||||
reason: "Reply needed",
|
||||
});
|
||||
const reviewWait = db.threads.setThreadWaitState({
|
||||
const reviewWait = db.core.threads.setThreadWaitState({
|
||||
threadId: thread.id,
|
||||
companyId,
|
||||
waitingFor: "user_review",
|
||||
relatedApprovalId: null,
|
||||
reason: "Review needed",
|
||||
});
|
||||
const approvalWait = db.threads.setThreadWaitState({
|
||||
const approvalWait = db.core.threads.setThreadWaitState({
|
||||
threadId: thread.id,
|
||||
companyId,
|
||||
waitingFor: "user_approval",
|
||||
@@ -328,7 +328,7 @@ describe("message dispatch", () => {
|
||||
body: "Board-wide update",
|
||||
});
|
||||
|
||||
let unresolved = db.threads.listThreadWaitStates(companyId);
|
||||
let unresolved = db.core.threads.listThreadWaitStates(companyId);
|
||||
expect(unresolved.some((entry) => entry.id === replyWait.id)).toBe(false);
|
||||
expect(unresolved.some((entry) => entry.id === reviewWait.id)).toBe(true);
|
||||
|
||||
@@ -346,7 +346,7 @@ describe("message dispatch", () => {
|
||||
body: "Specific reply for the thread",
|
||||
});
|
||||
|
||||
unresolved = db.threads.listThreadWaitStates(companyId);
|
||||
unresolved = db.core.threads.listThreadWaitStates(companyId);
|
||||
expect(unresolved.some((entry) => entry.id === reviewWait.id)).toBe(false);
|
||||
expect(unresolved.some((entry) => entry.id === approvalWait.id)).toBe(true);
|
||||
|
||||
|
||||
@@ -26,13 +26,13 @@ describe("AppDatabase.searchMessages", () => {
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
|
||||
companyId = db.company.saveCompany({
|
||||
companyId = db.core.company.saveCompany({
|
||||
name: "Search Test Corp",
|
||||
description: "Company for testing message search",
|
||||
status: "active",
|
||||
});
|
||||
|
||||
agentA = db.agents.saveAgent({
|
||||
agentA = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Agent Alpha",
|
||||
role: "Engineer",
|
||||
@@ -46,7 +46,7 @@ describe("AppDatabase.searchMessages", () => {
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
|
||||
agentB = db.agents.saveAgent({
|
||||
agentB = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Agent Beta",
|
||||
role: "Designer",
|
||||
@@ -61,7 +61,7 @@ describe("AppDatabase.searchMessages", () => {
|
||||
});
|
||||
|
||||
// Seed messages across different channels
|
||||
db.messages.sendAgentMessage({
|
||||
db.core.messages.sendAgentMessage({
|
||||
companyId,
|
||||
fromAgentId: agentA,
|
||||
toAgentId: agentB,
|
||||
@@ -71,7 +71,7 @@ describe("AppDatabase.searchMessages", () => {
|
||||
priority: "normal",
|
||||
});
|
||||
|
||||
db.messages.sendAgentMessage({
|
||||
db.core.messages.sendAgentMessage({
|
||||
companyId,
|
||||
fromAgentId: agentB,
|
||||
toAgentId: agentA,
|
||||
@@ -81,7 +81,7 @@ describe("AppDatabase.searchMessages", () => {
|
||||
priority: "normal",
|
||||
});
|
||||
|
||||
db.messages.sendAgentMessage({
|
||||
db.core.messages.sendAgentMessage({
|
||||
companyId,
|
||||
fromAgentId: agentA,
|
||||
channel: "company",
|
||||
@@ -90,7 +90,7 @@ describe("AppDatabase.searchMessages", () => {
|
||||
priority: "normal",
|
||||
});
|
||||
|
||||
db.messages.sendAgentMessage({
|
||||
db.core.messages.sendAgentMessage({
|
||||
companyId,
|
||||
fromAgentId: agentA,
|
||||
channel: "department",
|
||||
@@ -100,7 +100,7 @@ describe("AppDatabase.searchMessages", () => {
|
||||
priority: "urgent",
|
||||
});
|
||||
|
||||
db.messages.sendAgentMessage({
|
||||
db.core.messages.sendAgentMessage({
|
||||
companyId,
|
||||
fromAgentId: agentB,
|
||||
channel: "department",
|
||||
@@ -117,21 +117,21 @@ describe("AppDatabase.searchMessages", () => {
|
||||
});
|
||||
|
||||
it("returns messages where body contains the query string", () => {
|
||||
const results = db.messages.searchMessages(companyId, "velocity");
|
||||
const results = db.core.messages.searchMessages(companyId, "velocity");
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].companyId).toBe(companyId);
|
||||
expect(results[0].body.toLowerCase()).toContain("velocity");
|
||||
});
|
||||
|
||||
it("returns messages where subject contains the query string", () => {
|
||||
const results = db.messages.searchMessages(companyId, "retrospective");
|
||||
const results = db.core.messages.searchMessages(companyId, "retrospective");
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].subject).toBe("Sprint retrospective");
|
||||
});
|
||||
|
||||
it("escapes LIKE wildcards so % and _ match literally", () => {
|
||||
// Insert a message with literal % and _ characters
|
||||
db.messages.sendAgentMessage({
|
||||
db.core.messages.sendAgentMessage({
|
||||
companyId,
|
||||
fromAgentId: agentA,
|
||||
channel: "company",
|
||||
@@ -141,25 +141,25 @@ describe("AppDatabase.searchMessages", () => {
|
||||
});
|
||||
|
||||
// Search for the literal "95%"
|
||||
const percentResults = db.messages.searchMessages(companyId, "95%");
|
||||
const percentResults = db.core.messages.searchMessages(companyId, "95%");
|
||||
expect(percentResults.length).toBe(1);
|
||||
expect(percentResults[0].body).toContain("95%");
|
||||
|
||||
// Search for the literal "disk_io"
|
||||
const underscoreResults = db.messages.searchMessages(companyId, "disk_io");
|
||||
const underscoreResults = db.core.messages.searchMessages(companyId, "disk_io");
|
||||
expect(underscoreResults.length).toBe(1);
|
||||
expect(underscoreResults[0].body).toContain("disk_io");
|
||||
});
|
||||
|
||||
it("scopes to a specific channel when channel param is provided", () => {
|
||||
const results = db.messages.searchMessages(companyId, "approvals", { channel: "department" });
|
||||
const results = db.core.messages.searchMessages(companyId, "approvals", { channel: "department" });
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].channel).toBe("department");
|
||||
expect(results[0].body).toContain("approvals");
|
||||
});
|
||||
|
||||
it("scopes to channelTargetId when provided alongside channel", () => {
|
||||
const results = db.messages.searchMessages(companyId, "updated", {
|
||||
const results = db.core.messages.searchMessages(companyId, "updated", {
|
||||
channel: "department",
|
||||
channelTargetId: "design",
|
||||
});
|
||||
@@ -168,14 +168,14 @@ describe("AppDatabase.searchMessages", () => {
|
||||
});
|
||||
|
||||
it("returns empty array when no matches", () => {
|
||||
const results = db.messages.searchMessages(companyId, "xyznonexistentquery");
|
||||
const results = db.core.messages.searchMessages(companyId, "xyznonexistentquery");
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it("respects the limit parameter (default 50)", () => {
|
||||
// Insert 60 messages
|
||||
for (let i = 0; i < 60; i++) {
|
||||
db.messages.sendAgentMessage({
|
||||
db.core.messages.sendAgentMessage({
|
||||
companyId,
|
||||
fromAgentId: agentA,
|
||||
channel: "company",
|
||||
@@ -186,16 +186,16 @@ describe("AppDatabase.searchMessages", () => {
|
||||
}
|
||||
|
||||
// Default limit should be 50
|
||||
const defaultResults = db.messages.searchMessages(companyId, "flood test");
|
||||
const defaultResults = db.core.messages.searchMessages(companyId, "flood test");
|
||||
expect(defaultResults.length).toBe(50);
|
||||
|
||||
// Explicit limit of 10
|
||||
const limitedResults = db.messages.searchMessages(companyId, "flood test", { limit: 10 });
|
||||
const limitedResults = db.core.messages.searchMessages(companyId, "flood test", { limit: 10 });
|
||||
expect(limitedResults.length).toBe(10);
|
||||
});
|
||||
|
||||
it("returns results ordered by created_at DESC (most recent first)", () => {
|
||||
const results = db.messages.searchMessages(companyId, "migration");
|
||||
const results = db.core.messages.searchMessages(companyId, "migration");
|
||||
expect(results.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
|
||||
@@ -32,18 +32,18 @@ import {
|
||||
*/
|
||||
|
||||
function seedCompany(db: AppDatabase) {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Parallel Co",
|
||||
description: "Regression fixture",
|
||||
status: "active",
|
||||
});
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "openai-codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
notes: "fixture",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Default",
|
||||
localPath: "/tmp/fixture",
|
||||
@@ -97,7 +97,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
|
||||
it("persisted requiresFilesystem=false stays lock-free for engineering-assigned tasks", () => {
|
||||
const { companyId, workspaceId } = seedCompany(db);
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Engineering Lead",
|
||||
role: "CTO",
|
||||
@@ -111,7 +111,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
capabilities: "Architecture",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Review launch plan",
|
||||
description: "Read the plan and respond with strategic comments.",
|
||||
@@ -121,8 +121,8 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
status: "todo",
|
||||
requiresFilesystem: false,
|
||||
});
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const agent = db.entityCrud.getAgent(agentId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
const agent = db.core.entity.getAgent(agentId);
|
||||
const result = resolveWorkspaceWithLockMode(db, task, agent, companyId);
|
||||
expect(task.requiresFilesystem).toBe(false);
|
||||
expect(result.source).toBe("none");
|
||||
@@ -132,13 +132,13 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
|
||||
it("requiresFilesystem + project primary workspace → exclusive on project workspace", () => {
|
||||
const { companyId } = seedCompany(db);
|
||||
const projectId = db.entityCrud.saveProject({
|
||||
const projectId = db.core.entity.saveProject({
|
||||
companyId,
|
||||
name: "Platform",
|
||||
description: "",
|
||||
status: "active",
|
||||
});
|
||||
const projectWsId = db.workspacesMod.saveWorkspace({
|
||||
const projectWsId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Project repo",
|
||||
localPath: "/tmp/project-ws",
|
||||
@@ -185,7 +185,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
|
||||
it("task.workspaceId wins over agent.workspaceId even when both are set", () => {
|
||||
const { companyId, workspaceId: agentWsId } = seedCompany(db);
|
||||
const taskWsId = db.workspacesMod.saveWorkspace({
|
||||
const taskWsId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Task-only",
|
||||
localPath: "/tmp/task-ws",
|
||||
@@ -205,7 +205,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
});
|
||||
|
||||
it("filesystem-requiring task with no resolvable workspace → throws WorkspaceConfigurationError", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Empty Co",
|
||||
description: "",
|
||||
status: "active",
|
||||
@@ -221,7 +221,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
});
|
||||
|
||||
it("non-coding role with no workspace bindings → none (never throws, never falls back to shared lock)", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Empty Co",
|
||||
description: "",
|
||||
status: "active",
|
||||
@@ -271,7 +271,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
});
|
||||
|
||||
it("lockMode=none observationPath is null when company has no workspace at all", () => {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "No-workspace Co",
|
||||
description: "",
|
||||
status: "active",
|
||||
|
||||
@@ -28,19 +28,19 @@ function createLogger() {
|
||||
}
|
||||
|
||||
function createCompanyRunFixture(db: AppDatabase, profileDir: string) {
|
||||
db.connectorsCrud.updateConnectorHealth({
|
||||
db.core.connectors.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Context Integrity Co",
|
||||
description: "Validates run context failures",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: profileDir,
|
||||
@@ -48,7 +48,7 @@ function createCompanyRunFixture(db: AppDatabase, profileDir: string) {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Runtime Engineer",
|
||||
role: "Engineer",
|
||||
@@ -61,7 +61,7 @@ function createCompanyRunFixture(db: AppDatabase, profileDir: string) {
|
||||
capabilities: "Ships runtime fixes",
|
||||
budgetMonthlyUsd: 250,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Assemble safe run context",
|
||||
description: "The worker must start only with a complete context.",
|
||||
@@ -110,7 +110,7 @@ function createHarness(
|
||||
runDispatch.resolveWorkspace,
|
||||
runDispatch.getRunEligibilityFailure,
|
||||
runDispatch.dispatchRunToWorker,
|
||||
(filter) => db.runs.hasActiveRun(filter),
|
||||
(filter) => db.core.runs.hasActiveRun(filter),
|
||||
);
|
||||
return { enqueueWorkerRun, emitEvent, logger, taskRunner };
|
||||
}
|
||||
@@ -145,9 +145,9 @@ describe("run context input failures", () => {
|
||||
throw new Error("Expected the task runner to persist a failed run.");
|
||||
}
|
||||
expect(enqueueWorkerRun).not.toHaveBeenCalled();
|
||||
const run = db.runs.getRun(result.data);
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const workspace = db.workspacesMod.getWorkspace(task.workspaceId);
|
||||
const run = db.core.runs.getRun(result.data);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
const workspace = db.core.workspaces.getWorkspace(task.workspaceId);
|
||||
expect(run.status).toBe("failed");
|
||||
expect(run.errorMessage).toContain("run.skills.fetch_failed");
|
||||
expect(task.status).toBe("todo");
|
||||
@@ -172,8 +172,8 @@ describe("run context input failures", () => {
|
||||
throw new Error("Expected the task runner to persist a failed run.");
|
||||
}
|
||||
expect(enqueueWorkerRun).not.toHaveBeenCalled();
|
||||
const run = db.runs.getRun(result.data);
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const run = db.core.runs.getRun(result.data);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(run.status).toBe("failed");
|
||||
expect(run.errorMessage).toContain("run.session_handoff.load_failed");
|
||||
expect(task.status).toBe("todo");
|
||||
|
||||
@@ -21,12 +21,12 @@ import type { ConnectorId, TaskSessionRecord } from "@shared/types";
|
||||
// Helpers — create a minimal company + agent + task scaffold
|
||||
// ---------------------------------------------------------------------------
|
||||
function scaffold(db: AppDatabase) {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Session Test Co",
|
||||
description: "Testing session management",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.workspacesMod.saveWorkspace({
|
||||
const workspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: "/tmp/session-test",
|
||||
@@ -34,7 +34,7 @@ function scaffold(db: AppDatabase) {
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.agents.saveAgent({
|
||||
const agentId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Worker Agent",
|
||||
role: "developer",
|
||||
@@ -47,14 +47,14 @@ function scaffold(db: AppDatabase) {
|
||||
capabilities: "Writes code",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const goalId = db.entityCrud.saveGoal({
|
||||
const goalId = db.core.entity.saveGoal({
|
||||
companyId,
|
||||
title: "Ship feature",
|
||||
description: "Build session management",
|
||||
ownerAgentId: agentId,
|
||||
status: "active",
|
||||
});
|
||||
const projectId = db.entityCrud.saveProject({
|
||||
const projectId = db.core.entity.saveProject({
|
||||
companyId,
|
||||
goalId,
|
||||
name: "Session Project",
|
||||
@@ -63,7 +63,7 @@ function scaffold(db: AppDatabase) {
|
||||
status: "in_progress",
|
||||
targetDate: null,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId,
|
||||
projectId,
|
||||
goalId,
|
||||
@@ -96,19 +96,19 @@ describe("task_sessions CRUD", () => {
|
||||
});
|
||||
|
||||
it("getSessionById returns null for an unknown id", () => {
|
||||
const session = db.tasks.getSessionById("nonexistent");
|
||||
const session = db.core.tasks.getSessionById("nonexistent");
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
|
||||
it("getSessionById returns TaskSessionRecord when valid session exists", () => {
|
||||
const { companyId, agentId } = scaffold(db);
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude-pro",
|
||||
});
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session).not.toBeNull();
|
||||
expect(session!.id).toBe(sessionId);
|
||||
expect(session!.companyId).toBe(companyId);
|
||||
@@ -124,23 +124,23 @@ describe("task_sessions CRUD", () => {
|
||||
|
||||
it("incrementSessionUsage atomically updates counters", () => {
|
||||
const { companyId, agentId } = scaffold(db);
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude-pro",
|
||||
});
|
||||
|
||||
db.tasks.incrementSessionUsage(sessionId, 1000, 500, "run-001");
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 1000, 500, "run-001");
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session!.runCount).toBe(1);
|
||||
expect(session!.totalInputTokens).toBe(1000);
|
||||
expect(session!.totalOutputTokens).toBe(500);
|
||||
expect(session!.lastRunId).toBe("run-001");
|
||||
|
||||
// Second increment accumulates
|
||||
db.tasks.incrementSessionUsage(sessionId, 2000, 800, "run-002");
|
||||
const session2 = db.tasks.getSessionById(sessionId);
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 2000, 800, "run-002");
|
||||
const session2 = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session2!.runCount).toBe(2);
|
||||
expect(session2!.totalInputTokens).toBe(3000);
|
||||
expect(session2!.totalOutputTokens).toBe(1300);
|
||||
@@ -149,15 +149,15 @@ describe("task_sessions CRUD", () => {
|
||||
|
||||
it("archiveSession sets rotated_at, rotation_reason, handoff_markdown", () => {
|
||||
const { companyId, agentId } = scaffold(db);
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude-pro",
|
||||
});
|
||||
|
||||
db.tasks.archiveSession(sessionId, "max_runs", "# Handoff\nSession reached 20 runs.");
|
||||
db.core.tasks.archiveSession(sessionId, "max_runs", "# Handoff\nSession reached 20 runs.");
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session!.rotatedAt).toBeTruthy();
|
||||
expect(session!.rotationReason).toBe("max_runs");
|
||||
expect(session!.handoffMarkdown).toBe("# Handoff\nSession reached 20 runs.");
|
||||
@@ -166,7 +166,7 @@ describe("task_sessions CRUD", () => {
|
||||
it("createNewSession returns an id; adapter type persists", () => {
|
||||
const { companyId, agentId } = scaffold(db);
|
||||
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
@@ -175,7 +175,7 @@ describe("task_sessions CRUD", () => {
|
||||
expect(typeof sessionId).toBe("string");
|
||||
expect(sessionId.length).toBeGreaterThan(0);
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session).not.toBeNull();
|
||||
expect(session!.id).toBe(sessionId);
|
||||
expect(session!.adapterType).toBe("gemini-cli");
|
||||
@@ -185,8 +185,8 @@ describe("task_sessions CRUD", () => {
|
||||
const { taskId, companyId, agentId, workspaceId } = scaffold(db);
|
||||
|
||||
// Create some runs
|
||||
const run1 = db.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" });
|
||||
db.runs.finishRun({
|
||||
const run1 = db.core.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" });
|
||||
db.core.runs.finishRun({
|
||||
runId: run1.id,
|
||||
status: "succeeded",
|
||||
summary: "First run done",
|
||||
@@ -197,8 +197,8 @@ describe("task_sessions CRUD", () => {
|
||||
unattributedCost: false,
|
||||
});
|
||||
|
||||
const run2 = db.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" });
|
||||
db.runs.finishRun({
|
||||
const run2 = db.core.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" });
|
||||
db.core.runs.finishRun({
|
||||
runId: run2.id,
|
||||
status: "failed",
|
||||
summary: "Second run failed",
|
||||
@@ -209,7 +209,7 @@ describe("task_sessions CRUD", () => {
|
||||
unattributedCost: false,
|
||||
});
|
||||
|
||||
const runs = db.tasks.getRecentRunsForTask(taskId, 5);
|
||||
const runs = db.core.tasks.getRecentRunsForTask(taskId, 5);
|
||||
expect(runs.length).toBe(2);
|
||||
// Most recent first
|
||||
expect(runs[0].id).toBe(run2.id);
|
||||
@@ -401,7 +401,7 @@ describe("session management service", () => {
|
||||
describe("token accumulation", () => {
|
||||
it("accumulateTokenUsage calls db.incrementSessionUsage with correct args", () => {
|
||||
const { taskId, companyId, agentId } = scaffold(db);
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "claude-pro",
|
||||
@@ -410,7 +410,7 @@ describe("session management service", () => {
|
||||
|
||||
svc.accumulateTokenUsage(sessionId, 5000, 2000, "run-abc");
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId);
|
||||
const session = db.core.tasks.getSessionById(sessionId);
|
||||
expect(session!.runCount).toBe(1);
|
||||
expect(session!.totalInputTokens).toBe(5000);
|
||||
expect(session!.totalOutputTokens).toBe(2000);
|
||||
@@ -424,15 +424,15 @@ describe("session management service", () => {
|
||||
describe("handoff generation", () => {
|
||||
it("generateHandoff produces markdown with task title and run history", () => {
|
||||
const { taskId, companyId, agentId, workspaceId } = scaffold(db);
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
});
|
||||
|
||||
// Create a completed run for history
|
||||
const run = db.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "gemini-cli" as ConnectorId });
|
||||
db.runs.finishRun({
|
||||
const run = db.core.runs.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "gemini-cli" as ConnectorId });
|
||||
db.core.runs.finishRun({
|
||||
runId: run.id,
|
||||
status: "succeeded",
|
||||
summary: "Implemented initial logic",
|
||||
@@ -444,9 +444,9 @@ describe("session management service", () => {
|
||||
});
|
||||
|
||||
// Increment session usage to have stats
|
||||
db.tasks.incrementSessionUsage(sessionId, 10000, 5000, run.id);
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 10000, 5000, run.id);
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId)!;
|
||||
const session = db.core.tasks.getSessionById(sessionId)!;
|
||||
const svc = createSessionManagementService({ db, logger: mockLogger });
|
||||
// Handoff is now agent-scoped: it walks the agent's recent runs
|
||||
// (across tasks + heartbeats) rather than a single task. The task
|
||||
@@ -468,16 +468,16 @@ describe("session management service", () => {
|
||||
describe("session rotation", () => {
|
||||
it("rotateSession archives old session, creates new, copies session_params", () => {
|
||||
const { taskId, companyId, agentId } = scaffold(db);
|
||||
const oldSessionId = db.tasks.createNewSession({
|
||||
const oldSessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
});
|
||||
|
||||
// Add some usage
|
||||
db.tasks.incrementSessionUsage(oldSessionId, 300000, 200000, "run-10");
|
||||
db.core.tasks.incrementSessionUsage(oldSessionId, 300000, 200000, "run-10");
|
||||
|
||||
const oldSession = db.tasks.getSessionById(oldSessionId)!;
|
||||
const oldSession = db.core.tasks.getSessionById(oldSessionId)!;
|
||||
const svc = createSessionManagementService({ db, logger: mockLogger });
|
||||
const newSessionId = svc.rotateSession(oldSession);
|
||||
|
||||
@@ -493,7 +493,7 @@ describe("session management service", () => {
|
||||
expect(archivedRows[0].handoffMarkdown).toContain("# Session Handoff");
|
||||
|
||||
// New session is a distinct row with fresh counters.
|
||||
const newSession = db.tasks.getSessionById(newSessionId)!;
|
||||
const newSession = db.core.tasks.getSessionById(newSessionId)!;
|
||||
expect(newSession.id).toBe(newSessionId);
|
||||
expect(newSession.runCount).toBe(0);
|
||||
expect(newSession.totalInputTokens).toBe(0);
|
||||
@@ -510,13 +510,13 @@ describe("session management service", () => {
|
||||
const svc = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
// Create and rotate a session
|
||||
const session1Id = db.tasks.createNewSession({
|
||||
const session1Id = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
});
|
||||
db.tasks.incrementSessionUsage(session1Id, 100000, 50000, "run-1");
|
||||
const session1 = db.tasks.getSessionById(session1Id)!;
|
||||
db.core.tasks.incrementSessionUsage(session1Id, 100000, 50000, "run-1");
|
||||
const session1 = db.core.tasks.getSessionById(session1Id)!;
|
||||
svc.rotateSession(session1);
|
||||
|
||||
const handoff = svc.getHandoffContext(taskId);
|
||||
@@ -526,7 +526,7 @@ describe("session management service", () => {
|
||||
|
||||
it("getHandoffContext returns null when no rotated sessions exist", () => {
|
||||
const { taskId, companyId, agentId } = scaffold(db);
|
||||
db.tasks.createNewSession({
|
||||
db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
@@ -544,16 +544,16 @@ describe("session management service", () => {
|
||||
describe("usage delta", () => {
|
||||
it("getUsageDelta returns token difference between snapshot and current totals", () => {
|
||||
const { taskId, companyId, agentId } = scaffold(db);
|
||||
const sessionId = db.tasks.createNewSession({
|
||||
const sessionId = db.core.tasks.createNewSession({
|
||||
companyId,
|
||||
agentId,
|
||||
adapterType: "gemini-cli",
|
||||
});
|
||||
|
||||
// Pre-populate some usage
|
||||
db.tasks.incrementSessionUsage(sessionId, 1000, 500, "run-1");
|
||||
db.core.tasks.incrementSessionUsage(sessionId, 1000, 500, "run-1");
|
||||
|
||||
const session = db.tasks.getSessionById(sessionId)!;
|
||||
const session = db.core.tasks.getSessionById(sessionId)!;
|
||||
const svc = createSessionManagementService({ db, logger: mockLogger });
|
||||
|
||||
// Snapshot current state
|
||||
|
||||
@@ -14,16 +14,18 @@ describe("sweepStaleReviews", () => {
|
||||
function makeSweepDeps(overrides: Partial<SweepStaleReviewsDeps> = {}): SweepStaleReviewsDeps {
|
||||
return {
|
||||
db: {
|
||||
tasks: {
|
||||
listReviewTasksForSweep: vi.fn().mockReturnValue([]),
|
||||
advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_review" }),
|
||||
},
|
||||
agents: {
|
||||
getManagerForAgent: vi.fn().mockReturnValue(null),
|
||||
getChainOfCommand: vi.fn().mockReturnValue([]),
|
||||
},
|
||||
approvalsMod: {
|
||||
findPendingApprovalForTask: vi.fn().mockReturnValue(null),
|
||||
core: {
|
||||
tasks: {
|
||||
listReviewTasksForSweep: vi.fn().mockReturnValue([]),
|
||||
advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_review" }),
|
||||
},
|
||||
agents: {
|
||||
getManagerForAgent: vi.fn().mockReturnValue(null),
|
||||
getChainOfCommand: vi.fn().mockReturnValue([]),
|
||||
},
|
||||
approvals: {
|
||||
findPendingApprovalForTask: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
},
|
||||
} as unknown as AppDatabase,
|
||||
getCompanyExecutionMode: vi.fn().mockReturnValue("ai_ceo"),
|
||||
@@ -49,14 +51,14 @@ describe("sweepStaleReviews", () => {
|
||||
getManagerForAgent: ReturnType<typeof vi.fn>;
|
||||
getChainOfCommand: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
approvalsMod: {
|
||||
approvals: {
|
||||
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
};
|
||||
|
||||
it("escalates to manager's manager when task in_review for 35 minutes", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as SweepDbMock;
|
||||
const dbMock = (deps.db as unknown as { core: SweepDbMock }).core;
|
||||
|
||||
dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
@@ -81,7 +83,7 @@ describe("sweepStaleReviews", () => {
|
||||
|
||||
it("does not escalate when task in_review for only 25 minutes", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as SweepDbMock;
|
||||
const dbMock = (deps.db as unknown as { core: SweepDbMock }).core;
|
||||
|
||||
dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: TWENTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
@@ -95,12 +97,12 @@ describe("sweepStaleReviews", () => {
|
||||
|
||||
it("notifies the human when the reviewer has no higher manager", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as SweepDbMock;
|
||||
const dbMock = (deps.db as unknown as { core: SweepDbMock }).core;
|
||||
|
||||
dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
]);
|
||||
dbMock.approvalsMod.findPendingApprovalForTask.mockReturnValueOnce(null);
|
||||
dbMock.approvals.findPendingApprovalForTask.mockReturnValueOnce(null);
|
||||
dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "ceo-1", name: "CEO", companyId: "company-1" });
|
||||
dbMock.agents.getChainOfCommand.mockReturnValueOnce([]);
|
||||
|
||||
@@ -119,14 +121,14 @@ describe("sweepStaleReviews", () => {
|
||||
|
||||
it("auto-approves pending approval linked to a stale review task", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as SweepDbMock;
|
||||
const dbMock = (deps.db as unknown as { core: SweepDbMock }).core;
|
||||
|
||||
dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
]);
|
||||
dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" });
|
||||
dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]);
|
||||
dbMock.approvalsMod.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
|
||||
dbMock.approvals.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
|
||||
|
||||
sweepStaleReviews("company-1", deps, NOW);
|
||||
|
||||
@@ -144,14 +146,14 @@ describe("sweepStaleReviews", () => {
|
||||
|
||||
it("swallows resolveApprovalDecision races gracefully", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as SweepDbMock;
|
||||
const dbMock = (deps.db as unknown as { core: SweepDbMock }).core;
|
||||
|
||||
dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
]);
|
||||
dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" });
|
||||
dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]);
|
||||
dbMock.approvalsMod.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
|
||||
dbMock.approvals.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
|
||||
(deps.resolveApprovalDecision as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
||||
throw new Error("Approval already decided");
|
||||
});
|
||||
@@ -161,14 +163,14 @@ describe("sweepStaleReviews", () => {
|
||||
|
||||
it("advances task status to done after escalation", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as SweepDbMock;
|
||||
const dbMock = (deps.db as unknown as { core: SweepDbMock }).core;
|
||||
|
||||
dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" },
|
||||
]);
|
||||
dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" });
|
||||
dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]);
|
||||
dbMock.approvalsMod.findPendingApprovalForTask.mockReturnValueOnce(null);
|
||||
dbMock.approvals.findPendingApprovalForTask.mockReturnValueOnce(null);
|
||||
|
||||
sweepStaleReviews("company-1", deps, NOW);
|
||||
|
||||
@@ -182,7 +184,7 @@ describe("sweepStaleReviews", () => {
|
||||
|
||||
it("skips tasks without an assignee", () => {
|
||||
const deps = makeSweepDeps();
|
||||
const dbMock = deps.db as unknown as SweepDbMock;
|
||||
const dbMock = (deps.db as unknown as { core: SweepDbMock }).core;
|
||||
|
||||
dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: null, updatedAt: THIRTY_FIVE_MIN_AGO, title: "Orphan task" },
|
||||
@@ -202,7 +204,7 @@ describe("sweepStaleReviews", () => {
|
||||
const deps = makeSweepDeps({
|
||||
getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"),
|
||||
});
|
||||
const dbMock = deps.db as unknown as SweepDbMock;
|
||||
const dbMock = (deps.db as unknown as { core: SweepDbMock }).core;
|
||||
|
||||
dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: TWENTY_FIVE_MIN_AGO, title: "Review me" },
|
||||
@@ -220,12 +222,12 @@ describe("sweepStaleReviews", () => {
|
||||
const deps = makeSweepDeps({
|
||||
getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"),
|
||||
});
|
||||
const dbMock = deps.db as unknown as SweepDbMock;
|
||||
const dbMock = (deps.db as unknown as { core: SweepDbMock }).core;
|
||||
|
||||
dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
|
||||
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Review me" },
|
||||
]);
|
||||
dbMock.approvalsMod.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
|
||||
dbMock.approvals.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
|
||||
dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" });
|
||||
|
||||
const count = sweepStaleReviews("company-1", deps, NOW);
|
||||
|
||||
@@ -151,12 +151,14 @@ describe("ACCEPTANCE_CRITERIA_KEYWORDS", () => {
|
||||
describe("checkCapabilityMatch", () => {
|
||||
function makeMockDb(agent: { name: string; capabilities: string }, skillAssignments: string[] = [], skills: Record<string, { name: string; description: string | null; slug: string }> = {}) {
|
||||
return {
|
||||
entityCrud: {
|
||||
getAgent: (_id: string) => agent,
|
||||
},
|
||||
skills: {
|
||||
listAgentSkillAssignments: (_id: string) => skillAssignments,
|
||||
getCompanySkill: (_companyId: string, skillId: string) => skills[skillId] ?? null,
|
||||
core: {
|
||||
entity: {
|
||||
getAgent: (_id: string) => agent,
|
||||
},
|
||||
skills: {
|
||||
listAgentSkillAssignments: (_id: string) => skillAssignments,
|
||||
getCompanySkill: (_companyId: string, skillId: string) => skills[skillId] ?? null,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,18 +15,18 @@ vi.mock("electron", () => ({
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
|
||||
function seedCompany(db: AppDatabase) {
|
||||
const companyId = db.company.saveCompany({
|
||||
const companyId = db.core.company.saveCompany({
|
||||
name: "Review Co",
|
||||
description: "Task update transaction tests",
|
||||
status: "active",
|
||||
});
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "openai-codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
notes: "fixture",
|
||||
});
|
||||
const managerId = db.agents.saveAgent({
|
||||
const managerId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Manager",
|
||||
role: "VP Engineering",
|
||||
@@ -40,7 +40,7 @@ function seedCompany(db: AppDatabase) {
|
||||
capabilities: "Review",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const workerId = db.agents.saveAgent({
|
||||
const workerId = db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Worker",
|
||||
role: "Engineer",
|
||||
@@ -58,7 +58,7 @@ function seedCompany(db: AppDatabase) {
|
||||
}
|
||||
|
||||
function createReviewTask(db: AppDatabase, input: { companyId: string; assigneeAgentId: string }) {
|
||||
const taskId = db.tasks.saveTask({
|
||||
const taskId = db.core.tasks.saveTask({
|
||||
companyId: input.companyId,
|
||||
title: "Ship review packet",
|
||||
description: "Prepare a review packet.",
|
||||
@@ -67,7 +67,7 @@ function createReviewTask(db: AppDatabase, input: { companyId: string; assigneeA
|
||||
status: "todo",
|
||||
requiresFilesystem: false,
|
||||
});
|
||||
db.tasks.setTaskRequiresUserReview(taskId, true);
|
||||
db.core.tasks.setTaskRequiresUserReview(taskId, true);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ describe("updateTaskWithReviewSideEffects", () => {
|
||||
const taskId = createReviewTask(db, { companyId, assigneeAgentId: managerId });
|
||||
|
||||
expect(() =>
|
||||
db.tasks.updateTaskWithReviewSideEffects({
|
||||
db.core.tasks.updateTaskWithReviewSideEffects({
|
||||
taskId,
|
||||
companyId,
|
||||
actorAgentId: managerId,
|
||||
@@ -120,18 +120,18 @@ describe("updateTaskWithReviewSideEffects", () => {
|
||||
}),
|
||||
).toThrow(/cannot enter in_review/);
|
||||
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
expect(task.status).toBe("todo");
|
||||
expect(task.requiresUserReview).toBe(true);
|
||||
expect(db.snap.listSnapshot().approvals).toHaveLength(0);
|
||||
expect(db.activityMod.listComments(taskId)).toHaveLength(0);
|
||||
expect(db.projection.snapshot.listSnapshot().approvals).toHaveLength(0);
|
||||
expect(db.core.activity.listComments(taskId)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("commits review approval, flag clear, status, main row, comment, and activity together", () => {
|
||||
const { companyId, managerId, workerId } = seedCompany(db);
|
||||
const taskId = createReviewTask(db, { companyId, assigneeAgentId: workerId });
|
||||
|
||||
const result = db.tasks.updateTaskWithReviewSideEffects({
|
||||
const result = db.core.tasks.updateTaskWithReviewSideEffects({
|
||||
taskId,
|
||||
companyId,
|
||||
actorAgentId: managerId,
|
||||
@@ -159,8 +159,8 @@ describe("updateTaskWithReviewSideEffects", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const approvals = db.snap.listSnapshot().approvals;
|
||||
const task = db.core.entity.getTask(taskId);
|
||||
const approvals = db.projection.snapshot.listSnapshot().approvals;
|
||||
expect(result.previousStatus).toBe("todo");
|
||||
expect(result.newStatus).toBe("in_review");
|
||||
expect(result.reviewRequested).toBe(true);
|
||||
@@ -170,7 +170,7 @@ describe("updateTaskWithReviewSideEffects", () => {
|
||||
expect(task.requiresUserReview).toBe(false);
|
||||
expect(approvals).toHaveLength(1);
|
||||
expect(approvals[0]?.type).toBe("deliverable_review");
|
||||
expect(db.activityMod.listComments(taskId).map((comment) => comment.body)).toEqual([
|
||||
expect(db.core.activity.listComments(taskId).map((comment) => comment.body)).toEqual([
|
||||
"Deliverable submitted for board review.",
|
||||
"Ready for board review.",
|
||||
]);
|
||||
|
||||
@@ -19,7 +19,7 @@ import { AppDatabase } from "../../src/main/database";
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createTestCompany(db: AppDatabase): string {
|
||||
return db.company.saveCompany({
|
||||
return db.core.company.saveCompany({
|
||||
name: "Wakeup Test Company",
|
||||
description: "Wakeup lifecycle tests",
|
||||
status: "active",
|
||||
@@ -27,7 +27,7 @@ function createTestCompany(db: AppDatabase): string {
|
||||
}
|
||||
|
||||
function createTestConnector(db: AppDatabase): void {
|
||||
db.connectorsCrud.saveConnector({
|
||||
db.core.connectors.saveConnector({
|
||||
id: "openai-codex",
|
||||
model: "gpt-5",
|
||||
envBindingText: "",
|
||||
@@ -36,7 +36,7 @@ function createTestConnector(db: AppDatabase): void {
|
||||
}
|
||||
|
||||
function createTestWorkspace(db: AppDatabase, companyId: string): string {
|
||||
return db.workspacesMod.saveWorkspace({
|
||||
return db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Wakeup Workspace",
|
||||
localPath: "/tmp/wakeup-workspace",
|
||||
@@ -47,7 +47,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string {
|
||||
}
|
||||
|
||||
function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string {
|
||||
return db.agents.saveAgent({
|
||||
return db.core.agents.saveAgent({
|
||||
companyId,
|
||||
name,
|
||||
role: "IC",
|
||||
@@ -87,9 +87,9 @@ describe("wakeup lifecycle", () => {
|
||||
const workspaceId = createTestWorkspace(db, companyId);
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
|
||||
db.wakeup.enqueueWakeupRequest(companyId, agentId, "task.created", "New task assigned");
|
||||
db.automation.wakeup.enqueueWakeupRequest(companyId, agentId, "task.created", "New task assigned");
|
||||
|
||||
const wakes = db.wakeup.listWakeupRequests(companyId);
|
||||
const wakes = db.automation.wakeup.listWakeupRequests(companyId);
|
||||
expect(wakes.length).toBe(1);
|
||||
expect(wakes[0].agentId).toBe(agentId);
|
||||
expect(wakes[0].status).toBe("queued");
|
||||
@@ -107,10 +107,10 @@ describe("wakeup lifecycle", () => {
|
||||
const workspaceId = createTestWorkspace(db, companyId);
|
||||
const agentId = createTestAgent(db, companyId, workspaceId);
|
||||
|
||||
db.wakeup.enqueueWakeupRequest(companyId, agentId, "task.created", "First task");
|
||||
db.wakeup.enqueueWakeupRequest(companyId, agentId, "task.status_changed", "Second trigger");
|
||||
db.automation.wakeup.enqueueWakeupRequest(companyId, agentId, "task.created", "First task");
|
||||
db.automation.wakeup.enqueueWakeupRequest(companyId, agentId, "task.status_changed", "Second trigger");
|
||||
|
||||
const wakes = db.wakeup.listWakeupRequests(companyId);
|
||||
const wakes = db.automation.wakeup.listWakeupRequests(companyId);
|
||||
// D-07: Per-agent scope -- only one wakeup row should exist
|
||||
expect(wakes.length).toBe(1);
|
||||
expect(wakes[0].coalescedCount).toBe(1);
|
||||
@@ -128,15 +128,15 @@ describe("wakeup lifecycle", () => {
|
||||
const agent1Id = createTestAgent(db, companyId, workspaceId, "Agent1");
|
||||
const agent2Id = createTestAgent(db, companyId, workspaceId, "Agent2");
|
||||
|
||||
db.wakeup.enqueueWakeupRequest(companyId, agent1Id, "task.created", "Agent1 task");
|
||||
db.wakeup.enqueueWakeupRequest(companyId, agent2Id, "task.created", "Agent2 task");
|
||||
db.automation.wakeup.enqueueWakeupRequest(companyId, agent1Id, "task.created", "Agent1 task");
|
||||
db.automation.wakeup.enqueueWakeupRequest(companyId, agent2Id, "task.created", "Agent2 task");
|
||||
|
||||
// List all for company
|
||||
const allWakes = db.wakeup.listWakeupRequests(companyId);
|
||||
const allWakes = db.automation.wakeup.listWakeupRequests(companyId);
|
||||
expect(allWakes.length).toBe(2);
|
||||
|
||||
// Filter by agentId
|
||||
const agent1Wakes = db.wakeup.listWakeupRequests(companyId, { agentId: agent1Id });
|
||||
const agent1Wakes = db.automation.wakeup.listWakeupRequests(companyId, { agentId: agent1Id });
|
||||
expect(agent1Wakes.length).toBe(1);
|
||||
expect(agent1Wakes[0].agentId).toBe(agent1Id);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user