From 0b2759fcca2ade7a19995ee8b50d17a95ea23c53 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 18 Aug 2026 13:09:50 +0000 Subject: [PATCH] fix(smoke): use scoped PostHog live bindings Co-Authored-By: Paperclip --- doc/DEVELOPING.md | 16 ++++---- scripts/smoke/posthog-live-lib.mjs | 46 +++++++++++++++++----- scripts/smoke/posthog-live.mjs | 3 ++ scripts/smoke/posthog-live.test.mjs | 61 ++++++++++++++++++++++++----- 4 files changed, 99 insertions(+), 27 deletions(-) diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index c00f324b02e..2e38a077bf3 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -1032,15 +1032,17 @@ uses a live vendor OAuth flow and creates a short-lived connection plus one fresh-run proof issue. ```sh -PAPERCLIP_E2E_BASE_URL=https://paperclip.example.test \ -PAPERCLIP_E2E_EMAIL=operator@example.test \ -PAPERCLIP_DEV_LOGIN_PASSWORD='' \ -POSTHOG_PROJECT_ID=483530 \ -pnpm smoke:posthog-live +INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL=operator@example.test \ +INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD='' \ +INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID=483530 \ +pnpm smoke:posthog-live https://paperclip.example.test ``` -The command fails before browser launch unless all four variables are present, -and never prints their values. It creates no trace, video, or HAR, begins +The command fails before browser launch unless all three integration bindings +are present, and never prints their values. A Paperclip heartbeat derives the +target origin from its injected `PAPERCLIP_API_URL`; the positional URL (or +`--base-url `) selects the running instance for a manual invocation. It +creates no trace, video, or HAR, begins screenshots only after OAuth returns to Paperclip, enables read actions only, installs the connection on `CodexCoderPro` only, runs `project-get` with `{}` from the board Test panel and a fresh agent run, then removes the connection. diff --git a/scripts/smoke/posthog-live-lib.mjs b/scripts/smoke/posthog-live-lib.mjs index ec3df0951bf..abda02ebf78 100644 --- a/scripts/smoke/posthog-live-lib.mjs +++ b/scripts/smoke/posthog-live-lib.mjs @@ -1,9 +1,9 @@ const REQUIRED_ENVIRONMENT = [ - "PAPERCLIP_E2E_BASE_URL", - "PAPERCLIP_E2E_EMAIL", - "PAPERCLIP_DEV_LOGIN_PASSWORD", - "POSTHOG_PROJECT_ID", + "INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL", + "INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD", + "INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID", ]; +const REQUIRED_PROJECT_ID = "483530"; export class PosthogLivePreflightError extends Error { constructor(code, details = {}) { @@ -19,15 +19,31 @@ function requiredValue(environment, key) { return typeof value === "string" ? value.trim() : ""; } -export function preflightPosthogLive(environment = process.env) { +export function parsePosthogLiveArguments(args = []) { + if (args.length === 0) return {}; + if (args.length === 1 && !args[0].startsWith("-")) return { baseUrl: args[0] }; + if (args.length === 2 && args[0] === "--base-url" && args[1].trim()) { + return { baseUrl: args[1] }; + } + throw new PosthogLivePreflightError("invalid_arguments"); +} + +export function preflightPosthogLive(environment = process.env, options = {}) { const missing = REQUIRED_ENVIRONMENT.filter((key) => requiredValue(environment, key) === ""); if (missing.length > 0) { throw new PosthogLivePreflightError("missing_environment", { missing }); } + const baseUrlValue = typeof options.baseUrl === "string" && options.baseUrl.trim() + ? options.baseUrl.trim() + : requiredValue(environment, "PAPERCLIP_API_URL"); + if (!baseUrlValue) { + throw new PosthogLivePreflightError("missing_base_url"); + } + let baseUrl; try { - baseUrl = new URL(requiredValue(environment, "PAPERCLIP_E2E_BASE_URL")); + baseUrl = new URL(baseUrlValue); } catch { throw new PosthogLivePreflightError("invalid_base_url"); } @@ -41,11 +57,14 @@ export function preflightPosthogLive(environment = process.env) { } baseUrl.pathname = baseUrl.pathname.replace(/\/+$/, "") || "/"; - const projectId = requiredValue(environment, "POSTHOG_PROJECT_ID"); + const projectId = requiredValue(environment, "INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID"); if (!/^\d+$/.test(projectId)) { throw new PosthogLivePreflightError("invalid_project_id"); } - const email = requiredValue(environment, "PAPERCLIP_E2E_EMAIL"); + if (projectId !== REQUIRED_PROJECT_ID) { + throw new PosthogLivePreflightError("unexpected_project_id"); + } + const email = requiredValue(environment, "INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL"); if (!email.includes("@")) { throw new PosthogLivePreflightError("invalid_email"); } @@ -53,17 +72,18 @@ export function preflightPosthogLive(environment = process.env) { return { baseUrl: baseUrl.origin, email, - password: environment.PAPERCLIP_DEV_LOGIN_PASSWORD, + password: environment.INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD, projectId, }; } export async function preparePosthogLiveSmoke({ environment = process.env, + baseUrl, fetchImpl = globalThis.fetch, loadBrowser, }) { - const config = preflightPosthogLive(environment); + const config = preflightPosthogLive(environment, { baseUrl }); let response; try { response = await fetchImpl(new URL("/api/health", config.baseUrl), { @@ -217,5 +237,11 @@ export function preflightFailureMessage(error) { if (error.code === "health_http_error") { return `PostHog live smoke preflight failed: /api/health returned HTTP ${error.details.status}.`; } + if (error.code === "missing_base_url") { + return "PostHog live smoke preflight failed: pass the target Paperclip URL or run inside a Paperclip heartbeat."; + } + if (error.code === "invalid_arguments") { + return "PostHog live smoke preflight failed: expected an optional Paperclip URL or --base-url ."; + } return `PostHog live smoke preflight failed: ${error.code}.`; } diff --git a/scripts/smoke/posthog-live.mjs b/scripts/smoke/posthog-live.mjs index dce88dc1402..69bb4cd13ae 100644 --- a/scripts/smoke/posthog-live.mjs +++ b/scripts/smoke/posthog-live.mjs @@ -5,6 +5,7 @@ import path from "node:path"; import { assertSanitizedEvidence, extractProjectSummary, + parsePosthogLiveArguments, parseSanitizedAgentProof, PosthogLivePreflightError, preflightFailureMessage, @@ -776,7 +777,9 @@ async function runSmoke({ config, chromium }) { async function main() { let prepared; try { + const options = parsePosthogLiveArguments(process.argv.slice(2)); prepared = await preparePosthogLiveSmoke({ + baseUrl: options.baseUrl, loadBrowser: () => import("@playwright/test"), }); } catch (error) { diff --git a/scripts/smoke/posthog-live.test.mjs b/scripts/smoke/posthog-live.test.mjs index 53b9cd8a1a6..5bdff161004 100644 --- a/scripts/smoke/posthog-live.test.mjs +++ b/scripts/smoke/posthog-live.test.mjs @@ -3,6 +3,7 @@ import test from "node:test"; import { assertSanitizedEvidence, extractProjectSummary, + parsePosthogLiveArguments, parseSanitizedAgentProof, PosthogLivePreflightError, preflightPosthogLive, @@ -10,22 +11,24 @@ import { } from "./posthog-live-lib.mjs"; const COMPLETE_ENV = { - PAPERCLIP_E2E_BASE_URL: "https://paperclip.example.test", - PAPERCLIP_E2E_EMAIL: "operator@example.test", - PAPERCLIP_DEV_LOGIN_PASSWORD: "not-a-real-password", - POSTHOG_PROJECT_ID: "483530", + PAPERCLIP_API_URL: "https://paperclip.example.test/api", + INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL: "operator@example.test", + INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD: "not-a-real-password", + INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID: "483530", }; test("preflight reports only missing binding names", () => { assert.throws( - () => preflightPosthogLive({ PAPERCLIP_DEV_LOGIN_PASSWORD: "present" }), + () => preflightPosthogLive({ + PAPERCLIP_API_URL: "https://paperclip.example.test/api", + INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD: "present", + }), (error) => { assert.ok(error instanceof PosthogLivePreflightError); assert.equal(error.code, "missing_environment"); assert.deepEqual(error.details.missing, [ - "PAPERCLIP_E2E_BASE_URL", - "PAPERCLIP_E2E_EMAIL", - "POSTHOG_PROJECT_ID", + "INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL", + "INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID", ]); assert.doesNotMatch(error.message, /present/); return true; @@ -40,16 +43,54 @@ test("preflight rejects credential-bearing and non-HTTPS remote URLs", () => { "http://example.test", ]) { assert.throws( - () => preflightPosthogLive({ ...COMPLETE_ENV, PAPERCLIP_E2E_BASE_URL: baseUrl }), + () => preflightPosthogLive({ ...COMPLETE_ENV, PAPERCLIP_API_URL: baseUrl }), (error) => error instanceof PosthogLivePreflightError && error.code === "unsafe_base_url", ); } assert.equal( - preflightPosthogLive({ ...COMPLETE_ENV, PAPERCLIP_E2E_BASE_URL: "http://127.0.0.1:3100" }).baseUrl, + preflightPosthogLive(COMPLETE_ENV, { baseUrl: "http://127.0.0.1:3100" }).baseUrl, "http://127.0.0.1:3100", ); }); +test("preflight derives the current Paperclip origin and accepts an explicit target", () => { + assert.equal(preflightPosthogLive(COMPLETE_ENV).baseUrl, "https://paperclip.example.test"); + assert.equal( + preflightPosthogLive(COMPLETE_ENV, { baseUrl: "https://other-paperclip.example.test" }).baseUrl, + "https://other-paperclip.example.test", + ); + assert.throws( + () => preflightPosthogLive({ ...COMPLETE_ENV, PAPERCLIP_API_URL: "" }), + (error) => error instanceof PosthogLivePreflightError && error.code === "missing_base_url", + ); +}); + +test("preflight fails closed unless the PostHog project is exactly 483530", () => { + assert.throws( + () => preflightPosthogLive({ + ...COMPLETE_ENV, + INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID: "42", + }), + (error) => error instanceof PosthogLivePreflightError && error.code === "unexpected_project_id", + ); +}); + +test("live smoke arguments accept a target URL without another environment binding", () => { + assert.deepEqual(parsePosthogLiveArguments([]), {}); + assert.deepEqual( + parsePosthogLiveArguments(["https://paperclip.example.test"]), + { baseUrl: "https://paperclip.example.test" }, + ); + assert.deepEqual( + parsePosthogLiveArguments(["--base-url", "https://paperclip.example.test"]), + { baseUrl: "https://paperclip.example.test" }, + ); + assert.throws( + () => parsePosthogLiveArguments(["--unknown"]), + (error) => error instanceof PosthogLivePreflightError && error.code === "invalid_arguments", + ); +}); + test("browser loading happens only after binding and health preflight", async () => { let fetchCalled = false; let browserLoaded = false;