ccqa 1.50.1 → 1.51.1
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 +74 -47
- package/dist/hub-client/index.d.mts +6 -3
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -11126,6 +11126,7 @@ ${stepsText}
|
|
|
11126
11126
|
- Judge ONLY this step's \`Expected\` condition. Do not infer pass/fail from steps that have not run yet.
|
|
11127
11127
|
- If the page shows an error banner, a 404, a login wall, or any blocker that prevents the expected outcome — fail.
|
|
11128
11128
|
- If the expected outcome is partially satisfied (e.g. the page loaded but the asserted element is missing) — fail, and say which part is missing.
|
|
11129
|
+
- **An \`Expected\` may label its parts.** A \`must:\` line is the assertion — the rule above applies to it. A \`when present:\` line is observed, not required: judge it when it is there, and when it is absent say so in your reason and pass on the \`must:\` lines alone. Every line of a labelled \`Expected\` carries a label; text with no labels at all is all \`must:\`.
|
|
11129
11130
|
- Pass only when you have *positive* evidence (a successful snapshot, a verified URL, a wait that resolved). "No error shown" is not enough on its own.
|
|
11130
11131
|
- Do not invent success when blocked: fail honestly with a short reason.
|
|
11131
11132
|
- **Evidence discipline**: when the assertion target is a specific row / message / banner / URL, scroll it into view (or focus the relevant pane) before letting the step end. The "after" screenshot is captured for you automatically — your job is to make sure that screenshot shows the thing your STEP_RESULT line is talking about.
|
|
@@ -12634,12 +12635,21 @@ function isStateSelector(selector) {
|
|
|
12634
12635
|
function plainSelectorOf(action) {
|
|
12635
12636
|
return action.locator && action.index === void 0 ? locatorToSelector(action.locator) : void 0;
|
|
12636
12637
|
}
|
|
12638
|
+
/**
|
|
12639
|
+
* A `wait` the post-trace validator actually ran and saw fail. Cascade-skipped
|
|
12640
|
+
* actions carry the flag without ever having been attempted, so they are not
|
|
12641
|
+
* evidence of anything and stay.
|
|
12642
|
+
*/
|
|
12643
|
+
function isProvenFailedWait(action) {
|
|
12644
|
+
return action.replayUnstable === true && !(action.replayReason ?? "").includes("skipped after a preceding action failed");
|
|
12645
|
+
}
|
|
12637
12646
|
function actionToLine$1(action) {
|
|
12638
12647
|
if (isRefSelector(plainSelectorOf(action))) return null;
|
|
12639
12648
|
if (action.action === "assert" && action.replayUnstable && typeof action.replayReason === "string" && action.replayReason.includes("selector not present")) {
|
|
12640
12649
|
const sel = plainSelectorOf(action) ?? action.observation ?? "(unknown)";
|
|
12641
12650
|
return `// [warn] replay-unstable: dropped over-assertion (${action.assert ?? "assert"} ${sel}) — selector not present on replay`;
|
|
12642
12651
|
}
|
|
12652
|
+
if (action.action === "wait" && isProvenFailedWait(action)) return `// [warn] replay-unstable: dropped wait (${plainSelectorOf(action) ?? "(unknown)"}) — did not resolve on replay`;
|
|
12643
12653
|
switch (action.action) {
|
|
12644
12654
|
case "snapshot": return action.observation ? `// ${action.observation}` : null;
|
|
12645
12655
|
case "wait": {
|
|
@@ -13725,6 +13735,7 @@ function actionToLine(action) {
|
|
|
13725
13735
|
const sel = action.locator?.value ?? action.observation ?? "(unknown)";
|
|
13726
13736
|
return `// [warn] replay-unstable: dropped over-assertion (${action.assert ?? "assert"} ${sel}) — selector not present on replay`;
|
|
13727
13737
|
}
|
|
13738
|
+
if (action.action === "wait" && action.replayUnstable === true && !(action.replayReason ?? "").includes("skipped after a preceding action failed")) return `// [warn] replay-unstable: dropped wait (${action.locator?.value ?? "(unknown)"}) — did not resolve on replay`;
|
|
13728
13739
|
const locator = action.locator ? locatorToPlaywright(action.locator, action.index) : null;
|
|
13729
13740
|
const subject = locator !== null && action.index === void 0 ? `${locator}.first()` : locator;
|
|
13730
13741
|
switch (action.action) {
|
|
@@ -14664,32 +14675,42 @@ function requireReportToHubConnection(conn) {
|
|
|
14664
14675
|
process.exit(2);
|
|
14665
14676
|
}
|
|
14666
14677
|
/**
|
|
14667
|
-
* Open the run a `--report-to-hub` command patches into
|
|
14668
|
-
*
|
|
14669
|
-
*
|
|
14670
|
-
*
|
|
14671
|
-
*
|
|
14678
|
+
* Open the run a `--report-to-hub` command patches into, and name it on stderr
|
|
14679
|
+
* — stdout belongs to the report (`--report-format json`), and a caller that
|
|
14680
|
+
* links to the run needs its id before the command ends (docs/hub.md).
|
|
14681
|
+
*
|
|
14682
|
+
* Failure is fatal: a job that asked to publish and cannot reach the hub has
|
|
14683
|
+
* not done what it was told. Thrown rather than exited, so a caller's
|
|
14684
|
+
* `finally` still runs (the audit releases its spec claims there). Not
|
|
14685
|
+
* retried: a dropped response after the hub committed would leave a second
|
|
14686
|
+
* orphan running run.
|
|
14672
14687
|
*/
|
|
14673
|
-
async function openHubRun(kind, conn, cwd,
|
|
14674
|
-
const [branch, gitHead] = await Promise.all([detectBranch(cwd), getGitHead(cwd)]);
|
|
14688
|
+
async function openHubRun(kind, conn, cwd, opts = {}) {
|
|
14689
|
+
const [branch, gitHead] = await Promise.all([detectBranch(cwd), opts.gitHead !== void 0 ? opts.gitHead : getGitHead(cwd)]);
|
|
14690
|
+
let opened;
|
|
14675
14691
|
try {
|
|
14676
|
-
|
|
14692
|
+
opened = await conn.hub.openRun({
|
|
14677
14693
|
project: conn.project,
|
|
14678
14694
|
kind,
|
|
14679
14695
|
...branch ? { branch } : {},
|
|
14680
|
-
...profile ? { profile } : {},
|
|
14696
|
+
...opts.profile ? { profile: opts.profile } : {},
|
|
14681
14697
|
...gitHead ? { gitHead } : {},
|
|
14698
|
+
...opts.deployedSha ? { deployedSha: opts.deployedSha } : {},
|
|
14682
14699
|
...ciProvenance()
|
|
14683
14700
|
});
|
|
14684
|
-
return {
|
|
14685
|
-
hub: conn.hub,
|
|
14686
|
-
kind,
|
|
14687
|
-
runId: run.id,
|
|
14688
|
-
gitHead
|
|
14689
|
-
};
|
|
14690
14701
|
} catch (err) {
|
|
14691
14702
|
throw new RunUsageError(`--report-to-hub: could not open a run on the hub (${errMessage(err)})`);
|
|
14692
14703
|
}
|
|
14704
|
+
process.stderr.write(`[hub-run] ${JSON.stringify({
|
|
14705
|
+
id: opened.id,
|
|
14706
|
+
kind
|
|
14707
|
+
})}\n`);
|
|
14708
|
+
return {
|
|
14709
|
+
hub: conn.hub,
|
|
14710
|
+
kind,
|
|
14711
|
+
runId: opened.id,
|
|
14712
|
+
gitHead
|
|
14713
|
+
};
|
|
14693
14714
|
}
|
|
14694
14715
|
/**
|
|
14695
14716
|
* Close an open run with its final rows and envelope, answering whether it
|
|
@@ -15437,25 +15458,17 @@ async function executeRun(targets, opts) {
|
|
|
15437
15458
|
reportDir: null
|
|
15438
15459
|
};
|
|
15439
15460
|
}
|
|
15440
|
-
const det = await runDeterministicSpecs(detSpecs, opts, cwd, reportDir, resources);
|
|
15441
15461
|
let hubRunId = null;
|
|
15442
15462
|
let hubSink;
|
|
15443
15463
|
let hubPublishBroken = false;
|
|
15444
|
-
if (hubCtx != null && opts.reportToHub)
|
|
15445
|
-
const
|
|
15446
|
-
const opened = await hubCtx.hub.openRun({
|
|
15447
|
-
project: hubCtx.project,
|
|
15448
|
-
...branch ? { branch } : {},
|
|
15464
|
+
if (hubCtx != null && opts.reportToHub) {
|
|
15465
|
+
const { runId } = await openHubRun("run", hubCtx, cwd, {
|
|
15449
15466
|
...opts.hubProfile ? { profile: opts.hubProfile } : {},
|
|
15450
|
-
|
|
15451
|
-
|
|
15452
|
-
...ciProvenance(),
|
|
15453
|
-
kind: "run"
|
|
15467
|
+
gitHead: git.head,
|
|
15468
|
+
deployedSha
|
|
15454
15469
|
});
|
|
15455
|
-
hubRunId =
|
|
15456
|
-
|
|
15457
|
-
await coverage?.linkHubRun(opened.id);
|
|
15458
|
-
const runId = opened.id;
|
|
15470
|
+
hubRunId = runId;
|
|
15471
|
+
await coverage?.linkHubRun(runId);
|
|
15459
15472
|
let hubPatchEverSucceeded = false;
|
|
15460
15473
|
hubSink = { onUpsert: async (row) => {
|
|
15461
15474
|
try {
|
|
@@ -15473,8 +15486,6 @@ async function executeRun(targets, opts) {
|
|
|
15473
15486
|
} else warn(`hub: incremental push failed for ${row.feature}/${row.spec}: ${errMessage(err)}`);
|
|
15474
15487
|
}
|
|
15475
15488
|
} };
|
|
15476
|
-
} catch (err) {
|
|
15477
|
-
throw new RunUsageError(`--report-to-hub: could not open a run on the hub (${errMessage(err)})`);
|
|
15478
15489
|
}
|
|
15479
15490
|
const incrementalReport = createIncrementalReport(reportDir, buildReportEnvelope({
|
|
15480
15491
|
git,
|
|
@@ -15499,6 +15510,7 @@ async function executeRun(targets, opts) {
|
|
|
15499
15510
|
warn(`hub: could not finalize interrupted run ${hubRunId}: ${errMessage(err)}`);
|
|
15500
15511
|
}
|
|
15501
15512
|
});
|
|
15513
|
+
const det = await runDeterministicSpecs(detSpecs, opts, cwd, reportDir, resources);
|
|
15502
15514
|
const externalRows = await runExternalSpecs(dispatch, {
|
|
15503
15515
|
cwd,
|
|
15504
15516
|
reportDir,
|
|
@@ -16828,6 +16840,8 @@ CCQA_STEP=<step-id> CCQA_ASSERT=url_contains:/dashboard agent-browser --session
|
|
|
16828
16840
|
|
|
16829
16841
|
- **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.
|
|
16830
16842
|
|
|
16843
|
+
- **An \`expected\` that labels its parts is recorded down to the \`must:\` lines.** A \`when present:\` line names something the product does not produce on every run, so **nothing that fails on its absence may be recorded for it** — no assert, and no \`wait --text\` either: a recorded wait throws on the next run that legitimately lacks it, which is the same failure an assert would have caused. Look at it with \`snapshot\` or \`get count\` if you want to see it, and record nothing. When the step needs to wait for something asynchronous to arrive, anchor that wait on a \`must:\` signal — waiting on the optional one makes the whole step hinge on it. Every line of a labelled \`expected\` carries a label; text with no labels at all is all \`must:\`.
|
|
16844
|
+
|
|
16831
16845
|
- **\`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.
|
|
16832
16846
|
- **When you do assert a URL, the substring may come from ONE place only:** a \`\${VAR}\` URL that *this step's own instruction* opened, written as that \`\${VAR}\` followed by the literal tail after it. If the step opens \`\${APP_URL}/policies\`, assert \`\${APP_URL}/policies\` (or the tail \`/policies\`); the recorder resolves \`\${APP_URL}\` per environment. Never assert on a URL you merely *observed* — login redirects, identity-provider pages, and OAuth callbacks all live on a **different, environment-named origin** than the app, so any substring of them (host, origin, OR path) names the environment.
|
|
16833
16847
|
- **A leading slash does NOT make a substring safe.** \`/auth-staging\`, \`/env-qa\`, \`/tenant-acme\` look like paths but are environment labels — the first segment of an identity-provider or tenant URL, not an application route. If a substring contains an environment name, a stage token (\`dev\`, \`stg\`, \`prod\`), a tenant/org name, or any fragment of a hostname, it is forbidden even with a leading slash. The only safe path substrings are stable *application* routes off the app's own origin (\`/dashboard\`, \`/policies/new\`), taken from a \`\${VAR}\` you opened — not from a redirect you watched.
|
|
@@ -17986,8 +18000,7 @@ async function runRecord(specPath, opts) {
|
|
|
17986
18000
|
}
|
|
17987
18001
|
throw e;
|
|
17988
18002
|
});
|
|
17989
|
-
const push = pushConn ? await openHubRun("record", pushConn, cwdForProfile, opts.hubProfile) : null;
|
|
17990
|
-
if (push) info(`hub: record run opened (${push.runId})`);
|
|
18003
|
+
const push = pushConn ? await openHubRun("record", pushConn, cwdForProfile, { ...opts.hubProfile ? { profile: opts.hubProfile } : {} }) : null;
|
|
17991
18004
|
let recorded = false;
|
|
17992
18005
|
let sealed = true;
|
|
17993
18006
|
let tracingStep;
|
|
@@ -19065,10 +19078,7 @@ async function runAudit(specPath, opts) {
|
|
|
19065
19078
|
let push = null;
|
|
19066
19079
|
try {
|
|
19067
19080
|
promptCtx = await resolveAuditPromptContext(opts, cwd);
|
|
19068
|
-
if (pushConn) {
|
|
19069
|
-
push = await openHubRun("drift", pushConn, cwd, opts.hubProfile);
|
|
19070
|
-
if (format === "text") info(`hub: incremental drift run opened (${push.runId})`);
|
|
19071
|
-
}
|
|
19081
|
+
if (pushConn) push = await openHubRun("drift", pushConn, cwd, { ...opts.hubProfile ? { profile: opts.hubProfile } : {} });
|
|
19072
19082
|
results = await analyzeDrift({
|
|
19073
19083
|
targets,
|
|
19074
19084
|
cwd,
|
|
@@ -20613,7 +20623,7 @@ function createPatchRunHandler(config) {
|
|
|
20613
20623
|
sendJson(ctx.res, 200, updated);
|
|
20614
20624
|
};
|
|
20615
20625
|
}
|
|
20616
|
-
/** GET /api/v1/runs?project&branch&status&kind&since&until&limit */
|
|
20626
|
+
/** GET /api/v1/runs?project&branch&status&kind&ciRunId&since&until&limit */
|
|
20617
20627
|
function createListRunsHandler(storage) {
|
|
20618
20628
|
return async (ctx) => {
|
|
20619
20629
|
const project = ctx.url.searchParams.get("project");
|
|
@@ -20621,11 +20631,13 @@ function createListRunsHandler(storage) {
|
|
|
20621
20631
|
const status = ctx.url.searchParams.get("status");
|
|
20622
20632
|
const limitRaw = ctx.url.searchParams.get("limit");
|
|
20623
20633
|
const kindsRaw = ctx.url.searchParams.get("kind");
|
|
20634
|
+
const ciRunId = boundedParam(ctx.url.searchParams.get("ciRunId"), "ciRunId", 128);
|
|
20624
20635
|
const runs = await storage.runs.list({
|
|
20625
20636
|
...project ? { project } : {},
|
|
20626
20637
|
...branch ? { branch } : {},
|
|
20627
20638
|
...status ? { status } : {},
|
|
20628
20639
|
...kindsRaw ? { kinds: kindsRaw.split(",").map(requireKind) } : {},
|
|
20640
|
+
...ciRunId ? { ciRunId } : {},
|
|
20629
20641
|
...requireWindowParams(ctx.url),
|
|
20630
20642
|
...limitRaw ? { limit: Number(limitRaw) } : {}
|
|
20631
20643
|
});
|
|
@@ -24817,12 +24829,15 @@ const CLIENT_JS = `
|
|
|
24817
24829
|
// Disconnected: the full-screen login gate is the only thing to show.
|
|
24818
24830
|
if (!state.token) { showAuthGate(false); return; }
|
|
24819
24831
|
showAuthGate(true);
|
|
24832
|
+
// A run is addressed by its own id, not through a project, so a link from
|
|
24833
|
+
// outside (a chat message, a CI job) opens it even in a browser that has
|
|
24834
|
+
// never picked one — ahead of the gate below, which would drop the link.
|
|
24835
|
+
var m = parseRunHash(location.hash);
|
|
24836
|
+
if (m) { openRunDetail(m.runId, m.caseKey); return; }
|
|
24820
24837
|
// With no project chosen yet, the Projects picker is the only useful view —
|
|
24821
24838
|
// land there (e.g. right after login) instead of an empty Runs list, and
|
|
24822
|
-
// gate any deep-linked
|
|
24839
|
+
// gate any deep-linked project-scoped view to it too.
|
|
24823
24840
|
if (location.hash === "#/projects" || !state.project) { openProjects(); return; }
|
|
24824
|
-
var m = parseRunHash(location.hash);
|
|
24825
|
-
if (m) { openRunDetail(m.runId, m.caseKey); return; }
|
|
24826
24841
|
if (location.hash === "#/perspectives") { openPerspectives(); return; }
|
|
24827
24842
|
if (location.hash === "#/coverage") { openCoverage(); return; }
|
|
24828
24843
|
if (location.hash === "#/secrets") { openSecrets(); return; }
|
|
@@ -26406,6 +26421,11 @@ const CLIENT_JS = `
|
|
|
26406
26421
|
var runGone = false;
|
|
26407
26422
|
|
|
26408
26423
|
apiFetch("/api/v1/runs/" + encodeURIComponent(runId)).then(function (run) {
|
|
26424
|
+
// Arrived by an outside link: the selected project is whatever the last
|
|
26425
|
+
// visit or the boot auto-select left, which need not be this run's.
|
|
26426
|
+
// Follow the run, so the nav around it answers for the project the page
|
|
26427
|
+
// is actually showing.
|
|
26428
|
+
if (run.project && run.project !== state.project) adoptProject(run.project);
|
|
26409
26429
|
renderRunHead(run);
|
|
26410
26430
|
}).catch(function (err) {
|
|
26411
26431
|
runGone = err.status === 404;
|
|
@@ -28483,14 +28503,20 @@ const CLIENT_JS = `
|
|
|
28483
28503
|
updateNavGate();
|
|
28484
28504
|
}
|
|
28485
28505
|
|
|
28486
|
-
// Scope to a project
|
|
28487
|
-
//
|
|
28488
|
-
//
|
|
28489
|
-
//
|
|
28490
|
-
function
|
|
28506
|
+
// Scope the UI to a project, restoring the profile last chosen for it (or
|
|
28507
|
+
// "default" if none was saved); the Secrets tab reloads the profile list when
|
|
28508
|
+
// opened. Separate from the navigation below because arriving on a run by
|
|
28509
|
+
// link adopts a project without going anywhere.
|
|
28510
|
+
function adoptProject(p) {
|
|
28491
28511
|
setProject(p);
|
|
28492
28512
|
storeProject(p);
|
|
28493
28513
|
setProfile(storedProfileForProject(p) || "default");
|
|
28514
|
+
}
|
|
28515
|
+
|
|
28516
|
+
// Scope to a project and land on its Runs view. Shared by the top menu, the
|
|
28517
|
+
// Projects grid, and the "new project" flow.
|
|
28518
|
+
function chooseProject(p) {
|
|
28519
|
+
adoptProject(p);
|
|
28494
28520
|
location.hash = "#/runs";
|
|
28495
28521
|
route();
|
|
28496
28522
|
}
|
|
@@ -30188,7 +30214,7 @@ function createFileRunStore(root) {
|
|
|
30188
30214
|
async delete(id) {
|
|
30189
30215
|
await serialize(runMetaPath(root, id), () => removePath(runDir(root, id)));
|
|
30190
30216
|
},
|
|
30191
|
-
async list({ project, branch, status, kinds, since, until, limit }) {
|
|
30217
|
+
async list({ project, branch, status, kinds, ciRunId, since, until, limit }) {
|
|
30192
30218
|
const ids = await listSubdirsOrEmpty(runsDir(root));
|
|
30193
30219
|
const inWindow = windowFilter({
|
|
30194
30220
|
since,
|
|
@@ -30202,6 +30228,7 @@ function createFileRunStore(root) {
|
|
|
30202
30228
|
if (branch !== void 0 && run.branch !== branch) continue;
|
|
30203
30229
|
if (status !== void 0 && run.status !== status) continue;
|
|
30204
30230
|
if (kinds !== void 0 && !kinds.includes(run.kind)) continue;
|
|
30231
|
+
if (ciRunId !== void 0 && run.ciRunId !== ciRunId) continue;
|
|
30205
30232
|
if (!inWindow(run.createdAt)) continue;
|
|
30206
30233
|
runs.push(run);
|
|
30207
30234
|
}
|
|
@@ -1147,9 +1147,10 @@ interface HubClient {
|
|
|
1147
1147
|
* `gitHead` stamps the run's commit at open time, so even a run that dies
|
|
1148
1148
|
* before its final reconcile patch is attributable to a commit.
|
|
1149
1149
|
*
|
|
1150
|
-
* `deployedSha` does the same for the environment's commit
|
|
1151
|
-
*
|
|
1152
|
-
* the
|
|
1150
|
+
* `deployedSha` does the same for the environment's commit: the baseline the
|
|
1151
|
+
* caller selected against, which a `--rerun` inherits from an earlier run.
|
|
1152
|
+
* Left unset the hub reads its own deploy-log head, which by the time this
|
|
1153
|
+
* call lands can already name a later deploy.
|
|
1153
1154
|
*/
|
|
1154
1155
|
openRun(meta: {
|
|
1155
1156
|
project: string;
|
|
@@ -1165,11 +1166,13 @@ interface HubClient {
|
|
|
1165
1166
|
}): Promise<Run>;
|
|
1166
1167
|
/** Add finished spec rows (+ evidence) to a running run; `done` closes it. */
|
|
1167
1168
|
patchRun(id: string, body: PatchRunRequest): Promise<Run>;
|
|
1169
|
+
/** `ciRunId` answers "which runs did this CI job create", whatever the window. */
|
|
1168
1170
|
listRuns(q?: {
|
|
1169
1171
|
project?: string;
|
|
1170
1172
|
branch?: string;
|
|
1171
1173
|
status?: RunStatus;
|
|
1172
1174
|
kind?: Run["kind"];
|
|
1175
|
+
ciRunId?: string;
|
|
1173
1176
|
limit?: number;
|
|
1174
1177
|
}): Promise<Run[]>;
|
|
1175
1178
|
getRun(id: string): Promise<Run>;
|
package/dist/package.json
CHANGED