mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix: retain unresolved browser secret declarations as input errors
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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<Record<string, string | undefined>>,
|
||||
): BrowserScenarioSecrets | undefined {
|
||||
): Result<BrowserScenarioSecrets, AnalysisInputError> {
|
||||
const values = new Map<string, string>();
|
||||
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 {
|
||||
|
||||
@@ -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 () => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
@@ -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<string, string | undefined> = {
|
||||
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();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -147,9 +147,9 @@ export class PlaywrightScenarioSession implements BrowserScenarioSessionPort {
|
||||
},
|
||||
): Promise<PlaywrightScenarioSession> {
|
||||
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<AnalysisCleanupLease>();
|
||||
const opening = openPlaywrightScenarioBrowser(scenario, environment, {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<string, number>();
|
||||
const collector = new PlaywrightScenarioNetwork({
|
||||
|
||||
@@ -207,7 +207,6 @@ const BROWSER_ERROR_CODES: Readonly<
|
||||
cleanup_failed: "execution_failure",
|
||||
cancelled: "cancelled",
|
||||
timeout: "provider_timeout",
|
||||
secret_unavailable: "unreadable_output",
|
||||
};
|
||||
|
||||
const artifactOperationCode = (
|
||||
|
||||
@@ -35,5 +35,4 @@ export type BrowserObservationFailureReason =
|
||||
| "process_ownership_unavailable"
|
||||
| "cleanup_failed"
|
||||
| "cancelled"
|
||||
| "timeout"
|
||||
| "secret_unavailable";
|
||||
| "timeout";
|
||||
|
||||
+12
@@ -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<Record<string, string | undefined>>,
|
||||
): BrowserScenarioSecrets => {
|
||||
const result = BrowserScenarioSecrets.resolve(scenario, environment);
|
||||
if (!result.ok) throw result.error;
|
||||
return result.value;
|
||||
};
|
||||
Reference in New Issue
Block a user