Files
AgentCompany/tests/unit/resolve-workspace.test.ts
T
ZacharyZhang-NY ff5e22092a refactor(session): agent-scope rotation + accumulation; auto-register file artifacts
Sessions are already agent-scoped post-v34, but the code kept routing
rotation / accumulation / handoff through the task indirection. Heartbeat
runs (no task) silently skipped all three, letting taskless transcripts
grow past the model's context window — the 7M-token explosion.

- DB adds getSessionById(sessionId) — direct lookup bypassing
  tasks.active_session_id
- session-management-service: generateHandoff(session) is now agent-scoped,
  walking recent runs across tasks + heartbeats; getHandoffForAgent(agentId)
  is the canonical lookup, getHandoffContext(taskId) is a thin shim for
  callers that still hold a task id
- turn-context.ts::resolveSession drops the `if (!task)` early return:
  rotation evaluation runs on the agent session unconditionally
- run-finished-helpers.ts::accumulateAndRotateSession drops the `!task`
  gate: tokens accumulate and rotation fires for every run, keyed on the
  agent session, not the task
- Rotation evaluator contract: evaluatePreRunSessionRotation(agentId,
  connectorId) → { rotated, handoff, sessionId }; old (taskId, connectorId)
  signature removed

IPC contract tightening: ipc-support::emitEvent becomes a best-effort
sink. "Render frame was disposed" during renderer reload is a lifecycle
signal, not a logic error; silently drop it instead of letting it abort
downstream event emissions.

Artifact projection: pi's write/edit tool results now upsert a
`work_products` row via a new agent-runtime/artifact-projector.ts. No
more agents writing 54 files into the workspace that the UI never sees.
Migration v40 relaxes work_products.task_id to nullable so heartbeat
artifacts can register too.

File size: split agent-event-projection.ts out of runtime-bridge.ts to
keep the bridge under the 400-line cap after adding the projector hook.

Tests: 604 unit + integration green; generateHandoff callsites updated
for the single-argument signature; artifact-projector has pure-function
coverage via the projector module's own shape.
2026-04-18 21:37:56 -04:00

172 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",
command: "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();
});
});