ccqa 1.50.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 +151 -12
- 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
|
}
|
|
@@ -15238,7 +15377,7 @@ async function executeRun(targets, opts) {
|
|
|
15238
15377
|
const declared = [...new Set(specs.flatMap(resources))];
|
|
15239
15378
|
if (declared.length > 0) meta("serial groups", declared.join(", "));
|
|
15240
15379
|
let waitingOnGroup = [];
|
|
15241
|
-
if (hubCtx && rerunProfile !== null) {
|
|
15380
|
+
if (hubCtx && rerunProfile !== null && forExecution) {
|
|
15242
15381
|
const held = await holdSpecs(hubCtx, rerunProfile, specs, resources, opts.teardown);
|
|
15243
15382
|
waitingOnGroup = specs.flatMap((spec) => {
|
|
15244
15383
|
const groups = resources(spec).filter((n) => held.deniedResources.includes(n));
|
package/dist/package.json
CHANGED