diff --git a/tests/runner-e2e/chat-catalog-domains/internal-repositories-projection.test.ts b/tests/runner-e2e/chat-catalog-domains/internal-repositories-projection.test.ts new file mode 100644 index 00000000000..5a4169ab172 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/internal-repositories-projection.test.ts @@ -0,0 +1,13 @@ +import { expect, it } from "vitest"; +import { projectRequestedRepositories } from "./internal-repositories.js"; +const requested = [{ id: "42", fullName: "owned/qa" }]; +const actual = { id: "42", fullName: "owned/qa", url: "https://github.com/owned/qa", connectionIds: ["actual", "unrelated-connection"], description: "metadata not needed in public observations" }; +it("keeps only the actual requested repository tuple and current connection attribution", () => { + expect(projectRequestedRepositories([actual, { ...actual, id: "99", fullName: "other/private", url: "https://github.com/other/private" }], requested, "actual")) + .toEqual([{ id: "42", fullName: "owned/qa", url: "https://github.com/owned/qa", connectionIds: ["actual"] }]); +}); +it("never rescues wrong IDs, names, URLs or foreign-connection discovery", () => { + for (const delta of [{ id: "43" }, { fullName: "other/qa" }, { url: "https://foreign.invalid/owned/qa" }, { connectionIds: ["foreign"] }]) + expect(projectRequestedRepositories([{ ...actual, ...delta }], requested, "actual")).toEqual([]); + expect(projectRequestedRepositories([actual], [], "actual")).toEqual([]); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/internal-repositories.ts b/tests/runner-e2e/chat-catalog-domains/internal-repositories.ts index 14154864c77..07bc5f71b69 100644 --- a/tests/runner-e2e/chat-catalog-domains/internal-repositories.ts +++ b/tests/runner-e2e/chat-catalog-domains/internal-repositories.ts @@ -54,6 +54,17 @@ export async function connectRepositoryFixture(ctx: ChatCatalogContext) { throw error; } } +/** Keep broad authorized metadata discovery private. Public case observations + * retain only the selected resources and this actual connection's attribution. */ +export function projectRequestedRepositories( + repositories: readonly Repository[], + requested: readonly { id: string; fullName: string }[], + connectionId: string, +): Repository[] { + return repositories.filter(row => requested.some(target => target.id === row.id && target.fullName === row.fullName) + && row.url === `https://github.com/${row.fullName}` && row.connectionIds?.includes(connectionId)) + .map(row => ({ id: row.id, fullName: row.fullName, url: row.url, connectionIds: [connectionId] })); +} async function agentReplacementStatus(ctx: ChatCatalogContext, projectId: string, repositoryId: string) { const agentId = ctx.input.fixtures.agent.id; const key = await ctx.input.api.postSensitive<{ id: string; token: string }>(`/api/agents/${agentId}/keys`, { name: "repository-replacement-boundary-control" }); @@ -147,7 +158,7 @@ function repositoryCase(scenarioId: string): ChatCatalogCase { ctx.input.api.get(`/api/tool-connections/${state.connectionId}`), ]); return { ...o, repositoryProject: p ? await ctx.input.api.get(`/api/projects/${p.id}`) : undefined, - repositories: options.repositories, connection }; + repositories: projectRequestedRepositories(options.repositories, state.requested as Array<{ id: string; fullName: string }>, String(state.connectionId)), connection }; }, ...(scenarioId === "projects.replace-repository" ? { async act(ctx: ChatCatalogContext, state: Record) { const first = await pollUntil({ label: "Repository owner handoff", deadlineAt: Date.now() + 150_000,