ccqa 1.44.0 → 1.45.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/dist/bin/ccqa.mjs CHANGED
@@ -12337,7 +12337,7 @@ var StreamResolution = class {
12337
12337
  asOf = 0;
12338
12338
  lastSeq = 0;
12339
12339
  pushesDuringRun = 0;
12340
- /** `markers` needs to hold every marker event of the stream; pushes are ignored here. */
12340
+ /** `markers` needs to hold every marker `runId` issued; other runs' markers and pushes are ignored. */
12341
12341
  constructor(markers, runId) {
12342
12342
  this.runId = runId;
12343
12343
  const specIds = /* @__PURE__ */ new Set();
@@ -12460,16 +12460,22 @@ var StreamResolution = class {
12460
12460
  /**
12461
12461
  * Every run that opened a spec in this stream, most recently heard-from
12462
12462
  * first — recency by the arrival position of each run's latest spec-open,
12463
- * the one order the hub's stamps establish.
12463
+ * the one order the hub's stamps establish. Fed one event at a time, so the
12464
+ * question can be asked of a stream too large to hold.
12464
12465
  */
12465
- function listRunIds(events) {
12466
- const lastOpenIndex = /* @__PURE__ */ new Map();
12467
- events.forEach((event, index) => {
12466
+ var RunIdIndex = class {
12467
+ /** Re-inserted on every `spec-open`, so insertion order is order of latest open. */
12468
+ opened = /* @__PURE__ */ new Set();
12469
+ accept(event) {
12468
12470
  const body = event.body;
12469
- if ("kind" in body && body.kind === "spec-open") lastOpenIndex.set(body.runId, index);
12470
- });
12471
- return [...lastOpenIndex.entries()].sort((a, b) => b[1] - a[1]).map(([id]) => id);
12472
- }
12471
+ if (!("kind" in body) || body.kind !== "spec-open") return;
12472
+ this.opened.delete(body.runId);
12473
+ this.opened.add(body.runId);
12474
+ }
12475
+ newestFirst() {
12476
+ return [...this.opened].reverse();
12477
+ }
12478
+ };
12473
12479
  //#endregion
12474
12480
  //#region src/cli/session.ts
12475
12481
  const AB = resolveAgentBrowserBin$1();
@@ -17128,7 +17134,8 @@ async function executeRun(targets, opts) {
17128
17134
  const liveSpecs = withMode.filter((s) => s.mode === "live");
17129
17135
  meta("modes", `${detSpecs.length} deterministic / ${liveSpecs.length} live`);
17130
17136
  if (dispatch.external.length > 0) meta("external", dispatch.external.map((g) => `${g.targetId} ${g.specs.length}`).join(" / "));
17131
- const coverage = opts.coverage && forExecution ? await startCoverage(cwd, projectConfig.coverage, actors, dispatch, opts.teardown, coverageInbox, storedSourceMapReader(hubCtx, deployedSha), hubCtx !== null) : void 0;
17137
+ const storedMaps = hubCtx !== null && deployedSha !== null ? createStoredSourceMaps(hubCtx, deployedSha) : void 0;
17138
+ const coverage = opts.coverage && forExecution ? await startCoverage(cwd, projectConfig.coverage, actors, dispatch, opts.teardown, coverageInbox, storedMaps?.read, hubCtx !== null) : void 0;
17132
17139
  if (liveSpecs.length === 0) {
17133
17140
  const why = "it only applies to agent-browser 'mode: live' specs, and this run has none";
17134
17141
  if (typeof opts.liveStepRetry === "number" && opts.liveStepRetry > 0) warn(`--live-step-retry is ignored: ${why}`);
@@ -17241,8 +17248,11 @@ async function executeRun(targets, opts) {
17241
17248
  };
17242
17249
  const live = await runLiveSpecs(liveSpecs, liveOpts);
17243
17250
  let streamedEdges = {};
17244
- if (coverage && !coverage.streamsToHub) reportCoverageHealth(coverage, [...externalRows, ...live.reportResults]);
17245
- else if (coverage && hubCtx != null) streamedEdges = await reportStreamedCoverageHealth(coverage, hubCtx);
17251
+ if (coverage && !coverage.streamsToHub) reportCoverageHealth(coverage, [...externalRows, ...live.reportResults], storedMaps);
17252
+ else if (coverage && hubCtx != null) {
17253
+ streamedEdges = await reportStreamedCoverageHealth(coverage, hubCtx);
17254
+ storedMaps?.warnIfNothingAnswered();
17255
+ }
17246
17256
  let overallExitCode = det.exitCode !== 0 ? 1 : 0;
17247
17257
  if (live.failedCount > 0) overallExitCode = 1;
17248
17258
  if (externalRows.some((r) => r.status === "failed")) overallExitCode = 1;
@@ -17336,6 +17346,12 @@ async function executeRun(targets, opts) {
17336
17346
  reportDir
17337
17347
  };
17338
17348
  }
17349
+ /**
17350
+ * Starts the run's coverage measurement before any spec runs: the sink has to
17351
+ * be listening before the first request reaches the application, and the set
17352
+ * of spec ids it will accept is only known once dispatch has resolved. With
17353
+ * an `inbox`, nothing binds — the run streams its events to the hub instead.
17354
+ */
17339
17355
  async function startCoverage(cwd, config, actors, dispatch, teardown, inbox, fetchStoredSourceMap, hubConfigured) {
17340
17356
  if (config === void 0) throw new RunUsageError("--coverage needs a `coverage:` block in .ccqa/config.yaml whose `instrumentedOrigins` names the origins the spec cookie may be attached to");
17341
17357
  let session;
@@ -17361,25 +17377,25 @@ async function startCoverage(cwd, config, actors, dispatch, teardown, inbox, fet
17361
17377
  return session;
17362
17378
  }
17363
17379
  /**
17364
- * Starts the run's coverage measurement before any spec runs: the sink has to
17365
- * be listening before the first request reaches the application, and the set
17366
- * of spec ids it will accept is only known once dispatch has resolved. With
17367
- * an `inbox`, nothing binds — the run streams its events to the hub instead.
17380
+ * The maps a deploy pushed for one commit. One answer per asset for the run:
17381
+ * the commit is fixed, so a miss stays a miss, and every spec would otherwise
17382
+ * re-ask for the same scripts.
17368
17383
  */
17369
- /**
17370
- * Needs both a hub and the commit under test: without either there is nothing
17371
- * to address a stored map by, and reading some other commit's maps would name
17372
- * the wrong files. See `SourceMapStore`.
17373
- */
17374
- function storedSourceMapReader(hubCtx, deployedSha) {
17375
- if (hubCtx === null || deployedSha === null) return void 0;
17384
+ function createStoredSourceMaps(hubCtx, deployedSha) {
17376
17385
  const seen = /* @__PURE__ */ new Map();
17377
- return async (assetPath) => {
17378
- const cached = seen.get(assetPath);
17379
- if (cached !== void 0 || seen.has(assetPath)) return cached;
17380
- const map = await hubCtx.hub.getSourceMap(hubCtx.project, deployedSha, assetPath) ?? void 0;
17381
- seen.set(assetPath, map);
17382
- return map;
17386
+ return {
17387
+ read: async (assetPath) => {
17388
+ const cached = seen.get(assetPath);
17389
+ if (cached !== void 0 || seen.has(assetPath)) return cached;
17390
+ const map = await hubCtx.hub.getSourceMap(hubCtx.project, deployedSha, assetPath) ?? void 0;
17391
+ seen.set(assetPath, map);
17392
+ return map;
17393
+ },
17394
+ warnIfNothingAnswered() {
17395
+ const answered = [...seen.values()].some((map) => map !== void 0);
17396
+ if (seen.size === 0 || answered) return;
17397
+ warn(`coverage: no stored source map answered for any of the ${seen.size} script(s) this run asked about, under commit ${deployedSha.slice(0, 12)}. Either that deploy pushed none, or the hub's deploy log names a commit the environment is no longer serving`);
17398
+ }
17383
17399
  };
17384
17400
  }
17385
17401
  /**
@@ -17460,8 +17476,17 @@ async function upsertMeasuredEdges(hubCtx, specs) {
17460
17476
  warn(`coverage: could not record measured edges on the hub (${errMessage(error)})`);
17461
17477
  }
17462
17478
  }
17479
+ /**
17480
+ * Whether the browser half came back with nothing anywhere. `every` rather
17481
+ * than `some`: a run that resolved some chunks from the copies the server
17482
+ * served has no gap the stored maps were needed for, and warning there would
17483
+ * fire on every build that strips maps from a few chunks and not the rest.
17484
+ */
17485
+ function noFrontendResolved(rows) {
17486
+ return rows.every((row) => (row.coverage?.frontendFiles ?? 0) === 0);
17487
+ }
17463
17488
  /** Everything the measurement could not place; silence here reads as "never reached". */
17464
- function reportCoverageHealth(coverage, rows) {
17489
+ function reportCoverageHealth(coverage, rows, storedMaps) {
17465
17490
  if (!coverage.heardFromApplication()) warn("no instrumented application process reported — only the browser half was measured. The server needs ccqa-tools and CCQA_COVERAGE_ENDPOINT pointed at this run's sink");
17466
17491
  if (rows.filter((row) => row.coverage !== void 0).length > 0 && coverage.heardFromApplication() && coverage.attributedSpecs() === 0) warn("instrumented application processes reported, but no spec was attributed to them — the spec cookie is not reaching the application, so every spec's server-side reach is zero for that reason rather than because the server ran nothing. Check coverage.instrumentedOrigins covers every origin the spec's requests go to, and that a server which is not node:http-based installs the middleware itself");
17467
17492
  const boot = coverage.boot();
@@ -17475,6 +17500,7 @@ function reportCoverageHealth(coverage, rows) {
17475
17500
  if (rejected > 0) warn(`${rejected} coverage push(es) named a spec id this run never issued — dropped`);
17476
17501
  const malformed = coverage.malformedPushes();
17477
17502
  if (malformed > 0) warn(`${malformed} coverage push(es) could not be read — the application's ccqa-tools and this CLI disagree on the wire format`);
17503
+ if (noFrontendResolved(rows)) storedMaps?.warnIfNothingAnswered();
17478
17504
  }
17479
17505
  function buildLiveRunSummary(results) {
17480
17506
  const sections = [];
@@ -23587,6 +23613,36 @@ function createResolveMemo(limit) {
23587
23613
  };
23588
23614
  }
23589
23615
  /**
23616
+ * `runId`'s answer, in two more scans: one keeping its markers, one replaying
23617
+ * the stream through the resolver they seed. Both stop at `head`, where the
23618
+ * scan that chose the run stopped — the resolver's markers are frozen there,
23619
+ * so a later push would credit a spec it was never told had opened and count
23620
+ * as rejected: a fault in the read, reading as a fault in the run.
23621
+ */
23622
+ /** A marker `runId` issued — what `StreamResolution` needs before it is fed the stream. */
23623
+ function isMarkerOf(event, runId) {
23624
+ const body = event.body;
23625
+ return "kind" in body && body.runId === runId;
23626
+ }
23627
+ async function resolveRun(config, key, project, runId, head, collected) {
23628
+ async function scanToHead(visit) {
23629
+ await scanStream(config, key, project, 0, (event) => {
23630
+ if (event.seq <= head) visit(event);
23631
+ });
23632
+ }
23633
+ let markers = collected;
23634
+ if (markers === null) {
23635
+ const found = [];
23636
+ await scanToHead((event) => {
23637
+ if (isMarkerOf(event, runId)) found.push(event);
23638
+ });
23639
+ markers = found;
23640
+ }
23641
+ const resolution = new StreamResolution(markers, runId);
23642
+ await scanToHead((event) => resolution.accept(event));
23643
+ return resolution.finish();
23644
+ }
23645
+ /**
23590
23646
  * GET /api/v1/coverage?project=[&runId=] — the stream, interpreted for one
23591
23647
  * run by the shared resolver (resolve-stream.ts); this handler only reads,
23592
23648
  * memoizes and serves. `runId` omitted means the run the stream most
@@ -23607,24 +23663,18 @@ function createResolveCoverageHandler(config) {
23607
23663
  sendJson(ctx.res, 200, hit);
23608
23664
  return;
23609
23665
  }
23610
- const markers = [];
23666
+ const runIndex = new RunIdIndex();
23667
+ const named = runKey !== "" ? [] : null;
23611
23668
  let seen = 0;
23612
23669
  const first = await scanStream(config, key, project, 0, (event) => {
23613
23670
  seen += 1;
23614
- if ("kind" in event.body) markers.push(event);
23671
+ runIndex.accept(event);
23672
+ if (named !== null && isMarkerOf(event, runKey)) named.push(event);
23615
23673
  });
23616
- const runIds = listRunIds(markers).slice(0, RUN_IDS_LIMIT);
23674
+ const runIds = runIndex.newestFirst().slice(0, RUN_IDS_LIMIT);
23617
23675
  const runId = runKey !== "" ? runKey : runIds[0];
23618
- let resolved = null;
23619
- if (runId !== void 0 && seen > 0) {
23620
- const resolution = new StreamResolution(markers, runId);
23621
- await scanStream(config, key, project, 0, (event) => {
23622
- if (event.seq <= first.lastSeq) resolution.accept(event);
23623
- });
23624
- resolved = resolution.finish();
23625
- }
23626
23676
  const answer = {
23627
- resolved,
23677
+ resolved: runId !== void 0 && seen > 0 ? await resolveRun(config, key, project, runId, first.lastSeq, named) : null,
23628
23678
  runIds
23629
23679
  };
23630
23680
  memo.put(project, runKey, first.lastSeq, answer);
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
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.44.0",
3
+ "version": "1.45.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {