The CLI-adapter era `connector.command` field became vestigial once every provider dispatch went through pi-ai. The schema still required a non-empty command, so onboarding saveConnector silently rejected every Pi Mono row (which carries an empty default). Remove the field from the payload chain (renderer → IPC schema → DB CRUD → run dispatch → worker job), narrow RunJob.taskId to match EnqueueRunPayload, and drop the dead helper plus its test. Schema column stays NOT NULL for DB compatibility; seedConnectors writes a literal empty string so no migration is needed.
171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
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();
|
|
});
|
|
});
|