import { join } from "node:path"; import { tmpdir } from "node:os"; import { readFileSync } from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("electron", () => ({ safeStorage: { isEncryptionAvailable: () => false, getSelectedStorageBackend: () => "basic_text", encryptString: (value: string) => Buffer.from(value, "utf8"), decryptString: (value: Buffer) => value.toString("utf8"), }, })); import { AppDatabase } from "../../src/main/database"; import { resolveWorkspaceWithLockMode, WorkspaceConfigurationError, } from "../../src/main/orchestration/resolve-workspace"; /** * Workspace lock policy (v52+). * * `lockMode='exclusive'` is opt-in: the task's requiresFilesystem * column is true. A workspaceId can still provide the observation * root for artifact projection, but it does not imply a write lock. * 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) { const companyId = db.core.company.saveCompany({ name: "Parallel Co", description: "Regression fixture", status: "active", }); db.core.connectors.saveConnector({ id: "openai-codex", model: "gpt-5", envBindingText: "", notes: "fixture", }); const workspaceId = db.core.workspaces.saveWorkspace({ companyId, name: "Default", localPath: "/tmp/fixture", repoUrl: "https://github.com/example/fixture", repoRef: "main", isPrimary: true, }); return { companyId, workspaceId }; } describe("resolveWorkspaceWithLockMode", () => { let tempDir: string; let db: AppDatabase; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "resolve-workspace-")); db = new AppDatabase(tempDir); db.init(); }); afterEach(async () => { db.close(); await rm(tempDir, { recursive: true, force: true }); }); it("workspace-bound cognitive task stays lock-free and observes that workspace", () => { const { companyId } = seedCompany(db); const taskWorkspaceId = db.core.workspaces.saveWorkspace({ companyId, name: "Task workspace", localPath: "/tmp/task-observation", repoUrl: "https://github.com/example/task-observation", repoRef: "main", isPrimary: false, }); const result = resolveWorkspaceWithLockMode( db, { workspaceId: taskWorkspaceId, projectId: null, requiresFilesystem: false }, { workspaceId: null, role: "CMO", department: "marketing" }, companyId, ); expect(result.source).toBe("none"); expect(result.lockMode).toBe("none"); expect(result.workspace).toBeNull(); expect(result.observationPath).toBe("/tmp/task-observation"); }); it("requiresFilesystem + task.workspaceId → exclusive on that workspace", () => { const { companyId, workspaceId } = seedCompany(db); const result = resolveWorkspaceWithLockMode( db, { workspaceId, projectId: null, requiresFilesystem: true }, { workspaceId: null, role: "CMO", department: "marketing" }, companyId, ); expect(result.source).toBe("task"); expect(result.lockMode).toBe("exclusive"); expect(result.workspace?.id).toBe(workspaceId); }); it("agent role and department do not infer filesystem locking", () => { const { companyId, workspaceId } = seedCompany(db); const result = resolveWorkspaceWithLockMode( db, null, { workspaceId, role: "CTO", department: "engineering" }, companyId, ); expect(result.source).toBe("none"); expect(result.lockMode).toBe("none"); expect(result.workspace).toBeNull(); }); it("keeps the role-regex workspace safety net deleted", () => { const source = readFileSync( join(process.cwd(), "src/main/orchestration/resolve-workspace.ts"), "utf8", ); expect(source).not.toContain("CODING_ROLE_PATTERN"); expect(source).not.toContain("post-v52 stabilisation"); expect(source).not.toContain("2026-05-20"); }); it("persisted requiresFilesystem=false stays lock-free for engineering-assigned tasks", () => { const { companyId, workspaceId } = seedCompany(db); const agentId = db.core.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.core.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.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"); 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.core.entity.saveProject({ companyId, name: "Platform", description: "", status: "active", }); const projectWsId = db.core.workspaces.saveWorkspace({ companyId, name: "Project repo", localPath: "/tmp/project-ws", repoUrl: "https://github.com/example/project", repoRef: "main", projectId, isPrimary: true, }); const result = resolveWorkspaceWithLockMode( db, { workspaceId: null, projectId, requiresFilesystem: true }, { workspaceId: null, role: "CEO", department: "executive" }, companyId, ); expect(result.source).toBe("project"); expect(result.lockMode).toBe("exclusive"); expect(result.workspace?.id).toBe(projectWsId); }); it("task.requiresFilesystem signals exclusive even for non-coding role", () => { const { companyId, workspaceId } = seedCompany(db); const result = resolveWorkspaceWithLockMode( db, { workspaceId: null, projectId: null, requiresFilesystem: true }, { workspaceId, role: "COO", department: "operations" }, companyId, ); expect(result.lockMode).toBe("exclusive"); expect(result.source).toBe("agent"); }); it("pure cognitive task without requiresFilesystem → none", () => { const { companyId } = seedCompany(db); const result = resolveWorkspaceWithLockMode( db, { workspaceId: null, projectId: null }, { workspaceId: null, role: "CMO", department: "marketing" }, companyId, ); expect(result.source).toBe("none"); expect(result.lockMode).toBe("none"); expect(result.workspace).toBeNull(); }); it("filesystem task workspaceId wins over agent.workspaceId when both are set", () => { const { companyId, workspaceId: agentWsId } = seedCompany(db); const taskWsId = db.core.workspaces.saveWorkspace({ companyId, name: "Task-only", localPath: "/tmp/task-ws", repoUrl: "https://github.com/example/task", repoRef: "main", isPrimary: false, }); const result = resolveWorkspaceWithLockMode( db, { workspaceId: taskWsId, projectId: null, requiresFilesystem: true }, { workspaceId: agentWsId, role: "CTO", department: "engineering" }, companyId, ); expect(result.source).toBe("task"); expect(result.lockMode).toBe("exclusive"); expect(result.workspace?.id).toBe(taskWsId); }); it("filesystem-requiring task with no resolvable workspace → throws WorkspaceConfigurationError", () => { const companyId = db.core.company.saveCompany({ name: "Empty Co", description: "", status: "active", }); expect(() => resolveWorkspaceWithLockMode( db, { workspaceId: null, projectId: null, requiresFilesystem: true }, { workspaceId: null, role: "CTO", department: "engineering" }, companyId, ), ).toThrow(WorkspaceConfigurationError); }); it("non-coding role with no workspace bindings → none (never throws, never falls back to shared lock)", () => { const companyId = db.core.company.saveCompany({ name: "Empty Co", description: "", status: "active", }); const result = resolveWorkspaceWithLockMode( db, null, { workspaceId: null, role: "CFO", department: "finance" }, companyId, ); expect(result.lockMode).toBe("none"); expect(result.workspace).toBeNull(); }); it("four non-coding agents resolving in parallel all get lockMode=none (no shared lock collision)", () => { const { companyId } = seedCompany(db); const roles = [ { role: "CEO", department: "executive" as const }, { role: "CMO", department: "marketing" as const }, { role: "CPO", department: "product" as const }, { role: "COO", department: "operations" as const }, ]; const modes = roles.map((r) => resolveWorkspaceWithLockMode(db, null, { workspaceId: null, ...r }, companyId), ); for (const m of modes) { expect(m.lockMode).toBe("none"); expect(m.workspace).toBeNull(); } }); it("lockMode=none still returns observationPath when company has a workspace (artifact projector regression)", () => { const { companyId } = seedCompany(db); const result = resolveWorkspaceWithLockMode( db, null, { workspaceId: null, role: "CMO", department: "marketing" }, companyId, ); expect(result.lockMode).toBe("none"); expect(result.workspace).toBeNull(); // Observation path is the company's primary workspace localPath // even though no lock is claimed — the artifact projector needs // somewhere to baseline + diff so files written by cognitive // runs surface in the DocumentCenter. expect(result.observationPath).toBe("/tmp/fixture"); }); it("lockMode=none observationPath is null when company has no workspace at all", () => { const companyId = db.core.company.saveCompany({ name: "No-workspace Co", description: "", status: "active", }); const result = resolveWorkspaceWithLockMode( db, null, { workspaceId: null, role: "CFO", department: "finance" }, companyId, ); expect(result.lockMode).toBe("none"); expect(result.observationPath).toBeNull(); }); it("lockMode=exclusive observationPath equals workspace.localPath (single source)", () => { const { companyId, workspaceId } = seedCompany(db); const result = resolveWorkspaceWithLockMode( db, { workspaceId: null, projectId: null, requiresFilesystem: true }, { workspaceId, role: "CTO", department: "engineering" }, companyId, ); expect(result.lockMode).toBe("exclusive"); expect(result.observationPath).toBe(result.workspace?.localPath); }); });