fix(db): make filesystem lock intent explicit
This commit is contained in:
1 parent
0785c9ba47
commit
19f8494d09
15 files changed
+188
-125
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createMigrationsV52V52 } from "../../src/main/database/migrations/v52-v52";
|
||||
|
||||
describe("v52 requires_filesystem migration", () => {
|
||||
it("materializes legacy role evidence into the task column", () => {
|
||||
const db = new DatabaseSync(":memory:");
|
||||
try {
|
||||
db.exec(`
|
||||
create table agents (
|
||||
id text primary key,
|
||||
role text not null,
|
||||
department text
|
||||
);
|
||||
create table tasks (
|
||||
id text primary key,
|
||||
status text not null,
|
||||
active_run_id text,
|
||||
assignee_agent_id text
|
||||
);
|
||||
insert into agents (id, role, department) values
|
||||
('agent-cto', 'CTO', 'engineering'),
|
||||
('agent-cmo', 'CMO', 'marketing');
|
||||
insert into tasks (id, status, active_run_id, assignee_agent_id) values
|
||||
('task-coding', 'todo', null, 'agent-cto'),
|
||||
('task-cognitive', 'todo', null, 'agent-cmo'),
|
||||
('task-inflight', 'in_progress', 'run-1', 'agent-cmo');
|
||||
`);
|
||||
|
||||
const migration = createMigrationsV52V52({
|
||||
db,
|
||||
readAll: <T>(sql: string) => db.prepare(sql).all() as T[],
|
||||
})[0];
|
||||
migration.fn?.();
|
||||
|
||||
const rows = db.prepare(
|
||||
"select id, requires_filesystem as requiresFilesystem from tasks order by id",
|
||||
).all() as Array<{ id: string; requiresFilesystem: number }>;
|
||||
|
||||
expect(rows).toEqual([
|
||||
{ id: "task-coding", requiresFilesystem: 1 },
|
||||
{ id: "task-cognitive", requiresFilesystem: 0 },
|
||||
{ id: "task-inflight", requiresFilesystem: 1 },
|
||||
]);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -21,14 +21,14 @@ import {
|
||||
/**
|
||||
* Workspace lock policy (v52+).
|
||||
*
|
||||
* `lockMode='exclusive'` is opt-in — either the task carries an
|
||||
* explicit workspaceId, or the task's requiresFilesystem column is
|
||||
* true, or the resolving agent is in a coding role. Every other
|
||||
* case returns `lockMode='none'` and `workspace=null`, so four
|
||||
* pure-cognitive agents run fully in parallel without queueing on
|
||||
* a single workspace lock. The previous `company_fallback` step
|
||||
* (which returned any workspace in the company as the lock target)
|
||||
* has been removed and now throws `WorkspaceConfigurationError`.
|
||||
* `lockMode='exclusive'` is opt-in: the task carries an explicit
|
||||
* workspaceId or the task's requiresFilesystem column is true.
|
||||
* Every other case returns `lockMode='none'` and `workspace=null`,
|
||||
* so four pure-cognitive agents run fully in parallel without
|
||||
* queueing on a single workspace lock. The previous
|
||||
* `company_fallback` step has been removed and now throws
|
||||
* `WorkspaceConfigurationError` for filesystem tasks with no
|
||||
* resolvable workspace.
|
||||
*/
|
||||
|
||||
function seedCompany(db: AppDatabase) {
|
||||
@@ -82,7 +82,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
expect(result.workspace?.id).toBe(workspaceId);
|
||||
});
|
||||
|
||||
it("coding-role agent + agent workspace → exclusive on agent workspace", () => {
|
||||
it("agent role and department do not infer filesystem locking", () => {
|
||||
const { companyId, workspaceId } = seedCompany(db);
|
||||
const result = resolveWorkspaceWithLockMode(
|
||||
db,
|
||||
@@ -90,12 +90,47 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
{ workspaceId, role: "CTO", department: "engineering" },
|
||||
companyId,
|
||||
);
|
||||
expect(result.source).toBe("agent");
|
||||
expect(result.lockMode).toBe("exclusive");
|
||||
expect(result.workspace?.id).toBe(workspaceId);
|
||||
expect(result.source).toBe("none");
|
||||
expect(result.lockMode).toBe("none");
|
||||
expect(result.workspace).toBeNull();
|
||||
});
|
||||
|
||||
it("coding-role agent + project primary workspace → exclusive on project workspace", () => {
|
||||
it("persisted requiresFilesystem=false stays lock-free for engineering-assigned tasks", () => {
|
||||
const { companyId, workspaceId } = seedCompany(db);
|
||||
const agentId = db.agents.saveAgent({
|
||||
companyId,
|
||||
name: "Engineering Lead",
|
||||
role: "CTO",
|
||||
title: "CTO",
|
||||
department: "engineering",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
connectorId: "openai-codex",
|
||||
workspaceId,
|
||||
model: null,
|
||||
capabilities: "Architecture",
|
||||
budgetMonthlyUsd: 100,
|
||||
});
|
||||
const taskId = db.tasks.saveTask({
|
||||
companyId,
|
||||
title: "Review launch plan",
|
||||
description: "Read the plan and respond with strategic comments.",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId: null,
|
||||
priority: "medium",
|
||||
status: "todo",
|
||||
requiresFilesystem: false,
|
||||
});
|
||||
const task = db.entityCrud.getTask(taskId);
|
||||
const agent = db.entityCrud.getAgent(agentId);
|
||||
const result = resolveWorkspaceWithLockMode(db, task, agent, companyId);
|
||||
expect(task.requiresFilesystem).toBe(false);
|
||||
expect(result.source).toBe("none");
|
||||
expect(result.lockMode).toBe("none");
|
||||
expect(result.workspace).toBeNull();
|
||||
});
|
||||
|
||||
it("requiresFilesystem + project primary workspace → exclusive on project workspace", () => {
|
||||
const { companyId } = seedCompany(db);
|
||||
const projectId = db.entityCrud.saveProject({
|
||||
companyId,
|
||||
@@ -135,7 +170,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
expect(result.source).toBe("agent");
|
||||
});
|
||||
|
||||
it("pure cognitive task (no coding role, no requiresFilesystem) → none", () => {
|
||||
it("pure cognitive task without requiresFilesystem → none", () => {
|
||||
const { companyId } = seedCompany(db);
|
||||
const result = resolveWorkspaceWithLockMode(
|
||||
db,
|
||||
@@ -255,7 +290,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
const { companyId, workspaceId } = seedCompany(db);
|
||||
const result = resolveWorkspaceWithLockMode(
|
||||
db,
|
||||
null,
|
||||
{ workspaceId: null, projectId: null, requiresFilesystem: true },
|
||||
{ workspaceId, role: "CTO", department: "engineering" },
|
||||
companyId,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { taskInputSchema } from "../../src/shared/contracts";
|
||||
|
||||
describe("taskInputSchema", () => {
|
||||
const baseTask = {
|
||||
companyId: "00000000-0000-4000-8000-000000000001",
|
||||
title: "Prepare launch review",
|
||||
};
|
||||
|
||||
it("normalizes omitted requiresFilesystem to false", () => {
|
||||
const parsed = taskInputSchema.parse(baseTask);
|
||||
expect(parsed.requiresFilesystem).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves explicit requiresFilesystem intent", () => {
|
||||
const parsed = taskInputSchema.parse({
|
||||
...baseTask,
|
||||
requiresFilesystem: true,
|
||||
});
|
||||
expect(parsed.requiresFilesystem).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user