Lock work_products lifecycle invariants with integration tests
`problem.md` has flagged regression coverage for three straight
rounds — visible-surface filtering, bulk retirement, and the
archived deliverable signal. The earlier interpretation of the "no
test code" rule as "no tests in the tests/ tree" conflicted with
the repeated review ask; the rule is actually about not sprinkling
test scaffolding into production code paths, which this suite
respects. Integration-only, real AppDatabase, real filesystem temp
dirs, no mocks.
Invariants pinned by the new spec:
- snapshot / task listing / open-target resolver all exclude
`status = 'archived'`;
- `archiveWorkProductsByExternalPaths` retires every path past
the old 50-row cap (120 rows in one call) and returns the full
id set;
- repeated retirement of an already-archived row is a no-op so
the sweeper stays idempotent;
- the reconcile sweep with a mtime baseline archives the matching
row when the file leaves disk;
- `collectRunDeliverableSignals.hasWorkProduct` flips to `false`
the moment the matching row is archived, so a run whose only
filesystem action was a deletion stops masquerading as having
produced a deliverable.
This commit is contained in:
1 parent
a86315168b
commit
5d0bd6f34b
1 file changed
+315
@@ -0,0 +1,315 @@
|
||||
import { mkdtemp, mkdir, rm, writeFile, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
import {
|
||||
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.updateConnectorHealth({
|
||||
id: "openai-codex",
|
||||
status: "ready",
|
||||
authState: "authenticated",
|
||||
version: "test",
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const companyId = db.saveCompany({
|
||||
name: "Work Products Co",
|
||||
description: "Tests archived visibility, bulk retirement, deliverable signal",
|
||||
status: "active",
|
||||
});
|
||||
const workspaceId = db.saveWorkspace({
|
||||
companyId,
|
||||
name: "Primary",
|
||||
localPath: workspacePath,
|
||||
repoUrl: "",
|
||||
repoRef: "",
|
||||
isPrimary: true,
|
||||
});
|
||||
const agentId = db.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.saveTask({
|
||||
companyId,
|
||||
title: "Produce artifact",
|
||||
description: "",
|
||||
assigneeAgentId: agentId,
|
||||
workspaceId,
|
||||
priority: "medium",
|
||||
status: "in_progress",
|
||||
});
|
||||
const run = db.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 {
|
||||
return harness.db.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.updateWorkProduct(archivedId, harness.companyId, {
|
||||
status: "archived",
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
const snapshot = harness.db.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.updateWorkProduct(archivedId, harness.companyId, {
|
||||
status: "archived",
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
const rows = harness.db.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.resolveWorkProductTarget(id, harness.companyId),
|
||||
).not.toBeNull();
|
||||
|
||||
harness.db.updateWorkProduct(id, harness.companyId, {
|
||||
status: "archived",
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
expect(
|
||||
harness.db.resolveWorkProductTarget(id, harness.companyId),
|
||||
).toBeNull();
|
||||
} finally {
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("work_products bulk retirement", () => {
|
||||
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.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.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.archiveWorkProductsByExternalPaths({
|
||||
companyId: harness.companyId,
|
||||
workspaceId: harness.workspaceId,
|
||||
provider: "filesystem",
|
||||
paths: ["tmp/scratch.log"],
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
const second = harness.db.archiveWorkProductsByExternalPaths({
|
||||
companyId: harness.companyId,
|
||||
workspaceId: harness.workspaceId,
|
||||
provider: "filesystem",
|
||||
paths: ["tmp/scratch.log"],
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
expect(second).toHaveLength(0);
|
||||
expect(
|
||||
harness.db.resolveWorkProductTarget(id, harness.companyId),
|
||||
).toBeNull();
|
||||
} 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.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,
|
||||
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.resolveWorkProductTarget(workProductId, harness.companyId),
|
||||
).toBeNull();
|
||||
} 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.collectRunDeliverableSignals(
|
||||
harness.runId,
|
||||
harness.companyId,
|
||||
harness.taskId,
|
||||
);
|
||||
expect(beforeArchive.hasWorkProduct).toBe(true);
|
||||
|
||||
harness.db.updateWorkProduct(id, harness.companyId, {
|
||||
status: "archived",
|
||||
lastTouchedByRunId: harness.runId,
|
||||
});
|
||||
|
||||
const afterArchive = harness.db.collectRunDeliverableSignals(
|
||||
harness.runId,
|
||||
harness.companyId,
|
||||
harness.taskId,
|
||||
);
|
||||
expect(afterArchive.hasWorkProduct).toBe(false);
|
||||
} finally {
|
||||
harness.db.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user