ccqa 1.40.6 → 1.40.8
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 +75 -5
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -15289,12 +15289,18 @@ function assertToLine(action, locator) {
|
|
|
15289
15289
|
return assertLine ?? comment;
|
|
15290
15290
|
}
|
|
15291
15291
|
/**
|
|
15292
|
-
* `url_contains` →
|
|
15293
|
-
*
|
|
15294
|
-
*
|
|
15292
|
+
* `url_contains` → "the URL contains this".
|
|
15293
|
+
*
|
|
15294
|
+
* A `${VAR}` only has a value at run time, which rules out both forms that take
|
|
15295
|
+
* the pattern up front: a regular expression would have to match the reference
|
|
15296
|
+
* span with `.*` (so `${APP_BASE_URL}` alone would assert nothing at all), and
|
|
15297
|
+
* the glob `toHaveURL` accepts is compared against the whole URL, so an
|
|
15298
|
+
* absolute one never matches. Polling `page.url()` keeps the substring
|
|
15299
|
+
* semantic and the resolved value.
|
|
15295
15300
|
*/
|
|
15296
15301
|
function urlContainsAssert(value) {
|
|
15297
|
-
|
|
15302
|
+
const expr = jExpr(value);
|
|
15303
|
+
if (expr.startsWith("`")) return `await expect.poll(() => page.url()).toContain(${expr});`;
|
|
15298
15304
|
return `await expect(page).toHaveURL(new RegExp(${j(escapeRegExp(value))}));`;
|
|
15299
15305
|
}
|
|
15300
15306
|
function escapeRegExp(s) {
|
|
@@ -16354,6 +16360,53 @@ async function collectChangedSpecs(specs, opts) {
|
|
|
16354
16360
|
};
|
|
16355
16361
|
}
|
|
16356
16362
|
//#endregion
|
|
16363
|
+
//#region src/run/measure-backfill.ts
|
|
16364
|
+
/**
|
|
16365
|
+
* `ccqa run --measure-backfill <n>`: keep the measured-reach edges alive.
|
|
16366
|
+
*
|
|
16367
|
+
* Selection (ADR-0024) consumes each spec's most recent measured reach, and
|
|
16368
|
+
* an edge expires after `EDGE_MAX_AGE_MS`. Nothing else re-measures: an
|
|
16369
|
+
* unmeasured spec answers `unknown`, `unknown` marks nothing due (ADR-0023),
|
|
16370
|
+
* and a suite can settle into a state where no run ever fires — the seed
|
|
16371
|
+
* deadlock. Appending a few unmeasured-or-aging specs to every selected run
|
|
16372
|
+
* breaks that loop and keeps the whole suite inside the freshness window
|
|
16373
|
+
* without a scheduled full sweep.
|
|
16374
|
+
*/
|
|
16375
|
+
/**
|
|
16376
|
+
* Re-measure once an edge has spent half its lifetime. Half, not "expired":
|
|
16377
|
+
* a spec re-measured only after expiry answers `unknown` for the gap between
|
|
16378
|
+
* expiry and the next run, which is exactly the window this flag exists to
|
|
16379
|
+
* close.
|
|
16380
|
+
*/
|
|
16381
|
+
const REMEASURE_AFTER_MS = EDGE_MAX_AGE_MS / 2;
|
|
16382
|
+
/**
|
|
16383
|
+
* Picks up to `limit` specs from `inventory` worth re-measuring: ones with no
|
|
16384
|
+
* edge at all first (they cost an `unknown` verdict today), then the oldest
|
|
16385
|
+
* measured ones. Specs already selected for this run are never doubled.
|
|
16386
|
+
*/
|
|
16387
|
+
function chooseMeasureBackfill(inventory, selected, edges, limit, now) {
|
|
16388
|
+
const alreadyRunning = new Set(selected.map(specKey));
|
|
16389
|
+
const missing = [];
|
|
16390
|
+
const aging = [];
|
|
16391
|
+
for (const spec of inventory) {
|
|
16392
|
+
if (alreadyRunning.has(specKey(spec))) continue;
|
|
16393
|
+
const edge = edges.get(specKey(spec));
|
|
16394
|
+
if (edge === void 0) missing.push(spec);
|
|
16395
|
+
else if (now - edge.measuredAt > REMEASURE_AFTER_MS) aging.push({
|
|
16396
|
+
spec,
|
|
16397
|
+
measuredAt: edge.measuredAt
|
|
16398
|
+
});
|
|
16399
|
+
}
|
|
16400
|
+
aging.sort((a, b) => a.measuredAt - b.measuredAt);
|
|
16401
|
+
const specs = [...missing, ...aging.map((entry) => entry.spec)].slice(0, limit);
|
|
16402
|
+
const missingTaken = Math.min(missing.length, specs.length);
|
|
16403
|
+
return {
|
|
16404
|
+
specs,
|
|
16405
|
+
missing: missingTaken,
|
|
16406
|
+
aging: specs.length - missingTaken
|
|
16407
|
+
};
|
|
16408
|
+
}
|
|
16409
|
+
//#endregion
|
|
16357
16410
|
//#region src/run/pipeline.ts
|
|
16358
16411
|
async function resolveVitestConfig(cwd) {
|
|
16359
16412
|
const userConfig = resolve(cwd, ".ccqa/vitest.config.ts");
|
|
@@ -16532,6 +16585,10 @@ async function executeRun(targets, opts) {
|
|
|
16532
16585
|
if (rerunProfile !== null && hubCtx == null) throw new RunUsageError(needsHubConnection("--only-hub-rerun-needed"));
|
|
16533
16586
|
if (opts.reportToHub && hubCtx == null) throw new RunUsageError(REPORT_TO_HUB_NEEDS_CONNECTION);
|
|
16534
16587
|
if (opts.learnHubLivePrompt && hubCtx == null) throw new RunUsageError(needsHubConnection("--learn-hub-live-prompt"));
|
|
16588
|
+
if ((opts.measureBackfill ?? 0) > 0) {
|
|
16589
|
+
if (opts.coverage !== true) throw new RunUsageError("--measure-backfill does nothing without --coverage — there is no measurement to keep fresh");
|
|
16590
|
+
if (!filtering) throw new RunUsageError("--measure-backfill needs a selection flag (--only-hub-rerun-needed / --only-affected-by); an explicit spec list runs exactly what was asked");
|
|
16591
|
+
}
|
|
16535
16592
|
let coverageInbox;
|
|
16536
16593
|
if (opts.coverageInbox === "hub") {
|
|
16537
16594
|
if (opts.coverage !== true) throw new RunUsageError("--coverage-inbox hub does nothing without --coverage — there is no measurement to stream");
|
|
@@ -16580,6 +16637,7 @@ async function executeRun(targets, opts) {
|
|
|
16580
16637
|
};
|
|
16581
16638
|
const enumerateAll = () => listAllSpecsWithSpecFile(cwd);
|
|
16582
16639
|
let specs = dedupeSpecs((await Promise.all((targets.length ? targets : [void 0]).map((t) => resolveSpecTargets(t, enumerateAll, cwd)))).flat());
|
|
16640
|
+
const inventory = specs;
|
|
16583
16641
|
if (filtering) {
|
|
16584
16642
|
const before = specs.length;
|
|
16585
16643
|
let inProgress = 0;
|
|
@@ -16610,6 +16668,14 @@ async function executeRun(targets, opts) {
|
|
|
16610
16668
|
throw new RunUsageError("nothing was selected and no spec was cleared to run: exiting non-zero rather than reporting a green run that verified nothing");
|
|
16611
16669
|
}
|
|
16612
16670
|
}
|
|
16671
|
+
if (filtering && (opts.measureBackfill ?? 0) > 0 && hubCtx != null) {
|
|
16672
|
+
const edges = await loadCoverageEdges(hubCtx);
|
|
16673
|
+
const pick = chooseMeasureBackfill(inventory, specs, edges, opts.measureBackfill ?? 0, Date.now());
|
|
16674
|
+
if (pick.specs.length > 0) {
|
|
16675
|
+
specs = [...specs, ...pick.specs];
|
|
16676
|
+
meta("measure-backfill", `${pick.specs.length} spec(s) appended (${pick.missing} unmeasured / ${pick.aging} aging)`);
|
|
16677
|
+
}
|
|
16678
|
+
}
|
|
16613
16679
|
if (specs.length === 0) {
|
|
16614
16680
|
warn("no specs to run");
|
|
16615
16681
|
return {
|
|
@@ -17564,7 +17630,11 @@ const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command(
|
|
|
17564
17630
|
}, "text").option("--report-to-hub", "Incrementally push the run report to the hub as the run progresses (open → patch per spec → finalize). Requires --hub-url/--hub-token (or CCQA_HUB_URL/CCQA_HUB_TOKEN). Without it, hub credentials are used only to fetch variables/sessions/prompts, not to push.").option("--coverage", "Measure what each spec actually reached in the application under test, and record it on the spec's report row. Needs a `coverage:` block in .ccqa/config.yaml naming the instrumented origins the spec cookie may go to. The browser half attaches to the target's browser from outside (nothing is emitted into generated tests; needs node 22+); the server half needs the application running with ccqa-tools.").option("--coverage-inbox <where>", "With --coverage: where the measurement's two sides meet. 'local' (default) binds a loopback inbox on this machine for the run's duration; 'hub' appends every event to the hub's durable coverage inbox instead — nothing listens on the runner, report.json carries no coverage, and the hub resolves per-spec results on read (requires a hub connection).", (raw) => {
|
|
17565
17631
|
if (COVERAGE_INBOX_MODES.includes(raw)) return raw;
|
|
17566
17632
|
throw new Error(`--coverage-inbox must be one of ${COVERAGE_INBOX_MODES.join(" | ")}`);
|
|
17567
|
-
}, "local").
|
|
17633
|
+
}, "local").option("--measure-backfill <n>", "With --coverage and a selection flag (--only-hub-rerun-needed / --only-affected-by): also run up to <n> specs whose measured reach is missing or past half its freshness window, unmeasured-first then oldest-first. This is what keeps selection's edges alive — without it an unmeasured spec answers `unknown` forever, because `unknown` marks nothing due and nothing else re-measures.", (raw) => {
|
|
17634
|
+
const n = Number(raw);
|
|
17635
|
+
if (!Number.isInteger(n) || n < 0) throw new Error("--measure-backfill must be a non-negative integer");
|
|
17636
|
+
return n;
|
|
17637
|
+
}).optionsGroup("Learning:").option("--learn-hub-live-prompt", "(live only) After the run finishes, ask Claude to refresh the \"live.agent\" prompt on the hub from a summary of the run. Requires a hub connection.").optionsGroup("Environment and connection:").option("--cwd <path>", "Working directory containing the .ccqa/ tree (monorepo support). Defaults to the current directory.").option("--project <name>", "Project name for the hub. Defaults to the current directory's name.")))).action(async (targets, opts) => {
|
|
17568
17638
|
await runCliAction(targets, opts);
|
|
17569
17639
|
});
|
|
17570
17640
|
/** Parse --concurrency: a positive integer. Rejects 0, negatives, non-integers. */
|
package/dist/package.json
CHANGED