import { join } from "node:path"; import { tmpdir } from "node:os"; 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 } from "../../src/main/orchestration/resolve-workspace"; /** * Regression tests for the workspace lock policy introduced in v0.4.168+: * only `task.workspaceId` grants exclusive access. Agent-, project-, and * company-scoped resolutions are all `shared` so teammates can run in * parallel instead of queuing on a single workspace lock. * * See `resolve-workspace.ts` for the full rationale. */ function seedCompany(db: AppDatabase) { const companyId = db.saveCompany({ name: "Parallel Co", description: "Regression fixture", status: "active", }); db.saveConnector({ id: "openai-codex", model: "gpt-5", envBindingText: "", notes: "fixture", }); const workspaceId = db.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("task-bound workspace → exclusive (explicit opt-in)", () => { const { companyId, workspaceId } = seedCompany(db); const result = resolveWorkspaceWithLockMode( db, { workspaceId, projectId: null }, { workspaceId: null }, companyId, ); expect(result.source).toBe("task"); expect(result.lockMode).toBe("exclusive"); expect(result.workspace?.id).toBe(workspaceId); }); it("agent-bound workspace → shared (normal case: teammates share a repo)", () => { const { companyId, workspaceId } = seedCompany(db); const result = resolveWorkspaceWithLockMode( db, null, { workspaceId }, companyId, ); expect(result.source).toBe("agent"); expect(result.lockMode).toBe("shared"); }); it("project-bound workspace → shared (team collaboration space)", () => { const { companyId } = seedCompany(db); const projectId = db.saveProject({ companyId, name: "Platform", description: "", status: "active", }); // Create a second workspace bound to the project so the resolver's // project-primary branch actually fires (the default seeded workspace // has no project_id set). db.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 }, { workspaceId: null }, companyId, ); expect(result.source).toBe("project"); expect(result.lockMode).toBe("shared"); }); it("company-fallback → shared (unchanged legacy behavior)", () => { const { companyId } = seedCompany(db); const result = resolveWorkspaceWithLockMode( db, null, { workspaceId: null }, companyId, ); expect(result.source).toBe("company_fallback"); expect(result.lockMode).toBe("shared"); }); it("task workspace wins over agent workspace even when both are set", () => { const { companyId, workspaceId: agentWsId } = seedCompany(db); const taskWsId = db.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 }, { workspaceId: agentWsId }, companyId, ); expect(result.source).toBe("task"); expect(result.lockMode).toBe("exclusive"); expect(result.workspace?.id).toBe(taskWsId); }); it("no workspace at all → none", () => { const companyId = db.saveCompany({ name: "Empty Co", description: "", status: "active", }); const result = resolveWorkspaceWithLockMode( db, null, { workspaceId: null }, companyId, ); expect(result.source).toBe("none"); expect(result.lockMode).toBe("none"); expect(result.workspace).toBeNull(); }); });