ccqa 1.45.0 → 1.46.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 +98 -31
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -223,6 +223,7 @@ const AGENT_BROWSER_TARGET = "agent-browser";
|
|
|
223
223
|
*/
|
|
224
224
|
const TestSpecSchema = z.object({
|
|
225
225
|
title: z.string().min(1),
|
|
226
|
+
disabled: z.boolean().optional(),
|
|
226
227
|
target: TargetIdSchema.optional(),
|
|
227
228
|
mode: SpecModeSchema.optional(),
|
|
228
229
|
session: SessionFieldSchema.optional(),
|
|
@@ -647,6 +648,21 @@ async function getTestScript(featureName, specName, cwd) {
|
|
|
647
648
|
async function listAllSpecsWithSpecFile(cwd) {
|
|
648
649
|
return listAllSpecsFilteredBy(SPEC_FILE, cwd);
|
|
649
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* The active suite — the tree minus the specs marked `disabled` — which is
|
|
653
|
+
* what `ccqa run` expands "all specs" to. Kept separate from the enumeration
|
|
654
|
+
* above because `serialGroups` and `actors` validate that a spec *name*
|
|
655
|
+
* exists, and a disabled spec is still a name.
|
|
656
|
+
*
|
|
657
|
+
* A spec that will not read or parse stays in: unreadable is not the same as
|
|
658
|
+
* asking to be skipped.
|
|
659
|
+
*/
|
|
660
|
+
async function listActiveSpecs(cwd) {
|
|
661
|
+
const all = await listAllSpecsWithSpecFile(cwd);
|
|
662
|
+
return (await Promise.all(all.map(async (ref) => {
|
|
663
|
+
return tryParseTestSpec(await tryReadSpecFile(ref.featureName, ref.specName, cwd))?.disabled === true ? null : ref;
|
|
664
|
+
}))).filter((ref) => ref !== null);
|
|
665
|
+
}
|
|
650
666
|
async function listAllSpecsFilteredBy(requiredFilename, cwd) {
|
|
651
667
|
const featuresDir = join(getCcqaDir(cwd), "features");
|
|
652
668
|
const featureDirs = await readdir(featuresDir).catch(() => []);
|
|
@@ -676,13 +692,7 @@ async function resolveSpecTargets(target, enumerateAll, cwd) {
|
|
|
676
692
|
specName
|
|
677
693
|
}];
|
|
678
694
|
}
|
|
679
|
-
return (await
|
|
680
|
-
featureName: target,
|
|
681
|
-
specName
|
|
682
|
-
}));
|
|
683
|
-
}
|
|
684
|
-
async function listSpecsForFeature(featureName, cwd) {
|
|
685
|
-
return readdir(join(getFeatureDir(featureName, cwd), "test-cases")).catch(() => []);
|
|
695
|
+
return (await listActiveSpecs(cwd)).filter((ref) => ref.featureName === target);
|
|
686
696
|
}
|
|
687
697
|
/**
|
|
688
698
|
* Lists every feature/spec dir under .ccqa/features/, regardless of whether
|
|
@@ -4968,6 +4978,7 @@ const PerspectiveSpecSchema = z.object({
|
|
|
4968
4978
|
steps: z.array(PerspectiveStepSchema).optional(),
|
|
4969
4979
|
status: PerspectiveStatusSchema,
|
|
4970
4980
|
changedAt: z.string().optional(),
|
|
4981
|
+
disabled: z.boolean().optional(),
|
|
4971
4982
|
note: z.string().optional()
|
|
4972
4983
|
}).strip();
|
|
4973
4984
|
const PerspectiveFeatureSchema = z.object({
|
|
@@ -7167,6 +7178,7 @@ async function startBrowserCoverage(opts) {
|
|
|
7167
7178
|
var Engine = class {
|
|
7168
7179
|
client;
|
|
7169
7180
|
connectFn;
|
|
7181
|
+
/** Last known address of the browser; see `refreshWsUrl`. */
|
|
7170
7182
|
wsUrl;
|
|
7171
7183
|
reconnects = 0;
|
|
7172
7184
|
reconnectInFlight = false;
|
|
@@ -7285,6 +7297,22 @@ var Engine = class {
|
|
|
7285
7297
|
this.stopped = true;
|
|
7286
7298
|
this.clearTimer();
|
|
7287
7299
|
}
|
|
7300
|
+
/**
|
|
7301
|
+
* Answers why the address could not be refreshed, so the retry that follows
|
|
7302
|
+
* says whether it is dialling a stale address or a dead browser. Told apart
|
|
7303
|
+
* because one is a driver to fix and the other is a browser to wait for, and
|
|
7304
|
+
* a silent stale address is the shape of the bug this exists to prevent.
|
|
7305
|
+
*/
|
|
7306
|
+
async refreshWsUrl() {
|
|
7307
|
+
const ask = this.opts.currentCdpUrl;
|
|
7308
|
+
if (ask === void 0) return void 0;
|
|
7309
|
+
try {
|
|
7310
|
+
this.wsUrl = await browserWebSocketUrl(await ask());
|
|
7311
|
+
return;
|
|
7312
|
+
} catch (error) {
|
|
7313
|
+
return message(error);
|
|
7314
|
+
}
|
|
7315
|
+
}
|
|
7288
7316
|
async reconnect(initialReason) {
|
|
7289
7317
|
if (this.reconnectInFlight) return;
|
|
7290
7318
|
this.reconnectInFlight = true;
|
|
@@ -7300,11 +7328,12 @@ var Engine = class {
|
|
|
7300
7328
|
this.opts.warn(`browser coverage transport dropped (${reason}); reconnecting in ${delay}ms (${this.reconnects}/${MAX_RECONNECTS})`);
|
|
7301
7329
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
7302
7330
|
if (this.stopped) return;
|
|
7331
|
+
const stale = await this.refreshWsUrl();
|
|
7303
7332
|
let fresh;
|
|
7304
7333
|
try {
|
|
7305
7334
|
fresh = await this.connectFn(this.wsUrl);
|
|
7306
7335
|
} catch (error) {
|
|
7307
|
-
reason = `reconnect failed: ${message(error)}`;
|
|
7336
|
+
reason = stale ? `reconnect failed: ${message(error)} (address may be stale: ${stale})` : `reconnect failed: ${message(error)}`;
|
|
7308
7337
|
continue;
|
|
7309
7338
|
}
|
|
7310
7339
|
if (this.stopped) {
|
|
@@ -7319,6 +7348,7 @@ var Engine = class {
|
|
|
7319
7348
|
reason = `reconnect failed: ${message(error)}`;
|
|
7320
7349
|
continue;
|
|
7321
7350
|
}
|
|
7351
|
+
this.reconnects = 0;
|
|
7322
7352
|
if (this.stopped) {
|
|
7323
7353
|
this.clearTimer();
|
|
7324
7354
|
fresh.close();
|
|
@@ -7749,9 +7779,10 @@ var CoverageSession = class CoverageSession {
|
|
|
7749
7779
|
* destinations, the roots — lives here, so the caller only supplies where
|
|
7750
7780
|
* the browser is.
|
|
7751
7781
|
*/
|
|
7752
|
-
armBrowser(ref,
|
|
7782
|
+
armBrowser(ref, browser, coverageDir) {
|
|
7753
7783
|
return startBrowserCoverage({
|
|
7754
|
-
cdpUrl,
|
|
7784
|
+
cdpUrl: browser.cdpUrl,
|
|
7785
|
+
currentCdpUrl: browser.currentCdpUrl?.bind(browser),
|
|
7755
7786
|
specId: specIdFor(this.runId, ref),
|
|
7756
7787
|
origins: this.origins,
|
|
7757
7788
|
assetOrigins: this.assetOrigins,
|
|
@@ -8257,7 +8288,7 @@ async function runOneSpec$1(ref, opts, blocks) {
|
|
|
8257
8288
|
});
|
|
8258
8289
|
const acquired = browserHandle;
|
|
8259
8290
|
opts.teardown?.onFinalize(() => acquired.dispose());
|
|
8260
|
-
browserEngine = await measurement.collector.armBrowser(ref, browserHandle
|
|
8291
|
+
browserEngine = await measurement.collector.armBrowser(ref, browserHandle, coverageDir);
|
|
8261
8292
|
if (browserHandle.amendCommand) command = browserHandle.amendCommand(command);
|
|
8262
8293
|
Object.assign(childEnv, browserHandle.env);
|
|
8263
8294
|
} catch (err) {
|
|
@@ -12146,6 +12177,7 @@ async function loadSpecInventory(cwd) {
|
|
|
12146
12177
|
const content = await tryReadSpecFile(featureName, specName, cwd);
|
|
12147
12178
|
if (content === null) return null;
|
|
12148
12179
|
const spec = parseTestSpec(content, `${featureName}/${specName}/spec.yaml`);
|
|
12180
|
+
if (spec.disabled) return null;
|
|
12149
12181
|
return {
|
|
12150
12182
|
featureName,
|
|
12151
12183
|
specName,
|
|
@@ -13884,6 +13916,37 @@ async function acquireAgentBrowserEndpoint(ctx) {
|
|
|
13884
13916
|
"about:blank"
|
|
13885
13917
|
]);
|
|
13886
13918
|
if (warm.status !== 0) throw new Error(`could not start the session's browser: ${warm.stderr || warm.stdout}`);
|
|
13919
|
+
return {
|
|
13920
|
+
cdpUrl: askCdpUrl(session),
|
|
13921
|
+
currentCdpUrl: () => askCdpUrlSoon(session),
|
|
13922
|
+
dispose: async () => {}
|
|
13923
|
+
};
|
|
13924
|
+
}
|
|
13925
|
+
/**
|
|
13926
|
+
* The same question on a short leash. Its caller is racing its own backoff
|
|
13927
|
+
* between reconnect attempts, so an answer that arrives after the budget is
|
|
13928
|
+
* spent is worth less than no answer: `spawnAB` would sit through a 30s EAGAIN
|
|
13929
|
+
* retry and a 35s hard timeout, which is the whole reconnect window several
|
|
13930
|
+
* times over.
|
|
13931
|
+
*/
|
|
13932
|
+
const CDP_URL_TIMEOUT_MS = 2e3;
|
|
13933
|
+
async function askCdpUrlSoon(session) {
|
|
13934
|
+
const { promisify } = await import("node:util");
|
|
13935
|
+
const { execFile } = await import("node:child_process");
|
|
13936
|
+
const { stdout } = await promisify(execFile)(resolveAgentBrowserBin$1(), [
|
|
13937
|
+
"--session",
|
|
13938
|
+
session,
|
|
13939
|
+
"get",
|
|
13940
|
+
"cdp-url"
|
|
13941
|
+
], {
|
|
13942
|
+
timeout: CDP_URL_TIMEOUT_MS,
|
|
13943
|
+
encoding: "utf8"
|
|
13944
|
+
});
|
|
13945
|
+
const cdpUrl = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
13946
|
+
if (!/^wss?:\/\//.test(cdpUrl)) throw new Error(`agent-browser answered no cdp-url for session ${session}`);
|
|
13947
|
+
return cdpUrl;
|
|
13948
|
+
}
|
|
13949
|
+
function askCdpUrl(session) {
|
|
13887
13950
|
const answer = spawnAB([
|
|
13888
13951
|
"--session",
|
|
13889
13952
|
session,
|
|
@@ -13892,10 +13955,7 @@ async function acquireAgentBrowserEndpoint(ctx) {
|
|
|
13892
13955
|
]);
|
|
13893
13956
|
const cdpUrl = answer.stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
13894
13957
|
if (answer.status !== 0 || !/^wss?:\/\//.test(cdpUrl)) throw new Error(`agent-browser did not answer \`get cdp-url\` for session ${session}: ${answer.stderr || answer.stdout}`);
|
|
13895
|
-
return
|
|
13896
|
-
cdpUrl,
|
|
13897
|
-
dispose: async () => {}
|
|
13898
|
-
};
|
|
13958
|
+
return cdpUrl;
|
|
13899
13959
|
}
|
|
13900
13960
|
//#endregion
|
|
13901
13961
|
//#region src/diagnose/snapshot.ts
|
|
@@ -14279,7 +14339,7 @@ async function runOneSpec(args) {
|
|
|
14279
14339
|
browserEngine = await opts.coverage.armBrowser({
|
|
14280
14340
|
featureName,
|
|
14281
14341
|
specName
|
|
14282
|
-
}, handle
|
|
14342
|
+
}, handle, coverageDir);
|
|
14283
14343
|
} catch (err) {
|
|
14284
14344
|
coverageBroken = errMessage(err);
|
|
14285
14345
|
warn(`coverage: could not attach to the live browser (${coverageBroken})`);
|
|
@@ -17048,7 +17108,7 @@ async function executeRun(targets, opts) {
|
|
|
17048
17108
|
customPrompt,
|
|
17049
17109
|
triageUserPrompt
|
|
17050
17110
|
};
|
|
17051
|
-
const enumerateAll = () =>
|
|
17111
|
+
const enumerateAll = () => listActiveSpecs(cwd);
|
|
17052
17112
|
let specs = dedupeSpecs((await Promise.all((targets.length ? targets : [void 0]).map((t) => resolveSpecTargets(t, enumerateAll, cwd)))).flat());
|
|
17053
17113
|
if (filtering) {
|
|
17054
17114
|
const before = specs.length;
|
|
@@ -21080,13 +21140,15 @@ async function collectTargets(specPath, cwd) {
|
|
|
21080
21140
|
specName
|
|
21081
21141
|
}];
|
|
21082
21142
|
}
|
|
21143
|
+
const active = new Set((await listActiveSpecs(cwd)).map(specKey));
|
|
21083
21144
|
const out = [];
|
|
21084
21145
|
for (const feature of tree) for (const spec of feature.specs) {
|
|
21085
21146
|
if (!spec.hasSpecFile) continue;
|
|
21086
|
-
|
|
21147
|
+
const target = {
|
|
21087
21148
|
featureName: feature.featureName,
|
|
21088
21149
|
specName: spec.specName
|
|
21089
|
-
}
|
|
21150
|
+
};
|
|
21151
|
+
if (active.has(specKey(target))) out.push(target);
|
|
21090
21152
|
}
|
|
21091
21153
|
return out;
|
|
21092
21154
|
}
|
|
@@ -21398,7 +21460,7 @@ async function buildSkeleton(tree) {
|
|
|
21398
21460
|
const config = await loadProjectConfig(process.cwd()).catch(() => null);
|
|
21399
21461
|
const changedAt = await readSpecChangedAt(process.cwd());
|
|
21400
21462
|
return (await Promise.all(tree.map(async (feature) => {
|
|
21401
|
-
const
|
|
21463
|
+
const built = await Promise.all(feature.specs.filter((s) => s.hasSpecFile).map(async (s) => {
|
|
21402
21464
|
const specYaml = await tryReadSpecFile(feature.featureName, s.specName);
|
|
21403
21465
|
const meta = readSpecMeta(s.specName, specYaml);
|
|
21404
21466
|
const plugin = resolveSpecTarget(specYaml, config);
|
|
@@ -21410,12 +21472,13 @@ async function buildSkeleton(tree) {
|
|
|
21410
21472
|
summary: "",
|
|
21411
21473
|
...meta.steps.length > 0 ? { steps: meta.steps } : {},
|
|
21412
21474
|
status,
|
|
21413
|
-
...lastEdit ? { changedAt: lastEdit } : {}
|
|
21475
|
+
...lastEdit ? { changedAt: lastEdit } : {},
|
|
21476
|
+
...meta.disabled ? { disabled: true } : {}
|
|
21414
21477
|
};
|
|
21415
21478
|
}));
|
|
21416
21479
|
return {
|
|
21417
21480
|
featureName: feature.featureName,
|
|
21418
|
-
specs
|
|
21481
|
+
specs: built
|
|
21419
21482
|
};
|
|
21420
21483
|
}))).filter((f) => f.specs.length > 0).map((f) => ({
|
|
21421
21484
|
featureName: f.featureName,
|
|
@@ -21478,13 +21541,19 @@ function withoutGeneratedAt(yamlText) {
|
|
|
21478
21541
|
function noteKey(featureName, specName) {
|
|
21479
21542
|
return `${featureName}/${specName}`;
|
|
21480
21543
|
}
|
|
21481
|
-
/**
|
|
21544
|
+
/**
|
|
21545
|
+
* Lenient read of an already-loaded spec.yaml. A file that is missing or will
|
|
21546
|
+
* not parse falls back to `defaults`, which reports the spec as enabled: a
|
|
21547
|
+
* spec nobody could read has not asked to be skipped.
|
|
21548
|
+
*/
|
|
21482
21549
|
function readSpecMeta(specName, specYaml) {
|
|
21483
|
-
|
|
21550
|
+
const defaults = {
|
|
21484
21551
|
title: specName,
|
|
21485
21552
|
mode: DEFAULT_SPEC_MODE,
|
|
21486
|
-
steps: []
|
|
21553
|
+
steps: [],
|
|
21554
|
+
disabled: false
|
|
21487
21555
|
};
|
|
21556
|
+
if (specYaml === null) return defaults;
|
|
21488
21557
|
try {
|
|
21489
21558
|
const parsed = parse(specYaml);
|
|
21490
21559
|
const title = typeof parsed.title === "string" && parsed.title.length > 0 ? parsed.title : specName;
|
|
@@ -21492,14 +21561,11 @@ function readSpecMeta(specName, specYaml) {
|
|
|
21492
21561
|
return {
|
|
21493
21562
|
title,
|
|
21494
21563
|
mode: modeResult.success ? modeResult.data : DEFAULT_SPEC_MODE,
|
|
21495
|
-
steps: transcribeSteps(parsed.steps)
|
|
21564
|
+
steps: transcribeSteps(parsed.steps),
|
|
21565
|
+
disabled: tryParseTestSpec(specYaml)?.disabled === true
|
|
21496
21566
|
};
|
|
21497
21567
|
} catch {
|
|
21498
|
-
return
|
|
21499
|
-
title: specName,
|
|
21500
|
-
mode: DEFAULT_SPEC_MODE,
|
|
21501
|
-
steps: []
|
|
21502
|
-
};
|
|
21568
|
+
return defaults;
|
|
21503
21569
|
}
|
|
21504
21570
|
}
|
|
21505
21571
|
/**
|
|
@@ -23028,6 +23094,7 @@ function readSpecTargets(doc) {
|
|
|
23028
23094
|
for (const spec of specs) {
|
|
23029
23095
|
const specName = prop(spec, "specName");
|
|
23030
23096
|
if (typeof specName !== "string") continue;
|
|
23097
|
+
if (prop(spec, "disabled") === true) continue;
|
|
23031
23098
|
const changedAt = prop(spec, "changedAt");
|
|
23032
23099
|
out.push({
|
|
23033
23100
|
key: `${featureName}/${specName}`,
|
package/dist/package.json
CHANGED