From cbfc2bd9c9839e776121882398c34a11d4d025df Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:04:11 +0800 Subject: [PATCH] fix: retain unresolved browser secret declarations as input errors --- docs/browser-scenario-contract.md | 2 +- src/browser/BrowserScenario.test.ts | 58 ++++++++++------- src/browser/BrowserScenarioSecrets.ts | 28 ++++++-- .../PlaywrightScenarioArtifacts.test.ts | 21 ++---- ...wrightScenarioEvents.sharedContext.test.ts | 14 ++-- src/browser/PlaywrightScenarioEvents.test.ts | 20 ++---- src/browser/PlaywrightScenarioSession.test.ts | 64 +++++++++++++++++++ src/browser/PlaywrightScenarioSession.ts | 6 +- src/browser/PlaywrightScenarioStorage.test.ts | 10 +-- .../network/PlaywrightScenarioNetwork.test.ts | 5 +- src/domain/analysisErrorProjection.ts | 1 - src/domain/browserObservationErrors.ts | 3 +- tests/fixtures/browserScenarioSecrets.ts | 12 ++++ 13 files changed, 165 insertions(+), 79 deletions(-) create mode 100644 tests/fixtures/browserScenarioSecrets.ts diff --git a/docs/browser-scenario-contract.md b/docs/browser-scenario-contract.md index 7f165f18f7..33ae5f9d00 100644 --- a/docs/browser-scenario-contract.md +++ b/docs/browser-scenario-contract.md @@ -26,7 +26,7 @@ Navigation can run destination code before a newly discovered worker is rejected that rejection does not roll back browser effects. REA removes its owned future-document scripts, restores live frame methods, then disconnects. Add secret declarations, storage seeds, and additional artifact or event -capture only when needed. Action, secret, and storage counts are not capped. +capture only when needed. Missing environment-backed secrets fail as input errors before browser acquisition, naming the declaration and required variable. An explicitly set empty string is a resolved value. Action, secret, and storage counts are not capped. Duration, action, and navigation timeouts remain separate from CDP command deadlines. `command_timeout_ms` (default 20000; omit it to keep that default and leave it out of Evidence) bounds launch startup, the connect-over-CDP diff --git a/src/browser/BrowserScenario.test.ts b/src/browser/BrowserScenario.test.ts index f5cd8e1a54..a30c82f85c 100644 --- a/src/browser/BrowserScenario.test.ts +++ b/src/browser/BrowserScenario.test.ts @@ -3,6 +3,7 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js"; +import { resolveScenarioSecrets } from "../../tests/fixtures/browserScenarioSecrets.js"; import { browserScenarioSchema } from "../domain/browserScenario.js"; import { DEFAULT_BROWSER_SCENARIO_CAPTURE_LIMITS } from "../domain/browserScenarioCaptureLimits.js"; @@ -85,7 +86,7 @@ const baseScenario = () => ({ describe("browser scenario secret redaction", () => { it("matches percent-escape hex case without case-folding raw secrets", () => { - const secrets = BrowserScenarioSecrets.resolve( + const secrets = resolveScenarioSecrets( browserScenarioSchema.parse(baseScenario()), { REA_TEST_SESSION: "a/b?", @@ -93,7 +94,6 @@ describe("browser scenario secret redaction", () => { REA_TEST_COOKIE: "cookie", }, ); - if (secrets === undefined) throw new Error("Expected resolved secrets"); expect(secrets.redact("logged a%2fb%3f, a%2Fb%3F and a%2fb%3F")).toBe( "logged [REDACTED:session], [REDACTED:session] and [REDACTED:session]", @@ -124,10 +124,34 @@ describe("browser scenario secret redaction", () => { value_sha256: null, }); }); + it("does not resolve inherited environment properties as secret values", () => { + const scenario = browserScenarioSchema.parse(baseScenario()); + const environment = Object.assign( + { REA_TEST_PASSWORD: "password", REA_TEST_COOKIE: "cookie" }, + Object.create({ REA_TEST_SESSION: "session" }) as object, + ); + expect(BrowserScenarioSecrets.resolve(scenario, environment)).toMatchObject( + { + ok: false, + error: { + _tag: "AnalysisInputError", + issues: [ + { + path: ["secrets", 0, "environment_variable"], + expected: { + environment_variable: "REA_TEST_SESSION", + secret_id: "session", + }, + }, + ], + }, + }, + ); + }); }); describe("storage value fingerprint encoding", () => { - const secrets = BrowserScenarioSecrets.resolve( + const secrets = resolveScenarioSecrets( browserScenarioSchema.parse(baseScenario()), { REA_TEST_SESSION: "declared-session", @@ -135,7 +159,6 @@ describe("storage value fingerprint encoding", () => { REA_TEST_COOKIE: "declared-cookie", }, ); - if (secrets === undefined) throw new Error("Expected resolved secrets"); it.each(["", "ordinary", "\u0000", "\u0620", "\ufffd", "\ud83d\ude00"])( "preserves the UTF-8 digest of well-formed %j", @@ -242,16 +265,16 @@ describe("browserScenarioSchema", () => { it("redacts overlapping secrets longest-first", () => { const scenario = browserScenarioSchema.parse(baseScenario()); - const secrets = BrowserScenarioSecrets.resolve(scenario, { + const secrets = resolveScenarioSecrets(scenario, { REA_TEST_SESSION: "prefix", REA_TEST_PASSWORD: "prefix-suffix", REA_TEST_COOKIE: "cookie", }); - expect(secrets?.redact("prefix-suffix")).toBe("[REDACTED:login_input]"); + expect(secrets.redact("prefix-suffix")).toBe("[REDACTED:login_input]"); }); it("does not leave a declared secret inside its redaction marker", () => { - const secrets = BrowserScenarioSecrets.resolve( + const secrets = resolveScenarioSecrets( browserScenarioSchema.parse({ ...baseScenario(), secrets: [ @@ -269,11 +292,11 @@ describe("browserScenarioSchema", () => { REA_TEST_SHORT_PASSWORD: "pass", }, ); - expect(secrets?.redact("seen pass in the page")).toBe( + expect(secrets.redact("seen pass in the page")).toBe( "seen [REDACTED] in the page", ); - const bytes = BrowserScenarioSecrets.resolve( + const bytes = resolveScenarioSecrets( browserScenarioSchema.parse({ ...baseScenario(), secrets: [ @@ -292,20 +315,9 @@ describe("browserScenarioSchema", () => { REA_TEST_LONG_SECRET: "session-secret-value", REA_TEST_PIN: "pass", }, - )?.redactBytes(Buffer.from("body session-secret-value tail")); - expect(bytes?.toString("utf8")).toBe("body [REDACTED] tail"); - expect(bytes?.includes(Buffer.from("pass"))).toBe(false); - }); - - it("does not resolve inherited environment properties as secret values", () => { - const scenario = browserScenarioSchema.parse(baseScenario()); - const environment = Object.assign( - { REA_TEST_PASSWORD: "password", REA_TEST_COOKIE: "cookie" }, - Object.create({ REA_TEST_SESSION: "session" }) as object, - ); - expect( - BrowserScenarioSecrets.resolve(scenario, environment), - ).toBeUndefined(); + ).redactBytes(Buffer.from("body session-secret-value tail")); + expect(bytes.toString("utf8")).toBe("body [REDACTED] tail"); + expect(bytes.includes(Buffer.from("pass"))).toBe(false); }); it("rejects unsupported actions", () => { diff --git a/src/browser/BrowserScenarioSecrets.ts b/src/browser/BrowserScenarioSecrets.ts index f2f49a82e8..19a097a911 100644 --- a/src/browser/BrowserScenarioSecrets.ts +++ b/src/browser/BrowserScenarioSecrets.ts @@ -1,3 +1,5 @@ +import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import { err, ok, type Result } from "../domain/result.js"; import { createHash } from "node:crypto"; import type { BrowserScenario } from "../domain/browserScenario.js"; @@ -256,18 +258,36 @@ export class BrowserScenarioSecrets { static resolve( scenario: BrowserScenario, environment: Readonly>, - ): BrowserScenarioSecrets | undefined { + ): Result { const values = new Map(); - for (const declaration of scenario.secrets) { + for (const [index, declaration] of scenario.secrets.entries()) { const value = environment[declaration.environment_variable]; if ( !Object.hasOwn(environment, declaration.environment_variable) || typeof value !== "string" ) - return undefined; + return err( + new AnalysisInputError( + "capture_browser_scenario", + { + userMessage: `Set environment variable ${declaration.environment_variable} for browser scenario secret ${declaration.secret_id} in the environment that starts REA, then retry.`, + }, + [ + { + path: ["secrets", index, "environment_variable"], + reason: "missing_argument", + message: `Environment variable ${declaration.environment_variable} is not set for declared secret ${declaration.secret_id}.`, + expected: { + secret_id: declaration.secret_id, + environment_variable: declaration.environment_variable, + }, + }, + ], + ), + ); values.set(declaration.secret_id, value); } - return new BrowserScenarioSecrets(values); + return ok(new BrowserScenarioSecrets(values)); } value(source: BrowserScenarioValue): string { diff --git a/src/browser/PlaywrightScenarioArtifacts.test.ts b/src/browser/PlaywrightScenarioArtifacts.test.ts index d594b42a90..c4c2c9d382 100644 --- a/src/browser/PlaywrightScenarioArtifacts.test.ts +++ b/src/browser/PlaywrightScenarioArtifacts.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import type { BrowserContext, Page } from "playwright-core"; import { browserScenarioSchema } from "../domain/browserScenario.js"; -import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js"; +import { resolveScenarioSecrets } from "../../tests/fixtures/browserScenarioSecrets.js"; import { capturePlaywrightStepArtifacts } from "./PlaywrightScenarioArtifacts.js"; const scenario = browserScenarioSchema.parse({ @@ -13,9 +13,7 @@ const scenario = browserScenarioSchema.parse({ start_url: { url: "https://app.example.test/" }, actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }], }); -const secrets = BrowserScenarioSecrets.resolve(scenario, {}); -if (secrets === undefined) - throw new Error("Expected resolved scenario secrets"); +const secrets = resolveScenarioSecrets(scenario, {}); it("returns a complete requested text artifact beyond the former inline ceiling", async () => { const page = { @@ -57,14 +55,9 @@ it("redacts declared secrets from current and historical URL artifacts", async ( ], capture: { at_end: ["url", "history"] }, }); - const scenarioSecrets = BrowserScenarioSecrets.resolve( - scenarioWithSecretUrl, - { - REA_URL_TOKEN: secretValue, - }, - ); - if (scenarioSecrets === undefined) - throw new Error("Expected resolved URL secret"); + const scenarioSecrets = resolveScenarioSecrets(scenarioWithSecretUrl, { + REA_URL_TOKEN: secretValue, + }); const query = new URLSearchParams([["token", secretValue]]).toString(); const url = `https://app.example.test/?${query}#${encodeURIComponent(secretValue)}`; const page = { @@ -121,11 +114,9 @@ it("redacts a declared secret used as a cookie or storage name", async () => { { secret_id: "storage_key", environment_variable: "REA_STORAGE_KEY" }, ], }); - const scenarioSecrets = BrowserScenarioSecrets.resolve(scenarioWithSecret, { + const scenarioSecrets = resolveScenarioSecrets(scenarioWithSecret, { REA_STORAGE_KEY: secretValue, }); - if (scenarioSecrets === undefined) - throw new Error("Expected resolved storage secret"); const page = { url: () => "https://app.example.test/", evaluate: async () => ({ diff --git a/src/browser/PlaywrightScenarioEvents.sharedContext.test.ts b/src/browser/PlaywrightScenarioEvents.sharedContext.test.ts index 5a304976f3..0c48895c93 100644 --- a/src/browser/PlaywrightScenarioEvents.sharedContext.test.ts +++ b/src/browser/PlaywrightScenarioEvents.sharedContext.test.ts @@ -3,7 +3,7 @@ import type { Page } from "playwright-core"; import { expect, it } from "vitest"; import { browserScenarioSchema } from "../domain/browserScenario.js"; -import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js"; +import { resolveScenarioSecrets } from "../../tests/fixtures/browserScenarioSecrets.js"; import { PlaywrightScenarioEvents } from "./PlaywrightScenarioEvents.js"; it("omits a request without a frame in a shared context", () => { @@ -15,8 +15,7 @@ it("omits a request without a frame in a shared context", () => { actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }], capture: { events: ["network"] }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("Expected resolved secrets"); + const secrets = resolveScenarioSecrets(scenario, {}); const events = new PlaywrightScenarioEvents({ page, context: context as never, @@ -50,8 +49,7 @@ it("detaches root and popup listeners when a scenario finishes", async () => { actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }], capture: { events: ["console", "popups"] }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("Expected resolved secrets"); + const secrets = resolveScenarioSecrets(scenario, {}); const events = new PlaywrightScenarioEvents({ page, context: context as never, @@ -87,8 +85,7 @@ it("detaches worker and websocket listeners when a scenario finishes", async () actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }], capture: { events: ["websockets", "workers"] }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("Expected resolved secrets"); + const secrets = resolveScenarioSecrets(scenario, {}); const events = new PlaywrightScenarioEvents({ page, context: context as never, @@ -116,8 +113,7 @@ it("detaches owned-context popup discovery when a scenario finishes", async () = actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }], capture: { events: ["console"] }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("Expected resolved secrets"); + const secrets = resolveScenarioSecrets(scenario, {}); const events = new PlaywrightScenarioEvents({ page, context: context as never, diff --git a/src/browser/PlaywrightScenarioEvents.test.ts b/src/browser/PlaywrightScenarioEvents.test.ts index c813d3f290..d8da9bcc05 100644 --- a/src/browser/PlaywrightScenarioEvents.test.ts +++ b/src/browser/PlaywrightScenarioEvents.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import type { BrowserContext, Page, Request, Response } from "playwright-core"; import { browserScenarioSchema } from "../domain/browserScenario.js"; -import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js"; +import { resolveScenarioSecrets } from "../../tests/fixtures/browserScenarioSecrets.js"; import { PlaywrightScenarioEvents } from "./PlaywrightScenarioEvents.js"; describe("declared secrets in scenario events", () => { @@ -26,10 +26,9 @@ describe("declared secrets in scenario events", () => { ], capture: { events: ["console", "page-errors", "websockets"] }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, { + const secrets = resolveScenarioSecrets(scenario, { REA_TEST_TOKEN: secret, }); - if (secrets === undefined) throw new Error("Expected resolved secrets"); const events = new PlaywrightScenarioEvents({ page, enabled: new Set(["console", "page-errors", "websockets"]), @@ -109,8 +108,7 @@ const captureNetworkBudgetFixture = async ( }), }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("Expected resolved secrets"); + const secrets = resolveScenarioSecrets(scenario, {}); const redactBytes = vi.spyOn(secrets, "redactBytes"); const capture = new PlaywrightScenarioEvents({ page, @@ -216,8 +214,7 @@ describe("PlaywrightScenarioEvents", () => { ], capture: { events: ["page-errors"] }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("Expected resolved secrets"); + const secrets = resolveScenarioSecrets(scenario, {}); const events = new PlaywrightScenarioEvents({ page, enabled: new Set(["page-errors"]), @@ -266,8 +263,7 @@ describe("PlaywrightScenarioEvents", () => { events: ["page-errors"], }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("Expected resolved secrets"); + const secrets = resolveScenarioSecrets(scenario, {}); const events = new PlaywrightScenarioEvents({ page, enabled: new Set(["page-errors"]), @@ -304,8 +300,7 @@ it("retains ordinary event volumes and reports event-count exhaustion", () => { actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }], capture: { events: ["page-errors"] }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("Expected resolved secrets"); + const secrets = resolveScenarioSecrets(scenario, {}); const events = new PlaywrightScenarioEvents({ page, enabled: new Set(["page-errors"]), @@ -382,8 +377,7 @@ it("caps live descendant listeners and reuses capacity after closure", () => { start_url: { url: "https://example.test" }, actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }], }); - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("Expected resolved secrets"); + const secrets = resolveScenarioSecrets(scenario, {}); const events = new PlaywrightScenarioEvents({ page, enabled: new Set(["websockets"]), diff --git a/src/browser/PlaywrightScenarioSession.test.ts b/src/browser/PlaywrightScenarioSession.test.ts index 9bbd4a3bb0..715f5a8845 100644 --- a/src/browser/PlaywrightScenarioSession.test.ts +++ b/src/browser/PlaywrightScenarioSession.test.ts @@ -1559,3 +1559,67 @@ it.each([false, true])( } }, ); + +it.each(["absent", "undefined", "inherited", "empty"] as const)( + "resolves declared environment secrets before browser acquisition: %s", + async (state) => { + const { page } = fixture(); + Object.assign(page, { waitForTimeout: async () => undefined }); + const environment: Record = { + AVAILABLE_SECRET: "already-supplied-private-value", + }; + if (state === "inherited") + Object.setPrototypeOf(environment, { + REQUIRED_SECRET: "inherited-value", + }); + if (state === "undefined") environment.REQUIRED_SECRET = undefined; + if (state === "empty") environment.REQUIRED_SECRET = ""; + const input = browserScenarioSchema.parse({ + ...scenario, + secrets: [ + { secret_id: "available", environment_variable: "AVAILABLE_SECRET" }, + { secret_id: "required", environment_variable: "REQUIRED_SECRET" }, + ], + }); + const provider = new PlaywrightBrowserScenarioProvider( + (scenario, options) => + PlaywrightScenarioSession.open(scenario, environment, options), + ); + try { + const result = await provider.captureScenario(input, {}); + if (state === "empty") { + if (!result.ok) throw result.error; + expect(mocks.openBrowser).toHaveBeenCalledOnce(); + } else { + if (result.ok) throw new Error("Expected unresolved environment input"); + const projected = projectAnalysisError(result.error); + expect(projected).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + issues: [ + { + path: ["secrets", 1, "environment_variable"], + reason: "missing_argument", + expected: { + secret_id: "required", + environment_variable: "REQUIRED_SECRET", + }, + }, + ], + }, + }); + expect(projected.message).toContain( + "Set environment variable REQUIRED_SECRET", + ); + expect(JSON.stringify(projected)).not.toContain( + environment.AVAILABLE_SECRET, + ); + expect(result.error.cleanupIncomplete).toBe(false); + expect(mocks.openBrowser).not.toHaveBeenCalled(); + } + } finally { + await provider.close(); + } + }, +); diff --git a/src/browser/PlaywrightScenarioSession.ts b/src/browser/PlaywrightScenarioSession.ts index bbcac22ab6..31cb3ae90a 100644 --- a/src/browser/PlaywrightScenarioSession.ts +++ b/src/browser/PlaywrightScenarioSession.ts @@ -147,9 +147,9 @@ export class PlaywrightScenarioSession implements BrowserScenarioSessionPort { }, ): Promise { throwIfScenarioCancelled(options.signal); - const secrets = BrowserScenarioSecrets.resolve(scenario, environment); - if (secrets === undefined) - throw new BrowserObservationError(OPERATION, "secret_unavailable"); + const resolved = BrowserScenarioSecrets.resolve(scenario, environment); + if (!resolved.ok) throw resolved.error; + const secrets = resolved.value; const startedAt = performance.now(); const openingCleanup = new Set(); const opening = openPlaywrightScenarioBrowser(scenario, environment, { diff --git a/src/browser/PlaywrightScenarioStorage.test.ts b/src/browser/PlaywrightScenarioStorage.test.ts index dde3632e1c..0518f56a4c 100644 --- a/src/browser/PlaywrightScenarioStorage.test.ts +++ b/src/browser/PlaywrightScenarioStorage.test.ts @@ -3,7 +3,7 @@ import { EventEmitter } from "node:events"; import { expect, it } from "vitest"; import { browserScenarioSchema } from "../domain/browserScenario.js"; -import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js"; +import { resolveScenarioSecrets } from "../../tests/fixtures/browserScenarioSecrets.js"; import { PlaywrightScenarioStorage } from "./PlaywrightScenarioStorage.js"; class FakeCdp extends EventEmitter { @@ -160,7 +160,7 @@ const createInitializedStorage = async ( const owner = await PlaywrightScenarioStorage.create( { context: context as never, page: page as never }, input, - BrowserScenarioSecrets.resolve(input, {})!, + resolveScenarioSecrets(input, {}), options, ); await owner?.initialize(page as never, context as never); @@ -427,7 +427,7 @@ it("reports an unconfirmed stop when root initialization is still pending", asyn const owner = await PlaywrightScenarioStorage.create( { context: contextApi as never, page: page as never }, input, - BrowserScenarioSecrets.resolve(input, {})!, + resolveScenarioSecrets(input, {}), ); expect(owner).toBeDefined(); const initialization = owner?.initialize(page as never, contextApi as never); @@ -471,7 +471,7 @@ it("does not create storage instrumentation when there are no storage seeds", as await PlaywrightScenarioStorage.create( { context: contextApi as never, page: page as never }, empty, - BrowserScenarioSecrets.resolve(empty, {})!, + resolveScenarioSecrets(empty, {}), ), ).toBeUndefined(); expect(cdp.calls).toEqual([]); @@ -486,7 +486,7 @@ it("detaches after setup cancellation even while script installation is pending" const owner = await PlaywrightScenarioStorage.create( { context: contextApi as never, page: page as never }, input, - BrowserScenarioSecrets.resolve(input, {})!, + resolveScenarioSecrets(input, {}), { signal: controller.signal }, ); expect(owner).toBeDefined(); diff --git a/src/browser/network/PlaywrightScenarioNetwork.test.ts b/src/browser/network/PlaywrightScenarioNetwork.test.ts index d30e6b7496..12e82a8335 100644 --- a/src/browser/network/PlaywrightScenarioNetwork.test.ts +++ b/src/browser/network/PlaywrightScenarioNetwork.test.ts @@ -7,7 +7,7 @@ import { type BrowserScenarioEvent, } from "../../domain/browserScenarioCaptureValues.js"; import type { BrowserNetworkContentSelection } from "../../domain/browserNetworkEvidence.js"; -import { BrowserScenarioSecrets } from "../BrowserScenarioSecrets.js"; +import { resolveScenarioSecrets } from "../../../tests/fixtures/browserScenarioSecrets.js"; import { PlaywrightScenarioNetwork, type ScenarioNetworkRequest, @@ -112,10 +112,9 @@ const harness = ( ], capture: { limits: limitsInput }, }); - const secrets = BrowserScenarioSecrets.resolve(scenario, { + const secrets = resolveScenarioSecrets(scenario, { FIXTURE_SECRET: secretValue, }); - if (secrets === undefined) throw new Error("Fixture secret was not resolved"); const events: BrowserScenarioEvent[] = []; const omittedEventCounts = new Map(); const collector = new PlaywrightScenarioNetwork({ diff --git a/src/domain/analysisErrorProjection.ts b/src/domain/analysisErrorProjection.ts index abbf8a4549..924c84030a 100644 --- a/src/domain/analysisErrorProjection.ts +++ b/src/domain/analysisErrorProjection.ts @@ -207,7 +207,6 @@ const BROWSER_ERROR_CODES: Readonly< cleanup_failed: "execution_failure", cancelled: "cancelled", timeout: "provider_timeout", - secret_unavailable: "unreadable_output", }; const artifactOperationCode = ( diff --git a/src/domain/browserObservationErrors.ts b/src/domain/browserObservationErrors.ts index 3027553f51..c13928bd02 100644 --- a/src/domain/browserObservationErrors.ts +++ b/src/domain/browserObservationErrors.ts @@ -35,5 +35,4 @@ export type BrowserObservationFailureReason = | "process_ownership_unavailable" | "cleanup_failed" | "cancelled" - | "timeout" - | "secret_unavailable"; + | "timeout"; diff --git a/tests/fixtures/browserScenarioSecrets.ts b/tests/fixtures/browserScenarioSecrets.ts new file mode 100644 index 0000000000..847eb5a214 --- /dev/null +++ b/tests/fixtures/browserScenarioSecrets.ts @@ -0,0 +1,12 @@ +import type { BrowserScenario } from "../../src/domain/browserScenario.js"; +import { BrowserScenarioSecrets } from "../../src/browser/BrowserScenarioSecrets.js"; + +/** Resolve valid fixture declarations, failing with the producer's typed input error. */ +export const resolveScenarioSecrets = ( + scenario: BrowserScenario, + environment: Readonly>, +): BrowserScenarioSecrets => { + const result = BrowserScenarioSecrets.resolve(scenario, environment); + if (!result.ok) throw result.error; + return result.value; +};