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 +36 -12
- package/dist/hub-client/index.d.mts +2 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
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
|
|
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 \`
|
|
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
|
|
13783
|
-
|
|
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 =
|
|
14035
|
-
const hasAfter =
|
|
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 (
|
|
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