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;
|
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 {
|
function joinWorkspaceRelative(root: string, rel: string): string | null {
|
||||||
if (isAbsolute(rel)) return null;
|
if (isAbsolute(rel)) return null;
|
||||||
const joined = resolvePath(root, rel);
|
const joined = resolvePath(root, rel);
|
||||||
@@ -155,7 +187,7 @@ export function buildWorkProductMethods(
|
|||||||
companyId: string,
|
companyId: string,
|
||||||
):
|
):
|
||||||
| { kind: "url" | "path"; value: 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(
|
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 = ?",
|
"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
|
).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` };
|
return { kind: "error", reason: "no_target", detail: `provider=${row.provider} external_id=${row.external_id ?? "null"} url=null` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceRoot = resolveWorkspaceRoot(
|
const workspaceRoots = orderedUnique([
|
||||||
getDb,
|
resolveWorkspaceRoot(
|
||||||
companyId,
|
getDb,
|
||||||
row.workspace_id,
|
companyId,
|
||||||
row.task_id,
|
row.workspace_id,
|
||||||
row.last_touched_by_run_id ?? row.created_by_run_id,
|
row.task_id,
|
||||||
);
|
row.last_touched_by_run_id ?? row.created_by_run_id,
|
||||||
if (workspaceRoot) {
|
),
|
||||||
|
...listCompanyWorkspaceRoots(getDb, companyId),
|
||||||
|
]);
|
||||||
|
let firstCandidate: string | null = null;
|
||||||
|
for (const workspaceRoot of workspaceRoots) {
|
||||||
const absolute = joinWorkspaceRelative(workspaceRoot, row.external_id);
|
const absolute = joinWorkspaceRelative(workspaceRoot, row.external_id);
|
||||||
if (!absolute) {
|
if (!absolute) {
|
||||||
return { kind: "error", reason: "path_escape", detail: `external_id=${row.external_id} escapes workspace root ${workspaceRoot}` };
|
return { kind: "error", reason: "path_escape", detail: `external_id=${row.external_id} escapes workspace root ${workspaceRoot}` };
|
||||||
}
|
}
|
||||||
return { kind: "path", value: absolute };
|
firstCandidate ??= absolute;
|
||||||
}
|
if (fileExists(absolute)) {
|
||||||
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (firstCandidate) {
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
reason: "path_missing",
|
||||||
|
detail: `No registered company workspace contains ${row.external_id}; first candidate was ${firstCandidate}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: "error",
|
kind: "error",
|
||||||
reason: "workspace_unresolved",
|
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",
|
no_target: "WORK_PRODUCT_NO_TARGET",
|
||||||
workspace_unresolved: "WORK_PRODUCT_WORKSPACE_UNRESOLVED",
|
workspace_unresolved: "WORK_PRODUCT_WORKSPACE_UNRESOLVED",
|
||||||
path_escape: "WORK_PRODUCT_PATH_ESCAPE",
|
path_escape: "WORK_PRODUCT_PATH_ESCAPE",
|
||||||
|
path_missing: "WORK_PRODUCT_PATH_MISSING",
|
||||||
} as const;
|
} 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);
|
return fail(codeByReason[target.reason], target.detail);
|
||||||
}
|
}
|
||||||
if (target.kind === "url") {
|
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 { 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 { tmpdir } from "node:os";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { AppDatabase } from "../../src/main/database";
|
import { AppDatabase } from "../../src/main/database";
|
||||||
@@ -107,6 +108,9 @@ async function createHarness(): Promise<Harness> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function registerArtifact(harness: Harness, relativePath: string): string {
|
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({
|
return harness.db.core.documents.createWorkProduct({
|
||||||
companyId: harness.companyId,
|
companyId: harness.companyId,
|
||||||
taskId: harness.taskId,
|
taskId: harness.taskId,
|
||||||
@@ -179,6 +183,56 @@ describe("work_products archived visibility", () => {
|
|||||||
harness.db.close();
|
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", () => {
|
describe("work_products bulk retirement", () => {
|
||||||
|
|||||||
Reference in new issue
Block a user