ccqa 1.49.0 → 1.50.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 +237 -17
- package/dist/hub-client/index.d.mts +8 -4
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -1502,6 +1502,8 @@ const ReportSpecResultSchema = z.object({
|
|
|
1502
1502
|
failed: z.number()
|
|
1503
1503
|
}).nullable(),
|
|
1504
1504
|
durationMs: z.number().nullable(),
|
|
1505
|
+
startedAt: z.string().optional(),
|
|
1506
|
+
finishedAt: z.string().optional(),
|
|
1505
1507
|
assertions: z.array(ReportAssertionSchema).nullable(),
|
|
1506
1508
|
analysis: FailureAnalysisSchema.nullable(),
|
|
1507
1509
|
analysisSkipped: z.string().nullable(),
|
|
@@ -6198,6 +6200,7 @@ async function runOneSpec$1(ref, opts, blocks) {
|
|
|
6198
6200
|
meta("command", command);
|
|
6199
6201
|
blank();
|
|
6200
6202
|
const started = Date.now();
|
|
6203
|
+
const startedAt = new Date(started).toISOString();
|
|
6201
6204
|
let outcome;
|
|
6202
6205
|
let spawnFailure;
|
|
6203
6206
|
let measured;
|
|
@@ -6218,6 +6221,7 @@ async function runOneSpec$1(ref, opts, blocks) {
|
|
|
6218
6221
|
const coverageFields = coverageRowFields(opts, measured, attachError);
|
|
6219
6222
|
if (spawnFailure !== void 0 || outcome === void 0) return {
|
|
6220
6223
|
...didNotExecute(`could not spawn runCommand: ${spawnFailure ?? "unknown error"}`, "the runCommand could not be spawned"),
|
|
6224
|
+
startedAt,
|
|
6221
6225
|
...coverageFields
|
|
6222
6226
|
};
|
|
6223
6227
|
const durationMs = Date.now() - started;
|
|
@@ -6244,6 +6248,8 @@ async function runOneSpec$1(ref, opts, blocks) {
|
|
|
6244
6248
|
status: "passed"
|
|
6245
6249
|
}),
|
|
6246
6250
|
target: opts.targetId,
|
|
6251
|
+
startedAt,
|
|
6252
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6247
6253
|
durationMs,
|
|
6248
6254
|
...artifactFields,
|
|
6249
6255
|
...evidenceFields,
|
|
@@ -6251,6 +6257,8 @@ async function runOneSpec$1(ref, opts, blocks) {
|
|
|
6251
6257
|
};
|
|
6252
6258
|
return {
|
|
6253
6259
|
...failedRow([`command failed (exit ${outcome.exitCode}): ${command}`, outcome.tail.length > 0 ? `--- output (tail) ---\n${outcome.tail}` : null].filter((p) => p !== null).join("\n")),
|
|
6260
|
+
startedAt,
|
|
6261
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6254
6262
|
durationMs,
|
|
6255
6263
|
...artifactFields,
|
|
6256
6264
|
...evidenceFields,
|
|
@@ -8595,11 +8603,11 @@ async function buildLiveTranscriptExcerpt(result, options = {}) {
|
|
|
8595
8603
|
return combined.length > maxBytes ? `${combined.slice(0, maxBytes)}\n…[transcript excerpt truncated at ${maxBytes} bytes]` : combined;
|
|
8596
8604
|
}
|
|
8597
8605
|
function formatPreviousStep(step) {
|
|
8598
|
-
const reason = oneLine$
|
|
8606
|
+
const reason = oneLine$3(step.reasoning) || "(no reason given)";
|
|
8599
8607
|
return `[${step.stepId} ${step.status}: ${reason}]`;
|
|
8600
8608
|
}
|
|
8601
8609
|
async function formatFailingStep(step, headBytes, tailBytes) {
|
|
8602
|
-
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)"}`;
|
|
8603
8611
|
if (!step.logTxt) return `${header}\n(No assistant log file recorded for this step.)`;
|
|
8604
8612
|
const raw = await readFile(step.logTxt, "utf-8").catch((err) => {
|
|
8605
8613
|
return `[log file unreadable: ${err instanceof Error ? err.message : String(err)}]`;
|
|
@@ -8609,7 +8617,7 @@ async function formatFailingStep(step, headBytes, tailBytes) {
|
|
|
8609
8617
|
const tail = raw.slice(raw.length - tailBytes);
|
|
8610
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}`;
|
|
8611
8619
|
}
|
|
8612
|
-
function oneLine$
|
|
8620
|
+
function oneLine$3(s) {
|
|
8613
8621
|
return s.replace(/\s+/g, " ").trim();
|
|
8614
8622
|
}
|
|
8615
8623
|
//#endregion
|
|
@@ -10121,10 +10129,10 @@ function describeStep(step) {
|
|
|
10121
10129
|
return describeStepBody(step);
|
|
10122
10130
|
}
|
|
10123
10131
|
function describeStepBody(step) {
|
|
10124
|
-
if (isJudgeBody(step)) return `judge: ${oneLine$
|
|
10125
|
-
return `${oneLine$
|
|
10132
|
+
if (isJudgeBody(step)) return `judge: ${oneLine$2(step.judgeByLlm)}`;
|
|
10133
|
+
return `${oneLine$2(step.instruction)} → ${oneLine$2(step.expected)}`;
|
|
10126
10134
|
}
|
|
10127
|
-
function oneLine$
|
|
10135
|
+
function oneLine$2(text) {
|
|
10128
10136
|
return text.trim().replace(/\s+/g, " ");
|
|
10129
10137
|
}
|
|
10130
10138
|
function emptyDeployLog() {
|
|
@@ -10189,6 +10197,36 @@ function foldTouchIndex(current, entry, selection) {
|
|
|
10189
10197
|
}
|
|
10190
10198
|
return out;
|
|
10191
10199
|
}
|
|
10200
|
+
/**
|
|
10201
|
+
* How much clock skew between the runner (which stamps a row's window) and
|
|
10202
|
+
* the hub (which stamps a deploy) the placement tolerates. The window is
|
|
10203
|
+
* widened by this on both ends, so a skewed clock costs a row its credit
|
|
10204
|
+
* rather than crediting a row that straddled.
|
|
10205
|
+
*/
|
|
10206
|
+
const PLACEMENT_SKEW_MS = 1e4;
|
|
10207
|
+
/**
|
|
10208
|
+
* Place one row's execution window against the deploy log (ADR-0027).
|
|
10209
|
+
*
|
|
10210
|
+
* The window is widened by {@link PLACEMENT_SKEW_MS} and its start is
|
|
10211
|
+
* exclusive, so a deploy landing exactly as the spec began reads as a
|
|
10212
|
+
* straddle. Both choices err the same way: toward `ambiguous`, never toward
|
|
10213
|
+
* crediting a spec with a commit it may not have exercised.
|
|
10214
|
+
*
|
|
10215
|
+
* `entries` must be in append order, which the log guarantees and which is
|
|
10216
|
+
* time order.
|
|
10217
|
+
*/
|
|
10218
|
+
function placeRowInDeployLog(entries, window) {
|
|
10219
|
+
const before = (ms, inclusive) => entries.findLast((e) => {
|
|
10220
|
+
const at = Date.parse(e.at);
|
|
10221
|
+
return inclusive ? at <= ms : at < ms;
|
|
10222
|
+
});
|
|
10223
|
+
const opened = before(window.startMs - PLACEMENT_SKEW_MS, false);
|
|
10224
|
+
const closed = before(Math.max(window.startMs, window.endMs) + PLACEMENT_SKEW_MS, true);
|
|
10225
|
+
return {
|
|
10226
|
+
deployedSha: opened?.sha ?? null,
|
|
10227
|
+
deployedShaAmbiguous: opened?.sha !== closed?.sha
|
|
10228
|
+
};
|
|
10229
|
+
}
|
|
10192
10230
|
//#endregion
|
|
10193
10231
|
//#region src/coverage/resolve-stream.ts
|
|
10194
10232
|
/**
|
|
@@ -11774,6 +11812,8 @@ async function liveRunToReportResult(args) {
|
|
|
11774
11812
|
target: AGENT_BROWSER_TARGET,
|
|
11775
11813
|
status: result.status,
|
|
11776
11814
|
testCounts: null,
|
|
11815
|
+
startedAt: result.startedAt,
|
|
11816
|
+
finishedAt: new Date(Date.parse(result.startedAt) + result.durationMs).toISOString(),
|
|
11777
11817
|
durationMs: result.durationMs,
|
|
11778
11818
|
assertions: null,
|
|
11779
11819
|
analysis: null,
|
|
@@ -12392,15 +12432,15 @@ function renderRunMarkdown(featureName, specName, result) {
|
|
|
12392
12432
|
].join("\n") + result.steps.map((s) => [
|
|
12393
12433
|
`## ${s.stepId} — ${s.status}`,
|
|
12394
12434
|
`- duration: ${(s.durationMs / 1e3).toFixed(1)}s`,
|
|
12395
|
-
`- instruction: ${oneLine(s.instruction)}`,
|
|
12396
|
-
`- expected: ${oneLine(s.expected)}`,
|
|
12397
|
-
`- reasoning: ${oneLine(s.reasoning)}`,
|
|
12435
|
+
`- instruction: ${oneLine$1(s.instruction)}`,
|
|
12436
|
+
`- expected: ${oneLine$1(s.expected)}`,
|
|
12437
|
+
`- reasoning: ${oneLine$1(s.reasoning)}`,
|
|
12398
12438
|
...s.beforePng ? [`- before: ${s.beforePng}`] : [],
|
|
12399
12439
|
...s.afterPng ? [`- after: ${s.afterPng}`] : [],
|
|
12400
12440
|
""
|
|
12401
12441
|
].join("\n")).join("\n");
|
|
12402
12442
|
}
|
|
12403
|
-
function oneLine(s) {
|
|
12443
|
+
function oneLine$1(s) {
|
|
12404
12444
|
return s.replace(/\s+/g, " ").trim();
|
|
12405
12445
|
}
|
|
12406
12446
|
//#endregion
|
|
@@ -13964,6 +14004,136 @@ async function waitForCdp(port) {
|
|
|
13964
14004
|
}
|
|
13965
14005
|
}
|
|
13966
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
|
|
13967
14137
|
//#region src/targets/playwright/index.ts
|
|
13968
14138
|
const PLAYWRIGHT_TARGET = "playwright";
|
|
13969
14139
|
/**
|
|
@@ -14035,12 +14205,21 @@ async function generatePlaywrightTest(ctx) {
|
|
|
14035
14205
|
});
|
|
14036
14206
|
const missing = await missingInjectedCalls(result, stepMarkers, judgements);
|
|
14037
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);
|
|
14038
14216
|
return {
|
|
14039
14217
|
...result,
|
|
14040
14218
|
warnings: [
|
|
14041
14219
|
...result.warnings,
|
|
14042
14220
|
...judgeWarnings,
|
|
14043
|
-
...missing
|
|
14221
|
+
...missing,
|
|
14222
|
+
...unchecked
|
|
14044
14223
|
]
|
|
14045
14224
|
};
|
|
14046
14225
|
}
|
|
@@ -15198,7 +15377,7 @@ async function executeRun(targets, opts) {
|
|
|
15198
15377
|
const declared = [...new Set(specs.flatMap(resources))];
|
|
15199
15378
|
if (declared.length > 0) meta("serial groups", declared.join(", "));
|
|
15200
15379
|
let waitingOnGroup = [];
|
|
15201
|
-
if (hubCtx && rerunProfile !== null) {
|
|
15380
|
+
if (hubCtx && rerunProfile !== null && forExecution) {
|
|
15202
15381
|
const held = await holdSpecs(hubCtx, rerunProfile, specs, resources, opts.teardown);
|
|
15203
15382
|
waitingOnGroup = specs.flatMap((spec) => {
|
|
15204
15383
|
const groups = resources(spec).filter((n) => held.deniedResources.includes(n));
|
|
@@ -15729,6 +15908,7 @@ async function runOneDeterministicSpec(spec, index, ctx) {
|
|
|
15729
15908
|
CCQA_RUN_ID: runId
|
|
15730
15909
|
};
|
|
15731
15910
|
if (evidenceDir) specEnv[EVIDENCE_DIR_ENV] = evidenceDir;
|
|
15911
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15732
15912
|
const proc = spawnVitestStreaming([
|
|
15733
15913
|
"run",
|
|
15734
15914
|
"--config",
|
|
@@ -15745,11 +15925,14 @@ async function runOneDeterministicSpec(spec, index, ctx) {
|
|
|
15745
15925
|
await Promise.all([streamFiltered(proc.stdout, sink, tail), streamFiltered(proc.stderr, sink, tail)]);
|
|
15746
15926
|
const specExitCode = await proc.exited;
|
|
15747
15927
|
blank();
|
|
15928
|
+
const report = await readReport$1(reportFile);
|
|
15748
15929
|
return {
|
|
15749
15930
|
featureName,
|
|
15750
15931
|
specName,
|
|
15751
15932
|
scriptFile,
|
|
15752
|
-
|
|
15933
|
+
startedAt,
|
|
15934
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15935
|
+
report,
|
|
15753
15936
|
exitCode: specExitCode,
|
|
15754
15937
|
outputTail: tail ? tail.toString() : null,
|
|
15755
15938
|
evidenceDir
|
|
@@ -15778,6 +15961,8 @@ async function analyzeDeterministicSummaries(summaries, cwd, reportDir, { pass,
|
|
|
15778
15961
|
spec: s.specName,
|
|
15779
15962
|
title: parsedSpec?.title ?? null,
|
|
15780
15963
|
target: AGENT_BROWSER_TARGET,
|
|
15964
|
+
startedAt: s.startedAt,
|
|
15965
|
+
finishedAt: s.finishedAt,
|
|
15781
15966
|
testCounts: s.report ? {
|
|
15782
15967
|
total: s.report.numTotalTests,
|
|
15783
15968
|
passed: s.report.numPassedTests,
|
|
@@ -20186,17 +20371,20 @@ function countSpecs(results) {
|
|
|
20186
20371
|
async function updateSpecLedger(storage, run, results) {
|
|
20187
20372
|
const { gitHead, branch } = run;
|
|
20188
20373
|
if (run.kind !== "run" || !gitHead || !branch) return;
|
|
20189
|
-
const
|
|
20374
|
+
const placeRow = await rowDeployPlacer(storage, run);
|
|
20375
|
+
const base = {
|
|
20190
20376
|
gitHead,
|
|
20191
20377
|
runId: run.id,
|
|
20192
|
-
at: run.reportCreatedAt
|
|
20193
|
-
deployedSha: run.deployedSha ?? null,
|
|
20194
|
-
deployedShaAmbiguous: run.deployedShaAmbiguous ?? false
|
|
20378
|
+
at: run.reportCreatedAt
|
|
20195
20379
|
};
|
|
20196
20380
|
const ledger = emptyLedger();
|
|
20197
20381
|
for (const row of results) {
|
|
20198
20382
|
if (row.status === "skipped") continue;
|
|
20199
20383
|
const key = `${row.feature}/${row.spec}`;
|
|
20384
|
+
const entry = {
|
|
20385
|
+
...base,
|
|
20386
|
+
...placeRow(row)
|
|
20387
|
+
};
|
|
20200
20388
|
ledger.run[key] = entry;
|
|
20201
20389
|
if (row.status === "passed") ledger.green[key] = entry;
|
|
20202
20390
|
else ledger.red[key] = redEntry(entry, row);
|
|
@@ -20299,6 +20487,38 @@ async function resolveDeployedSha(storage, kind, project, profile, explicit) {
|
|
|
20299
20487
|
};
|
|
20300
20488
|
}
|
|
20301
20489
|
/**
|
|
20490
|
+
* Place each row against the deploy log by its own execution window rather
|
|
20491
|
+
* than the run's (ADR-0027), so a run that outlives a deploy loses only the
|
|
20492
|
+
* specs that straddled it.
|
|
20493
|
+
*
|
|
20494
|
+
* Falls back to the run's placement where a finer answer isn't available: a
|
|
20495
|
+
* client-asserted sha is the caller's claim about its whole run, and a row
|
|
20496
|
+
* with no `startedAt` (an older client) has nothing finer to say. A row with
|
|
20497
|
+
* no end is measured to now, which only widens its window.
|
|
20498
|
+
*/
|
|
20499
|
+
async function rowDeployPlacer(storage, run) {
|
|
20500
|
+
const runPlacement = {
|
|
20501
|
+
deployedSha: run.deployedSha ?? null,
|
|
20502
|
+
deployedShaAmbiguous: run.deployedShaAmbiguous ?? false
|
|
20503
|
+
};
|
|
20504
|
+
if (run.deployedShaSource !== "hub-deploy-log") return () => runPlacement;
|
|
20505
|
+
const log = await storage.deploys.getLog(run.project, run.profile ?? "default").catch((err) => {
|
|
20506
|
+
console.error(`hub: could not read the deploy log to place rows of run "${run.id}": ${errMsg(err)}`);
|
|
20507
|
+
return null;
|
|
20508
|
+
});
|
|
20509
|
+
if (!log) return () => runPlacement;
|
|
20510
|
+
const sealedAt = Date.now();
|
|
20511
|
+
return (row) => {
|
|
20512
|
+
const startMs = row.startedAt ? Date.parse(row.startedAt) : NaN;
|
|
20513
|
+
if (Number.isNaN(startMs)) return runPlacement;
|
|
20514
|
+
const endMs = row.finishedAt ? Date.parse(row.finishedAt) : sealedAt;
|
|
20515
|
+
return placeRowInDeployLog(log.entries, {
|
|
20516
|
+
startMs,
|
|
20517
|
+
endMs: Number.isNaN(endMs) ? sealedAt : endMs
|
|
20518
|
+
});
|
|
20519
|
+
};
|
|
20520
|
+
}
|
|
20521
|
+
/**
|
|
20302
20522
|
* True when the deploy-log head moved while the run was open: the run
|
|
20303
20523
|
* straddled a deploy, so which commit it exercised is not knowable and re-run
|
|
20304
20524
|
* selection must report `unknown` instead of picking one. Only meaningful for
|
|
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
|
|
|
35
35
|
running: "running";
|
|
36
36
|
}>;
|
|
37
37
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
38
|
-
record: "record";
|
|
39
38
|
run: "run";
|
|
40
39
|
drift: "drift";
|
|
40
|
+
record: "record";
|
|
41
41
|
}>>;
|
|
42
42
|
drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
43
43
|
specs: z.ZodNumber;
|
|
@@ -640,8 +640,8 @@ declare const ReportSpecResultSchema: z.ZodObject<{
|
|
|
640
640
|
title: z.ZodNullable<z.ZodString>;
|
|
641
641
|
target: z.ZodOptional<z.ZodString>;
|
|
642
642
|
mode: z.ZodOptional<z.ZodEnum<{
|
|
643
|
-
deterministic: "deterministic";
|
|
644
643
|
live: "live";
|
|
644
|
+
deterministic: "deterministic";
|
|
645
645
|
}>>;
|
|
646
646
|
status: z.ZodEnum<{
|
|
647
647
|
passed: "passed";
|
|
@@ -655,6 +655,8 @@ declare const ReportSpecResultSchema: z.ZodObject<{
|
|
|
655
655
|
failed: z.ZodNumber;
|
|
656
656
|
}, z.core.$strip>>;
|
|
657
657
|
durationMs: z.ZodNullable<z.ZodNumber>;
|
|
658
|
+
startedAt: z.ZodOptional<z.ZodString>;
|
|
659
|
+
finishedAt: z.ZodOptional<z.ZodString>;
|
|
658
660
|
assertions: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
659
661
|
name: z.ZodString;
|
|
660
662
|
status: z.ZodEnum<{
|
|
@@ -811,9 +813,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
|
|
|
811
813
|
declare const RunReportDataSchema: z.ZodObject<{
|
|
812
814
|
schemaVersion: z.ZodLiteral<1>;
|
|
813
815
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
814
|
-
record: "record";
|
|
815
816
|
run: "run";
|
|
816
817
|
drift: "drift";
|
|
818
|
+
record: "record";
|
|
817
819
|
}>>;
|
|
818
820
|
createdAt: z.ZodString;
|
|
819
821
|
runId: z.ZodNullable<z.ZodString>;
|
|
@@ -854,8 +856,8 @@ declare const RunReportDataSchema: z.ZodObject<{
|
|
|
854
856
|
title: z.ZodNullable<z.ZodString>;
|
|
855
857
|
target: z.ZodOptional<z.ZodString>;
|
|
856
858
|
mode: z.ZodOptional<z.ZodEnum<{
|
|
857
|
-
deterministic: "deterministic";
|
|
858
859
|
live: "live";
|
|
860
|
+
deterministic: "deterministic";
|
|
859
861
|
}>>;
|
|
860
862
|
status: z.ZodEnum<{
|
|
861
863
|
passed: "passed";
|
|
@@ -869,6 +871,8 @@ declare const RunReportDataSchema: z.ZodObject<{
|
|
|
869
871
|
failed: z.ZodNumber;
|
|
870
872
|
}, z.core.$strip>>;
|
|
871
873
|
durationMs: z.ZodNullable<z.ZodNumber>;
|
|
874
|
+
startedAt: z.ZodOptional<z.ZodString>;
|
|
875
|
+
finishedAt: z.ZodOptional<z.ZodString>;
|
|
872
876
|
assertions: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
873
877
|
name: z.ZodString;
|
|
874
878
|
status: z.ZodEnum<{
|
package/dist/package.json
CHANGED