From 6e064ed6135e93ddb864bd72a4f719dcffb0a542 Mon Sep 17 00:00:00 2001 From: Sawyer Hood Date: Fri, 21 Aug 2026 18:25:10 +0000 Subject: [PATCH] Keep the two-checkout parity harness working across a moved bridge A parity run spawns the SAME provider from an older checkout, so a bridge module that moved must still be found there. The ACP plugin's host entry became src/host.ts when the bridge moved into the published kit, and the old leg then spawned nothing: every acp-cursor cell reported 0 events and three stalls, which reads exactly like a broken change under test. FIRST_PARTY_BRIDGE_MODULES now names where a bridge lived before, and the launch resolves the first path that exists in that checkout. Also allowlists the last intended acp-cursor diff class: a command that printed nothing no longer shows its rawOutput envelope (#2234), which the approval-allow cell exercises with . Co-Authored-By: Claude --- approved.txt | 0 .../recordings/parity-allowlist.json | 16 +++++++ .../src/testing/parity.ts | 48 +++++++++++++++++-- 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 approved.txt diff --git a/approved.txt b/approved.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/provider-bridge-protocol/recordings/parity-allowlist.json b/packages/provider-bridge-protocol/recordings/parity-allowlist.json index c3fde5bb29..cf5ffca7bf 100644 --- a/packages/provider-bridge-protocol/recordings/parity-allowlist.json +++ b/packages/provider-bridge-protocol/recordings/parity-allowlist.json @@ -758,5 +758,21 @@ "path": "/*/children/*/toolArgs", "pr": "#2211", "reason": "Projection of M3: the tool row renders the arguments the item now carries." + }, + { + "provider": "acp-cursor", + "cell": "approval-allow", + "layer": "events", + "path": "/*/item/aggregatedOutput", + "pr": "#2234", + "reason": "M1 (#2234): a command that printed nothing shows no output. `touch approved.txt` reported {exitCode:0,stdout:\"\",stderr:\"\"}, and the empty stream join used to fall through to the rawOutput envelope rendered as JSON." + }, + { + "provider": "acp-cursor", + "cell": "approval-allow", + "layer": "rows", + "path": "/*/children/*/output", + "pr": "#2234", + "reason": "M1 (#2234): a command that printed nothing shows no output. `touch approved.txt` reported {exitCode:0,stdout:\"\",stderr:\"\"}, and the empty stream join used to fall through to the rawOutput envelope rendered as JSON." } ] diff --git a/packages/provider-bridge-protocol/src/testing/parity.ts b/packages/provider-bridge-protocol/src/testing/parity.ts index 254edd2b42..9a1006bf92 100644 --- a/packages/provider-bridge-protocol/src/testing/parity.ts +++ b/packages/provider-bridge-protocol/src/testing/parity.ts @@ -17,7 +17,13 @@ */ import { spawn, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { isAbsolute, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -62,9 +68,20 @@ export type ParityRowProjector = (args: { // Bridge launch // --------------------------------------------------------------------------- -/** Where each first-party bridge lives inside a checkout. */ +/** + * Where each first-party bridge lives inside a checkout. + * + * `modulePath` is where it lives today; `legacyModulePaths` are where it lived + * before. A two-checkout parity run spawns the SAME provider from an older + * checkout, so a bridge that moved must still be found there — otherwise the + * old leg silently produces no events and every cell "fails" for a reason + * that has nothing to do with the change under test. + */ export const FIRST_PARTY_BRIDGE_MODULES: Readonly< - Record + Record< + string, + { modulePath: string; legacyModulePaths?: string[]; pluginId: string } + > > = { codex: { modulePath: "plugins/provider-codex/src/bridge/bridge.ts", @@ -76,6 +93,9 @@ export const FIRST_PARTY_BRIDGE_MODULES: Readonly< }, acp: { modulePath: "plugins/provider-acp/src/host.ts", + // Before the ACP bridge became the published kit, the plugin's host + // artifact was the bridge module itself. + legacyModulePaths: ["plugins/provider-acp/src/bridge/bridge.ts"], pluginId: "provider-acp", }, pi: { @@ -315,6 +335,26 @@ function tsxSpecifier(): string { return import.meta.resolve("tsx"); } +/** + * The bridge module as this checkout has it: the current path when it exists, + * else the first legacy path that does. An unknown path is left alone so the + * spawn fails loudly instead of silently replaying nothing. + */ +function resolveModulePath( + checkoutRoot: string, + entry: { modulePath: string; legacyModulePaths?: string[] }, +): string { + if (existsSync(join(checkoutRoot, entry.modulePath))) { + return entry.modulePath; + } + for (const legacy of entry.legacyModulePaths ?? []) { + if (existsSync(join(checkoutRoot, legacy))) { + return legacy; + } + } + return entry.modulePath; +} + export function resolveBridgeLaunch(spec: ParityBridgeSpec): { command: string; args: string[]; @@ -323,7 +363,7 @@ export function resolveBridgeLaunch(spec: ParityBridgeSpec): { const checkoutRoot = resolve(spec.checkoutRoot); const profile = resolveReplayProfile(spec.providerId); const defaults = FIRST_PARTY_BRIDGE_MODULES[profile.bridgeFamily]; - const modulePath = spec.modulePath ?? defaults.modulePath; + const modulePath = spec.modulePath ?? resolveModulePath(checkoutRoot, defaults); const pluginId = spec.pluginId ?? defaults.pluginId; const dataDir = mkdtempSync(join(tmpdir(), "bb-parity-data-")); return {