451 lines
14 KiB
TypeScript
451 lines
14 KiB
TypeScript
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import { mkdtemp, mkdir, rm, writeFile, unlink } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { AppDatabase } from "../../src/main/database";
|
|
import {
|
|
projectToolExecutionToWorkProduct,
|
|
reconcileWorkspaceArtifacts,
|
|
type ArtifactProjectorRunContext,
|
|
} from "../../src/main/agent-runtime/artifact-projector";
|
|
|
|
/**
|
|
* Regression coverage for the `work_products` retirement + visibility
|
|
* contract. The projector stamps `status = 'archived'` when a file
|
|
* disappears; every reader (renderer snapshot, task list, open target
|
|
* resolver, deliverable signal) must honour that tombstone so the
|
|
* DocumentCenter stops surfacing removed files and the review gate
|
|
* stops counting deleted artifacts as produced output.
|
|
*/
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
async function createTempDir(prefix: string): Promise<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), prefix));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
});
|
|
|
|
interface Harness {
|
|
db: AppDatabase;
|
|
companyId: string;
|
|
workspaceId: string;
|
|
workspacePath: string;
|
|
agentId: string;
|
|
taskId: string;
|
|
runId: string;
|
|
}
|
|
|
|
async function createHarness(): Promise<Harness> {
|
|
const profileDir = await createTempDir("agentcompany-workproducts-");
|
|
const workspacePath = await createTempDir("agentcompany-workspace-");
|
|
const db = new AppDatabase(profileDir);
|
|
db.init();
|
|
|
|
db.core.connectors.updateConnectorHealth({
|
|
id: "openai-codex",
|
|
status: "ready",
|
|
authState: "authenticated",
|
|
version: "test",
|
|
lastError: null,
|
|
});
|
|
|
|
const companyId = db.core.company.saveCompany({
|
|
name: "Work Products Co",
|
|
description: "Tests archived visibility, bulk retirement, deliverable signal",
|
|
status: "active",
|
|
});
|
|
const workspaceId = db.core.workspaces.saveWorkspace({
|
|
companyId,
|
|
name: "Primary",
|
|
localPath: workspacePath,
|
|
repoUrl: "",
|
|
repoRef: "",
|
|
isPrimary: true,
|
|
});
|
|
const agentId = db.core.agents.saveAgent({
|
|
companyId,
|
|
name: "Artifact Engineer",
|
|
role: "Engineer",
|
|
title: "Artifact Engineer",
|
|
status: "active",
|
|
reportsTo: null,
|
|
connectorId: "openai-codex",
|
|
workspaceId,
|
|
model: null,
|
|
capabilities: "Writes files",
|
|
budgetMonthlyUsd: 100,
|
|
});
|
|
const taskId = db.core.tasks.saveTask({
|
|
companyId,
|
|
title: "Produce artifact",
|
|
description: "",
|
|
assigneeAgentId: agentId,
|
|
workspaceId,
|
|
priority: "medium",
|
|
status: "in_progress",
|
|
});
|
|
const run = db.core.runs.createRun({
|
|
companyId,
|
|
taskId,
|
|
agentId,
|
|
workspaceId,
|
|
connectorId: "openai-codex",
|
|
});
|
|
return {
|
|
db,
|
|
companyId,
|
|
workspaceId,
|
|
workspacePath,
|
|
agentId,
|
|
taskId,
|
|
runId: run.id,
|
|
};
|
|
}
|
|
|
|
function registerArtifact(harness: Harness, relativePath: string): string {
|
|
const absPath = join(harness.workspacePath, relativePath);
|
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
writeFileSync(absPath, "tracked artifact", "utf8");
|
|
return harness.db.core.documents.createWorkProduct({
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
workspaceId: harness.workspaceId,
|
|
type: "file",
|
|
provider: "filesystem",
|
|
externalId: relativePath,
|
|
title: relativePath,
|
|
url: null,
|
|
createdByRunId: harness.runId,
|
|
});
|
|
}
|
|
|
|
describe("work_products archived visibility", () => {
|
|
it("excludes archived rows from the renderer snapshot", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const activeId = registerArtifact(harness, "docs/active.md");
|
|
const archivedId = registerArtifact(harness, "docs/retired.md");
|
|
harness.db.core.documents.updateWorkProduct(archivedId, harness.companyId, {
|
|
status: "archived",
|
|
lastTouchedByRunId: harness.runId,
|
|
});
|
|
|
|
const snapshot = harness.db.projection.snapshot.listSnapshot();
|
|
const visibleIds = snapshot.workProducts.map((wp) => wp.id);
|
|
expect(visibleIds).toContain(activeId);
|
|
expect(visibleIds).not.toContain(archivedId);
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("excludes archived rows from the task-scoped listing", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const activeId = registerArtifact(harness, "src/feature.ts");
|
|
const archivedId = registerArtifact(harness, "src/removed.ts");
|
|
harness.db.core.documents.updateWorkProduct(archivedId, harness.companyId, {
|
|
status: "archived",
|
|
lastTouchedByRunId: harness.runId,
|
|
});
|
|
|
|
const rows = harness.db.core.documents.listWorkProductsForTask(harness.taskId, harness.companyId);
|
|
const ids = rows.map((r) => r.id);
|
|
expect(ids).toContain(activeId);
|
|
expect(ids).not.toContain(archivedId);
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("refuses to resolve an archived work product open target", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const id = registerArtifact(harness, "notes/ghost.md");
|
|
expect(
|
|
harness.db.core.documents.resolveWorkProductTarget(id, harness.companyId),
|
|
).not.toBeNull();
|
|
|
|
harness.db.core.documents.updateWorkProduct(id, harness.companyId, {
|
|
status: "archived",
|
|
lastTouchedByRunId: harness.runId,
|
|
});
|
|
|
|
expect(
|
|
harness.db.core.documents.resolveWorkProductTarget(id, harness.companyId),
|
|
).toMatchObject({ kind: "error", reason: "archived" });
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("resolves workspace-prefixed filesystem rows through another company workspace when the bound root misses", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const parentPath = await createTempDir("agentcompany-parent-workspace-");
|
|
const projectPath = join(parentPath, "booking-app");
|
|
await mkdir(projectPath, { recursive: true });
|
|
await writeFile(join(projectPath, "AGENTS.md"), "instructions", "utf8");
|
|
harness.db.core.workspaces.saveWorkspace({
|
|
companyId: harness.companyId,
|
|
name: "Parent",
|
|
localPath: parentPath,
|
|
repoUrl: "",
|
|
repoRef: "",
|
|
isPrimary: false,
|
|
});
|
|
|
|
const id = harness.db.core.documents.createWorkProduct({
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
workspaceId: harness.workspaceId,
|
|
type: "file",
|
|
provider: "filesystem",
|
|
externalId: "booking-app/AGENTS.md",
|
|
title: "AGENTS.md",
|
|
url: null,
|
|
createdByRunId: harness.runId,
|
|
});
|
|
|
|
expect(
|
|
harness.db.core.documents.resolveWorkProductTarget(id, harness.companyId),
|
|
).toEqual({ kind: "path", value: join(projectPath, "AGENTS.md") });
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("reports missing filesystem rows when no company workspace contains the file", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const id = registerArtifact(harness, "docs/missing.md");
|
|
await unlink(join(harness.workspacePath, "docs/missing.md"));
|
|
|
|
expect(
|
|
harness.db.core.documents.resolveWorkProductTarget(id, harness.companyId),
|
|
).toMatchObject({ kind: "error", reason: "path_missing" });
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("work_products bulk retirement", () => {
|
|
it("registers bash artifacts from non-exclusive observation workspaces", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const ctx: ArtifactProjectorRunContext = {
|
|
runId: harness.runId,
|
|
agentId: harness.agentId,
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
workspaceId: null,
|
|
workspacePath: harness.workspacePath,
|
|
workspaceMode: "none",
|
|
workspaceBaseline: {
|
|
mtimeByRelPath: new Map<string, number>(),
|
|
capturedAt: Date.now(),
|
|
truncated: false,
|
|
},
|
|
};
|
|
await mkdir(join(harness.workspacePath, "reports"), { recursive: true });
|
|
await writeFile(join(harness.workspacePath, "reports", "non-exclusive.md"), "observed", "utf8");
|
|
|
|
const outcomes = projectToolExecutionToWorkProduct(harness.db, ctx, {
|
|
toolName: "bash",
|
|
args: { command: "printf observed > reports/non-exclusive.md" },
|
|
result: null,
|
|
isError: false,
|
|
});
|
|
|
|
expect(outcomes).toEqual([
|
|
expect.objectContaining({
|
|
kind: "inserted",
|
|
relativePath: "reports/non-exclusive.md",
|
|
}),
|
|
]);
|
|
expect(
|
|
harness.db.projection.snapshot.listSnapshot().workProducts.some(
|
|
(entry) => entry.externalId === "reports/non-exclusive.md",
|
|
),
|
|
).toBe(true);
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("retires deleted artifacts from non-exclusive observation workspaces", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const relPath = "reports/non-exclusive-retired.md";
|
|
const workProductId = registerArtifact(harness, relPath);
|
|
await unlink(join(harness.workspacePath, relPath));
|
|
|
|
const outcomes = reconcileWorkspaceArtifacts(harness.db, {
|
|
runId: harness.runId,
|
|
agentId: harness.agentId,
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
workspaceId: null,
|
|
workspacePath: harness.workspacePath,
|
|
workspaceMode: "none",
|
|
workspaceBaseline: {
|
|
mtimeByRelPath: new Map<string, number>([[relPath, Date.now()]]),
|
|
capturedAt: Date.now(),
|
|
truncated: false,
|
|
},
|
|
});
|
|
|
|
expect(outcomes).toEqual([
|
|
expect.objectContaining({
|
|
kind: "retired",
|
|
relativePath: relPath,
|
|
}),
|
|
]);
|
|
expect(
|
|
harness.db.core.documents.resolveWorkProductTarget(workProductId, harness.companyId),
|
|
).toMatchObject({ kind: "error", reason: "archived" });
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("archives every deletion past the previous 50-path budget", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const ids: string[] = [];
|
|
const paths: string[] = [];
|
|
for (let i = 0; i < 120; i += 1) {
|
|
const rel = `build/asset-${i}.bin`;
|
|
paths.push(rel);
|
|
ids.push(registerArtifact(harness, rel));
|
|
}
|
|
|
|
const archived = harness.db.core.documents.archiveWorkProductsByExternalPaths({
|
|
companyId: harness.companyId,
|
|
workspaceId: harness.workspaceId,
|
|
provider: "filesystem",
|
|
paths,
|
|
lastTouchedByRunId: harness.runId,
|
|
});
|
|
|
|
expect(archived.length).toBe(120);
|
|
const archivedIdSet = new Set(archived.map((r) => r.id));
|
|
for (const id of ids) {
|
|
expect(archivedIdSet.has(id)).toBe(true);
|
|
}
|
|
const visible = harness.db.projection.snapshot.listSnapshot().workProducts;
|
|
expect(visible.length).toBe(0);
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("skips rows that are already archived so repeated sweeps stay idempotent", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const id = registerArtifact(harness, "tmp/scratch.log");
|
|
harness.db.core.documents.archiveWorkProductsByExternalPaths({
|
|
companyId: harness.companyId,
|
|
workspaceId: harness.workspaceId,
|
|
provider: "filesystem",
|
|
paths: ["tmp/scratch.log"],
|
|
lastTouchedByRunId: harness.runId,
|
|
});
|
|
|
|
const second = harness.db.core.documents.archiveWorkProductsByExternalPaths({
|
|
companyId: harness.companyId,
|
|
workspaceId: harness.workspaceId,
|
|
provider: "filesystem",
|
|
paths: ["tmp/scratch.log"],
|
|
lastTouchedByRunId: harness.runId,
|
|
});
|
|
|
|
expect(second).toHaveLength(0);
|
|
expect(
|
|
harness.db.core.documents.resolveWorkProductTarget(id, harness.companyId),
|
|
).toMatchObject({ kind: "error", reason: "archived" });
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
|
|
it("archives rows via the reconcile sweep when the baseline detects a missing file", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
await mkdir(join(harness.workspacePath, "reports"), { recursive: true });
|
|
const relPath = "reports/summary.md";
|
|
const absPath = join(harness.workspacePath, relPath);
|
|
await writeFile(absPath, "initial content", "utf8");
|
|
|
|
const workProductId = registerArtifact(harness, relPath);
|
|
expect(
|
|
harness.db.core.documents.resolveWorkProductTarget(workProductId, harness.companyId),
|
|
).not.toBeNull();
|
|
|
|
const baseline = {
|
|
mtimeByRelPath: new Map<string, number>([[relPath, Date.now() / 1000]]),
|
|
capturedAt: Date.now(),
|
|
truncated: false,
|
|
};
|
|
|
|
await unlink(absPath);
|
|
|
|
const ctx: ArtifactProjectorRunContext = {
|
|
runId: harness.runId,
|
|
agentId: harness.agentId,
|
|
companyId: harness.companyId,
|
|
taskId: harness.taskId,
|
|
workspaceId: harness.workspaceId,
|
|
workspacePath: harness.workspacePath,
|
|
workspaceMode: "exclusive",
|
|
workspaceBaseline: baseline,
|
|
};
|
|
const outcomes = reconcileWorkspaceArtifacts(harness.db, ctx);
|
|
const retired = outcomes.filter((o) => o.kind === "retired");
|
|
expect(retired.map((o) => (o as { relativePath: string }).relativePath)).toContain(relPath);
|
|
|
|
expect(
|
|
harness.db.core.documents.resolveWorkProductTarget(workProductId, harness.companyId),
|
|
).toMatchObject({ kind: "error", reason: "archived" });
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("work_products deliverable signal", () => {
|
|
it("does not count an archived row as a produced deliverable", async () => {
|
|
const harness = await createHarness();
|
|
try {
|
|
const id = registerArtifact(harness, "deliverables/final.md");
|
|
const beforeArchive = harness.db.core.knowledge.collectRunDeliverableSignals(
|
|
harness.runId,
|
|
harness.companyId,
|
|
harness.taskId,
|
|
);
|
|
expect(beforeArchive.hasWorkProduct).toBe(true);
|
|
|
|
harness.db.core.documents.updateWorkProduct(id, harness.companyId, {
|
|
status: "archived",
|
|
lastTouchedByRunId: harness.runId,
|
|
});
|
|
|
|
const afterArchive = harness.db.core.knowledge.collectRunDeliverableSignals(
|
|
harness.runId,
|
|
harness.companyId,
|
|
harness.taskId,
|
|
);
|
|
expect(afterArchive.hasWorkProduct).toBe(false);
|
|
} finally {
|
|
harness.db.close();
|
|
}
|
|
});
|
|
});
|