ccqa 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ //#region src/runtime/evidence-constants.ts
2
+ /**
3
+ * Shared constants + helpers for step-boundary evidence: the env var that
4
+ * enables capture, the file-name sanitizer, and the reserved failure ids.
5
+ *
6
+ * Three producers write the `<id>.png` + `<id>.json` pairs — `abStepEvidence()`
7
+ * / `captureFailureEvidence()` in `test-helpers.ts` (agent-browser replays) and
8
+ * `ccqaStepBefore`/`ccqaStepAfter` in `step-evidence.ts` (external targets) —
9
+ * and one consumer reads them back (`loadEvidenceForSpec` in
10
+ * `report/evidence.ts`). All four agree only on the contract here, so it is
11
+ * kept under `runtime/` (free of CLI-side imports) so the generated-test
12
+ * modules — imported via `ccqa/test-helpers` and `ccqa/step-evidence` — can
13
+ * share it without dragging the CLI in.
14
+ */
15
+ /**
16
+ * Env var naming the directory a spec's step evidence is written to. `ccqa
17
+ * run` sets it per spec; unset means "capture nothing", which is what keeps a
18
+ * hand-run generated test (or the generation-time verify loop) from
19
+ * scattering screenshots.
20
+ */
21
+ const EVIDENCE_DIR_ENV = "CCQA_EVIDENCE_DIR";
22
+ /**
23
+ * Make a step id safe to use as an evidence file-name stem. Both evidence
24
+ * producers (agent-browser `abStepEvidence`, external `ccqa/step-evidence`)
25
+ * name their PNG/JSON pair `<stem>.png` / `<stem>.json`, so they must agree on
26
+ * this exact mapping.
27
+ */
28
+ function sanitizeStepId(stepId) {
29
+ return stepId.replace(/[^A-Za-z0-9_.-]/g, "_");
30
+ }
31
+ /** stepId reserved for the screenshot captured by fail() at the moment of an assertion failure. */
32
+ const FAILURE_STEP_ID = "failure";
33
+ /** source value paired with FAILURE_STEP_ID so the report can tell failure captures apart from step captures. */
34
+ const FAILURE_SOURCE = "failed";
35
+ //#endregion
36
+ export { sanitizeStepId as i, FAILURE_SOURCE as n, FAILURE_STEP_ID as r, EVIDENCE_DIR_ENV as t };
@@ -64,6 +64,7 @@ type Run = z.infer<typeof RunSchema>;
64
64
  declare const TriageCaseSchema: z.ZodObject<{
65
65
  feature: z.ZodString;
66
66
  spec: z.ZodString;
67
+ target: z.ZodOptional<z.ZodString>;
67
68
  predicted: z.ZodObject<{
68
69
  label: z.ZodEnum<{
69
70
  TEST_DRIFT: "TEST_DRIFT";
@@ -92,6 +93,7 @@ declare const RunTriageSchema: z.ZodObject<{
92
93
  cases: z.ZodArray<z.ZodObject<{
93
94
  feature: z.ZodString;
94
95
  spec: z.ZodString;
96
+ target: z.ZodOptional<z.ZodString>;
95
97
  predicted: z.ZodObject<{
96
98
  label: z.ZodEnum<{
97
99
  TEST_DRIFT: "TEST_DRIFT";
@@ -242,6 +244,7 @@ declare const ReportSpecResultSchema: z.ZodObject<{
242
244
  stepId: z.ZodString;
243
245
  source: z.ZodString;
244
246
  pngPath: z.ZodString;
247
+ beforePngPath: z.ZodOptional<z.ZodNullable<z.ZodString>>;
245
248
  url: z.ZodNullable<z.ZodString>;
246
249
  title: z.ZodNullable<z.ZodString>;
247
250
  capturedAt: z.ZodNullable<z.ZodString>;
@@ -252,6 +255,7 @@ declare const ReportSpecResultSchema: z.ZodObject<{
252
255
  }>>;
253
256
  failureSummary: z.ZodDefault<z.ZodNullable<z.ZodString>>;
254
257
  }, z.core.$strip>>>;
258
+ evidenceUnavailable: z.ZodOptional<z.ZodString>;
255
259
  artifacts: z.ZodOptional<z.ZodArray<z.ZodObject<{
256
260
  name: z.ZodString;
257
261
  path: z.ZodString;
@@ -407,6 +411,7 @@ declare const RunReportDataSchema: z.ZodObject<{
407
411
  stepId: z.ZodString;
408
412
  source: z.ZodString;
409
413
  pngPath: z.ZodString;
414
+ beforePngPath: z.ZodOptional<z.ZodNullable<z.ZodString>>;
410
415
  url: z.ZodNullable<z.ZodString>;
411
416
  title: z.ZodNullable<z.ZodString>;
412
417
  capturedAt: z.ZodNullable<z.ZodString>;
@@ -417,6 +422,7 @@ declare const RunReportDataSchema: z.ZodObject<{
417
422
  }>>;
418
423
  failureSummary: z.ZodDefault<z.ZodNullable<z.ZodString>>;
419
424
  }, z.core.$strip>>>;
425
+ evidenceUnavailable: z.ZodOptional<z.ZodString>;
420
426
  artifacts: z.ZodOptional<z.ZodArray<z.ZodObject<{
421
427
  name: z.ZodString;
422
428
  path: z.ZodString;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
@@ -19,6 +19,10 @@
19
19
  "types": "./dist/runtime/test-helpers.d.mts",
20
20
  "import": "./dist/runtime/test-helpers.mjs"
21
21
  },
22
+ "./step-evidence": {
23
+ "types": "./dist/runtime/step-evidence.d.mts",
24
+ "import": "./dist/runtime/step-evidence.mjs"
25
+ },
22
26
  "./hub-client": {
23
27
  "types": "./dist/hub-client/index.d.mts",
24
28
  "import": "./dist/hub-client/index.mjs"
@@ -0,0 +1,53 @@
1
+ //#region src/runtime/step-evidence.d.ts
2
+ /**
3
+ * Step-boundary screenshot capture for tests ccqa generates for external
4
+ * targets (Playwright today). Generated tests import this through the
5
+ * `ccqa/step-evidence` subpath and call it at each spec-step boundary; the
6
+ * `<id>.png` + `<id>.json` pairs it writes are exactly what `ccqa run`'s
7
+ * report loader consumes, so an external target's rows carry the same
8
+ * per-step evidence the built-in agent-browser path produces.
9
+ *
10
+ * Two constraints shape the whole module:
11
+ *
12
+ * - **No test-framework dependency.** The page handle is typed
13
+ * structurally, so ccqa never imports `@playwright/test` and a consumer
14
+ * installs nothing beyond ccqa itself. Any object exposing these three
15
+ * members works — including a Playwright `Page`.
16
+ * - **Never fail the user's test.** Capture is best-effort: every error is
17
+ * swallowed with a stderr note. A missing screenshot costs a frame in the
18
+ * report; it must never flip a passing spec to red.
19
+ *
20
+ * Capture is opt-in at runtime via `CCQA_EVIDENCE_DIR`, which only `ccqa run`
21
+ * sets. Running the generated test directly (or through the generation-time
22
+ * verify/fix loop) writes nothing.
23
+ */
24
+ /**
25
+ * The subset of a browser page this module needs. Structural by design — see
26
+ * the module comment.
27
+ */
28
+ interface CcqaEvidencePage {
29
+ screenshot(options: {
30
+ path: string;
31
+ }): Promise<unknown>;
32
+ url(): string;
33
+ title(): Promise<string>;
34
+ }
35
+ /**
36
+ * Capture the screen as the step is entered. Pair with `ccqaStepAfter` at the
37
+ * end of the same step.
38
+ */
39
+ declare function ccqaStepBefore(page: CcqaEvidencePage, stepId: string, source: string): Promise<void>;
40
+ /**
41
+ * Capture the screen as the step closes and finalise the step's metadata:
42
+ * the closing shot becomes the step's primary screenshot, the entry shot (if
43
+ * one was taken) rides along as `beforePngFile`, and the "did not complete"
44
+ * caption written by `ccqaStepBefore` is cleared.
45
+ *
46
+ * If the closing shot itself fails, the step still COMPLETED — it just lost its
47
+ * final frame. Rewrite the meta without the failure caption (keeping the entry
48
+ * shot as the frame) so a passing step doesn't render red; the capture failure
49
+ * is surfaced via the stderr warn in `capture()`.
50
+ */
51
+ declare function ccqaStepAfter(page: CcqaEvidencePage, stepId: string, source: string): Promise<void>;
52
+ //#endregion
53
+ export { CcqaEvidencePage, ccqaStepAfter, ccqaStepBefore };
@@ -0,0 +1,104 @@
1
+ import { i as sanitizeStepId, t as EVIDENCE_DIR_ENV } from "../evidence-constants-C425F7ZG.mjs";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ //#region src/runtime/step-evidence.ts
5
+ /**
6
+ * Caption recorded on the entry shot's metadata and cleared by
7
+ * `ccqaStepAfter`. If the test dies inside the step, this is what survives —
8
+ * so the report shows the failing step with the screen it started from,
9
+ * marked failed, instead of dropping the step entirely.
10
+ */
11
+ const INCOMPLETE_STEP_SUMMARY = "the test stopped inside this step (no closing screenshot)";
12
+ /**
13
+ * Capture the screen as the step is entered. Pair with `ccqaStepAfter` at the
14
+ * end of the same step.
15
+ */
16
+ async function ccqaStepBefore(page, stepId, source) {
17
+ const dir = process.env[EVIDENCE_DIR_ENV];
18
+ if (!dir) return;
19
+ const id = sanitizeStepId(stepId);
20
+ const beforeFile = `${id}.before.png`;
21
+ if (!await capture(page, dir, beforeFile)) return;
22
+ await writeMeta(page, dir, id, {
23
+ stepId,
24
+ source,
25
+ pngFile: beforeFile,
26
+ failureSummary: INCOMPLETE_STEP_SUMMARY
27
+ });
28
+ }
29
+ /**
30
+ * Capture the screen as the step closes and finalise the step's metadata:
31
+ * the closing shot becomes the step's primary screenshot, the entry shot (if
32
+ * one was taken) rides along as `beforePngFile`, and the "did not complete"
33
+ * caption written by `ccqaStepBefore` is cleared.
34
+ *
35
+ * If the closing shot itself fails, the step still COMPLETED — it just lost its
36
+ * final frame. Rewrite the meta without the failure caption (keeping the entry
37
+ * shot as the frame) so a passing step doesn't render red; the capture failure
38
+ * is surfaced via the stderr warn in `capture()`.
39
+ */
40
+ async function ccqaStepAfter(page, stepId, source) {
41
+ const dir = process.env[EVIDENCE_DIR_ENV];
42
+ if (!dir) return;
43
+ const id = sanitizeStepId(stepId);
44
+ const afterFile = `${id}.png`;
45
+ const beforeFile = `${id}.before.png`;
46
+ const hasBefore = existsSync(join(dir, beforeFile));
47
+ if (!await capture(page, dir, afterFile)) {
48
+ if (hasBefore) await writeMeta(page, dir, id, {
49
+ stepId,
50
+ source,
51
+ pngFile: beforeFile
52
+ });
53
+ return;
54
+ }
55
+ await writeMeta(page, dir, id, {
56
+ stepId,
57
+ source,
58
+ pngFile: afterFile,
59
+ ...hasBefore ? { beforePngFile: beforeFile } : {}
60
+ });
61
+ }
62
+ /** Screenshot into `<dir>/<file>`; false when the shot could not be taken. */
63
+ async function capture(page, dir, file) {
64
+ try {
65
+ mkdirSync(dir, { recursive: true });
66
+ await page.screenshot({ path: join(dir, file) });
67
+ return true;
68
+ } catch (e) {
69
+ warn(`screenshot failed for ${file} (${message(e)})`);
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * Write the step's meta sidecar. `url`/`title` are read here rather than by
75
+ * the caller so a page that cannot answer them still yields a usable record
76
+ * (the screenshot alone is worth keeping).
77
+ */
78
+ async function writeMeta(page, dir, id, fields) {
79
+ let url = null;
80
+ let title = null;
81
+ try {
82
+ url = page.url();
83
+ title = await page.title();
84
+ } catch {}
85
+ const meta = {
86
+ ...fields,
87
+ url,
88
+ title,
89
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
90
+ };
91
+ try {
92
+ writeFileSync(join(dir, `${id}.json`), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
93
+ } catch (e) {
94
+ warn(`meta write failed for ${id} (${message(e)})`);
95
+ }
96
+ }
97
+ function message(e) {
98
+ return e instanceof Error ? e.message : String(e);
99
+ }
100
+ function warn(text) {
101
+ process.stderr.write(`[ccqa] step-evidence: ${text}\n`);
102
+ }
103
+ //#endregion
104
+ export { ccqaStepAfter, ccqaStepBefore };
@@ -1,4 +1,5 @@
1
- import { c as FAILURE_SOURCE, l as FAILURE_STEP_ID, n as spawnAB, t as sleepSync } from "../spawn-ab-B9Il2TMd.mjs";
1
+ import { i as sanitizeStepId, n as FAILURE_SOURCE, r as FAILURE_STEP_ID, t as EVIDENCE_DIR_ENV } from "../evidence-constants-C425F7ZG.mjs";
2
+ import { n as spawnAB, t as sleepSync } from "../spawn-ab-CRIVfWpw.mjs";
2
3
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
4
5
  //#region src/runtime/test-helpers.ts
@@ -30,7 +31,7 @@ function __setCurrentStep(stepId, source) {
30
31
  }
31
32
  function captureFailureEvidence(summary) {
32
33
  if (currentStep) {
33
- const safe = currentStep.stepId.replace(/[^A-Za-z0-9_.-]/g, "_");
34
+ const safe = sanitizeStepId(currentStep.stepId);
34
35
  captureEvidence({
35
36
  stepId: currentStep.stepId,
36
37
  source: currentStep.source,
@@ -244,7 +245,7 @@ function abStepEvidence(stepId, source) {
244
245
  captureEvidence({
245
246
  stepId,
246
247
  source,
247
- pngFile: `${stepId.replace(/[^A-Za-z0-9_.-]/g, "_")}.png`
248
+ pngFile: `${sanitizeStepId(stepId)}.png`
248
249
  });
249
250
  if (currentStep && currentStep.stepId === stepId) currentStep = null;
250
251
  }
@@ -255,7 +256,7 @@ function abStepEvidence(stepId, source) {
255
256
  * expression must itself stringify the payload — hence the double JSON.parse.
256
257
  */
257
258
  function captureEvidence(opts) {
258
- const dir = process.env["CCQA_EVIDENCE_DIR"];
259
+ const dir = process.env[EVIDENCE_DIR_ENV];
259
260
  if (!dir) return;
260
261
  const { stepId, source, pngFile, failureSummary, silent } = opts;
261
262
  const pngPath = join(dir, pngFile);
@@ -2,19 +2,6 @@ import { createRequire } from "node:module";
2
2
  import { statSync } from "node:fs";
3
3
  import { delimiter, dirname, join } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
- //#region src/runtime/evidence-constants.ts
6
- /**
7
- * Shared constants for step-boundary evidence captured by abStepEvidence() /
8
- * captureFailureEvidence() and consumed by the run report. Kept under
9
- * `runtime/` so the test-helpers module — which generated test scripts import
10
- * via `ccqa/test-helpers` — can stay free of CLI-side imports while still
11
- * sharing the literal with run.ts.
12
- */
13
- /** stepId reserved for the screenshot captured by fail() at the moment of an assertion failure. */
14
- const FAILURE_STEP_ID = "failure";
15
- /** source value paired with FAILURE_STEP_ID so the report can tell failure captures apart from step captures. */
16
- const FAILURE_SOURCE = "failed";
17
- //#endregion
18
5
  //#region src/runtime/agent-browser-bin.ts
19
6
  const require = createRequire(import.meta.url);
20
7
  function hasAgentBrowserShim(dir) {
@@ -207,4 +194,4 @@ function spawnAB(args) {
207
194
  return result;
208
195
  }
209
196
  //#endregion
210
- export { formatAgentBrowserUnavailableMessage as a, FAILURE_SOURCE as c, assertAgentBrowserAvailable as i, FAILURE_STEP_ID as l, spawnAB as n, pathWithAgentBrowserShim as o, AgentBrowserUnavailableError as r, resolveAgentBrowserBin as s, sleepSync as t };
197
+ export { formatAgentBrowserUnavailableMessage as a, assertAgentBrowserAvailable as i, spawnAB as n, pathWithAgentBrowserShim as o, AgentBrowserUnavailableError as r, resolveAgentBrowserBin as s, sleepSync as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
@@ -19,6 +19,10 @@
19
19
  "types": "./dist/runtime/test-helpers.d.mts",
20
20
  "import": "./dist/runtime/test-helpers.mjs"
21
21
  },
22
+ "./step-evidence": {
23
+ "types": "./dist/runtime/step-evidence.d.mts",
24
+ "import": "./dist/runtime/step-evidence.mjs"
25
+ },
22
26
  "./hub-client": {
23
27
  "types": "./dist/hub-client/index.d.mts",
24
28
  "import": "./dist/hub-client/index.mjs"