ccqa 1.47.0 → 1.47.2

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/dist/bin/ccqa.mjs CHANGED
@@ -13535,7 +13535,27 @@ const STEP_EVIDENCE_BEFORE = "ccqaStepBefore";
13535
13535
  const STEP_EVIDENCE_AFTER = "ccqaStepAfter";
13536
13536
  /** The exact boundary call for one step, as emitted and as the gate greps for it. */
13537
13537
  function stepEvidenceCall(fn, marker) {
13538
- return `await ${fn}(page, ${j(marker.stepId)}, ${j(marker.source)});`;
13538
+ return injectedCall(fn, [j(marker.stepId), j(marker.source)]);
13539
+ }
13540
+ /**
13541
+ * A receiver expression: anything up to the argument's comma, allowing one
13542
+ * level of parentheses so `await ctx.newPage()` and `page.context().pages()[1]`
13543
+ * — how a rewrite names a tab a click opened — count as receivers.
13544
+ */
13545
+ const RECEIVER = String.raw`(?:[^,()]|\([^()]*\))+`;
13546
+ /**
13547
+ * The call as emitted, plus the pattern that accepts it back on any page. The
13548
+ * receiver is the one part a rewrite is right to change: a click that opens a
13549
+ * new tab has to act on that tab. Everything else is required verbatim, the
13550
+ * `await` and `;` included — an unawaited capture races the end of the test,
13551
+ * and a mention in a comment is not a call.
13552
+ */
13553
+ function injectedCall(name, args) {
13554
+ const tail = args.map(escapeRegExp).join(String.raw`\s*,\s*`);
13555
+ return {
13556
+ code: `await ${name}(page, ${args.join(", ")});`,
13557
+ pattern: new RegExp(String.raw`await\s+${name}\s*\(\s*${RECEIVER}\s*,\s*${tail}\s*,?\s*\)\s*;`)
13558
+ };
13539
13559
  }
13540
13560
  /**
13541
13561
  * The "preserve the step-evidence calls" rule the library-rewrite prompt must
@@ -13545,7 +13565,7 @@ function stepEvidenceCall(fn, marker) {
13545
13565
  * engine, and the prompt then omits the rule entirely.
13546
13566
  */
13547
13567
  function stepEvidencePreserveRule() {
13548
- return `**Keep the \`${STEP_EVIDENCE_MODULE}\` calls.** The draft's \`await ${STEP_EVIDENCE_BEFORE}(page, ...)\` / \`await ${STEP_EVIDENCE_AFTER}(page, ...)\` lines are load-bearing: ccqa run reads the per-step screenshots they capture. Keep both calls for every step, in place around that step's actions, with their exact \`(page, "<stepId>", "<source>")\` arguments — and keep the import. If you move a step's actions into a page-object method, leave these two calls in the test body around the call to that method; do NOT move them inside the page object. Never wrap a step in a closure to hold them.`;
13568
+ return `**Keep the \`${STEP_EVIDENCE_MODULE}\` calls.** The draft's \`await ${STEP_EVIDENCE_BEFORE}(page, ...)\` / \`await ${STEP_EVIDENCE_AFTER}(page, ...)\` lines are load-bearing: ccqa run reads the per-step screenshots they capture. Keep both calls for every step, in place around that step's actions, with their exact \`"<stepId>", "<source>"\` arguments — and keep the import. Pass a different page only when the step genuinely acts on one (a click that opens a new tab). If you move a step's actions into a page-object method, leave these two calls in the test body around the call to that method; do NOT move them inside the page object. Never wrap a step in a closure to hold them.`;
13549
13569
  }
