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 CHANGED
@@ -15289,12 +15289,18 @@ function assertToLine(action, locator) {
15289
15289
  return assertLine ?? comment;
15290
15290
  }
15291
15291
  /**
15292
- * `url_contains` → toHaveURL. Literal values become an unanchored RegExp
15293
- * (substring match); values carrying env refs can't live in a regex literal,
15294
- * so they use toHaveURL's glob form with a template literal.
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
- if (jExpr(value).startsWith("`")) return `await expect(page).toHaveURL(${envRefsToJsExpression(`**${value}**`)});`;
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").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) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.40.6",
3
+ "version": "1.40.8",
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.40.6",
3
+ "version": "1.40.8",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {