aztrx-cli 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -196,7 +196,7 @@ jobs:
196
196
  permissions: { contents: read, pull-requests: write }
197
197
  steps:
198
198
  - uses: actions/checkout@v4
199
- - uses: DanisChaparov/aztrx@94d1173e6b363bc60e2237775cca11addd39b10f
199
+ - uses: DanisChaparov/aztrx@14a187d686710cf59392505075a54b00838fceae
200
200
  with:
201
201
  url: http://localhost:3000
202
202
  start-command: npm run dev # optional — boot the app in the background
@@ -210,7 +210,7 @@ Or as a reusable workflow:
210
210
  on: pull_request
211
211
  jobs:
212
212
  aztrx:
213
- uses: DanisChaparov/aztrx/.github/workflows/aztrx-pr.yml@94d1173e6b363bc60e2237775cca11addd39b10f
213
+ uses: DanisChaparov/aztrx/.github/workflows/aztrx-pr.yml@14a187d686710cf59392505075a54b00838fceae
214
214
  with:
215
215
  url: http://localhost:3000
216
216
  start-command: npm run dev
@@ -261,7 +261,7 @@ jobs:
261
261
  sleep 2
262
262
  done
263
263
  - name: Generate badge
264
- run: npx --yes aztrx-cli@0.2.1 run http://localhost:3000 --badge badge.svg
264
+ run: npx --yes aztrx-cli@0.2.3 run http://localhost:3000 --badge badge.svg
265
265
  - name: Commit badge
266
266
  run: |
267
267
  git config user.name "github-actions[bot]"
@@ -320,7 +320,7 @@ LLM call.
320
320
  CORS.
321
321
  - **`.aztrx/` is gitignored** on `init` — repro specs, reports, and patches stay
322
322
  out of history.