13550
13570
  /**
13551
13571
  * Told to the rewrite, because the alternative failure is silent: a claim
@@ -13564,22 +13584,22 @@ function emitPlaywrightDraft(input) {
13564
13584
  const flushJudgements = (afterActionIndex) => {
13565
13585
  for (const { step } of judgements.filter((j) => j.afterActionIndex === afterActionIndex)) {
13566
13586
  if (openMarker) {
13567
- lines.push(stepEvidenceCall(STEP_EVIDENCE_AFTER, openMarker));
13587
+ lines.push(stepEvidenceCall(STEP_EVIDENCE_AFTER, openMarker).code);
13568
13588
  openMarker = null;
13569
13589
  }
13570
13590
  if (lines.length > 0) lines.push("");
13571
13591
  lines.push(`// step: ${step.id} [${step.source}]`);
13572
- lines.push(judgeCall(step));
13592
+ lines.push(judgeCall(step).code);
13573
13593
  }
13574
13594
  };
13575
13595
  flushJudgements(-1);
13576
13596
  for (let i = 0; i < actions.length; i++) {
13577
13597
  const marker = markerByIndex.get(i);
13578
13598
  if (marker) {
13579
- if (openMarker) lines.push(stepEvidenceCall(STEP_EVIDENCE_AFTER, openMarker));
13599
+ if (openMarker) lines.push(stepEvidenceCall(STEP_EVIDENCE_AFTER, openMarker).code);
13580
13600
  if (lines.length > 0) lines.push("");
13581
13601
  lines.push(`// step: ${marker.stepId} [${marker.source}]`);
13582
- lines.push(stepEvidenceCall(STEP_EVIDENCE_BEFORE, marker));
13602
+ lines.push(stepEvidenceCall(STEP_EVIDENCE_BEFORE, marker).code);
13583
13603
  openMarker = marker;
13584
13604
  }
13585
13605
  const action = actions[i];
@@ -13591,7 +13611,7 @@ function emitPlaywrightDraft(input) {
13591
13611
  }
13592
13612
  flushJudgements(i);
13593
13613
  }
13594
- if (openMarker) lines.push(stepEvidenceCall(STEP_EVIDENCE_AFTER, openMarker));
13614
+ if (openMarker) lines.push(stepEvidenceCall(STEP_EVIDENCE_AFTER, openMarker).code);
13595
13615
  if (judgements.length > 0) lines.unshift("test.slow();", "");
13596
13616
  const body = lines.map((l) => l === "" ? "" : ` ${l}`).join("\n");
13597
13617
  return [
@@ -13779,8 +13799,9 @@ const j = (s) => JSON.stringify(s);
13779
13799
  const jExpr = (s) => envRefsToJsExpression(s);
13780
13800
  /** One claim, asserted through the judge. Exported so the generation gate can require it back. */
13781
13801
  function judgeCall(step) {
13782
- const from = step.from === void 0 ? "" : `, ${jExpr(step.from)}`;
13783
- return `await ${JUDGE_CALL}(page, ${bracedRefsToJsExpression(step.judgeByLlm.trim())}${from});`;
13802
+ const args = [bracedRefsToJsExpression(step.judgeByLlm.trim())];
13803
+ if (step.from !== void 0) args.push(jExpr(step.from));
13804
+ return injectedCall(JUDGE_CALL, args);
13784
13805
  }
13785
13806
  //#endregion
13786
13807
  //#region src/targets/playwright/browser-server.ts
