From 321d186229fdfd2daaa82444604c7e0c8d3583f7 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:49:40 +0800 Subject: [PATCH] fix(tests): align browser fixture backend origins Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../automation-cadence.mjs | 13 +++++++++++-- .../configuration-backup.mjs | 5 ++--- .../explore-composition-owner.mjs | 4 ++-- .../personal-workspace-browser/scenario-context.mjs | 13 +++++++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/examples/personal-workspace-browser/automation-cadence.mjs b/examples/personal-workspace-browser/automation-cadence.mjs index e503f239dc..050d5631f0 100644 --- a/examples/personal-workspace-browser/automation-cadence.mjs +++ b/examples/personal-workspace-browser/automation-cadence.mjs @@ -7,7 +7,7 @@ import { promisify } from "node:util"; import { resolveTestPython } from "../../scripts/test-python.mjs"; import { outputDir, repoRoot } from "./fixture.mjs"; -import { openWorkspacePage } from "./scenario-context.mjs"; +import { fetchWorkspaceBackend, openWorkspacePage } from "./scenario-context.mjs"; const execFileAsync = promisify(execFile); @@ -114,6 +114,15 @@ export const automationCadenceScenario = { } let context; try { + // Forwarding must not make an unrelated browser origin trusted. + const refused = await fetchWorkspaceBackend({ + request: () => ({ + url: () => new URL(`${path}/preview`, url).href, + headers: () => ({ origin: "https://untrusted.example", "content-type": "application/json" }), + }), + fetch: ({ url: targetUrl, headers }) => fetch(targetUrl, { method: "POST", headers, body: "{}" }), + }, authority.url); + if (refused.status !== 403) throw new Error("Forwarding admitted a foreign origin"); context = await openWorkspacePage(browser, url, { collectCoverage, beforeGoto: async (_api, page) => { @@ -123,7 +132,7 @@ export const automationCadenceScenario = { concurrentChange = false; await interleaveGoalChange(); } - const response = await route.fetch({ url: authority.url + parsed.pathname + parsed.search }); + const response = await fetchWorkspaceBackend(route, authority.url); if (parsed.pathname.endsWith("/apply") && response.ok()) { applyCount += 1; changes.push(route.request().postDataJSON()); diff --git a/examples/personal-workspace-browser/configuration-backup.mjs b/examples/personal-workspace-browser/configuration-backup.mjs index 0c9bd8d366..6f247fb368 100644 --- a/examples/personal-workspace-browser/configuration-backup.mjs +++ b/examples/personal-workspace-browser/configuration-backup.mjs @@ -4,7 +4,7 @@ import {mkdtemp, readFile, realpath, rm} from "node:fs/promises"; import {tmpdir} from "node:os"; import {join, resolve} from "node:path"; import {repoRoot, outputDir} from "./fixture.mjs"; -import {openWorkspacePage} from "./scenario-context.mjs"; +import {fetchWorkspaceBackend, openWorkspacePage} from "./scenario-context.mjs"; import {resolveTestPython} from "../../scripts/test-python.mjs"; // Only the backup routes use a real disposable server. Other workspace state @@ -66,8 +66,7 @@ export const configurationBackupScenario = { const {port} = backend; context = await openWorkspacePage(browser, url, {beforeGoto: async (_api, page) => { await page.route("**/api/chat/configuration-backup/**", async route => { - const path = new URL(route.request().url()).pathname; - const response = await route.fetch({url: `http://127.0.0.1:${port}${path}`}); + const response = await fetchWorkspaceBackend(route, `http://127.0.0.1:${port}`); await route.fulfill({response}); }); }}); diff --git a/examples/personal-workspace-browser/explore-composition-owner.mjs b/examples/personal-workspace-browser/explore-composition-owner.mjs index db79690cea..98a9b77c85 100644 --- a/examples/personal-workspace-browser/explore-composition-owner.mjs +++ b/examples/personal-workspace-browser/explore-composition-owner.mjs @@ -6,7 +6,7 @@ import { resolve } from "node:path"; import { createInterface } from "node:readline"; import { resolveTestPython } from "../../scripts/test-python.mjs"; import { outputDir, repoRoot } from "./fixture.mjs"; -import { openWorkspacePage } from "./scenario-context.mjs"; +import { fetchWorkspaceBackend, openWorkspacePage } from "./scenario-context.mjs"; const GOAL = "research-monitor"; const AGENT = "research-agent"; @@ -133,7 +133,7 @@ export const exploreCompositionOwnerScenario = { await page.route("**/api/chat/goal-configuration**", async (route) => { const target = new URL(route.request().url()); if (!target.pathname.startsWith("/api/chat/goal-configuration")) { await route.fallback(); return; } - const response = await route.fetch({ url: `${owner.url}${target.pathname}${target.search}` }); + const response = await fetchWorkspaceBackend(route, owner.url); ownerResponses.push({ method: route.request().method(), path: target.pathname, status: response.status() }); await route.fulfill({ response }); }); diff --git a/examples/personal-workspace-browser/scenario-context.mjs b/examples/personal-workspace-browser/scenario-context.mjs index fbbd41ad24..5296af9f3b 100644 --- a/examples/personal-workspace-browser/scenario-context.mjs +++ b/examples/personal-workspace-browser/scenario-context.mjs @@ -1,5 +1,18 @@ import { installApi } from "./fixture.mjs"; +// A disposable backend has a different port from the browser fixture. Rebase +// same-origin requests with it; foreign origins must still reach the real guard. +export function fetchWorkspaceBackend(route, backendUrl) { + const request = route.request(); + const source = new URL(request.url()); + const target = new URL(backendUrl); + target.pathname = source.pathname; + target.search = source.search; + const headers = { ...request.headers(), host: target.host }; + if (headers.origin === source.origin) headers.origin = target.origin; + return route.fetch({ url: target.href, headers }); +} + export async function openWorkspacePage( browser, url,