diff --git a/server/src/__tests__/file-resources.test.ts b/server/src/__tests__/file-resources.test.ts index c3276609070..2823abf0574 100644 --- a/server/src/__tests__/file-resources.test.ts +++ b/server/src/__tests__/file-resources.test.ts @@ -58,6 +58,7 @@ async function seedGraph(db: Db, input: { targetProjectRoot?: string; executionRoot?: string | null; projectSourceType?: string; + targetProjectSourceType?: string; }): Promise { const suffix = crypto.randomUUID().slice(0, 8); const companyId = crypto.randomUUID(); @@ -111,7 +112,7 @@ async function seedGraph(db: Db, input: { companyId, projectId: targetProjectId, name: "Target workspace", - sourceType: "local_path", + sourceType: input.targetProjectSourceType ?? "local_path", cwd: input.targetProjectRoot ?? input.projectRoot, isPrimary: true, }, @@ -286,6 +287,48 @@ describeEmbeddedPostgres("workspace file resources", () => { expect(JSON.stringify(read?.details)).not.toContain(targetProjectRoot); }); + it("reads explicit cross-project git_repo workspaces with a local checkout", async () => { + const { root, projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace(); + const graph = await seedGraph(db, { + projectRoot, + targetProjectRoot, + executionRoot, + targetProjectSourceType: "git_repo", + }); + const readmePath = "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/README.md"; + await fs.mkdir(path.join(targetProjectRoot, path.dirname(readmePath)), { recursive: true }); + await fs.writeFile(path.join(targetProjectRoot, readmePath), "# Bundled skills\n\nRendered from content project.\n", "utf8"); + + const app = createApp(db, { + type: "board", + userId: "board-user", + companyIds: [graph.companyId], + source: "session", + isInstanceAdmin: false, + }); + const res = await request(app) + .get(`/api/issues/${graph.issueId}/file-resources/content`) + .query({ + projectId: graph.targetProjectId, + workspaceId: graph.targetProjectWorkspaceId, + path: readmePath, + }); + + expect(res.status).toBe(200); + expect(res.body.resource).toMatchObject({ + workspaceKind: "project_workspace", + workspaceId: graph.targetProjectWorkspaceId, + projectId: graph.targetProjectId, + projectName: "Target project", + displayPath: `Target project / ${readmePath}`, + provider: "git_repo", + previewKind: "text", + }); + expect(res.body.content.encoding).toBe("utf8"); + expect(res.body.content.data).toContain("# Bundled skills"); + expect(JSON.stringify(res.body)).not.toContain(root); + }); + it("denies explicit cross-company project workspaces", async () => { const { projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace(); const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot }); diff --git a/server/src/services/workspace-file-resources.ts b/server/src/services/workspace-file-resources.ts index 8888914bc57..a2c1590768a 100644 --- a/server/src/services/workspace-file-resources.ts +++ b/server/src/services/workspace-file-resources.ts @@ -27,6 +27,7 @@ const TEXT_SNIFF_BYTES = 4096; const MAX_LIST_DEPTH = 20; const GIT_STATUS_MAX_BUFFER_BYTES = 1024 * 1024; const execFileAsync = promisify(execFile); +const LOCAL_PROJECT_WORKSPACE_SOURCE_TYPES = new Set(["local_path", "non_git_path", "git_repo", "git_worktree"]); const DENIED_SEGMENTS = new Set([ ".git", @@ -315,9 +316,9 @@ function candidateFromProjectWorkspace( row: ProjectWorkspaceRow, project?: { id: string; name: string } | null, ): WorkspaceCandidate { - const provider = row.sourceType === "git_worktree" ? "git_worktree" : row.sourceType === "local_path" ? "local_fs" : row.sourceType; + const provider = row.sourceType === "local_path" ? "local_fs" : row.sourceType; const rootPath = row.cwd ?? null; - const remote = !["local_fs", "git_worktree"].includes(provider) || !rootPath; + const remote = !LOCAL_PROJECT_WORKSPACE_SOURCE_TYPES.has(row.sourceType) || !rootPath; return { workspaceKind: "project_workspace", workspaceId: row.id,