@@ -14031,12 +14052,12 @@ async function missingInjectedCalls(result, markers, judgements) {
14031
14052
  const corpus = (await Promise.all(result.files.filter((f) => f.kind === "test").map((f) => readFile(f.path, "utf8").catch(() => "")))).join("\n");
14032
14053
  const warnings = [];
14033
14054
  for (const m of markers) {
14034
- const hasBefore = corpus.includes(stepEvidenceCall(STEP_EVIDENCE_BEFORE, m));
14035
- const hasAfter = corpus.includes(stepEvidenceCall(STEP_EVIDENCE_AFTER, m));
14055
+ const hasBefore = stepEvidenceCall(STEP_EVIDENCE_BEFORE, m).pattern.test(corpus);
14056
+ const hasAfter = stepEvidenceCall(STEP_EVIDENCE_AFTER, m).pattern.test(corpus);
14036
14057
  if (!hasBefore || !hasAfter) warnings.push(`step ${m.stepId}: generated test is missing its ${STEP_EVIDENCE_BEFORE}/${STEP_EVIDENCE_AFTER} call(s) — that step will have no report screenshots. A rewrite pass must not drop them.`);
14037
14058
  }
14038
14059
  for (const { step } of judgements) {
14039
- if (corpus.includes(judgeCall(step))) continue;
14060
+ if (judgeCall(step).pattern.test(corpus)) continue;
14040
14061
  warnings.push(`step ${step.id}: generated test is missing its ${JUDGE_CALL} call — that claim is never decided and the spec passes without testing it. A rewrite pass must not drop or reword it.`);
14041
14062
  }
14042
14063
  return warnings;
@@ -16431,6 +16452,7 @@ find nth <index> "<ALLOWED-css>" <action>
16431
16452
  5. \`--name "<n>"\` is **role-only**. Never pass it to \`find text\`, \`find label\`, etc.
16432
16453
  6. \`--name\` matches by substring. Add \`--exact\` whenever the name you read from the snapshot is the element's whole accessible name — without it a short name such as "Log in" also matches "Log in with Google" and "Log in with SSO", and the recording silently pins the first of them.
16433
16454
  7. \`find\` includes its own wait; do not chain a \`wait\` before it.
16455
+ 8. **A step that names an element by position gets a positional locator.** "the first row", "the topmost item", "the latest message" identify by \`first\`/\`last\`/\`nth\` on a stable inner selector — never by the title, subject, or body text that record happened to carry. That text is data the environment supplies: pinning it makes the test pass only while that same record stays in that position, and the next run opens a different one.
16434
16456
 
16435
16457
  **Examples:**
16436
16458
 
@@ -16611,6 +16633,8 @@ CCQA_STEP=<step-id> CCQA_ASSERT=url_contains:/dashboard agent-browser --session
16611
16633
  - A marked command that exits non-zero records nothing — fix the check and re-run it.
16612
16634
  - A marker that doesn't match its command (e.g. \`CCQA_ASSERT=1\` on a \`click\`) is ignored with a warning — never do that.
16613
16635
  - \`get count\` and \`get url\` exit 0 regardless of what they print. **Read the output**: if the printed count / URL contradicts the marker, the signal did NOT verify — treat it as a failed verification (emit \`ASSERTION_FAILED\` if the step cannot be confirmed another way).
16636
+ - **Assert the thing, not how much of it there is.** A step that says a list, a table or a section is shown is satisfied by the list being there. Do not assert the count it happened to have, and never assert an empty-state message ("no items yet") unless the step's own \`expected\` asks for emptiness — the next run has one more record than this one and the test breaks for a reason nobody chose.
16637
+
16614
16638
  - **Assert what the step asks about, nothing else.** The \`expected\` is the contract; anything else you happened to see on the way is not. A nav item, a heading or a greeting that the step never mentions adds no coverage, differs between recordings of the same spec, and is the first thing to break on replay — so the next recording quietly drops it and the test gets weaker without anyone deciding that.
16615
16639
 
16616
16640
  - **\`url_contains\` is opt-in, not a habit.** The same rule, in the form that gets broken most. Emit it ONLY when a step's own \`expected\` explicitly asks about the URL or path. Do NOT add a \`url_contains\` to "prove" a login succeeded, a page loaded, or a navigation happened — confirm those with \`text_visible\` / \`element_visible\` on something the destination page renders. An unrequested URL assertion adds no coverage the visible-content assert doesn't already give, and is the single most common way an environment gets baked into a test.
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
35
35
  running: "running";
36
36
  }>;
37
37
  kind: z.ZodDefault<z.ZodEnum<{
38
+ record: "record";
38
39
  run: "run";
39
40
  drift: "drift";
40
- record: "record";
41
41
  }>>;
42
42
  drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
43
43
  specs: z.ZodNumber;
@@ -811,9 +811,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
811
811
  declare const RunReportDataSchema: z.ZodObject<{
812
812
  schemaVersion: z.ZodLiteral<1>;
813
813
  kind: z.ZodDefault<z.ZodEnum<{
814
+ record: "record";
814
815
  run: "run";
815
816
  drift: "drift";
816
- record: "record";
817
817
  }>>;
818
818
  createdAt: z.ZodString;
819
819
  runId: z.ZodNullable<z.ZodString>;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.47.0",
3
+ "version": "1.47.2",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.47.0",
3
+ "version": "1.47.2",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {