@ait-co/devtools 0.1.126 → 0.1.128

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/mcp/cli.js CHANGED
@@ -10,7 +10,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
10
10
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { homedir, platform } from "node:os";
12
12
  import * as path from "node:path";
13
- import { isAbsolute, join, resolve } from "node:path";
13
+ import { basename, isAbsolute, join, resolve } from "node:path";
14
14
  import { randomBytes } from "node:crypto";
15
15
  import { Tunnel, bin, install } from "cloudflared";
16
16
  import * as fs from "node:fs/promises";
@@ -2024,7 +2024,7 @@ async function readMcpSdkVersion() {
2024
2024
  * some test environments that skip the build step).
2025
2025
  */
2026
2026
  function readDevtoolsVersion() {
2027
- return "0.1.126";
2027
+ return "0.1.128";
2028
2028
  }
2029
2029
  /**
2030
2030
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -3395,7 +3395,7 @@ function parseCaptureLines(raw) {
3395
3395
  //#endregion
3396
3396
  //#region src/test-runner/rpc.ts
3397
3397
  /** Maximum milliseconds to wait for a single evaluate round-trip. */
3398
- const DEFAULT_TIMEOUT_MS = 3e4;
3398
+ const DEFAULT_TIMEOUT_MS = 6e4;
3399
3399
  /**
3400
3400
  * Wraps bundle code in a self-executing IIFE that:
3401
3401
  * 1. Evaluates the bundle (registering describe/it/test).
@@ -3454,13 +3454,22 @@ function parseRunTestsResult(rawValue) {
3454
3454
  */
3455
3455
  async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TIMEOUT_MS) {
3456
3456
  const expression = buildRunTestsExpression(bundleCode);
3457
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs));
3457
+ const TIMEOUT_SENTINEL = Symbol("timeout");
3458
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs));
3458
3459
  const evalPromise = connection.send("Runtime.evaluate", {
3459
3460
  expression,
3460
3461
  returnByValue: true,
3461
3462
  awaitPromise: true
3462
3463
  });
3463
- const cdpResult = await Promise.race([evalPromise, timeoutPromise]);
3464
+ const raceResult = await Promise.race([evalPromise.then((v) => ({
3465
+ tag: "eval",
3466
+ v
3467
+ })), timeoutPromise.then(() => ({ tag: "timeout" }))]);
3468
+ if (raceResult.tag === "timeout") return {
3469
+ ok: false,
3470
+ error: `rpc: evaluate timed out after ${timeoutMs}ms`
3471
+ };
3472
+ const cdpResult = raceResult.v;
3464
3473
  if (cdpResult.exceptionDetails) {
3465
3474
  const msg = cdpResult.exceptionDetails.exception?.description ?? cdpResult.exceptionDetails.text ?? "Runtime.evaluate threw an exception";
3466
3475
  throw new Error(`rpc.injectAndRunBundle: ${msg}`);
@@ -3470,6 +3479,14 @@ async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TI
3470
3479
  //#endregion
3471
3480
  //#region src/test-runner/relay-worker.ts
3472
3481
  /**
3482
+ * Sentinel string embedded in the error message by `injectAndRunBundle` when
3483
+ * the per-file evaluate race hits the timeout. Used by the retry guard so only
3484
+ * genuine timeouts get a second attempt — not bundle errors or parse failures.
3485
+ *
3486
+ * Exported for unit tests that assert the retry path is taken.
3487
+ */
3488
+ const EVALUATE_TIMEOUT_MARKER = "rpc: evaluate timed out after";
3489
+ /**
3473
3490
  * Runs all `files` sequentially over the given CDP `connection`.
3474
3491
  *
3475
3492
  * For each file:
@@ -3512,15 +3529,53 @@ async function runTestFilesOverRelay(connection, files, opts) {
3512
3529
  let fileEntry;
3513
3530
  try {
3514
3531
  const { code } = await bundleTestFile(file, opts?.bundleOptions);
3515
- const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
3516
- if (rpcResult.ok) fileEntry = {
3517
- file,
3518
- result: rpcResult.report
3519
- };
3520
- else fileEntry = {
3521
- file,
3522
- result: { error: rpcResult.error }
3532
+ /**
3533
+ * Runs one evaluate attempt and returns a FileResult, or `null` when the
3534
+ * result is a genuine timeout and the caller should retry.
3535
+ *
3536
+ * We need to distinguish:
3537
+ * - `rpcResult.ok = false` + timeout error → retry candidate (`return null`)
3538
+ * - `rpcResult.ok = false` + other error → final error, no retry
3539
+ * - `injectAndRunBundle` throws → CDP exceptionDetails (page
3540
+ * engine threw); treated as a final (non-retryable) error.
3541
+ *
3542
+ * The Promise.race timeout in rpc.ts RETURNS `{ok:false, error: '…'}` (it
3543
+ * does NOT throw/reject). Only genuine CDP `exceptionDetails` cause a throw.
3544
+ * This distinction is what makes the EVALUATE_TIMEOUT_MARKER gate below
3545
+ * reachable — the timeout result surfaces as `rpcResult.ok=false` with the
3546
+ * marker string, not as a caught exception.
3547
+ */
3548
+ const attempt = async () => {
3549
+ let rpcResult;
3550
+ try {
3551
+ rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
3552
+ } catch (e) {
3553
+ return {
3554
+ file,
3555
+ result: { error: e instanceof Error ? e.message : String(e) }
3556
+ };
3557
+ }
3558
+ if (rpcResult.ok) return {
3559
+ file,
3560
+ result: rpcResult.report
3561
+ };
3562
+ if (rpcResult.error.includes("rpc: evaluate timed out after")) return null;
3563
+ return {
3564
+ file,
3565
+ result: { error: rpcResult.error }
3566
+ };
3523
3567
  };
3568
+ const firstResult = await attempt();
3569
+ if (firstResult !== null) fileEntry = firstResult;
3570
+ else {
3571
+ process.stderr.write(`relay-worker: evaluate timed out for ${file} — retrying once\n`);
3572
+ const retryResult = await attempt();
3573
+ if (retryResult !== null) fileEntry = retryResult;
3574
+ else fileEntry = {
3575
+ file,
3576
+ result: { error: `${EVALUATE_TIMEOUT_MARKER} ${opts?.timeoutMs ?? 3e4}ms (after retry)` }
3577
+ };
3578
+ }
3524
3579
  } catch (e) {
3525
3580
  fileEntry = {
3526
3581
  file,
@@ -3640,6 +3695,42 @@ EXAMPLE
3640
3695
 
3641
3696
  `.trimStart();
3642
3697
  /**
3698
+ * Renders per-file result lines and the aggregate totals line to a string.
3699
+ *
3700
+ * Each file gets one line:
3701
+ * - Error/timeout: `FAIL <basename>: <error-class>`
3702
+ * - Pass (0 tests): `OK <basename>: 0 passed (empty file)`
3703
+ * - Pass: `OK <basename>: N passed[, M failed][, K skipped]`
3704
+ *
3705
+ * The aggregate totals line always follows.
3706
+ *
3707
+ * SECRET-HANDLING: only `basename(file)` is used — no absolute paths, relay
3708
+ * URLs, wss URLs, scheme URLs, or TOTP codes appear in the output. The error
3709
+ * string comes from `result.error` which is already secret-free (relay-worker
3710
+ * produces only error-class messages like "rpc: evaluate timed out after
3711
+ * 30000ms").
3712
+ *
3713
+ * Exported so unit tests can assert the per-file lines without spawning a
3714
+ * subprocess or going through the full relay attach flow.
3715
+ */
3716
+ function renderSummary(report) {
3717
+ const lines = [];
3718
+ for (const { file, result } of report.files) {
3719
+ const name = basename(file);
3720
+ if ("error" in result) lines.push(`FAIL ${name}: ${result.error}`);
3721
+ else {
3722
+ const parts = [`${result.passed} passed`];
3723
+ if (result.failed > 0) parts.push(`${result.failed} failed`);
3724
+ if (result.skipped > 0) parts.push(`${result.skipped} skipped`);
3725
+ const suffix = result.passed + result.failed + result.skipped === 0 ? " (empty file)" : "";
3726
+ lines.push(`OK ${name}: ${parts.join(", ")}${suffix}`);
3727
+ }
3728
+ }
3729
+ const { totals, duration } = report;
3730
+ lines.push(`\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${duration}ms)`);
3731
+ return lines.join("\n");
3732
+ }
3733
+ /**
3643
3734
  * Runs `files` over `connection` and returns the aggregate report.
3644
3735
  * This pure function is the testable core of the CLI (and is what the
3645
3736
  * `run_tests` MCP tool calls against the daemon's attached connection); it is
@@ -3647,10 +3738,7 @@ EXAMPLE
3647
3738
  */
3648
3739
  async function runWithConnection(connection, files, opts) {
3649
3740
  const report = await runTestFilesOverRelay(connection, files, opts);
3650
- if (opts?.printSummary) {
3651
- const { totals } = report;
3652
- process.stdout.write(`\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\n`);
3653
- }
3741
+ if (opts?.printSummary) process.stdout.write(`\n${renderSummary(report)}\n`);
3654
3742
  return report;
3655
3743
  }
3656
3744
  //#endregion
@@ -6710,7 +6798,7 @@ function createDebugServer(deps) {
6710
6798
  };
6711
6799
  const server = new Server({
6712
6800
  name: "ait-debug",
6713
- version: "0.1.126"
6801
+ version: "0.1.128"
6714
6802
  }, { capabilities: { tools: { listChanged: true } } });
6715
6803
  server.setRequestHandler(ListToolsRequestSchema, () => {
6716
6804
  const conn = router.active;
@@ -8682,7 +8770,7 @@ function createDevServer(deps = {}) {
8682
8770
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
8683
8771
  const server = new Server({
8684
8772
  name: "ait-devtools",
8685
- version: "0.1.126"
8773
+ version: "0.1.128"
8686
8774
  }, { capabilities: { tools: {} } });
8687
8775
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
8688
8776
  server.setRequestHandler(CallToolRequestSchema, async (request) => {