ccqa 0.3.3 → 0.3.4
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 +67 -14
- package/dist/package.json +1 -1
- package/dist/runtime/test-helpers.mjs +23 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { Command } from "commander";
|
|
|
4
4
|
import { accessSync, readFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { access, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
|
-
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
8
8
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
9
9
|
import matter from "gray-matter";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
@@ -706,10 +706,10 @@ function bundledVitestConfigPath() {
|
|
|
706
706
|
}
|
|
707
707
|
//#endregion
|
|
708
708
|
//#region src/runtime/spawn-vitest.ts
|
|
709
|
-
const require = createRequire(import.meta.url);
|
|
709
|
+
const require$1 = createRequire(import.meta.url);
|
|
710
710
|
function resolveVitestBin() {
|
|
711
|
-
const pkgPath = require.resolve("vitest/package.json");
|
|
712
|
-
const pkg = require(pkgPath);
|
|
711
|
+
const pkgPath = require$1.resolve("vitest/package.json");
|
|
712
|
+
const pkg = require$1(pkgPath);
|
|
713
713
|
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.vitest;
|
|
714
714
|
if (!binRel) throw new Error(`vitest package.json has no bin entry (resolved at ${pkgPath})`);
|
|
715
715
|
return resolve(dirname(pkgPath), binRel);
|
|
@@ -761,6 +761,38 @@ function waitExit(child) {
|
|
|
761
761
|
});
|
|
762
762
|
}
|
|
763
763
|
//#endregion
|
|
764
|
+
//#region src/runtime/agent-browser-bin.ts
|
|
765
|
+
const require = createRequire(import.meta.url);
|
|
766
|
+
/**
|
|
767
|
+
* Resolves the directory containing the `agent-browser` shim that npm/pnpm
|
|
768
|
+
* exposes on PATH for the peer-installed package. Used by `ccqa trace` to
|
|
769
|
+
* prepend this directory to PATH so the Claude subprocess can invoke
|
|
770
|
+
* `agent-browser ...` without requiring a global install.
|
|
771
|
+
*
|
|
772
|
+
* Returns null if agent-browser cannot be resolved (peer not installed).
|
|
773
|
+
*/
|
|
774
|
+
function resolveAgentBrowserBinDir() {
|
|
775
|
+
let pkgJsonPath;
|
|
776
|
+
try {
|
|
777
|
+
pkgJsonPath = require.resolve("agent-browser/package.json");
|
|
778
|
+
} catch {
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
return join(dirname(pkgJsonPath), "node_modules", ".bin");
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Returns a PATH string with the agent-browser shim directory prepended,
|
|
785
|
+
* so `agent-browser ...` resolves without a global install. Falls back to
|
|
786
|
+
* the original PATH when the package can't be resolved.
|
|
787
|
+
*/
|
|
788
|
+
function pathWithAgentBrowserShim(currentPath) {
|
|
789
|
+
const path = currentPath ?? "";
|
|
790
|
+
const dir = resolveAgentBrowserBinDir();
|
|
791
|
+
if (!dir) return path;
|
|
792
|
+
if (path.split(delimiter).includes(dir)) return path;
|
|
793
|
+
return dir + delimiter + path;
|
|
794
|
+
}
|
|
795
|
+
//#endregion
|
|
764
796
|
//#region src/cli/trace.ts
|
|
765
797
|
const traceCommand = new Command("trace").argument("<feature/spec>", "Spec to trace (e.g. tasks/create-and-complete)").description("Run agent-browser, verify assertions, and record structured actions").action(async (specPath) => {
|
|
766
798
|
const { featureName, specName } = parseSpecPath(specPath);
|
|
@@ -801,7 +833,10 @@ async function runTrace(featureName, specName) {
|
|
|
801
833
|
"Grep",
|
|
802
834
|
"Glob"
|
|
803
835
|
],
|
|
804
|
-
env: {
|
|
836
|
+
env: {
|
|
837
|
+
AGENT_BROWSER_SESSION: sessionName,
|
|
838
|
+
PATH: pathWithAgentBrowserShim(process.env["PATH"])
|
|
839
|
+
},
|
|
805
840
|
onAbAction: (abAction) => {
|
|
806
841
|
const action = parseAbAction(abAction);
|
|
807
842
|
if (action) traceActions.push(action);
|
|
@@ -1214,18 +1249,36 @@ async function loadSetupScripts(setups) {
|
|
|
1214
1249
|
return result;
|
|
1215
1250
|
}
|
|
1216
1251
|
/**
|
|
1217
|
-
* Extract the test body (
|
|
1252
|
+
* Extract the test body (statements inside the test callback) from a setup
|
|
1253
|
+
* test script.
|
|
1254
|
+
*
|
|
1255
|
+
* Locates the first arrow callback (`=> {`) after a top-level `test(` call
|
|
1256
|
+
* and returns the text between the matching `{` and `}`. Handles both
|
|
1257
|
+
* single-line and multi-line `test(...)` formatting (the latter is what
|
|
1258
|
+
* prettier produces).
|
|
1259
|
+
*
|
|
1260
|
+
* Brace tracking is naive (string/regex/comment literals are not parsed
|
|
1261
|
+
* specially), but setup test scripts are themselves generated by ccqa and
|
|
1262
|
+
* follow a fixed shape, so this is sufficient in practice.
|
|
1218
1263
|
*/
|
|
1219
1264
|
function extractTestBody(script) {
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1265
|
+
const testCallMatch = /\btest\s*\(/.exec(script);
|
|
1266
|
+
if (!testCallMatch) return "";
|
|
1267
|
+
const arrowIdx = script.indexOf("=> {", testCallMatch.index);
|
|
1268
|
+
if (arrowIdx === -1) return "";
|
|
1269
|
+
const bodyStart = arrowIdx + 4;
|
|
1270
|
+
let depth = 1;
|
|
1271
|
+
let i = bodyStart;
|
|
1272
|
+
for (; i < script.length; i++) {
|
|
1273
|
+
const ch = script[i];
|
|
1274
|
+
if (ch === "{") depth++;
|
|
1275
|
+
else if (ch === "}") {
|
|
1276
|
+
depth--;
|
|
1277
|
+
if (depth === 0) break;
|
|
1278
|
+
}
|
|
1227
1279
|
}
|
|
1228
|
-
return
|
|
1280
|
+
if (depth !== 0) return "";
|
|
1281
|
+
return script.slice(bodyStart, i).replace(/^\n/, "").replace(/\n\s*$/, "");
|
|
1229
1282
|
}
|
|
1230
1283
|
function replacePlaceholders(body, params) {
|
|
1231
1284
|
let result = body;
|
package/dist/package.json
CHANGED
|
@@ -2,7 +2,18 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
//#region src/runtime/test-helpers.ts
|
|
4
4
|
const AB = createRequire(import.meta.url).resolve("agent-browser/bin/agent-browser.js");
|
|
5
|
-
|
|
5
|
+
const EAGAIN_PATTERN = /Resource temporarily unavailable|os error 35/i;
|
|
6
|
+
const EAGAIN_RETRIES = 3;
|
|
7
|
+
const EAGAIN_BACKOFF_MS = [
|
|
8
|
+
200,
|
|
9
|
+
500,
|
|
10
|
+
1e3
|
|
11
|
+
];
|
|
12
|
+
function sleepSync(ms) {
|
|
13
|
+
const buf = new SharedArrayBuffer(4);
|
|
14
|
+
Atomics.wait(new Int32Array(buf), 0, 0, ms);
|
|
15
|
+
}
|
|
16
|
+
function spawnABOnce(args) {
|
|
6
17
|
const result = spawnSync(AB, args, { stdio: "pipe" });
|
|
7
18
|
return {
|
|
8
19
|
status: result.status,
|
|
@@ -10,6 +21,17 @@ function spawnAB(args) {
|
|
|
10
21
|
stderr: result.stderr?.toString() ?? ""
|
|
11
22
|
};
|
|
12
23
|
}
|
|
24
|
+
function spawnAB(args) {
|
|
25
|
+
let result = spawnABOnce(args);
|
|
26
|
+
for (let attempt = 0; attempt < EAGAIN_RETRIES; attempt++) {
|
|
27
|
+
if (result.status === 0) return result;
|
|
28
|
+
const combined = `${result.stdout}\n${result.stderr}`;
|
|
29
|
+
if (!EAGAIN_PATTERN.test(combined)) return result;
|
|
30
|
+
sleepSync(EAGAIN_BACKOFF_MS[attempt] ?? 1e3);
|
|
31
|
+
result = spawnABOnce(args);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
13
35
|
function logStep(action, args) {
|
|
14
36
|
const pretty = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
15
37
|
process.stdout.write(` ▶ ${action.padEnd(14)} ${pretty}\n`);
|