Files
paseo/packages/app/e2e/browser/fork-workspace-setup-gate.spec.ts
Mohamed Boudra f75dff2322 Require explicit setup for fork pull requests (#4215)
* feat(workspaces): gate fork repository automation

Cross-repository change requests can execute contributor-controlled package hooks even when the setup command comes from the base repository. Persist the checkout provenance and require one explicit workspace-level setup action before repository automation runs.

* fix(workspaces): clarify blocked script errors

* fix(app): keep long toasts readable

* test(server): expect fork repository provenance

* test(server): make setup marker portable

* test(app): retry transient sidebar layout
2026-09-02 20:50:22 +02:00

113 lines
4.1 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { mkdirSync } from "node:fs";
import path from "node:path";
import { expect, test } from "../support/fixtures";
import { createTempGitRepo } from "../support/helpers/workspace";
import {
connectWorkspaceSetupClient,
openHomeWithProject,
openWorkspaceScriptsMenu,
seedProjectForWorkspaceSetup,
startWorkspaceScriptFromMenu,
} from "../support/helpers/workspace-setup";
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
import { getServerId } from "../support/helpers/server-id";
import { buildHostWorkspaceRoute } from "../../src/utils/host-routes";
const evidenceDir = "/tmp/fork-setup-gate";
test("fork workspace setup stays blocked until the user runs it", async ({ page }) => {
test.setTimeout(90_000);
mkdirSync(evidenceDir, { recursive: true });
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("fork-setup-gate-", {
withRemote: true,
originUrl: "https://github.com/paseo-e2e/local-fixture.git",
branches: ["pr-branch-2"],
paseoConfig: {
worktree: {
setup: ["node -e \"setTimeout(() => console.log('setup complete'), 1500)\""],
},
scripts: { dev: { command: 'node -e "setTimeout(() => {}, 30000)"' } },
},
});
try {
const localRemote = path.join(repo.path, "remote.git");
execFileSync("git", ["update-ref", "refs/pull/2/head", "refs/heads/pr-branch-2"], {
cwd: localRemote,
});
execFileSync(
"git",
["config", `url.${localRemote}.insteadOf`, "git@github.com:fork-owner/local-fixture.git"],
{ cwd: repo.path },
);
execFileSync(
"git",
[
"config",
"--add",
`url.${localRemote}.insteadOf`,
"https://github.com/paseo-e2e/local-fixture.git",
],
{ cwd: repo.path },
);
await seedProjectForWorkspaceSetup(client, repo.path);
const result = await client.createWorkspace({
source: {
kind: "worktree",
cwd: repo.path,
action: "checkout",
checkoutSource: { kind: "change_request", forge: "github", number: 2 },
},
});
if (!result.workspace || result.error)
throw new Error(result.error ?? "Workspace creation failed");
expect(result.setupSkippedReason).toContain("Scripts are blocked for PR #2");
await openHomeWithProject(page, repo.path);
await page.goto(buildHostWorkspaceRoute(getServerId(), result.workspace.id));
await waitForWorkspaceTabsVisible(page);
await test.step("the blocked setup tab opens with the fork warning", async () => {
await expect(page.getByTestId("workspace-setup-panel")).toBeVisible({ timeout: 30_000 });
await expect(page.getByRole("alert")).toContainText("fork-owner/local-fixture");
await expect(page.getByRole("button", { name: "Run setup" })).toBeVisible();
await page.screenshot({
path: path.join(evidenceDir, "blocked-fork-setup.png"),
fullPage: true,
});
});
await test.step("starting a script explains why it is blocked", async () => {
await openWorkspaceScriptsMenu(page);
await startWorkspaceScriptFromMenu(page, "dev");
await expect(page.getByText(/Scripts are blocked for PR #2/)).toBeVisible();
await page.screenshot({
path: path.join(evidenceDir, "blocked-script-error.png"),
fullPage: true,
});
await page.reload();
await waitForWorkspaceTabsVisible(page);
await expect(page.getByRole("button", { name: "Run setup" })).toBeVisible();
});
await test.step("running setup replaces the warning with normal progress", async () => {
await page.getByRole("button", { name: "Run setup" }).click();
await expect(page.getByTestId("workspace-setup-status")).toContainText(/Running|Completed/, {
timeout: 30_000,
});
await page.screenshot({
path: path.join(evidenceDir, "fork-setup-running.png"),
fullPage: true,
});
await expect(page.getByTestId("workspace-setup-status")).toContainText("Completed", {
timeout: 30_000,
});
});
} finally {
await client.close();
await repo.cleanup();
}
});