diff --git a/src/node/services/pinnedArtifactFiles.test.ts b/src/node/services/pinnedArtifactFiles.test.ts index 069b9ab1479..7de46e1e8c2 100644 --- a/src/node/services/pinnedArtifactFiles.test.ts +++ b/src/node/services/pinnedArtifactFiles.test.ts @@ -196,6 +196,38 @@ describe("pinned workspace files", () => { } }); + test("pinning on an SSH checkout resolves only stored pins with the same file name", async () => { + // Each folder resolution is one exec on the SSH host; other outside pins cannot be aliases. + const real = await fs.realpath(tempDir); + runtimeConfig = { type: "ssh", host: "example", srcBaseDir: real }; + projectPath = workspaceName = path.join(real, "checkout"); + const resolved: string[] = []; + class CountingRuntime extends LocalRuntime { + override exec(command: string, options: Parameters[1]) { + if (command.includes("XUMREALDIR1")) resolved.push(options.pathEnv?.XUM_DIR ?? ""); + return super.exec(command, options); + } + } + const create = spyOn(runtimeHelpers, "createRuntimeForWorkspace").mockReturnValue( + new CountingRuntime(real) + ); + try { + const sessionDir = path.join(tempDir, "sessions", "ws"); + for (const name of ["a.md", "b.md", "c.md"]) { + await addPinnedPath(sessionDir, path.join(real, "old", name)); + } + await fs.mkdir(path.join(real, "notes")); + await fs.writeFile(path.join(real, "notes", "plan.md"), "# Plan"); + expect( + await pinFile(context, { workspaceId: "ws", path: path.join(real, "notes", "plan.md") }) + ).toEqual({ success: true, data: { path: path.join(real, "notes", "plan.md") } }); + // The new pin's folder and the checkout root, nothing for a.md, b.md or c.md. + expect(resolved.filter((dir) => dir.startsWith(path.join(real, "old")))).toEqual([]); + } finally { + create.mockRestore(); + } + }); + test("re-pinning an alias stored before real folder paths keeps the one entry", async () => { const real = await fs.realpath(tempDir); await fs.mkdir(path.join(real, "notes")); diff --git a/src/node/services/pinnedArtifactFiles.ts b/src/node/services/pinnedArtifactFiles.ts index abefcb14945..5dd49fcca80 100644 --- a/src/node/services/pinnedArtifactFiles.ts +++ b/src/node/services/pinnedArtifactFiles.ts @@ -421,12 +421,22 @@ async function findExistingPin( storedPath: string ): Promise { if (pinned.includes(storedPath)) return storedPath; - const absolute = pinned.filter((p) => isAbsoluteOn(location, p)); + // Only an outside pin's folder is resolved, so an alias keeps the file name: resolve just the + // entries with that name (usually none). On SSH each resolution is one exec. + const paths = location.kind === "runtime" ? path.posix : path; + const name = paths.basename(storedPath); + const aliases = pinned.filter((p) => isAbsoluteOn(location, p) && paths.basename(p) === name); const real = await Promise.all( - absolute.map((p) => toStoredPinPath(location, p).catch(() => null)) + aliases.map((p) => + toStoredPinPath(location, p).catch((error: unknown) => { + // A failed resolution only means no match: the pin is added as a new entry. + log.debug("Could not resolve a stored pin", { error: getErrorMessage(error) }); + return null; + }) + ) ); const index = real.indexOf(storedPath); - return index < 0 ? null : absolute[index]; + return index < 0 ? null : aliases[index]; } export async function pinFile(