ccqa 0.3.10 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.3.10",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@anthropic-ai/claude-agent-sdk": "^0.2.87",
28
28
  "commander": "^14.0.3",
29
- "gray-matter": "^4.0.3",
29
+ "yaml": "^2.9.0",
30
30
  "zod": "^4.3.6"
31
31
  },
32
32
  "peerDependencies": {
@@ -4,9 +4,9 @@ declare function ab(...args: string[]): void;
4
4
  declare function abWait(selector: string, timeoutMs?: number): void;
5
5
  /** Assert stable text is visible on page (via wait --text). */
6
6
  declare function abAssertTextVisible(text: string, timeoutMs?: number): void;
7
- /** Assert element is visible (via wait). */
7
+ /** Assert element is visible (polls `get count`; never uses the blocking `wait <selector>`). */
8
8
  declare function abAssertVisible(selector: string, timeoutMs?: number): void;
9
- /** Assert element is NOT visible (via wait --state hidden or --fn for text). */
9
+ /** Assert element is NOT visible (polls `get count` for absence; --fn for text). */
10
10
  declare function abAssertNotVisible(selector: string, timeoutMs?: number): void;
11
11
  /** Assert URL contains a pattern (via get url). */
12
12
  declare function abAssertUrl(pattern: string): void;
@@ -1,58 +1,6 @@
1
- import { createRequire } from "node:module";
2
- import { spawnSync } from "node:child_process";
1
+ import { n as spawnAB, t as sleepSync } from "../spawn-ab-DjRh1-4T.mjs";
3
2
  //#region src/runtime/test-helpers.ts
4
- const AB = createRequire(import.meta.url).resolve("agent-browser/bin/agent-browser.js");
5
- const EAGAIN_PATTERN = /Resource temporarily unavailable|os error 35/i;
6
- const EAGAIN_TOTAL_BUDGET_MS = 3e4;
7
- const EAGAIN_BACKOFF_MS = [
8
- 100,
9
- 200,
10
- 300,
11
- 500,
12
- 700,
13
- 1e3,
14
- 1500,
15
- 2e3,
16
- 2500,
17
- 3e3,
18
- 3e3,
19
- 3e3,
20
- 3e3,
21
- 3e3,
22
- 3e3
23
- ];
24
3
  const POST_OPEN_SETTLE_MS = 600;
25
- function sleepSync(ms) {
26
- const buf = new SharedArrayBuffer(4);
27
- Atomics.wait(new Int32Array(buf), 0, 0, ms);
28
- }
29
- const PROCESS_HARD_TIMEOUT_MS = 9e4;
30
- function spawnABOnce(args) {
31
- const result = spawnSync(AB, args, {
32
- stdio: "pipe",
33
- timeout: PROCESS_HARD_TIMEOUT_MS
34
- });
35
- return {
36
- status: result.status,
37
- stdout: result.stdout?.toString() ?? "",
38
- stderr: (result.stderr?.toString() ?? "") + (result.signal === "SIGTERM" ? "\n[ccqa] agent-browser killed after hard timeout" : "")
39
- };
40
- }
41
- function spawnAB(args) {
42
- let result = spawnABOnce(args);
43
- let elapsed = 0;
44
- let attempt = 0;
45
- while (result.status !== 0 && elapsed < EAGAIN_TOTAL_BUDGET_MS) {
46
- const combined = `${result.stdout}\n${result.stderr}`;
47
- if (!EAGAIN_PATTERN.test(combined)) return result;
48
- const wait = EAGAIN_BACKOFF_MS[attempt] ?? 3e3;
49
- sleepSync(wait);
50
- elapsed += wait;
51
- attempt++;
52
- result = spawnABOnce(args);
53
- }
54
- return result;
55
- }
56
4
  function logStep(action, args) {
57
5
  const pretty = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
58
6
  process.stdout.write(` ▶ ${action.padEnd(14)} ${pretty}\n`);
@@ -70,22 +18,50 @@ function ab(...args) {
70
18
  if (result.status !== 0) fail(`agent-browser ${command} failed (exit ${result.status})`, result);
71
19
  if (command === "open") sleepSync(POST_OPEN_SETTLE_MS);
72
20
  }
21
+ const SELECTOR_POLL_INTERVAL_MS = 500;
22
+ function selectorCount(selector) {
23
+ const r = spawnAB([
24
+ "get",
25
+ "count",
26
+ selector
27
+ ]);
28
+ if (r.status !== 0) return 0;
29
+ const n = Number.parseInt(r.stdout.trim(), 10);
30
+ return Number.isNaN(n) ? 0 : n;
31
+ }
32
+ /**
33
+ * Poll until `get count <selector>` reaches the desired presence state or the
34
+ * timeout elapses. `want: "present"` waits for >=1 match; `"absent"` waits for
35
+ * 0 matches. Returns true on success, false on timeout.
36
+ */
37
+ function pollSelector(selector, want, timeoutMs) {
38
+ const deadline = Date.now() + timeoutMs;
39
+ for (;;) {
40
+ const count = selectorCount(selector);
41
+ if (want === "present" ? count > 0 : count === 0) return true;
42
+ if (Date.now() >= deadline) return false;
43
+ sleepSync(SELECTOR_POLL_INTERVAL_MS);
44
+ }
45
+ }
73
46
  /** Wait for element/text with an explicit timeout so long-running async ops don't hang. */
74
- function abWait(selector, timeoutMs = 18e4) {
47
+ function abWait(selector, timeoutMs = 3e4) {
75
48
  logStep("wait", [selector]);
76
- const result = spawnAB(selector.startsWith("text=") ? [
77
- "wait",
78
- "--text",
79
- selector.slice(5),
80
- "--timeout",
81
- String(timeoutMs)
82
- ] : [
83
- "wait",
84
- selector,
85
- "--timeout",
86
- String(timeoutMs)
87
- ]);
88
- if (result.status !== 0) fail(`wait failed: ${selector}`, result);
49
+ if (selector.startsWith("text=")) {
50
+ const result = spawnAB([
51
+ "wait",
52
+ "--text",
53
+ selector.slice(5),
54
+ "--timeout",
55
+ String(timeoutMs)
56
+ ]);
57
+ if (result.status !== 0) fail(`wait failed: ${selector}`, result);
58
+ return;
59
+ }
60
+ if (!pollSelector(selector, "present", timeoutMs)) fail(`wait failed: ${selector} not present within ${timeoutMs}ms`, {
61
+ status: 1,
62
+ stdout: "",
63
+ stderr: ""
64
+ });
89
65
  }
90
66
  /** Assert stable text is visible on page (via wait --text). */
91
67
  function abAssertTextVisible(text, timeoutMs = 3e4) {
@@ -99,35 +75,45 @@ function abAssertTextVisible(text, timeoutMs = 3e4) {
99
75
  ]);
100
76
  if (result.status !== 0) fail(`Assertion failed: text ${JSON.stringify(text)} not found within ${timeoutMs}ms`, result);
101
77
  }
102
- /** Assert element is visible (via wait). */
78
+ /** Assert element is visible (polls `get count`; never uses the blocking `wait <selector>`). */
103
79
  function abAssertVisible(selector, timeoutMs = 3e4) {
104
80
  logStep("assert.visible", [selector]);
105
- const result = spawnAB([
106
- "wait",
107
- selector,
108
- "--timeout",
109
- String(timeoutMs)
110
- ]);
111
- if (result.status !== 0) fail(`Assertion failed: ${JSON.stringify(selector)} not visible within ${timeoutMs}ms`, result);
81
+ if (selector.startsWith("text=")) {
82
+ const result = spawnAB([
83
+ "wait",
84
+ "--text",
85
+ selector.slice(5),
86
+ "--timeout",
87
+ String(timeoutMs)
88
+ ]);
89
+ if (result.status !== 0) fail(`Assertion failed: ${JSON.stringify(selector)} not visible within ${timeoutMs}ms`, result);
90
+ return;
91
+ }
92
+ if (!pollSelector(selector, "present", timeoutMs)) fail(`Assertion failed: ${JSON.stringify(selector)} not visible within ${timeoutMs}ms`, {
93
+ status: 1,
94
+ stdout: "",
95
+ stderr: ""
96
+ });
112
97
  }
113
- /** Assert element is NOT visible (via wait --state hidden or --fn for text). */
98
+ /** Assert element is NOT visible (polls `get count` for absence; --fn for text). */
114
99
  function abAssertNotVisible(selector, timeoutMs = 3e4) {
115
100
  logStep("assert.hidden", [selector]);
116
- const result = spawnAB(selector.startsWith("text=") ? [
117
- "wait",
118
- "--fn",
119
- `!document.body.innerText.includes(${JSON.stringify(selector.slice(5))})`,
120
- "--timeout",
121
- String(timeoutMs)
122
- ] : [
123
- "wait",
124
- selector,
125
- "--state",
126
- "hidden",
127
- "--timeout",
128
- String(timeoutMs)
129
- ]);
130
- if (result.status !== 0) fail(`Assertion failed: ${JSON.stringify(selector)} still visible after ${timeoutMs}ms`, result);
101
+ if (selector.startsWith("text=")) {
102
+ const result = spawnAB([
103
+ "wait",
104
+ "--fn",
105
+ `!document.body.innerText.includes(${JSON.stringify(selector.slice(5))})`,
106
+ "--timeout",
107
+ String(timeoutMs)
108
+ ]);
109
+ if (result.status !== 0) fail(`Assertion failed: ${JSON.stringify(selector)} still visible after ${timeoutMs}ms`, result);
110
+ return;
111
+ }
112
+ if (!pollSelector(selector, "absent", timeoutMs)) fail(`Assertion failed: ${JSON.stringify(selector)} still visible after ${timeoutMs}ms`, {
113
+ status: 1,
114
+ stdout: "",
115
+ stderr: ""
116
+ });
131
117
  }
132
118
  /** Assert URL contains a pattern (via get url). */
133
119
  function abAssertUrl(pattern) {
@@ -17,7 +17,7 @@ import SassEmbedded from "sass-embedded";
17
17
  import Less from "less";
18
18
  import Stylus from "stylus";
19
19
 
20
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/types/hmrPayload.d.ts
20
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/types/hmrPayload.d.ts
21
21
  type HotPayload = ConnectedPayload | PingPayload | UpdatePayload | FullReloadPayload | CustomPayload | ErrorPayload | PrunePayload;
22
22
  interface ConnectedPayload {
23
23
  type: 'connected';
@@ -82,7 +82,7 @@ interface ErrorPayload {
82
82
  };
83
83
  }
84
84
  //#endregion
85
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/dist/node/chunks/moduleRunnerTransport.d.ts
85
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/dist/node/chunks/moduleRunnerTransport.d.ts
86
86
  //#region src/shared/invokeMethods.d.ts
87
87
  interface FetchFunctionOptions {
88
88
  cached?: boolean;
@@ -135,7 +135,7 @@ interface ViteFetchResult {
135
135
  invalidate: boolean;
136
136
  }
137
137
  //#endregion
138
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/types/customEvent.d.ts
138
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/types/customEvent.d.ts
139
139
  interface CustomEventMap {
140
140
  // client events
141
141
  'vite:beforeUpdate': UpdatePayload;
@@ -6230,13 +6230,13 @@ interface TransformOptions$1 extends BindingEnhancedTransformOptions {}
6230
6230
  * @category Utilities
6231
6231
  */
6232
6232
  //#endregion
6233
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/types/internal/esbuildOptions.d.ts
6233
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/types/internal/esbuildOptions.d.ts
6234
6234
  /* eslint-enable @typescript-eslint/ban-ts-comment */
6235
6235
  type EsbuildTarget = string | string[];
6236
6236
  type EsbuildTransformOptions = esbuild.TransformOptions;
6237
6237
  type DepsOptimizerEsbuildOptions = Omit<esbuild.BuildOptions, 'bundle' | 'entryPoints' | 'external' | 'write' | 'watch' | 'outdir' | 'outfile' | 'outbase' | 'outExtension' | 'metafile'>;
6238
6238
  //#endregion
6239
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/types/metadata.d.ts
6239
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/types/metadata.d.ts
6240
6240
  interface AssetMetadata {
6241
6241
  importedAssets: Set<string>;
6242
6242
  importedCss: Set<string>;
@@ -6279,16 +6279,16 @@ declare module 'rolldown' {
6279
6279
  }
6280
6280
  }
6281
6281
  //#endregion
6282
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/types/internal/terserOptions.d.ts
6282
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/types/internal/terserOptions.d.ts
6283
6283
  /* eslint-enable @typescript-eslint/ban-ts-comment */
6284
6284
  type TerserMinifyOptions = Terser.MinifyOptions;
6285
6285
  //#endregion
6286
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/types/internal/lightningcssOptions.d.ts
6286
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/types/internal/lightningcssOptions.d.ts
6287
6287
  /* eslint-enable @typescript-eslint/ban-ts-comment */
6288
6288
  type LightningCSSOptions = Omit<Lightningcss.BundleAsyncOptions<Lightningcss.CustomAtRules>, 'filename' | 'resolver' | 'minify' | 'sourceMap' | 'analyzeDependencies' // properties not overridden by Vite, but does not make sense to set by end users
6289
6289
  | 'inputSourceMap' | 'projectRoot'>;
6290
6290
  //#endregion
6291
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/types/internal/cssPreprocessorOptions.d.ts
6291
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/types/internal/cssPreprocessorOptions.d.ts
6292
6292
  /* eslint-enable @typescript-eslint/ban-ts-comment */
6293
6293
  // https://github.com/type-challenges/type-challenges/issues/29285
6294
6294
  type IsAny<T> = boolean extends (T extends never ? true : false) ? true : false;
@@ -6307,7 +6307,7 @@ declare global {
6307
6307
  interface HTMLLinkElement {}
6308
6308
  }
6309
6309
  //#endregion
6310
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/types/importGlob.d.ts
6310
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/types/importGlob.d.ts
6311
6311
  /**
6312
6312
  * Declare Worker in case DOM is not added to the tsconfig lib causing
6313
6313
  * Worker interface is not defined. For developers with DOM lib added,
@@ -6318,7 +6318,7 @@ declare global {
6318
6318
  interface Worker {}
6319
6319
  }
6320
6320
  //#endregion
6321
- //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17/node_modules/vite/dist/node/index.d.ts
6321
+ //#region node_modules/.pnpm/vite@8.0.9_@types+node@22.19.17_yaml@2.9.0/node_modules/vite/dist/node/index.d.ts
6322
6322
  //#region \0rolldown/runtime.js
6323
6323
  //#endregion
6324
6324
  //#region ../../node_modules/.pnpm/@vitejs+devtools@0.1.14_typescript@6.0.2_vite@packages+vite/node_modules/@vitejs/devtools/dist/cli-commands.d.ts
@@ -0,0 +1,65 @@
1
+ import { createRequire } from "node:module";
2
+ import { spawnSync } from "node:child_process";
3
+ //#region src/runtime/spawn-ab.ts
4
+ const AB = createRequire(import.meta.url).resolve("agent-browser/bin/agent-browser.js");
5
+ const EAGAIN_PATTERN = /Resource temporarily unavailable|os error 35/i;
6
+ const EAGAIN_TOTAL_BUDGET_MS = 3e4;
7
+ const EAGAIN_BACKOFF_MS = [
8
+ 100,
9
+ 200,
10
+ 300,
11
+ 500,
12
+ 700,
13
+ 1e3,
14
+ 1500,
15
+ 2e3,
16
+ 2500,
17
+ 3e3,
18
+ 3e3,
19
+ 3e3,
20
+ 3e3,
21
+ 3e3,
22
+ 3e3
23
+ ];
24
+ const PROCESS_HARD_TIMEOUT_MS = 35e3;
25
+ function sleepSync(ms) {
26
+ const buf = new SharedArrayBuffer(4);
27
+ Atomics.wait(new Int32Array(buf), 0, 0, ms);
28
+ }
29
+ function spawnABOnce(args) {
30
+ const result = spawnSync(AB, args, {
31
+ stdio: "pipe",
32
+ timeout: PROCESS_HARD_TIMEOUT_MS
33
+ });
34
+ return {
35
+ status: result.status,
36
+ stdout: result.stdout?.toString() ?? "",
37
+ stderr: (result.stderr?.toString() ?? "") + (result.signal === "SIGTERM" ? "\n[ccqa] agent-browser killed after hard timeout" : "")
38
+ };
39
+ }
40
+ /**
41
+ * Invoke `agent-browser` once and return its exit status/stdout/stderr,
42
+ * retrying internally up to ~30s while the daemon's state file is in the
43
+ * "Resource temporarily unavailable" race window. Used by both the test
44
+ * runtime (`test-helpers.ts`) and the post-trace replay validation
45
+ * (`replay-validate.ts`). Kept out of `test-helpers.ts` because that
46
+ * module is also the public surface for generated test scripts — exposing
47
+ * the raw spawner there would widen the contract for end users.
48
+ */
49
+ function spawnAB(args) {
50
+ let result = spawnABOnce(args);
51
+ let elapsed = 0;
52
+ let attempt = 0;
53
+ while (result.status !== 0 && elapsed < EAGAIN_TOTAL_BUDGET_MS) {
54
+ const combined = `${result.stdout}\n${result.stderr}`;
55
+ if (!EAGAIN_PATTERN.test(combined)) return result;
56
+ const wait = EAGAIN_BACKOFF_MS[attempt] ?? 3e3;
57
+ sleepSync(wait);
58
+ elapsed += wait;
59
+ attempt++;
60
+ result = spawnABOnce(args);
61
+ }
62
+ return result;
63
+ }
64
+ //#endregion
65
+ export { spawnAB as n, sleepSync as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.3.10",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@anthropic-ai/claude-agent-sdk": "^0.2.87",
28
28
  "commander": "^14.0.3",
29
- "gray-matter": "^4.0.3",
29
+ "yaml": "^2.9.0",
30
30
  "zod": "^4.3.6"
31
31
  },
32
32
  "peerDependencies": {