ccqa 1.50.0 → 1.51.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 +212 -59
- 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
|
@@ -8603,11 +8603,11 @@ async function buildLiveTranscriptExcerpt(result, options = {}) {
|
|
|
8603
8603
|
return combined.length > maxBytes ? `${combined.slice(0, maxBytes)}\n…[transcript excerpt truncated at ${maxBytes} bytes]` : combined;
|
|
8604
8604
|
}
|
|
8605
8605
|
function formatPreviousStep(step) {
|
|
8606
|
-
const reason = oneLine$
|
|
8606
|
+
const reason = oneLine$3(step.reasoning) || "(no reason given)";
|
|
8607
8607
|
return `[${step.stepId} ${step.status}: ${reason}]`;
|
|
8608
8608
|
}
|
|
8609
8609
|
async function formatFailingStep(step, headBytes, tailBytes) {
|
|
8610
|
-
const header = `\n>>> Failed step ${step.stepId}\nInstruction: ${oneLine$
|
|
8610
|
+
const header = `\n>>> Failed step ${step.stepId}\nInstruction: ${oneLine$3(step.instruction)}\nExpected: ${oneLine$3(step.expected)}\nReasoning (Claude's verdict): ${oneLine$3(step.reasoning) || "(none)"}`;
|
|
8611
8611
|
if (!step.logTxt) return `${header}\n(No assistant log file recorded for this step.)`;
|
|
8612
8612
|
const raw = await readFile(step.logTxt, "utf-8").catch((err) => {
|
|
8613
8613
|
return `[log file unreadable: ${err instanceof Error ? err.message : String(err)}]`;
|
|
@@ -8617,7 +8617,7 @@ async function formatFailingStep(step, headBytes, tailBytes) {
|
|
|
8617
8617
|
const tail = raw.slice(raw.length - tailBytes);
|
|
8618
8618
|
return `${header}\n--- assistant log (head ${headBytes}B) ---\n${head}\n…[${raw.length - headBytes - tailBytes} bytes omitted]…\n--- assistant log (tail ${tailBytes}B) ---\n${tail}`;
|
|
8619
8619
|
}
|
|
8620
|
-
function oneLine$
|
|
8620
|
+
function oneLine$3(s) {
|
|
8621
8621
|
return s.replace(/\s+/g, " ").trim();
|
|
8622
8622
|
}
|
|
8623
8623
|
//#endregion
|
|
@@ -10129,10 +10129,10 @@ function describeStep(step) {
|
|
|
10129
10129
|
return describeStepBody(step);
|
|
10130
10130
|
}
|
|
10131
10131
|
function describeStepBody(step) {
|
|
10132
|
-
if (isJudgeBody(step)) return `judge: ${oneLine$
|
|
10133
|
-
return `${oneLine$
|
|
10132
|
+
if (isJudgeBody(step)) return `judge: ${oneLine$2(step.judgeByLlm)}`;
|
|
10133
|
+
return `${oneLine$2(step.instruction)} → ${oneLine$2(step.expected)}`;
|
|
10134
10134
|
}
|
|
10135
|
-
function oneLine$
|
|
10135
|
+
function oneLine$2(text) {
|
|
10136
10136
|
return text.trim().replace(/\s+/g, " ");
|
|
10137
10137
|
}
|
|
10138
10138
|
function emptyDeployLog() {
|
|
@@ -12432,15 +12432,15 @@ function renderRunMarkdown(featureName, specName, result) {
|
|
|
12432
12432
|
].join("\n") + result.steps.map((s) => [
|
|
12433
12433
|
`## ${s.stepId} — ${s.status}`,
|
|
12434
12434
|
`- duration: ${(s.durationMs / 1e3).toFixed(1)}s`,
|
|
12435
|
-
`- instruction: ${oneLine(s.instruction)}`,
|
|
12436
|
-
`- expected: ${oneLine(s.expected)}`,
|
|
12437
|
-
`- reasoning: ${oneLine(s.reasoning)}`,
|
|
12435
|
+
`- instruction: ${oneLine$1(s.instruction)}`,
|
|
12436
|
+
`- expected: ${oneLine$1(s.expected)}`,
|
|
12437
|
+
`- reasoning: ${oneLine$1(s.reasoning)}`,
|
|
12438
12438
|
...s.beforePng ? [`- before: ${s.beforePng}`] : [],
|
|
12439
12439
|
...s.afterPng ? [`- after: ${s.afterPng}`] : [],
|
|
12440
12440
|
""
|
|
12441
12441
|
].join("\n")).join("\n");
|
|
12442
12442
|
}
|
|
12443
|
-
function oneLine(s) {
|
|
12443
|
+
function oneLine$1(s) {
|
|
12444
12444
|
return s.replace(/\s+/g, " ").trim();
|
|
12445
12445
|
}
|
|
12446
12446
|
//#endregion
|
|
@@ -14004,6 +14004,136 @@ async function waitForCdp(port) {
|
|
|
14004
14004
|
}
|
|
14005
14005
|
}
|
|
14006
14006
|
//#endregion
|
|
14007
|
+
//#region src/prompts/verifies-spec.ts
|
|
14008
|
+
/**
|
|
14009
|
+
* Asks whether a generated test actually decides what its spec claims.
|
|
14010
|
+
*
|
|
14011
|
+
* The generation loop's only bar is "the test goes green", and a rewrite that
|
|
14012
|
+
* weakens an assertion clears that bar as easily as one that keeps it. Green
|
|
14013
|
+
* therefore does not mean checked, and nothing else looks. Observed cases: a
|
|
14014
|
+
* step whose expectation was "the linked page opens" asserting instead that
|
|
14015
|
+
* the *link* is still visible on the page it clicked from; another asserting
|
|
14016
|
+
* on a navigation element unrelated to the step.
|
|
14017
|
+
*
|
|
14018
|
+
* Deliberately narrow. It reads only what the step says and what the code
|
|
14019
|
+
* does, and reports the step as unchecked when the two do not line up. It
|
|
14020
|
+
* does not review style, coverage, or whether the expectation is a good one.
|
|
14021
|
+
*/
|
|
14022
|
+
function verifiesSpecPrompt(input) {
|
|
14023
|
+
return [
|
|
14024
|
+
"You are reviewing whether a generated end-to-end test decides what its spec says.",
|
|
14025
|
+
"",
|
|
14026
|
+
"For each step below, the test must contain assertions that could FAIL if the",
|
|
14027
|
+
"step's `expected` stopped holding. Report a step when:",
|
|
14028
|
+
"",
|
|
14029
|
+
"- its assertions cannot fail while the product is broken in the way the",
|
|
14030
|
+
" expectation describes (e.g. the expectation says a page opens, and the",
|
|
14031
|
+
" code only re-checks the element it clicked);",
|
|
14032
|
+
"- what it asserts on is unrelated to what the step did (e.g. a navigation",
|
|
14033
|
+
" element that is present on every page);",
|
|
14034
|
+
"- it depends on something that varies between runs and is not part of the",
|
|
14035
|
+
" expectation (a count, an index, a position, wording that changes);",
|
|
14036
|
+
"- it has no assertion at all.",
|
|
14037
|
+
"",
|
|
14038
|
+
"Do NOT report: style, naming, structure, missing coverage the spec never",
|
|
14039
|
+
"asked for, or an expectation you merely disagree with. A step that checks",
|
|
14040
|
+
"less than you would have written, but still fails when the expectation",
|
|
14041
|
+
"breaks, is fine.",
|
|
14042
|
+
"",
|
|
14043
|
+
"## Steps",
|
|
14044
|
+
"",
|
|
14045
|
+
...input.steps.map(stepLine),
|
|
14046
|
+
"",
|
|
14047
|
+
"## Generated test",
|
|
14048
|
+
"",
|
|
14049
|
+
"```",
|
|
14050
|
+
input.source,
|
|
14051
|
+
"```",
|
|
14052
|
+
"",
|
|
14053
|
+
"Answer with one json block and nothing else:",
|
|
14054
|
+
"",
|
|
14055
|
+
"```json",
|
|
14056
|
+
"{ \"findings\": [ { \"stepId\": \"step-05\", \"problem\": \"…\" } ] }",
|
|
14057
|
+
"```",
|
|
14058
|
+
"",
|
|
14059
|
+
"`problem` is one sentence naming what the step claims and what the code",
|
|
14060
|
+
"checks instead. Empty `findings` means every step is decided.",
|
|
14061
|
+
languageDirective(input.language)
|
|
14062
|
+
].join("\n");
|
|
14063
|
+
}
|
|
14064
|
+
function stepLine(step) {
|
|
14065
|
+
if (!isExpandedActionStep(step)) return `- ${step.id}: judged by a model at run time — its claim is asserted by the injected call, so it needs no other assertion.`;
|
|
14066
|
+
return [
|
|
14067
|
+
`- ${step.id}`,
|
|
14068
|
+
` does: ${oneLine(step.instruction)}`,
|
|
14069
|
+
` expected: ${oneLine(step.expected)}`
|
|
14070
|
+
].join("\n");
|
|
14071
|
+
}
|
|
14072
|
+
function oneLine(text) {
|
|
14073
|
+
return text.trim().split("\n").map((l) => l.trim()).join(" ");
|
|
14074
|
+
}
|
|
14075
|
+
//#endregion
|
|
14076
|
+
//#region src/targets/verifies-spec.ts
|
|
14077
|
+
const FindingsSchema = z.object({ findings: z.array(z.object({
|
|
14078
|
+
stepId: z.string(),
|
|
14079
|
+
problem: z.string()
|
|
14080
|
+
})) });
|
|
14081
|
+
/**
|
|
14082
|
+
* Findings in the model's answer, or null when it did not answer in the
|
|
14083
|
+
* agreed shape. Null is not "no findings": the caller says so rather than
|
|
14084
|
+
* reporting a clean review it never got.
|
|
14085
|
+
*/
|
|
14086
|
+
function parseVerifiesSpecFindings(answer) {
|
|
14087
|
+
const json = extractJsonBlock(answer);
|
|
14088
|
+
if (!json) return null;
|
|
14089
|
+
try {
|
|
14090
|
+
return FindingsSchema.parse(JSON.parse(json)).findings;
|
|
14091
|
+
} catch {
|
|
14092
|
+
return null;
|
|
14093
|
+
}
|
|
14094
|
+
}
|
|
14095
|
+
/** The warning a finding becomes, phrased so the reader knows the test is green for nothing. */
|
|
14096
|
+
function formatFinding(finding) {
|
|
14097
|
+
return `step ${finding.stepId}: the generated test passes without deciding what this step claims — ${finding.problem}`;
|
|
14098
|
+
}
|
|
14099
|
+
/**
|
|
14100
|
+
* Read the generated test back and ask whether each step is actually decided
|
|
14101
|
+
* (see `verifiesSpecPrompt`). Returns warnings; an empty list means either a
|
|
14102
|
+
* clean review or one that could not be obtained, and the difference is
|
|
14103
|
+
* logged rather than encoded — a review that failed must not read as a pass,
|
|
14104
|
+
* but it must also not fail the generate that produced working files.
|
|
14105
|
+
*/
|
|
14106
|
+
async function reviewGeneratedTest(input) {
|
|
14107
|
+
const source = (await Promise.all(input.result.files.filter((f) => f.kind === "test").map((f) => readFile(f.path, "utf8").catch(() => "")))).filter((s) => s.length > 0).join("\n\n");
|
|
14108
|
+
if (source.length === 0) {
|
|
14109
|
+
warn("could not check whether the generated test decides its spec (no test file to read)");
|
|
14110
|
+
return [];
|
|
14111
|
+
}
|
|
14112
|
+
const { result: answer, isError } = await (input.invoke ?? invokeClaudeStreaming)({
|
|
14113
|
+
prompt: verifiesSpecPrompt({
|
|
14114
|
+
steps: input.steps,
|
|
14115
|
+
source,
|
|
14116
|
+
language: input.language
|
|
14117
|
+
}),
|
|
14118
|
+
allowedTools: [],
|
|
14119
|
+
disableThinking: true,
|
|
14120
|
+
maxTurns: 1,
|
|
14121
|
+
silenceBashLog: true,
|
|
14122
|
+
...input.model ? { model: input.model } : {},
|
|
14123
|
+
cwd: input.cwd
|
|
14124
|
+
}, () => {});
|
|
14125
|
+
if (isError) {
|
|
14126
|
+
warn("could not check whether the generated test decides its spec (Claude returned an error)");
|
|
14127
|
+
return [];
|
|
14128
|
+
}
|
|
14129
|
+
const findings = parseVerifiesSpecFindings(answer);
|
|
14130
|
+
if (findings === null) {
|
|
14131
|
+
warn("could not check whether the generated test decides its spec (no usable answer)");
|
|
14132
|
+
return [];
|
|
14133
|
+
}
|
|
14134
|
+
return findings.map(formatFinding);
|
|
14135
|
+
}
|
|
14136
|
+
//#endregion
|
|
14007
14137
|
//#region src/targets/playwright/index.ts
|
|
14008
14138
|
const PLAYWRIGHT_TARGET = "playwright";
|
|
14009
14139
|
/**
|
|
@@ -14075,12 +14205,21 @@ async function generatePlaywrightTest(ctx) {
|
|
|
14075
14205
|
});
|
|
14076
14206
|
const missing = await missingInjectedCalls(result, stepMarkers, judgements);
|
|
14077
14207
|
for (const w of missing) warn(w);
|
|
14208
|
+
const unchecked = await reviewGeneratedTest({
|
|
14209
|
+
result,
|
|
14210
|
+
steps: expanded,
|
|
14211
|
+
language: ctx.language,
|
|
14212
|
+
...ctx.model ? { model: ctx.model } : {},
|
|
14213
|
+
cwd: ctx.cwd
|
|
14214
|
+
});
|
|
14215
|
+
for (const w of unchecked) warn(w);
|
|
14078
14216
|
return {
|
|
14079
14217
|
...result,
|
|
14080
14218
|
warnings: [
|
|
14081
14219
|
...result.warnings,
|
|
14082
14220
|
...judgeWarnings,
|
|
14083
|
-
...missing
|
|
14221
|
+
...missing,
|
|
14222
|
+
...unchecked
|
|
14084
14223
|
]
|
|
14085
14224
|
};
|
|
14086
14225
|
}
|
|
@@ -14525,32 +14664,42 @@ function requireReportToHubConnection(conn) {
|
|
|
14525
14664
|
process.exit(2);
|
|
14526
14665
|
}
|
|
14527
14666
|
/**
|
|
14528
|
-
* Open the run a `--report-to-hub` command patches into
|
|
14529
|
-
*
|
|
14530
|
-
*
|
|
14531
|
-
*
|
|
14532
|
-
*
|
|
14667
|
+
* Open the run a `--report-to-hub` command patches into, and name it on stderr
|
|
14668
|
+
* — stdout belongs to the report (`--report-format json`), and a caller that
|
|
14669
|
+
* links to the run needs its id before the command ends (docs/hub.md).
|
|
14670
|
+
*
|
|
14671
|
+
* Failure is fatal: a job that asked to publish and cannot reach the hub has
|
|
14672
|
+
* not done what it was told. Thrown rather than exited, so a caller's
|
|
14673
|
+
* `finally` still runs (the audit releases its spec claims there). Not
|
|
14674
|
+
* retried: a dropped response after the hub committed would leave a second
|
|
14675
|
+
* orphan running run.
|
|
14533
14676
|
*/
|
|
14534
|
-
async function openHubRun(kind, conn, cwd,
|
|
14535
|
-
const [branch, gitHead] = await Promise.all([detectBranch(cwd), getGitHead(cwd)]);
|
|
14677
|
+
async function openHubRun(kind, conn, cwd, opts = {}) {
|
|
14678
|
+
const [branch, gitHead] = await Promise.all([detectBranch(cwd), opts.gitHead !== void 0 ? opts.gitHead : getGitHead(cwd)]);
|
|
14679
|
+
let opened;
|
|
14536
14680
|
try {
|
|
14537
|
-
|
|
14681
|
+
opened = await conn.hub.openRun({
|
|
14538
14682
|
project: conn.project,
|
|
14539
14683
|
kind,
|
|
14540
14684
|
...branch ? { branch } : {},
|
|
14541
|
-
...profile ? { profile } : {},
|
|
14685
|
+
...opts.profile ? { profile: opts.profile } : {},
|
|
14542
14686
|
...gitHead ? { gitHead } : {},
|
|
14687
|
+
...opts.deployedSha ? { deployedSha: opts.deployedSha } : {},
|
|
14543
14688
|
...ciProvenance()
|
|
14544
14689
|
});
|
|
14545
|
-
return {
|
|
14546
|
-
hub: conn.hub,
|
|
14547
|
-
kind,
|
|
14548
|
-
runId: run.id,
|
|
14549
|
-
gitHead
|
|
14550
|
-
};
|
|
14551
14690
|
} catch (err) {
|
|
14552
14691
|
throw new RunUsageError(`--report-to-hub: could not open a run on the hub (${errMessage(err)})`);
|
|
14553
14692
|
}
|
|
14693
|
+
process.stderr.write(`[hub-run] ${JSON.stringify({
|
|
14694
|
+
id: opened.id,
|
|
14695
|
+
kind
|
|
14696
|
+
})}\n`);
|
|
14697
|
+
return {
|
|
14698
|
+
hub: conn.hub,
|
|
14699
|
+
kind,
|
|
14700
|
+
runId: opened.id,
|
|
14701
|
+
gitHead
|
|
14702
|
+
};
|
|
14554
14703
|
}
|
|
14555
14704
|
/**
|
|
14556
14705
|
* Close an open run with its final rows and envelope, answering whether it
|
|
@@ -15238,7 +15387,7 @@ async function executeRun(targets, opts) {
|
|
|
15238
15387
|
const declared = [...new Set(specs.flatMap(resources))];
|
|
15239
15388
|
if (declared.length > 0) meta("serial groups", declared.join(", "));
|
|
15240
15389
|
let waitingOnGroup = [];
|
|
15241
|
-
if (hubCtx && rerunProfile !== null) {
|
|
15390
|
+
if (hubCtx && rerunProfile !== null && forExecution) {
|
|
15242
15391
|
const held = await holdSpecs(hubCtx, rerunProfile, specs, resources, opts.teardown);
|
|
15243
15392
|
waitingOnGroup = specs.flatMap((spec) => {
|
|
15244
15393
|
const groups = resources(spec).filter((n) => held.deniedResources.includes(n));
|
|
@@ -15298,25 +15447,17 @@ async function executeRun(targets, opts) {
|
|
|
15298
15447
|
reportDir: null
|
|
15299
15448
|
};
|
|
15300
15449
|
}
|
|
15301
|
-
const det = await runDeterministicSpecs(detSpecs, opts, cwd, reportDir, resources);
|
|
15302
15450
|
let hubRunId = null;
|
|
15303
15451
|
let hubSink;
|
|
15304
15452
|
let hubPublishBroken = false;
|
|
15305
|
-
if (hubCtx != null && opts.reportToHub)
|
|
15306
|
-
const
|
|
15307
|
-
const opened = await hubCtx.hub.openRun({
|
|
15308
|
-
project: hubCtx.project,
|
|
15309
|
-
...branch ? { branch } : {},
|
|
15453
|
+
if (hubCtx != null && opts.reportToHub) {
|
|
15454
|
+
const { runId } = await openHubRun("run", hubCtx, cwd, {
|
|
15310
15455
|
...opts.hubProfile ? { profile: opts.hubProfile } : {},
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
...ciProvenance(),
|
|
15314
|
-
kind: "run"
|
|
15456
|
+
gitHead: git.head,
|
|
15457
|
+
deployedSha
|
|
15315
15458
|
});
|
|
15316
|
-
hubRunId =
|
|
15317
|
-
|
|
15318
|
-
await coverage?.linkHubRun(opened.id);
|
|
15319
|
-
const runId = opened.id;
|
|
15459
|
+
hubRunId = runId;
|
|
15460
|
+
await coverage?.linkHubRun(runId);
|
|
15320
15461
|
let hubPatchEverSucceeded = false;
|
|
15321
15462
|
hubSink = { onUpsert: async (row) => {
|
|
15322
15463
|
try {
|
|
@@ -15334,8 +15475,6 @@ async function executeRun(targets, opts) {
|
|
|
15334
15475
|
} else warn(`hub: incremental push failed for ${row.feature}/${row.spec}: ${errMessage(err)}`);
|
|
15335
15476
|
}
|
|
15336
15477
|
} };
|
|
15337
|
-
} catch (err) {
|
|
15338
|
-
throw new RunUsageError(`--report-to-hub: could not open a run on the hub (${errMessage(err)})`);
|
|
15339
15478
|
}
|
|
15340
15479
|
const incrementalReport = createIncrementalReport(reportDir, buildReportEnvelope({
|
|
15341
15480
|
git,
|
|
@@ -15360,6 +15499,7 @@ async function executeRun(targets, opts) {
|
|
|
15360
15499
|
warn(`hub: could not finalize interrupted run ${hubRunId}: ${errMessage(err)}`);
|
|
15361
15500
|
}
|
|
15362
15501
|
});
|
|
15502
|
+
const det = await runDeterministicSpecs(detSpecs, opts, cwd, reportDir, resources);
|
|
15363
15503
|
const externalRows = await runExternalSpecs(dispatch, {
|
|
15364
15504
|
cwd,
|
|
15365
15505
|
reportDir,
|
|
@@ -17847,8 +17987,7 @@ async function runRecord(specPath, opts) {
|
|
|
17847
17987
|
}
|
|
17848
17988
|
throw e;
|
|
17849
17989
|
});
|
|
17850
|
-
const push = pushConn ? await openHubRun("record", pushConn, cwdForProfile, opts.hubProfile) : null;
|
|
17851
|
-
if (push) info(`hub: record run opened (${push.runId})`);
|
|
17990
|
+
const push = pushConn ? await openHubRun("record", pushConn, cwdForProfile, { ...opts.hubProfile ? { profile: opts.hubProfile } : {} }) : null;
|
|
17852
17991
|
let recorded = false;
|
|
17853
17992
|
let sealed = true;
|
|
17854
17993
|
let tracingStep;
|
|
@@ -18926,10 +19065,7 @@ async function runAudit(specPath, opts) {
|
|
|
18926
19065
|
let push = null;
|
|
18927
19066
|
try {
|
|
18928
19067
|
promptCtx = await resolveAuditPromptContext(opts, cwd);
|
|
18929
|
-
if (pushConn) {
|
|
18930
|
-
push = await openHubRun("drift", pushConn, cwd, opts.hubProfile);
|
|
18931
|
-
if (format === "text") info(`hub: incremental drift run opened (${push.runId})`);
|
|
18932
|
-
}
|
|
19068
|
+
if (pushConn) push = await openHubRun("drift", pushConn, cwd, { ...opts.hubProfile ? { profile: opts.hubProfile } : {} });
|
|
18933
19069
|
results = await analyzeDrift({
|
|
18934
19070
|
targets,
|
|
18935
19071
|
cwd,
|
|
@@ -20474,7 +20610,7 @@ function createPatchRunHandler(config) {
|
|
|
20474
20610
|
sendJson(ctx.res, 200, updated);
|
|
20475
20611
|
};
|
|
20476
20612
|
}
|
|
20477
|
-
/** GET /api/v1/runs?project&branch&status&kind&since&until&limit */
|
|
20613
|
+
/** GET /api/v1/runs?project&branch&status&kind&ciRunId&since&until&limit */
|
|
20478
20614
|
function createListRunsHandler(storage) {
|
|
20479
20615
|
return async (ctx) => {
|
|
20480
20616
|
const project = ctx.url.searchParams.get("project");
|
|
@@ -20482,11 +20618,13 @@ function createListRunsHandler(storage) {
|
|
|
20482
20618
|
const status = ctx.url.searchParams.get("status");
|
|
20483
20619
|
const limitRaw = ctx.url.searchParams.get("limit");
|
|
20484
20620
|
const kindsRaw = ctx.url.searchParams.get("kind");
|
|
20621
|
+
const ciRunId = boundedParam(ctx.url.searchParams.get("ciRunId"), "ciRunId", 128);
|
|
20485
20622
|
const runs = await storage.runs.list({
|
|
20486
20623
|
...project ? { project } : {},
|
|
20487
20624
|
...branch ? { branch } : {},
|
|
20488
20625
|
...status ? { status } : {},
|
|
20489
20626
|
...kindsRaw ? { kinds: kindsRaw.split(",").map(requireKind) } : {},
|
|
20627
|
+
...ciRunId ? { ciRunId } : {},
|
|
20490
20628
|
...requireWindowParams(ctx.url),
|
|
20491
20629
|
...limitRaw ? { limit: Number(limitRaw) } : {}
|
|
20492
20630
|
});
|
|
@@ -24678,12 +24816,15 @@ const CLIENT_JS = `
|
|
|
24678
24816
|
// Disconnected: the full-screen login gate is the only thing to show.
|
|
24679
24817
|
if (!state.token) { showAuthGate(false); return; }
|
|
24680
24818
|
showAuthGate(true);
|
|
24819
|
+
// A run is addressed by its own id, not through a project, so a link from
|
|
24820
|
+
// outside (a chat message, a CI job) opens it even in a browser that has
|
|
24821
|
+
// never picked one — ahead of the gate below, which would drop the link.
|
|
24822
|
+
var m = parseRunHash(location.hash);
|
|
24823
|
+
if (m) { openRunDetail(m.runId, m.caseKey); return; }
|
|
24681
24824
|
// With no project chosen yet, the Projects picker is the only useful view —
|
|
24682
24825
|
// land there (e.g. right after login) instead of an empty Runs list, and
|
|
24683
|
-
// gate any deep-linked
|
|
24826
|
+
// gate any deep-linked project-scoped view to it too.
|
|
24684
24827
|
if (location.hash === "#/projects" || !state.project) { openProjects(); return; }
|
|
24685
|
-
var m = parseRunHash(location.hash);
|
|
24686
|
-
if (m) { openRunDetail(m.runId, m.caseKey); return; }
|
|
24687
24828
|
if (location.hash === "#/perspectives") { openPerspectives(); return; }
|
|
24688
24829
|
if (location.hash === "#/coverage") { openCoverage(); return; }
|
|
24689
24830
|
if (location.hash === "#/secrets") { openSecrets(); return; }
|
|
@@ -26267,6 +26408,11 @@ const CLIENT_JS = `
|
|
|
26267
26408
|
var runGone = false;
|
|
26268
26409
|
|
|
26269
26410
|
apiFetch("/api/v1/runs/" + encodeURIComponent(runId)).then(function (run) {
|
|
26411
|
+
// Arrived by an outside link: the selected project is whatever the last
|
|
26412
|
+
// visit or the boot auto-select left, which need not be this run's.
|
|
26413
|
+
// Follow the run, so the nav around it answers for the project the page
|
|
26414
|
+
// is actually showing.
|
|
26415
|
+
if (run.project && run.project !== state.project) adoptProject(run.project);
|
|
26270
26416
|
renderRunHead(run);
|
|
26271
26417
|
}).catch(function (err) {
|
|
26272
26418
|
runGone = err.status === 404;
|
|
@@ -28344,14 +28490,20 @@ const CLIENT_JS = `
|
|
|
28344
28490
|
updateNavGate();
|
|
28345
28491
|
}
|
|
28346
28492
|
|
|
28347
|
-
// Scope to a project
|
|
28348
|
-
//
|
|
28349
|
-
//
|
|
28350
|
-
//
|
|
28351
|
-
function
|
|
28493
|
+
// Scope the UI to a project, restoring the profile last chosen for it (or
|
|
28494
|
+
// "default" if none was saved); the Secrets tab reloads the profile list when
|
|
28495
|
+
// opened. Separate from the navigation below because arriving on a run by
|
|
28496
|
+
// link adopts a project without going anywhere.
|
|
28497
|
+
function adoptProject(p) {
|
|
28352
28498
|
setProject(p);
|
|
28353
28499
|
storeProject(p);
|
|
28354
28500
|
setProfile(storedProfileForProject(p) || "default");
|
|
28501
|
+
}
|
|
28502
|
+
|
|
28503
|
+
// Scope to a project and land on its Runs view. Shared by the top menu, the
|
|
28504
|
+
// Projects grid, and the "new project" flow.
|
|
28505
|
+
function chooseProject(p) {
|
|
28506
|
+
adoptProject(p);
|
|
28355
28507
|
location.hash = "#/runs";
|
|
28356
28508
|
route();
|
|
28357
28509
|
}
|
|
@@ -30049,7 +30201,7 @@ function createFileRunStore(root) {
|
|
|
30049
30201
|
async delete(id) {
|
|
30050
30202
|
await serialize(runMetaPath(root, id), () => removePath(runDir(root, id)));
|
|
30051
30203
|
},
|
|
30052
|
-
async list({ project, branch, status, kinds, since, until, limit }) {
|
|
30204
|
+
async list({ project, branch, status, kinds, ciRunId, since, until, limit }) {
|
|
30053
30205
|
const ids = await listSubdirsOrEmpty(runsDir(root));
|
|
30054
30206
|
const inWindow = windowFilter({
|
|
30055
30207
|
since,
|
|
@@ -30063,6 +30215,7 @@ function createFileRunStore(root) {
|
|
|
30063
30215
|
if (branch !== void 0 && run.branch !== branch) continue;
|
|
30064
30216
|
if (status !== void 0 && run.status !== status) continue;
|
|
30065
30217
|
if (kinds !== void 0 && !kinds.includes(run.kind)) continue;
|
|
30218
|
+
if (ciRunId !== void 0 && run.ciRunId !== ciRunId) continue;
|
|
30066
30219
|
if (!inWindow(run.createdAt)) continue;
|
|
30067
30220
|
runs.push(run);
|
|
30068
30221
|
}
|
|
@@ -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