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.
- package/README.md +9 -5
- package/dist/bin/ccqa.mjs +8589 -7754
- package/dist/evidence-constants-C425F7ZG.mjs +36 -0
- package/dist/hub-client/index.d.mts +6 -0
- package/dist/package.json +5 -1
- package/dist/runtime/step-evidence.d.mts +53 -0
- package/dist/runtime/step-evidence.mjs +104 -0
- package/dist/runtime/test-helpers.mjs +5 -4
- package/dist/{spawn-ab-B9Il2TMd.mjs → spawn-ab-CRIVfWpw.mjs} +1 -14
- package/package.json +5 -1
|
@@ -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.
|
|
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 {
|
|
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
|
|
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
|
|
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[
|
|
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,
|
|
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.
|
|
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"
|