From 0be6d627d034f5b77084fb543b35371a859e6625 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 27 Oct 2026 04:36:48 -0500 Subject: [PATCH] test(evals): complete protected human GitHub grant continuation --- .../chat-catalog-domains/external-fixtures.ts | 2 +- ...time-workspace-repositories-auth-worker.ts | 142 ++++++++++++++++++ .../runtime-workspace-repositories-auth.ts | 77 ++++++++++ .../runtime-workspace-repositories-support.ts | 21 ++- .../runtime-workspace-repositories.test.ts | 77 +++++++++- .../runtime-workspace-repositories.ts | 43 ++++-- 6 files changed, 338 insertions(+), 24 deletions(-) create mode 100644 tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-auth-worker.ts create mode 100644 tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-auth.ts diff --git a/tests/runner-e2e/chat-catalog-domains/external-fixtures.ts b/tests/runner-e2e/chat-catalog-domains/external-fixtures.ts index e904b12097e..189cae842fa 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-fixtures.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-fixtures.ts @@ -14,7 +14,7 @@ export interface ExternalFixtureConfig { schemaVersion: 1; providers: Partial = { "projects.add-repository": { provider: "github", fields: ["siteRepositoryId", "siteRepositoryFullName", "tokenEnv"] }, "workspaces.prepare-repository": { provider: "github", fields: ["siteRepositoryId", "siteRepositoryFullName", "tokenEnv"] }, - "workspaces.private-repository-no-access": { provider: "github", fields: ["siteRepositoryId", "siteRepositoryFullName", "tokenEnv"] }, + "workspaces.private-repository-no-access": { provider: "github", fields: ["siteRepositoryId", "siteRepositoryFullName", "tokenEnv", "humanAuthEnv"] }, "projects.replace-repository": { provider: "github", fields: ["oldRepositoryId", "oldRepositoryFullName", "newRepositoryId", "newRepositoryFullName", "tokenEnv"] }, "app-permissions.revoke-account": { provider: "notion", fields: ["workspaceId", "mcpUrl", "apiVersion", "apiTokenEnv", "mcpTokenEnv"] }, "app-permissions.grant-one-agent": { provider: "notion", fields: ["workspaceId", "mcpUrl", "apiVersion", "apiTokenEnv", "mcpTokenEnv"] }, diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-auth-worker.ts b/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-auth-worker.ts new file mode 100644 index 00000000000..ee085026ff9 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-auth-worker.ts @@ -0,0 +1,142 @@ +import { readFile } from "node:fs/promises"; +import { chromium, test, expect } from "@playwright/test"; +import type { Agent } from "../../../packages/shared/src/types/agent.js"; +import type { ConnectionIntentSetupOptions } from "../../../packages/shared/src/types/connection-intent.js"; +import type { ConnectionGrant, ToolConnection, ToolConnectionInstall } from "../../../packages/shared/src/types/tool-access.js"; +import { parseHumanGitHubAuth, qualifiesReadOnlyApp, qualifiesReadOnlyGrant, type GitHubHumanRequest, type GitHubAppQualification, type ReadOnlyGitHubGrant } from "./runtime-workspace-repositories-auth.js"; +import { digest } from "./runtime-workspace-repositories-support.js"; +let privateValues: string[] = []; +async function main(): Promise { + try { test.info(); throw new Error("Attached Test worker refused"); } catch (error) { if (error instanceof Error && error.message === "Attached Test worker refused") throw error; } + const input = JSON.parse(await readFile(process.argv[2]!, "utf8")) as GitHubHumanRequest & { executablePath?: string; channel?: string }; + const auth = parseHumanGitHubAuth(JSON.stringify(input.auth)), origin = new URL(input.origin); + if (!(origin.protocol === "https:" || origin.protocol === "http:" && origin.hostname === "127.0.0.1") || origin.username || origin.password || origin.search || origin.hash + || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(input.repositoryFullName) || !/^[1-9]\d*$/.test(input.repositoryId)) throw new Error("Invalid owned human authorization target"); + for (const id of [input.companyId, input.taskId, input.workerId, input.interactionId, input.personId]) if (!/^[a-zA-Z0-9-]{1,100}$/.test(id)) throw new Error("Invalid product identity"); + privateValues = [auth.clientSecret, ...auth.storageState.cookies.map(cookie => cookie.value), ...input.cookies.map(cookie => cookie.value), ...auth.storageState.origins.flatMap(site => site.localStorage.map(row => row.value))]; + const browser = await chromium.launch({ headless: true, ...(input.executablePath ? { executablePath: input.executablePath } : {}), ...(input.channel ? { channel: input.channel } : {}) }); + let createdConnectionId: string | undefined; + const pendingResponses = new Set>(); + try { + const context = await browser.newContext({ storageState: auth.storageState }); await context.addCookies(input.cookies); + const page = await context.newPage(); page.setDefaultTimeout(20000); + page.on("response", response => { + const url = new URL(response.url()); + if (url.origin !== origin.origin || response.request().method() !== "POST" || url.pathname !== `/api/companies/${input.companyId}/tools/connections` || !response.ok()) return; + const tracked = response.json().then((body: ToolConnection | { connection: ToolConnection }) => { + const connection = "connection" in body ? body.connection : body; + if (connection.companyId === input.companyId && connection.config?.sourceTemplateKey === "github" && (connection.config.oauth as { clientId?: string } | undefined)?.clientId === auth.clientId) createdConnectionId = connection.id; + }).catch(() => {}).finally(() => pendingResponses.delete(tracked)); + pendingResponses.add(tracked); + }); + let productReads = 0, providerReads = 0; + const get = async (route: string): Promise => { + if (!route.startsWith("/api/") || new URL(route, origin).origin !== origin.origin || ++productReads > 90) throw new Error("Bounded public read escaped"); + const response = await context.request.get(new URL(route, origin).href, { maxRedirects: 0, timeout: 15000 }); + if (!response.ok()) { await response.dispose(); throw new Error("Required public authorization state is unavailable"); } + try { return await response.json() as T; } finally { await response.dispose(); } + }; + const appRead = async () => { + if (++providerReads > 2) throw new Error("Provider qualification bound exceeded"); + const response = await fetch(`https://api.github.com/apps/${auth.appSlug}`, { headers: { accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28" }, redirect: "error", signal: AbortSignal.timeout(20000) }); + if (!response.ok) { await response.body?.cancel(); throw new Error("Read-only GitHub App prerequisite unavailable"); } + const app = await response.json() as GitHubAppQualification; + if (!qualifiesReadOnlyApp(app, auth)) throw new Error("App is not the declared independently verified read-only client"); + return app; + }; + const session = await get<{ user?: { id: string } }>("/api/auth/get-session"); + const before = await get(`/api/connection-intents/${input.interactionId}/setup-options`); + if (session.user?.id !== input.personId || before.interaction.addresseeUserId !== input.personId || before.interaction.status !== "pending" || before.requestedAgentId !== input.workerId) throw new Error("Human/intent/worker prerequisite changed"); + const app = await appRead(); + // Actual human pause prevents any newly queued continuation from receiving + // a credential until independent app/installation/repository checks pass. + await page.goto(new URL(`/${input.issuePrefix}/agents/${input.workerId}`, origin).href); + await page.getByRole("button", { name: "Pause", exact: true }).click(); + const confirmation = page.getByRole("alertdialog"); if (await confirmation.isVisible()) await confirmation.getByRole("button", { name: "Pause", exact: true }).click(); + if ((await get(`/api/agents/${input.workerId}`)).status !== "paused") throw new Error("Worker pause was not saved before consent"); + const provider = await context.newPage(); + // This case requires a genuine pre-existing disposable read-only App + // installation. No new App or provider credential is manufactured. + const owner = input.repositoryFullName.split("/")[0]!; + await provider.goto(`https://github.com/settings/profile`); + const login = await provider.locator('meta[name="user-login"]').getAttribute("content"); + if (!login || /\/login(?:\/|\?|$)/.test(provider.url())) throw new Error("Genuine human GitHub session is unavailable"); + const installationUrl = owner.toLowerCase() === login.toLowerCase() + ? `https://github.com/settings/installations/${auth.installationId}` + : `https://github.com/organizations/${owner}/settings/installations/${auth.installationId}`; + await provider.goto(installationUrl); + await expect(provider.getByText(app.name, { exact: true }).first()).toBeVisible(); + const selectedOnly = provider.getByRole("radio", { name: /Only select repositories/i }); + await expect(selectedOnly).toBeChecked(); + // A real existing selected installation is independently read back after + // OAuth too. Never broaden to All repositories to make setup succeed. + await expect(provider.getByText(input.repositoryFullName.split("/")[1]!, { exact: true }).first()).toBeVisible(); + const installationSelected = true; + await provider.close(); + await page.goto(new URL(`/${input.issuePrefix}/issues/${input.taskId}`, origin).href); + const card = page.locator(`[id=${JSON.stringify(`connection-intent-focus-target-${input.interactionId}`)}]`); + await expect(card).toBeVisible(); + await card.getByRole("button", { name: /^(?:Connect|Connect \/ Use existing)$/ }).click(); + const dialog = page.getByRole("dialog", { name: "Connect GitHub", exact: true }); await expect(dialog).toBeVisible(); + const connectNew = dialog.getByRole("button", { name: "Connect new", exact: true }); + if (await connectNew.isVisible()) await connectNew.click(); + const useOwn = dialog.getByRole("button", { name: "Use your own GitHub OAuth app", exact: true }); + if (await useOwn.isVisible()) await useOwn.click(); + if (!await dialog.getByLabel("Client ID", { exact: true }).isVisible()) { + const change = dialog.getByRole("button", { name: "Change", exact: true }); + if (await change.count() !== 1) throw new Error("Customer-owned client disclosure is ambiguous"); + await change.click(); + } + await dialog.getByLabel("Client ID", { exact: true }).fill(auth.clientId); await dialog.getByLabel("Client secret", { exact: true }).fill(auth.clientSecret); + const mine = dialog.getByRole("radio", { name: /My GitHub account/ }); if (await mine.isVisible()) await mine.check(); + // The task dialog locks reach to the actual requesting agent. No API + // create/finish/complete call substitutes for the tested visible controls. + const signIn = dialog.getByRole("button", { name: /^(?:Continue to GitHub|Continue|Connect GitHub|Connect)$/ }); + const popupPromise = context.waitForEvent("page", { timeout: 30000 }); await signIn.last().click(); + const authorization = await popupPromise; + await authorization.waitForURL(url => url.hostname === "github.com", { timeout: 30000 }); + const oauth = new URL(authorization.url()); + if (oauth.searchParams.get("client_id") !== auth.clientId || !oauth.pathname.startsWith("/login/oauth/authorize")) throw new Error("Provider consent selected a different OAuth client"); + const redirect = oauth.searchParams.get("redirect_uri"); if (redirect && new URL(redirect).origin !== origin.origin) throw new Error("OAuth callback escaped owned product origin"); + await expect(authorization.getByText(app.name, { exact: false }).first()).toBeVisible(); + const authorize = authorization.getByRole("button", { name: /^Authorize(?:\s|$)/ }); + if (await authorize.count() !== 1) throw new Error("Unambiguous actual provider consent is unavailable"); + await authorize.click(); + let options: ConnectionIntentSetupOptions | undefined; + while (Date.now() < input.deadlineAt - 30000) { + options = await get(`/api/connection-intents/${input.interactionId}/setup-options`); + if (options.interaction.status === "accepted" && options.interaction.result?.outcome === "connected") break; + if (await authorization.isClosed() === false && /\/login(?:\/|\?|$)|two-factor|saml/i.test(authorization.url())) throw new Error("Genuine provider human session needs operator authentication"); + await new Promise(resolve => setTimeout(resolve, 1500)); + } + const result = options?.interaction.result; + if (!options || result?.outcome !== "connected" || !result.connectionId) throw new Error("Actual provider/UI callback did not complete the pending intent"); + createdConnectionId = result.connectionId; + const connection = await get(`/api/tool-connections/${result.connectionId}`); + const grantList = await get<{ grants: ConnectionGrant[] }>(`/api/tool-connections/${result.connectionId}/grants`); + const { installs } = await get<{ installs: ToolConnectionInstall[] }>(`/api/tool-connections/${result.connectionId}/installs`); + const currentApp = await appRead(); + const grants = grantList.grants.filter(grant => qualifiesReadOnlyGrant({ ...input, app: currentApp, auth, connection, grant, installs, interaction: options!.interaction })); + if (grants.length !== 1 || grants[0]!.providerTenant?.github?.login.toLowerCase() !== login.toLowerCase() || grants[0]!.providerTenant?.github?.installationIds[0] !== auth.installationId) throw new Error("Saved human grant is not exact-account/installation/repository read-only authority"); + if ((await get(`/api/agents/${input.workerId}`)).status !== "paused") throw new Error("Worker admitted before qualification"); + await page.goto(new URL(`/${input.issuePrefix}/agents/${input.workerId}`, origin).href); + await page.getByRole("button", { name: "Resume", exact: true }).click(); + if ((await get(`/api/agents/${input.workerId}`)).status === "paused") throw new Error("Actual UI worker resume did not persist"); + const github = grants[0]!.providerTenant!.github!; + return { appId: app.id, appSlug: app.slug, appClientSha256: digest(auth.clientId), appMetadataSha256: digest(JSON.stringify(currentApp)), connectionId: connection.id, grantId: grants[0]!.id, + personId: input.personId, workerId: input.workerId, installationId: auth.installationId, repositoryId: input.repositoryId, repositoryFullName: input.repositoryFullName, + interactionId: input.interactionId, providerUserId: github.userId, providerLogin: github.login, readonlyAppVerified: true, selectedRepositoryVerified: true, + actualProviderConsent: true, actualInstallationSelection: installationSelected, genuineHumanSession: true, uiCompletionObserved: true, workerPausedBeforeConsent: true, + isolatedPrivateBrowser: true, tracingEnabled: false, videoEnabled: false, testRunnerAttached: false }; + } catch { + // Cleanup only newly created Paperclip connection, never forge/retry the + // human action or resume a worker after failed authority qualification. + await Promise.allSettled([...pendingResponses]); + if (createdConnectionId) { + const cleanup = await browser.newContext(); await cleanup.addCookies(input.cookies); + try { await cleanup.request.delete(new URL(`/api/tool-connections/${createdConnectionId}`, origin).href, { maxRedirects: 0 }); } finally { await cleanup.close(); } + } + throw new Error("Protected human GitHub consent prerequisite or completion failed"); + } finally { await browser.close(); } +} +main().then(receipt => { const output = JSON.stringify(receipt); if (privateValues.some(value => !!value && [value, encodeURIComponent(value), Buffer.from(value).toString("base64")].some(encoded => output.includes(encoded)))) throw new Error("Closed receipt contains private value"); process.stdout.write(output); }).catch(() => { process.stderr.write("Protected human GitHub consent failed; details withheld"); process.exitCode = 1; }); diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-auth.ts b/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-auth.ts new file mode 100644 index 00000000000..914fcab4fd5 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-auth.ts @@ -0,0 +1,77 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { mkdtemp, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { chromium, type Cookie } from "@playwright/test"; +import type { ChatCatalogContext } from "../chat-catalog-contract.js"; +import type { ConnectionGrant, ToolConnection, ToolConnectionInstall } from "../../../packages/shared/src/types/tool-access.js"; +import type { ConnectionIntentInteraction } from "../../../packages/shared/src/types/issue.js"; +import { selectedExternalBinding, selectedExternalCredential } from "./external-fixtures.js"; +import { digest } from "./runtime-workspace-repositories-support.js"; +export const humanAuthReference = "PAPERCLIP_CHAT_EVAL_GITHUB_HUMAN_AUTH"; +export interface HumanGitHubAuth { clientId: string; clientSecret: string; appSlug: string; installationId: string; storageState: { cookies: Cookie[]; origins: Array<{ origin: string; localStorage: Array<{ name: string; value: string }> }> } } +export interface GitHubAppQualification { id: number; slug: string; client_id: string; name: string; permissions: Record; html_url: string } +export function parseHumanGitHubAuth(value: string): HumanGitHubAuth { + let auth: HumanGitHubAuth; + try { if (value.length > 220000) throw new Error(); auth = JSON.parse(value) as HumanGitHubAuth; } catch { throw new Error("Setup blocker: private GitHub human session/client bundle is invalid"); } + if (!auth || Object.keys(auth).sort().join(",") !== "appSlug,clientId,clientSecret,installationId,storageState" || !/^[a-z0-9][a-z0-9-]{0,99}$/.test(auth.appSlug) + || !/^[1-9][0-9]{0,19}$/.test(auth.installationId) || !/^[a-zA-Z0-9._-]{8,120}$/.test(auth.clientId) || typeof auth.clientSecret !== "string" || auth.clientSecret.length < 16 || auth.clientSecret.length > 256 + || !Array.isArray(auth.storageState?.cookies) || !auth.storageState.cookies.length || auth.storageState.cookies.length > 100 || !Array.isArray(auth.storageState.origins) + || auth.storageState.cookies.some(cookie => !["github.com", ".github.com"].includes(cookie.domain) || typeof cookie.value !== "string" || !cookie.value || !cookie.name) + || auth.storageState.origins.some(origin => origin.origin !== "https://github.com" || !Array.isArray(origin.localStorage))) throw new Error("Setup blocker: require a genuine GitHub-only human session and bounded customer-owned OAuth client"); + return auth; +} +export function qualifiesReadOnlyApp(app: GitHubAppQualification, auth: Pick) { + return Number.isSafeInteger(app.id) && app.id > 0 && app.slug === auth.appSlug && app.client_id === auth.clientId + && typeof app.name === "string" && !!app.name && app.html_url === `https://github.com/apps/${auth.appSlug}` + && app.permissions?.contents === "read" && Object.values(app.permissions).every(permission => permission === "read" || permission === "none"); +} +export interface ReadOnlyGitHubGrant { + appId: number; appSlug: string; appClientSha256: string; appMetadataSha256: string; + connectionId: string; grantId: string; personId: string; workerId: string; installationId: string; repositoryId: string; repositoryFullName: string; + interactionId: string; providerUserId: string; providerLogin: string; readonlyAppVerified: true; selectedRepositoryVerified: true; + actualProviderConsent: true; actualInstallationSelection: true; genuineHumanSession: true; uiCompletionObserved: true; workerPausedBeforeConsent: true; + isolatedPrivateBrowser: true; tracingEnabled: false; videoEnabled: false; testRunnerAttached: false; +} +export function qualifiesReadOnlyGrant(input: { companyId: string; personId: string; workerId: string; repositoryId: string; repositoryFullName: string; app: GitHubAppQualification; auth: Pick; + connection: ToolConnection; grant: ConnectionGrant; installs: ToolConnectionInstall[]; interaction: ConnectionIntentInteraction }) { + const { grant: g, connection: c, interaction: i } = input, github = g.providerTenant?.github; + return qualifiesReadOnlyApp(input.app, input.auth) && c.companyId === input.companyId && c.status === "active" && c.enabled && c.credentialPolicy === "per_user" + && c.config?.sourceTemplateKey === "github" && (c.config.oauth as { clientId?: string } | undefined)?.clientId === input.auth.clientId + && g.companyId === input.companyId && g.connectionId === c.id && g.status === "active" && g.kind === "user" && g.subjectUserId === input.personId + && !!github && github.appSlug === input.app.slug && github.installationCount === 1 && github.repositoryCount === 1 && github.repositorySelection === "selected" + && github.installationIds.length === 1 && github.repositories?.length === 1 && github.repositories[0]?.id === input.repositoryId + && github.repositories[0]?.fullName === input.repositoryFullName && github.repositories[0]?.private === true && github.repositories[0]?.installationId === github.installationIds[0] + && input.installs.length === 1 && input.installs[0]?.connectionId === c.id && input.installs[0]?.targetType === "agent" && input.installs[0]?.targetId === input.workerId + && i.kind === "connection_intent" && i.payload.serviceSlug === "github" && i.status === "accepted" && i.addresseeUserId === input.personId && i.resolvedByUserId === input.personId && !i.resolvedByAgentId + && i.result?.outcome === "connected" && i.result.connectionId === c.id; +} +export interface GitHubHumanRequest { origin: string; companyId: string; issuePrefix: string; taskId: string; workerId: string; interactionId: string; personId: string; repositoryId: string; repositoryFullName: string; auth: HumanGitHubAuth; cookies: Cookie[]; deadlineAt: number } +export async function completeHumanGitHubGrant(ctx: ChatCatalogContext, input: Omit): Promise { + const binding = selectedExternalBinding(ctx); + if (binding.prerequisites.humanAuthEnv !== humanAuthReference || !binding.credentialEnv.includes(humanAuthReference)) throw new Error(`Setup blocker: humanAuthEnv must explicitly declare ${humanAuthReference}`); + const auth = parseHumanGitHubAuth(selectedExternalCredential(ctx, binding, "humanAuthEnv")); + if (!ctx.input.privateSecrets) throw new Error("Private GitHub human grant requires its attempt redaction sink"); + const cookies = await ctx.input.page.context().cookies(); + ctx.input.privateSecrets.push(auth.clientSecret, ...auth.storageState.cookies.map(cookie => cookie.value), ...auth.storageState.origins.flatMap(origin => origin.localStorage.map(row => row.value)), ...cookies.map(cookie => cookie.value)); + const directory = await mkdtemp(path.join(tmpdir(), "paperclip-repository-human-")); + try { + const request = path.join(directory, "request.json"), channel = process.env.PAPERCLIP_PLAYWRIGHT_CHANNEL; + await writeFile(request, JSON.stringify({ ...input, origin: ctx.input.api.baseURL, auth, cookies, deadlineAt: Date.now() + 420000, + executablePath: process.env.PAPERCLIP_RUNNER_E2E_CHROMIUM_EXECUTABLE ?? (channel ? undefined : chromium.executablePath()), channel }), { mode: 0o600 }); + let output: string; + try { output = (await promisify(execFile)(process.execPath, ["--import", fileURLToPath(new URL("../../../server/node_modules/tsx/dist/loader.mjs", import.meta.url)), fileURLToPath(new URL("./runtime-workspace-repositories-auth-worker.ts", import.meta.url)), request], + { timeout: 425000, maxBuffer: 32768, env: { PATH: process.env.PATH ?? "", NODE_ENV: "test", DO_NOT_TRACK: "1", HOME: directory, TMPDIR: directory } })).stdout; } + catch { throw new Error("GitHub human authorization prerequisite or UI completion failed; private child diagnostics withheld"); } + let receipt: ReadOnlyGitHubGrant; + try { receipt = JSON.parse(output) as ReadOnlyGitHubGrant; } catch { throw new Error("Invalid closed human GitHub grant receipt"); } + if (receipt.interactionId !== input.interactionId || receipt.personId !== input.personId || receipt.workerId !== input.workerId || receipt.repositoryId !== input.repositoryId || receipt.repositoryFullName !== input.repositoryFullName + || receipt.appSlug !== auth.appSlug || receipt.appClientSha256 !== digest(auth.clientId) || receipt.readonlyAppVerified !== true || receipt.selectedRepositoryVerified !== true + || receipt.actualProviderConsent !== true || receipt.actualInstallationSelection !== true || receipt.genuineHumanSession !== true || receipt.uiCompletionObserved !== true || receipt.workerPausedBeforeConsent !== true + || receipt.isolatedPrivateBrowser !== true || receipt.tracingEnabled !== false || receipt.videoEnabled !== false || receipt.testRunnerAttached !== false + || Object.keys(receipt).sort().join(",") !== "actualInstallationSelection,actualProviderConsent,appClientSha256,appId,appMetadataSha256,appSlug,connectionId,genuineHumanSession,grantId,installationId,interactionId,isolatedPrivateBrowser,personId,providerLogin,providerUserId,readonlyAppVerified,repositoryFullName,repositoryId,selectedRepositoryVerified,testRunnerAttached,tracingEnabled,uiCompletionObserved,videoEnabled,workerId,workerPausedBeforeConsent") throw new Error("Invalid closed human GitHub grant receipt"); + return receipt; + } finally { await rm(directory, { recursive: true, force: true }); } +} diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-support.ts b/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-support.ts index 14ac896a343..87ffb2fcbbf 100644 --- a/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-support.ts +++ b/tests/runner-e2e/chat-catalog-domains/runtime-workspace-repositories-support.ts @@ -8,6 +8,13 @@ import type { executionWorkspaceRepositories } from "../../../packages/db/src/sc export type RepositoryReceipt = typeof executionWorkspaceRepositories.$inferSelect; export interface RepositoryWorkspace { issueId: string; workspace: { id: string; companyId: string; cwd: string; providerType?: string }; repositories: RepositoryReceipt[] } export const digest = (value: string | Buffer) => createHash("sha256").update(value).digest("hex"); +/** Require changed header/navigation markup on both sides, not a comment or + * assistant claim that happens to contain the word header. */ +export function changedHeaderMarkup(diff: string) { + const changed = diff.split("\n").filter(line => /^[+-](?![+-])/.test(line) && !/^[+-]\s*(?: