Resolve filesystem work product targets by existing file
This commit is contained in:
1 parent
c1349fceab
commit
d13c9d83e8
3 files changed
+118
-29
No files matched your search
@@ -33,6 +33,38 @@ function resolveWorkspaceRoot(
|
||||
return null;
|
||||
}
|
||||
|
||||
function listCompanyWorkspaceRoots(
|
||||
getDb: () => DatabaseSync,
|
||||
companyId: string,
|
||||
): string[] {
|
||||
return (getDb()
|
||||
.prepare(
|
||||
"SELECT local_path AS localPath FROM workspaces WHERE company_id = ? AND local_path IS NOT NULL ORDER BY is_primary DESC, created_at ASC",
|
||||
)
|
||||
.all(companyId) as Array<{ localPath: string }>)
|
||||
.map((row) => row.localPath)
|
||||
.filter((localPath) => localPath.length > 0);
|
||||
}
|
||||
|
||||
function orderedUnique(values: Array<string | null>): string[] {
|
||||
const seen = new Set<string>();
|
||||
const unique: string[] = [];
|
||||
for (const value of values) {
|
||||
if (!value || seen.has(value)) continue;
|
||||
seen.add(value);
|
||||
unique.push(value);
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
function fileExists(targetPath: string): boolean {
|
||||
try {
|
||||
return statSync(targetPath).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function joinWorkspaceRelative(root: string, rel: string): string | null {
|
||||
if (isAbsolute(rel)) return null;
|
||||
const joined = resolvePath(root, rel);
|
||||
@@ -155,7 +187,7 @@ export function buildWorkProductMethods(
|
||||
companyId: string,
|
||||
):
|
||||
| { kind: "url" | "path"; value: string }
|
||||
| { kind: "error"; reason: "not_found" | "archived" | "no_target" | "workspace_unresolved" | "path_escape"; detail: string } {
|
||||
| { kind: "error"; reason: "not_found" | "archived" | "no_target" | "workspace_unresolved" | "path_escape" | "path_missing"; detail: string } {
|
||||
const row = getDb().prepare(
|
||||
"SELECT provider, external_id, url, status, task_id, created_by_run_id, last_touched_by_run_id, workspace_id FROM work_products WHERE id = ? AND company_id = ?",
|
||||
).get(workProductId, companyId) as
|
||||
@@ -169,42 +201,40 @@ export function buildWorkProductMethods(
|
||||
return { kind: "error", reason: "no_target", detail: `provider=${row.provider} external_id=${row.external_id ?? "null"} url=null` };
|
||||
}
|
||||
|
||||
const workspaceRoot = resolveWorkspaceRoot(
|
||||
getDb,
|
||||
companyId,
|
||||
row.workspace_id,
|
||||
row.task_id,
|
||||
row.last_touched_by_run_id ?? row.created_by_run_id,
|
||||
);
|
||||
if (workspaceRoot) {
|
||||
const workspaceRoots = orderedUnique([
|
||||
resolveWorkspaceRoot(
|
||||
getDb,
|
||||
companyId,
|
||||
row.workspace_id,
|
||||
row.task_id,
|
||||
row.last_touched_by_run_id ?? row.created_by_run_id,
|
||||
),
|
||||
...listCompanyWorkspaceRoots(getDb, companyId),
|
||||
]);
|
||||
let firstCandidate: string | null = null;
|
||||
for (const workspaceRoot of workspaceRoots) {
|
||||
const absolute = joinWorkspaceRelative(workspaceRoot, row.external_id);
|
||||
if (!absolute) {
|
||||
return { kind: "error", reason: "path_escape", detail: `external_id=${row.external_id} escapes workspace root ${workspaceRoot}` };
|
||||
}
|
||||
return { kind: "path", value: absolute };
|
||||
}
|
||||
|
||||
const observation = getDb()
|
||||
.prepare(
|
||||
"SELECT local_path as localPath FROM workspaces WHERE company_id = ? AND local_path IS NOT NULL ORDER BY is_primary DESC, created_at ASC LIMIT 1",
|
||||
)
|
||||
.get(companyId) as { localPath: string } | undefined;
|
||||
if (observation?.localPath) {
|
||||
const candidate = joinWorkspaceRelative(observation.localPath, row.external_id);
|
||||
if (candidate) {
|
||||
try {
|
||||
if (statSync(candidate).isFile()) {
|
||||
return { kind: "path", value: candidate };
|
||||
}
|
||||
} catch {
|
||||
// fall through to error below
|
||||
}
|
||||
firstCandidate ??= absolute;
|
||||
if (fileExists(absolute)) {
|
||||
return { kind: "path", value: absolute };
|
||||
}
|
||||
}
|
||||
|
||||
if (firstCandidate) {
|
||||
return {
|
||||
kind: "error",
|
||||
reason: "path_missing",
|
||||
detail: `No registered company workspace contains ${row.external_id}; first candidate was ${firstCandidate}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "error",
|
||||
reason: "workspace_unresolved",
|
||||
detail: `workspace_id=${row.workspace_id ?? "null"} task_id=${row.task_id ?? "null"} run_id=${row.last_touched_by_run_id ?? row.created_by_run_id ?? "null"} — none led to a workspace with a local_path, and ${row.external_id} is not present in the company's observation workspace${observation?.localPath ? ` (${observation.localPath})` : " (no observation workspace exists)"}`,
|
||||
detail: `workspace_id=${row.workspace_id ?? "null"} task_id=${row.task_id ?? "null"} run_id=${row.last_touched_by_run_id ?? row.created_by_run_id ?? "null"} — none led to a workspace with a local_path`,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -76,7 +76,12 @@ export function registerCoreReadHandlers({
|
||||
no_target: "WORK_PRODUCT_NO_TARGET",
|
||||
workspace_unresolved: "WORK_PRODUCT_WORKSPACE_UNRESOLVED",
|
||||
path_escape: "WORK_PRODUCT_PATH_ESCAPE",
|
||||
path_missing: "WORK_PRODUCT_PATH_MISSING",
|
||||
} as const;
|
||||
if (target.reason === "path_missing") {
|
||||
db.core.documents.updateWorkProduct(parsed.data.id, parsed.data.companyId, { status: "archived" });
|
||||
publishDomainChanged();
|
||||
}
|
||||
return fail(codeByReason[target.reason], target.detail);
|
||||
}
|
||||
if (target.kind === "url") {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { mkdtemp, mkdir, rm, writeFile, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { dirname, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
@@ -107,6 +108,9 @@ async function createHarness(): Promise<Harness> {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -179,6 +183,56 @@ describe("work_products archived visibility", () => {
|
||||
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", () => {
|
||||
|
||||
Reference in new issue
Block a user