Repository navigation
feat: add bail-on-failure, granular recording, env var interpolation,secret masking, and trace viewer control - #15
Conversation
… secret masking, and trace viewer control
Five new config options to improve CI/CD ergonomics:
1. `bailOnFailure` (bool, default false): stop test run after first failure to save LLM credits and CI time
2. `recording` now accepts `{ trace: bool, video: bool }` in addition to boolean, allowing trace-only recording (useful when ffmpeg is unavailable in containers)
3. `showTrace` (bool, default true): when false, passes `--no-show-trace` to Playwright MCP to prevent auto-launching trace viewer in headless/CI environments
4. Environment variable interpolation in YAML configs: `${VAR}` and `$VAR` syntax, replacing unset vars with empty string — eliminates the need for external `envsubst`
5. `secrets` (string[], default []): values are masked as "***" in all reporter output to prevent credentials from leaking into CI logs
All changes are backwards-compatible — existing configs work without modification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| // Note: --no-show-trace is not yet supported by @playwright/mcp. | ||
| // When a future version adds this flag, uncomment to prevent | ||
| // auto-launching the trace viewer in headless/CI environments. | ||
| // if (config.showTrace === false) { | ||
| // playwrightArgs.push("--no-show-trace"); | ||
| // } |
| /** @deprecated Use `outcome` instead */ | ||
| passed: boolean; |
There was a problem hiding this comment.
Can we remove deprecated code - it's still early enough in the project that I'm not worried about backward compatability
| bailOnFailure: z.boolean().default(false), | ||
| showTrace: z.boolean().default(true), | ||
| viewportSize: z.string().regex(/^\d+x\d+$/, "Must be WIDTHxHEIGHT, e.g. '1280x720'").default("1280x720"), | ||
| secrets: z.array(z.string()).default([]), |
There was a problem hiding this comment.
What's the difference between secrets and just environment interpolation? What's the use-case for this?
Isn't there a case here where those are merged into a single concept and we just mask any env?
I'm mostly trying to think about it from the final config stand point - can we make it stupid simple that any AI / product manager can write those?
There was a problem hiding this comment.
The Difference
Environment interpolation (${VAR}) is for injection (getting values into the config), while secrets is for redaction (masking values in logs). You might interpolate a BASE_URL, which isn't a secret and should be visible in logs. However, a TEST_PASSWORD should be interpolated and masked.
Current implementation
secrets takes a list of env var names. The loader automatically resolves these to their values for the masker - see example config in PR description. We could potentially automatically mask any environment variable that is interpolated but some env vars (like URLs or browser types) are useful to see in logs.
Alternative
We could use a naming convention (e.g., any env var starting with SECRET_ is automatically masked) or a special interpolation syntax like ${SECRET:VAR_NAME}.
|
@techjacker - any thoughts on the comments above? |
- Fix per-test duration calculation in `test-runner.ts` — the catch block was using the run-level `startTime` instead of a per-test start time, reporting incorrectly high durations for failed tests that weren't the first in the suite - Fix recording paths in reporter output — previously always printed both `trace.zip` and `video.webm` paths even when video recording was disabled. Now shows just the recording directory, avoiding references to artifacts that don't exist Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
26bff67 to
f78080d
Compare
Summary -
v0.2.0bailOnFailureconfig option to stop test run after first failure, saving LLM credits and CI timerecording: { trace: true, video: false }alongside the existing boolean, enabling trace-only recording when ffmpeg is unavailable (common in containers)${VAR}/$VARenvironment variable interpolation in YAML config files, eliminating the need for external tools likeenvsubst. Referencing an unset variable is a hard error — throwsConfigLoadErrorlisting all missing variable namesviewportSizeconfig option (default:1280x720) to explicitly set browser viewport dimensions via--viewport-sizeflag, ensuring responsive web apps render at the expected breakpointsecretsconfig option for masking sensitive values as***in all reporter output. Secrets are specified as environment variable names (e.g.TEST_PASSWORD) — the loader resolves them to values viaprocess.env, so the YAML never contains raw credentialsTEST_SKIPPED/"skipped"test status so the AI agent can skip tests when appropriate (e.g. a resource is already being processed). The agent's system prompt now includesTEST_SKIPPEDas a valid response, and the runner handles it without retries or cache updatesdisplayNamesupport — tests with anamefield use it as the display label; unnamed tests show a truncated (80-char) version of the test case prompt. All reporter output uses the display name instead of the raw prompt textmessagereporting — the agent's natural language response (TEST_PASSED: ...,TEST_FAILED: ...,TEST_SKIPPED: ...) is now captured asTestResult.messageand shown in reporter output for all outcomes, giving per-test detail without repeating the full promptMotivation
These features address common pain points when running OpenCheck in CI/CD pipelines:
--save-videofail. Being able to record traces without video solves thisenvsubstisn't available on all platforms (e.g. Alpine, Windows); built-in interpolation is more portable. Failing on unset variables prevents silent misconfiguration (e.g. an emptybaseUrlfrom a missing$E2E_BASE_URL)viewportSizeensures tests run against a known layout breakpoint[RUNNING],[PASS]/[FAIL]lines, and the recordings section, making output hard to scan. Named tests and agent messages make output concise and informativeChanges
Config schema (
src/config/schema.ts)recordingfield now acceptsboolean | { trace: boolean, video: boolean }via Zod unionbailOnFailure: boolean(default:false)viewportSize: string(default:"1280x720", validated asWIDTHxHEIGHTformat)secrets: string[](default:[]) — values are environment variable names, not raw secretsConfig types (
src/config/types.ts)resolveRecording()helper to normalizeboolean | objectrecording config to{ trace, video }RecordingOptionstype exportConfig loader (
src/config/loader.ts)interpolateEnvVars()that replaces${VAR}and$VARpatterns withprocess.envvalues before YAML parsingConfigLoadErrorlisting all missing variable names — fail-fast prevents silent misconfigurationresolveSecrets()that maps env var names insecretsto theirprocess.envvalues, filtering out any that are unsetMCP client (
src/agent/mcp-client.ts)resolveRecording()to independently control--save-traceand--save-videoflags--viewport-sizefrom config to Playwright MCP, ensuring the browser opens at the specified dimensionsAgent types (
src/agent/types.ts)AgentOutcometype:"passed" | "failed" | "skipped"AgentExecutionResultusesoutcome: AgentOutcomeas the sole status field (no legacypassed: boolean)Agent factory (
src/agent/agent-factory.ts)TEST_SKIPPED: <brief explanation>as a valid response alongsideTEST_PASSEDandTEST_FAILEDTEST_SKIPPEDand setsoutcome: "skipped"Test executor (
src/runner/test-executor.ts)outcome === "skipped"— returns immediately without cache save, cache delete, or retryagentResult.messageinTestResult.messagefor all outcomes (pass/fail/skip)Runner types (
src/runner/types.ts)TestStatusnow includes"skipped"alongside"passed"and"failed"RunResultincludesskippedcountTestResultincludesdisplayName: stringand optionalmessage?: stringTest runner (
src/runner/test-runner.ts)displayNamefromtest.name(if set) ortruncateTestCase(test.case)(80-char truncation)displayNameandmessageto all reporter methodsonTestCompletewhenbailOnFailureistrueand test failedaggregateResultscounts skipped teststestStartTime(set before each test) instead of run-levelstartTimeReporter interface (
src/output/types.ts)onTestStart(displayName)— receives display name instead of raw test caseonTestComplete(displayName, status, source, durationMs, message?, error?)— receives display name and agent messageReporter (
src/output/reporter.ts)SecretMaskerin constructoronTestStart,onTestComplete,onRunCompletesummary) apply masking beforeconsole.log[SKIP]prefix for skipped testsSkipped:count in summary tablemessage(agent's response text) for all test outcomes under the status lineerroronly when it differs frommessage(avoids duplication)displayNameinstead of rawtestCaseDir: <path>instead of hardcoded trace.zip/video.webm filenamesSecret masker (
src/output/secret-masker.ts) — new file***using escaped regex patternsCLI (
src/cli.ts)config.secrets(already resolved to values by loader) intoSecretMasker→ConsoleReporterExample config
Example output with
nameandmessage:Backwards compatibility
All new fields have defaults matching previous behavior:
bailOnFailure: false— tests continue after failure (existing behavior)recording: true— both trace and video enabled (existing behavior);recording: { trace: true, video: false }is the new granular formviewportSize: "1280x720"— reasonable default; can be overridden for apps with wider responsive breakpointssecrets: []— no masking (existing behavior)$are unchangedTEST_SKIPPEDis opt-in — the agent only uses it when the test case instructions suggest skipping is appropriate. Existing test cases that only useTEST_PASSED/TEST_FAILEDare unaffecteddisplayNamedefaults to a truncated version oftestCasewhen nonameis set — existing configs see cleaner output without changesmessageis optional and only populated when the agent includes a response — no impact on configs that don't use itTest plan
bun run test)tsc --noEmit)bailOnFailure: trueand a failing first test — verify remaining tests are skippedrecording: { trace: true, video: false }— verify trace captured, no video filesecrets: ["password"]and a test containing "password" — verify***in output${MISSING_VAR}in config — verify clear error message naming the variable[SKIP]status andSkipped: 1in summary