323
- - **Pinned supply chain.** The GitHub Action pins `aztrx-cli@0.2.1` (never
323
+ - **Pinned supply chain.** The GitHub Action pins `aztrx-cli@0.2.3` (never
324
324
  `@latest`).
325
325
 
326
326
  ## Telemetry & privacy
@@ -14,64 +14,92 @@ export async function walkDom(page, bus, opts = {}) {
14
14
  const max = opts.maxActions ?? 100;
15
15
  const startUrl = page.url();
16
16
  let actions = 0;
17
- const handles = await page.$$(SELECTOR);
18
- for (const handle of handles) {
19
- if (actions >= max)
20
- break;
21
- if (page.url() !== startUrl)
22
- break; // navigated bail this pass
23
- const visible = await handle.isVisible().catch(() => false);
24
- const enabled = await handle.isEnabled().catch(() => false);
25
- if (!visible || !enabled)
26
- continue;
27
- let tag;
28
- try {
29
- tag = await handle.evaluate((el) => el.tagName.toLowerCase());
17
+ const seen = new Set();
18
+ // Re-scan the DOM after every action (the page may have mutated or navigated)
19
+ // rather than walking a stale snapshot. Mirrors the fuzzer's recover-and-iterate
20
+ // loop, but deterministically (document order) and without re-clicking elements
21
+ // it has already acted on.
22
+ while (actions < max) {
23
+ if (page.url() !== startUrl) {
24
+ // A previous action navigated away — return to the starting page.
25
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
26
+ await page.waitForTimeout(300);
30
27
  }
31
- catch {
32
- continue; // element unreadable mid-query — skip
33
- }
34
- let label = "";
28
+ let handles;
35
29
  try {
36
- label = await handle.evaluate((el) => {
37
- const t = el.innerText ||
38
- el.getAttribute("aria-label") ||
39
- el.getAttribute("value") ||
40
- el.getAttribute("placeholder") ||
41
- "";
42
- return t.trim();
43
- });
30
+ handles = await page.$$(SELECTOR);
44
31
  }
45
32
  catch {
46
- label = ""; // degradedno label to filter on
47
- }
48
- if (DESTRUCTIVE.test(label))
33
+ // A navigation raced the re-scan reset to the start page and retry.
34
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
35
+ await page.waitForTimeout(300);
49
36
  continue;
50
- if (tag === "a") {
51
- const href = (await handle.getAttribute("href")) ?? "";
52
- if (/^https?:\/\//.test(href) && !href.startsWith(originOf(startUrl)))
53
- continue;
54
- }
55
- if (tag === "input") {
56
- const type = (await handle.getAttribute("type")) ?? "";
57
- if (!TEXT_INPUT_TYPES.has(type))
58
- continue; // skip password/hidden/submit/checkbox/etc.
59
- }
60
- const selectors = await selectorCascade(page, handle);
61
- if (tag === "input" || tag === "textarea") {
62
- const action = { type: "input", selectors, value: "test", timestamp: Date.now() };
63
- bus.emit("action", action);
64
- if (!opts.dryRun)
65
- await handle.fill("test").catch(() => { });
66
37
  }
67
- else {
68
- const action = { type: "click", selectors, timestamp: Date.now() };
69
- bus.emit("action", action);
70
- if (!opts.dryRun)
71
- await handle.click({ timeout: 1500 }).catch(() => { });
38
+ let acted = false;
39
+ for (const handle of handles) {
40
+ if (actions >= max)
41
+ break;
42
+ const visible = await handle.isVisible().catch(() => false);
43
+ const enabled = await handle.isEnabled().catch(() => false);
44
+ if (!visible || !enabled)
45
+ continue;
46
+ let tag;
47
+ try {
48
+ tag = await handle.evaluate((el) => el.tagName.toLowerCase());
49
+ }
50
+ catch {
51
+ continue; // element unreadable mid-query — skip
52
+ }
53
+ let label = "";
54
+ try {
55
+ label = await handle.evaluate((el) => {
56
+ const t = el.innerText ||
57
+ el.getAttribute("aria-label") ||
58
+ el.getAttribute("value") ||
59
+ el.getAttribute("placeholder") ||
60
+ "";
61
+ return t.trim();
62
+ });
63
+ }
64
+ catch {
65
+ label = ""; // degraded — no label to filter on
66
+ }
67
+ if (DESTRUCTIVE.test(label))
68
+ continue;
69
+ if (tag === "a") {
70
+ const href = (await handle.getAttribute("href")) ?? "";
71
+ if (/^https?:\/\//.test(href) && !href.startsWith(originOf(startUrl)))
72
+ continue;
73
+ }
74
+ if (tag === "input") {
75
+ const type = (await handle.getAttribute("type")) ?? "";
76
+ if (!TEXT_INPUT_TYPES.has(type))
77
+ continue; // skip password/hidden/submit/checkbox/etc.
78
+ }
79
+ const selectors = await selectorCascade(page, handle);
80
+ const signature = selectors.join("|") || `${tag}:${label}`;
81
+ if (seen.has(signature))
82
+ continue; // already acted on this element
83
+ seen.add(signature);
84
+ if (tag === "input" || tag === "textarea") {
85
+ const action = { type: "input", selectors, value: "test", timestamp: Date.now() };
86
+ bus.emit("action", action);
87
+ if (!opts.dryRun)
88
+ await handle.fill("test").catch(() => { });
89
+ }
90
+ else {
91
+ const action = { type: "click", selectors, timestamp: Date.now() };
92
+ bus.emit("action", action);
93
+ if (!opts.dryRun)
94
+ await handle.click({ timeout: 1500 }).catch(() => { });
95
+ }
96
+ actions++;
97
+ acted = true;
98
+ await page.waitForTimeout(120);
99
+ break; // re-scan next iteration — the DOM may have changed
72
100
  }
73
- actions++;
74
- await page.waitForTimeout(120);
101
+ if (!acted)
102
+ break; // nothing left to act on
75
103
  }
76
104
  return actions;
77
105
  }
@@ -83,9 +83,15 @@ export function attachInterceptor(page, bus) {
83
83
  });
84
84
  page.on("requestfailed", (req) => {
85
85
  const f = req.failure();
86
+ const errText = f?.errorText ?? "unknown";
87
+ // Cancelled/blocked requests are not bugs — a video preload dropped when the
88
+ // walk navigates away, a `mailto:` click, or an adblocker all surface as
89
+ // `requestfailed`. Skip them so they don't become false-positive errors.
90
+ if (/ERR_ABORTED|ERR_BLOCKED_BY_CLIENT|ERR_BLOCKED_BY_RESPONSE/.test(errText))
91
+ return;
86
92
  bus.emit("telemetry", {
87
93
  type: "network_timeout",
88
- rawMessage: `Request failed: ${req.url()} (${f?.errorText ?? "unknown"})`,
94
+ rawMessage: `Request failed: ${req.url()} (${errText})`,
89
95
  rawStack: "",
90
96
  });
91
97
  });
@@ -117,6 +117,7 @@ export async function run(options) {
117
117
  baseline,
118
118
  guardOn,
119
119
  log: say,
120
+ forwardBus: bus,
120
121
  });
121
122
  // Replays reuse the swarm-captured auth state, or the explicit --storage-state.
122
123
  const replayStorageState = swarmAuthState ?? options.storageState;
@@ -213,7 +213,7 @@ export async function swarmDetect(opts) {
213
213
  httpFuzzMutations: opts.httpFuzzMutations,
214
214
  baseline: opts.baseline,
215
215
  log: (m) => opts.log(strategies.length > 1 ? `[w${i}] ${m}` : m),
216
- }, strategy)));
216
+ }, strategy, opts.forwardBus)));
217
217
  const results = [];
218
218
  settled.forEach((r, i) => {
219
219
  if (r.status === "fulfilled")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aztrx-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "description": "Aztrx AI — runtime stress-tester for web apps. Detect bugs, then prove them with an executable repro.",
6
6
  "license": "Apache-2.0",