diff --git a/test-parity/node-suite/inspector-promises/EVIDENCE.md b/test-parity/node-suite/inspector-promises/EVIDENCE.md new file mode 100644 index 0000000000..222de47536 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/EVIDENCE.md @@ -0,0 +1,44 @@ +# `node:inspector/promises` entry evidence + +All rows use Node 26.5.0 as oracle. `pass`, `diff`, `error`, and `timeout` +describe direct stdout/exit comparison under Deno 2.9.2 and Bun 1.3.14. Perry +classifications come from three identical focused release-runner passes (3/30, +27 diffs). Node was repeated five times with identical aggregate evidence. + +| Entry | Promise contract | Node source basis | Deno | Bun | Perry | +| ----------------------------------- | ------------------------------------ | ----------------------------------- | ----: | ------: | ----: | +| `events/notification-settlement.ts` | notification before fulfillment | `inspector.js` dispatch + promisify | pass | error | diff | +| `lifecycle/disconnected.ts` | pre/post-connect async rejection | Promise `post` wrapper | pass | diff | pass | +| `lifecycle/pending-disconnect.ts` | pending-post rejection/order | `disconnect()` + promisify | error | diff | diff | +| `lifecycle/reconnect.ts` | post after reconnect | API lifecycle | pass | error | diff | +| `lifecycle/repeated-sessions.ts` | independent Promise sessions | multisession contract | pass | error | diff | +| `lifecycle/sync-control.ts` | synchronous lifecycle controls | inherited `Session` methods | pass | diff | diff | +| `post/argument-validation.ts` | validation rejects, never throws | `promisify(post)` | pass | timeout | diff | +| `post/circular-params.ts` | serialization Promise rejection | `post()` JSON dispatch | pass | diff | diff | +| `post/concurrent-order.ts` | independent/input-order settlement | upstream promises test | pass | error | diff | +| `post/concurrent-rejection.ts` | one rejection does not cancel peers | Promise post mapping | pass | error | diff | +| `post/invalid-protocol-params.ts` | protocol -32602 rejection | inspector response mapping | pass | diff | diff | +| `post/optional-params.ts` | omitted/null/undefined params | Promise post signature | pass | error | diff | +| `post/rejection-identity.ts` | stable reason identity | Promise post rejection | pass | diff | pass | +| `post/settlement-async.ts` | no same-turn settlement | promisified callback | pass | error | diff | +| `post/unknown-command.ts` | protocol -32601 rejection | inspector response mapping | pass | diff | pass | +| `protocol/enable-disable.ts` | safe empty result shapes | inspector API examples | pass | error | diff | +| `protocol/schema-domains.ts` | Schema Promise payload | V8 Schema protocol | pass | error | diff | +| `runtime/await-promise.ts` | fulfilled awaited evaluation | inspector API examples | pass | error | diff | +| `runtime/await-rejection.ts` | rejection resolves exception details | V8 Runtime protocol | pass | error | diff | +| `runtime/exception-details.ts` | throw resolves exception details | V8 Runtime protocol | pass | error | diff | +| `runtime/get-properties-release.ts` | object lifecycle/rejection | V8 Runtime protocol | pass | error | diff | +| `runtime/numeric-specials.ts` | special/BigInt result shapes | V8 Runtime protocol | pass | error | diff | +| `runtime/primitives.ts` | primitive result shapes | inspector docs | pass | error | diff | +| `runtime/return-by-value.ts` | object/array by-value results | V8 Runtime protocol | pass | error | diff | +| `surface/constructor.ts` | constructor call/extra argument | subclass definition | pass | error | diff | +| `surface/exports.ts` | keys/descriptors/import identity | module object spread | diff | diff | diff | +| `surface/inherited-receivers.ts` | synchronous receiver brands | inherited `Session` methods | pass | diff | diff | +| `surface/post-descriptor.ts` | promisified method descriptor | `promisify(post)` assignment | pass | diff | diff | +| `surface/post-receiver.ts` | receiver error becomes rejection | `promisify(post)` | pass | diff | diff | +| `surface/session-class.ts` | subclass/prototype/method identity | Promise Session definition | pass | pass | diff | + +No Node fixture failed or timed out. Perry produced no compile failures or +execution timeouts. After the alternate-runtime timeout and all focused runs, +process, endpoint, and generated-artifact scans found no live suite process, +listening inspector endpoint, or repository artifact. diff --git a/test-parity/node-suite/inspector-promises/README.md b/test-parity/node-suite/inspector-promises/README.md new file mode 100644 index 0000000000..7885dca4da --- /dev/null +++ b/test-parity/node-suite/inspector-promises/README.md @@ -0,0 +1,111 @@ +# `node:inspector/promises` granular parity suite + +Deterministic Promise-API coverage for Node's `node:inspector/promises` module. +Node **26.5.0** is the oracle. Each fixture isolates one public Promise contract +and prints only stable semantic fields; protocol IDs, contexts, scripts, stacks, +locations, timestamps, profiles, and engine text are not compared. + +## Coverage + +- exact callback-module export-key parity, descriptors, import identities, the + distinct `Session` subclass, prototype chains, constructor behavior, inherited + control methods, and Promise `post()` receiver behavior +- synchronous `connect()`/`connectToMainThread()`/`disconnect()` control versus + asynchronous `post()` validation and connection rejection +- reconnect, independent sessions, pending-post disconnect, same-turn + non-settlement, stable rejection identity, and independent concurrent posts +- controlled out-of-order completion with a resolver barrier, proving both + independent settlement and `Promise.all()` input ordering without timers +- deterministic `Runtime`, `Schema`, Debugger, Profiler, and HeapProfiler + resolution/rejection shapes, including `getProperties`/`releaseObject` +- primitive, special-number, BigInt, by-value, fulfilled `awaitPromise`, thrown + evaluation, and rejected `awaitPromise` results +- the one Promise-distinct notification contract: specific and generic events + are delivered before the enabling `post()` Promise fulfills + +The module already runs in the sequential lane in `scripts/node_suite_run.py`. + +## Oracle and alternate-runtime evidence + +Primary sources audited at exact tags: + +- Node 26.5.0: `lib/inspector/promises.js`, `lib/inspector.js`, + `doc/api/inspector.md`, and `test/parallel/test-inspector-promises.js`. +- Deno 2.9.2: `ext/node/polyfills/inspector/promises.js`, `promises_esm.js`, the + callback implementation, and `tests/unit_node/inspector_test.ts`. +- Bun 1.3.14: `src/js/node/inspector.promises.ts`, `src/js/node/inspector.ts`, + and `test/js/node/inspector/inspector.test.ts`. + +Five complete Node oracle passes (150 executions) exited zero with identical +aggregate stdout/exit data at SHA-256 +`994cab8da5deb3aa67bb505560eae5f6d7ee0754dfdd0aad92f4c95266688d49`. Deno +produced 28 exact matches, one surface diff, and one deterministic process +error. Bun produced one exact match, 11 diffs, 17 errors, and one bounded +timeout. Three focused release-runner passes classified Perry identically at +**3/30**, with 27 stable output/exit diffs and no compile failure, runtime +timeout, or crash. + +Deno mirrors Node's subclass-plus-promisify design but omits `NetworkResources`. +Its pending-disconnect case reproducibly aborts with a V8 evaluate-callback +assertion instead of rejecting the Promise. Bun implements a Promise subclass +around its callback inspector, but only Profiler commands have a backend; +Runtime and Schema commands reject, and several validation/lifecycle semantics +differ from Node. Node remains the oracle for both divergences. + +See [EVIDENCE.md](EVIDENCE.md) for per-entry classification. + +## Upstream and callback-suite reconciliation + +Node's exact `test-inspector-promises.js` inventory is reconciled as follows: + +1. callback-module export keys: `surface/exports.ts`; +2. `Session.post()` returns a Promise: receiver, validation, disconnected, and + settlement fixtures; +3. resolved protocol payloads: safe Runtime/Schema/enable-disable fixtures; +4. submission/input order with a slow response: `post/concurrent-order.ts`, + using an explicit resolver barrier rather than the upstream 100 ms timer; +5. CPU-profile URL payload: excluded because sampling/profile contents and + source URLs are volatile; safe empty results and Runtime values prove + resolution. + +The 37 callback fixtures from PR #6490 are also accounted for: + +- Promise-specific counterparts retained: `lifecycle/main-thread-connect`, + `method-receivers`, `repeated-sessions`, `session-connect`; + `post/circular-params`, `disconnected`, `method-validation`, + `params-validation`, `pending-disconnect`, `unknown-command`; + `protocol/enable-disable`, `schema-domains`; `runtime/await-promise`, + `exception-details`, `get-properties-release`, `numeric-specials`, + `primitives`, `return-by-value`; `events/notification-order`; and + `surface/exports`, `session-class`. +- Callback-only and non-applicable: `post/callback-validation`, `overloads`, and + `session/callback-runtime`. +- Inherited or protocol-payload duplicates deliberately not copied: + `events/console-api`, `listener-lifecycle`, `script-parsed`; + `lifecycle/endpoint`, `open-range-validation`; `network/helpers`; + `protocol/debugger-metadata`, `get-script-source`; `runtime/object-preview`, + `release-object-group`, `remote-subtypes`; and `surface/domain-helpers`, + `domain-validation`. + +## Deliberate exclusions and stopping boundary + +- Endpoint `open`/`close`/`url`, range checks, domain helpers, and callback + event inventories are shared identities/inherited behavior already proven by + PR #6490; only their Promise-module import identity is retained here. +- No fixed port, external DevTools/WebSocket client, frontend, internet, worker + race, active `waitForDebugger`, breakpoint, signal, crash, large payload, or + stress case is used. +- CPU/heap profiles, sampling, coverage payloads, heap snapshots, + GC/finalization, `queryObjects` retention, memory pressure, and + source/position payloads remain excluded as timing-, allocation-, or + engine-dependent. +- An explicit third callback is outside the Promise signature and can strand the + promisified Promise forever; it is documented rather than tested. +- Every rejection is awaited/caught. Every connected session disconnects in a + `finally` path, every installed listener is removed, and the controlled global + resolver is deleted. The only derived Promise observers include rejection + handlers or are themselves awaited. + +This exhausts the reachable deterministic Promise-specific surface in Node +26.5.0 and the requested safe protocol subset without count-driven assertions, +arbitrary sleeps, or duplicated callback contracts. diff --git a/test-parity/node-suite/inspector-promises/events/notification-settlement.ts b/test-parity/node-suite/inspector-promises/events/notification-settlement.ts new file mode 100644 index 0000000000..462d5aa792 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/events/notification-settlement.ts @@ -0,0 +1,31 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +const order: string[] = []; +const specific = () => order.push("specific"); +const generic = (message: { method?: string }) => { + if (message.method === "Runtime.executionContextCreated") { + order.push("generic"); + } +}; +session.on("Runtime.executionContextCreated", specific); +session.on("inspectorNotification", generic); +session.connect(); +try { + const pending = session.post("Runtime.enable"); + const observed = pending.then((value) => { + order.push("promise"); + return value; + }); + await observed; + console.log("order:", order.join(",")); + console.log( + "listeners:", + session.listenerCount("Runtime.executionContextCreated"), + session.listenerCount("inspectorNotification"), + ); +} finally { + session.off("Runtime.executionContextCreated", specific); + session.off("inspectorNotification", generic); + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/lifecycle/disconnected.ts b/test-parity/node-suite/inspector-promises/lifecycle/disconnected.ts new file mode 100644 index 0000000000..cf559d0881 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/lifecycle/disconnected.ts @@ -0,0 +1,32 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +for (const label of ["before", "after"] as const) { + if (label === "after") { + session.connect(); + session.disconnect(); + } + let pending: Promise | undefined; + let synchronous = false; + try { + try { + pending = session.post("Runtime.enable"); + } catch (inner) { + // synchronous throw: record it and re-throw so the real error reaches the + // outer catch instead of being swallowed with pending undefined. + synchronous = true; + throw inner; + } + await pending; + console.log(label, "unexpected"); + } catch (error) { + const cause = error as { name?: string; code?: string }; + console.log( + label, + synchronous, + pending instanceof Promise, + cause.name, + cause.code, + ); + } +} diff --git a/test-parity/node-suite/inspector-promises/lifecycle/pending-disconnect.ts b/test-parity/node-suite/inspector-promises/lifecycle/pending-disconnect.ts new file mode 100644 index 0000000000..1af45e8042 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/lifecycle/pending-disconnect.ts @@ -0,0 +1,31 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +const order: string[] = []; +try { + const pending = session.post("Runtime.evaluate", { + expression: "new Promise(() => {})", + awaitPromise: true, + }); + order.push("posted"); + session.disconnect(); + order.push("disconnected"); + try { + await pending; + console.log("unexpected resolution"); + } catch (error) { + order.push("rejected"); + const cause = error as { name?: string; code?: string; message?: string }; + console.log( + "interrupted:", + cause.name, + cause.code, + cause.message?.includes("-32000"), + cause.message?.includes("Execution context was destroyed"), + ); + } + console.log("order:", order.join(",")); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/lifecycle/reconnect.ts b/test-parity/node-suite/inspector-promises/lifecycle/reconnect.ts new file mode 100644 index 0000000000..e9382e3e70 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/lifecycle/reconnect.ts @@ -0,0 +1,15 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +try { + session.connect(); + const first = await session.post("Runtime.evaluate", { expression: "6 * 7" }); + session.disconnect(); + session.connect(); + const second = await session.post("Runtime.evaluate", { + expression: "7 * 8", + }); + console.log("values:", first.result.value, second.result.value); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/lifecycle/repeated-sessions.ts b/test-parity/node-suite/inspector-promises/lifecycle/repeated-sessions.ts new file mode 100644 index 0000000000..a6bce65931 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/lifecycle/repeated-sessions.ts @@ -0,0 +1,16 @@ +import { Session } from "node:inspector/promises"; + +const first = new Session(); +const second = new Session(); +first.connect(); +second.connect(); +try { + const [one, two] = await Promise.all([ + first.post("Runtime.evaluate", { expression: "20 + 1" }), + second.post("Runtime.evaluate", { expression: "20 + 2" }), + ]); + console.log("values:", one.result.value, two.result.value); +} finally { + first.disconnect(); + second.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/lifecycle/sync-control.ts b/test-parity/node-suite/inspector-promises/lifecycle/sync-control.ts new file mode 100644 index 0000000000..78e0aecd46 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/lifecycle/sync-control.ts @@ -0,0 +1,35 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +try { + const connected = session.connect(); + console.log( + "connect:", + connected === undefined, + connected instanceof Promise, + ); + try { + session.connect(); + console.log("unexpected second connect"); + } catch (error) { + const cause = error as { name?: string; code?: string }; + console.log("second connect:", cause.name, cause.code); + } + const disconnected = session.disconnect(); + const repeated = session.disconnect(); + console.log( + "disconnect:", + disconnected === undefined, + disconnected instanceof Promise, + repeated === undefined, + ); + try { + session.connectToMainThread(); + console.log("unexpected main-thread connection"); + } catch (error) { + const cause = error as { name?: string; code?: string }; + console.log("main thread:", cause.name, cause.code); + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/post/argument-validation.ts b/test-parity/node-suite/inspector-promises/post/argument-validation.ts new file mode 100644 index 0000000000..e4777f8d32 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/argument-validation.ts @@ -0,0 +1,43 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const cases: Array<[string, () => Promise]> = [ + ["missing", () => session.post()], + ["number method", () => session.post(1 as never)], + ["number params", () => session.post("Runtime.enable", 1 as never)], + ["string params", () => session.post("Runtime.enable", "x" as never)], + [ + "function params", + () => session.post("Runtime.enable", (() => {}) as never), + ], + ]; + for (const [label, run] of cases) { + let pending: Promise | undefined; + let synchronous = false; + try { + try { + pending = run(); + } catch (inner) { + // synchronous throw: record it and re-throw so the real error (name/code) + // reaches the outer catch instead of being swallowed with pending undefined. + synchronous = true; + throw inner; + } + await pending; + console.log(label, "unexpected", synchronous); + } catch (error) { + const cause = error as { name?: string; code?: string }; + console.log( + label, + synchronous, + pending instanceof Promise, + cause.name, + cause.code, + ); + } + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/post/circular-params.ts b/test-parity/node-suite/inspector-promises/post/circular-params.ts new file mode 100644 index 0000000000..27229290f3 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/circular-params.ts @@ -0,0 +1,33 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const params: { self?: unknown } = {}; + params.self = params; + let pending: Promise | undefined; + let synchronous = false; + try { + try { + pending = session.post("Runtime.enable", params); + } catch (inner) { + // synchronous throw: record it and re-throw so the real error reaches the + // outer catch instead of being swallowed with pending undefined. + synchronous = true; + throw inner; + } + await pending; + console.log("unexpected resolution"); + } catch (error) { + const cause = error as { name?: string; message?: string }; + console.log( + "circular:", + synchronous, + pending instanceof Promise, + cause.name, + cause.message?.includes("circular structure"), + ); + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/post/concurrent-order.ts b/test-parity/node-suite/inspector-promises/post/concurrent-order.ts new file mode 100644 index 0000000000..6d1e9c2991 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/concurrent-order.ts @@ -0,0 +1,59 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const settled: string[] = []; + const first = session.post("Runtime.evaluate", { + expression: "2 + 2", + returnByValue: true, + }); + const delayed = session.post("Runtime.evaluate", { + expression: + "new Promise((resolve) => { globalThis.__perryResolve = resolve; })", + awaitPromise: true, + returnByValue: true, + }); + const third = session.post("Runtime.evaluate", { + expression: "4 + 4", + returnByValue: true, + }); + first.then( + () => settled.push("first"), + () => settled.push("first rejected"), + ); + delayed.then( + () => settled.push("delayed"), + () => settled.push("delayed rejected"), + ); + third.then( + () => settled.push("third"), + () => settled.push("third rejected"), + ); + + const independent = await Promise.all([first, third]); + console.log( + "before release:", + independent.map((entry) => entry.result.value).join(","), + settled.join(","), + ); + await session.post("Runtime.evaluate", { + expression: + "globalThis.__perryResolve(6); delete globalThis.__perryResolve", + }); + const ordered = await Promise.all([first, delayed, third]); + console.log( + "ordered:", + ordered.map((entry) => entry.result.value).join(","), + settled.join(","), + ); +} finally { + try { + await session.post("Runtime.evaluate", { + expression: "delete globalThis.__perryResolve", + }); + } catch { + // Cleanup must not mask the contract result on divergent runtimes. + } + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/post/concurrent-rejection.ts b/test-parity/node-suite/inspector-promises/post/concurrent-rejection.ts new file mode 100644 index 0000000000..0fe4a436e7 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/concurrent-rejection.ts @@ -0,0 +1,35 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const first = session.post("Runtime.evaluate", { expression: "10 + 1" }); + const invalid = session.post("Missing.concurrent"); + const third = session.post("Runtime.evaluate", { expression: "10 + 3" }); + try { + await Promise.all([first, invalid, third]); + console.log("unexpected aggregate resolution"); + } catch (error) { + const cause = error as { name?: string; code?: string; message?: string }; + console.log( + "aggregate:", + cause.name, + cause.code, + cause.message?.includes("-32601"), + ); + } + const [one, three] = await Promise.all([first, third]); + try { + await invalid; + } catch (error) { + const repeated = error as { code?: string }; + console.log( + "independent:", + one.result.value, + three.result.value, + repeated.code, + ); + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/post/invalid-protocol-params.ts b/test-parity/node-suite/inspector-promises/post/invalid-protocol-params.ts new file mode 100644 index 0000000000..75b4f8559c --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/invalid-protocol-params.ts @@ -0,0 +1,21 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + try { + await session.post("Runtime.evaluate", {}); + console.log("unexpected resolution"); + } catch (error) { + const cause = error as { name?: string; code?: string; message?: string }; + console.log( + "invalid params:", + cause.name, + cause.code, + cause.message?.includes("-32602"), + cause.message?.includes("Invalid parameters"), + ); + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/post/optional-params.ts b/test-parity/node-suite/inspector-promises/post/optional-params.ts new file mode 100644 index 0000000000..d7e517168e --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/optional-params.ts @@ -0,0 +1,17 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const omitted = await session.post("Runtime.enable"); + const nullValue = await session.post("Runtime.disable", null as never); + const undefinedValue = await session.post("Runtime.enable", undefined); + console.log( + "empty results:", + Reflect.ownKeys(omitted).length, + Reflect.ownKeys(nullValue).length, + Reflect.ownKeys(undefinedValue).length, + ); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/post/rejection-identity.ts b/test-parity/node-suite/inspector-promises/post/rejection-identity.ts new file mode 100644 index 0000000000..a57ee9937b --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/rejection-identity.ts @@ -0,0 +1,36 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const pending = session.post("Missing.identity"); + let first: unknown; + let repeated: unknown; + try { + await pending; + } catch (error) { + first = error; + } + try { + await pending; + } catch (error) { + repeated = error; + } + let separate: unknown; + try { + await session.post("Missing.identity"); + } catch (error) { + separate = error; + } + const cause = first as { name?: string; code?: string; message?: string }; + console.log( + "identity:", + first === repeated, + first !== separate, + cause.name, + cause.code, + cause.message?.includes("-32601"), + ); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/post/settlement-async.ts b/test-parity/node-suite/inspector-promises/post/settlement-async.ts new file mode 100644 index 0000000000..87a7364379 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/settlement-async.ts @@ -0,0 +1,17 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + let settled = false; + const pending = session.post("Runtime.evaluate", { expression: "42" }); + const observed = pending.then((value) => { + settled = true; + return value; + }); + console.log("same turn:", settled); + const value = await observed; + console.log("after await:", settled, value.result.value); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/post/unknown-command.ts b/test-parity/node-suite/inspector-promises/post/unknown-command.ts new file mode 100644 index 0000000000..c8196597ff --- /dev/null +++ b/test-parity/node-suite/inspector-promises/post/unknown-command.ts @@ -0,0 +1,21 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + try { + await session.post("Perry.missing"); + console.log("unexpected resolution"); + } catch (error) { + const cause = error as { name?: string; code?: string; message?: string }; + console.log( + "unknown:", + cause.name, + cause.code, + cause.message?.includes("-32601"), + cause.message?.includes("'Perry.missing' wasn't found"), + ); + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/protocol/enable-disable.ts b/test-parity/node-suite/inspector-promises/protocol/enable-disable.ts new file mode 100644 index 0000000000..2e27b80d3f --- /dev/null +++ b/test-parity/node-suite/inspector-promises/protocol/enable-disable.ts @@ -0,0 +1,17 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + for (const domain of ["Runtime", "Debugger", "Profiler", "HeapProfiler"]) { + const enabled = await session.post(`${domain}.enable`); + const disabled = await session.post(`${domain}.disable`); + console.log( + domain, + Reflect.ownKeys(enabled).length, + Reflect.ownKeys(disabled).length, + ); + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/protocol/schema-domains.ts b/test-parity/node-suite/inspector-promises/protocol/schema-domains.ts new file mode 100644 index 0000000000..af6bfba58d --- /dev/null +++ b/test-parity/node-suite/inspector-promises/protocol/schema-domains.ts @@ -0,0 +1,23 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const value = await session.post("Schema.getDomains"); + const names = value.domains.map((domain) => domain.name).sort(); + console.log("array:", Array.isArray(value.domains), names.length > 0); + console.log( + "required:", + ["Debugger", "HeapProfiler", "Profiler", "Runtime", "Schema"].every(( + name, + ) => names.includes(name)), + ); + console.log( + "shape:", + value.domains.every((domain) => + typeof domain.name === "string" && typeof domain.version === "string" + ), + ); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/runtime/await-promise.ts b/test-parity/node-suite/inspector-promises/runtime/await-promise.ts new file mode 100644 index 0000000000..7676fb5b19 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/runtime/await-promise.ts @@ -0,0 +1,21 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const pending = session.post("Runtime.evaluate", { + expression: "Promise.resolve(40).then((value) => value + 2)", + awaitPromise: true, + returnByValue: true, + }); + console.log("returned:", pending instanceof Promise); + const value = await pending; + console.log( + "fulfilled:", + value.result.type, + value.result.value, + Object.hasOwn(value, "exceptionDetails"), + ); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/runtime/await-rejection.ts b/test-parity/node-suite/inspector-promises/runtime/await-rejection.ts new file mode 100644 index 0000000000..014408ab5d --- /dev/null +++ b/test-parity/node-suite/inspector-promises/runtime/await-rejection.ts @@ -0,0 +1,21 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const value = await session.post("Runtime.evaluate", { + expression: 'Promise.reject(new RangeError("async marker"))', + awaitPromise: true, + }); + console.log( + "resolved rejection:", + value.result.type, + value.result.subtype, + value.result.className, + value.result.description?.startsWith("RangeError: async marker"), + value.exceptionDetails.text, + value.exceptionDetails.exception?.className, + ); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/runtime/exception-details.ts b/test-parity/node-suite/inspector-promises/runtime/exception-details.ts new file mode 100644 index 0000000000..b43d769aa0 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/runtime/exception-details.ts @@ -0,0 +1,28 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const pending = session.post("Runtime.evaluate", { + expression: 'throw new TypeError("promise marker")', + }); + const value = await pending; + console.log( + "resolved:", + value.result.type, + value.result.subtype, + value.result.className, + value.result.description?.startsWith("TypeError: promise marker"), + ); + console.log( + "exception:", + value.exceptionDetails.text, + typeof value.exceptionDetails.exceptionId, + value.exceptionDetails.exception?.className, + value.exceptionDetails.exception?.description?.startsWith( + "TypeError: promise marker", + ), + ); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/runtime/get-properties-release.ts b/test-parity/node-suite/inspector-promises/runtime/get-properties-release.ts new file mode 100644 index 0000000000..9d86fb5de3 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/runtime/get-properties-release.ts @@ -0,0 +1,36 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const evaluated = await session.post("Runtime.evaluate", { + expression: "({ alpha: 1, beta: 'two' })", + }); + const objectId = evaluated.result.objectId; + const properties = await session.post("Runtime.getProperties", { + objectId, + ownProperties: true, + }); + const own = properties.result + .filter((entry) => entry.enumerable) + .map((entry) => `${entry.name}:${entry.value?.value}`) + .sort(); + console.log("properties:", own.join(",")); + const released = await session.post("Runtime.releaseObject", { objectId }); + console.log("release:", Reflect.ownKeys(released).length); + try { + await session.post("Runtime.getProperties", { objectId }); + console.log("unexpected retained object"); + } catch (error) { + const cause = error as { name?: string; code?: string; message?: string }; + console.log( + "released error:", + cause.name, + cause.code, + cause.message?.includes("-32000"), + cause.message?.includes("Could not find object"), + ); + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/runtime/numeric-specials.ts b/test-parity/node-suite/inspector-promises/runtime/numeric-specials.ts new file mode 100644 index 0000000000..35e800777d --- /dev/null +++ b/test-parity/node-suite/inspector-promises/runtime/numeric-specials.ts @@ -0,0 +1,18 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + for (const expression of ["NaN", "Infinity", "-0", "123n"]) { + const { result } = await session.post("Runtime.evaluate", { expression }); + console.log( + expression, + result.type, + result.unserializableValue ?? "", + result.description ?? "", + Object.hasOwn(result, "value"), + ); + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/runtime/primitives.ts b/test-parity/node-suite/inspector-promises/runtime/primitives.ts new file mode 100644 index 0000000000..431f4e12a7 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/runtime/primitives.ts @@ -0,0 +1,18 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + for (const expression of ["undefined", "null", "true", '"text"', "42"]) { + const { result } = await session.post("Runtime.evaluate", { expression }); + console.log( + expression, + result.type, + result.subtype ?? "", + Object.hasOwn(result, "value") ? String(result.value) : "", + result.description ?? "", + ); + } +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/runtime/return-by-value.ts b/test-parity/node-suite/inspector-promises/runtime/return-by-value.ts new file mode 100644 index 0000000000..a287173f58 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/runtime/return-by-value.ts @@ -0,0 +1,30 @@ +import { Session } from "node:inspector/promises"; + +const session = new Session(); +session.connect(); +try { + const object = await session.post("Runtime.evaluate", { + expression: "({ alpha: 1, nested: { beta: true } })", + returnByValue: true, + }); + const array = await session.post("Runtime.evaluate", { + expression: "[1, 'two', null]", + returnByValue: true, + }); + console.log( + "object:", + object.result.type, + object.result.value.alpha, + object.result.value.nested.beta, + Object.hasOwn(object.result, "objectId"), + ); + console.log( + "array:", + array.result.type, + array.result.subtype, + Array.isArray(array.result.value), + array.result.value.join("|"), + ); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/session/runtime.ts b/test-parity/node-suite/inspector-promises/session/runtime.ts deleted file mode 100644 index be26444566..0000000000 --- a/test-parity/node-suite/inspector-promises/session/runtime.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Session } from "node:inspector/promises"; - -async function reportAsync(label: string, fn: () => Promise | unknown): Promise { - try { - const value = await fn(); - console.log(label, "ok", value === undefined ? "undefined" : typeof value); - } catch (err) { - const error = err as { constructor?: { name?: string }, code?: string, message?: string }; - console.log(label, "err", error.constructor?.name, error.code, String(error.message).split("\n")[0]); - } -} - -const session = new Session(); -console.log( - "surface:", - typeof session.connect, - typeof session.connectToMainThread, - typeof session.disconnect, - typeof session.post, - typeof session.on, - typeof session.once, -); - -await reportAsync("post before connect:", () => session.post("Runtime.evaluate", {})); -await reportAsync("connectToMainThread:", () => session.connectToMainThread()); -await reportAsync("connect:", () => session.connect()); - -const result = await session.post("Runtime.evaluate", { expression: "21 * 2", returnByValue: true }); -console.log( - "eval promise:", - result?.result?.type, - result?.result?.value, - result?.result?.description, -); - -await reportAsync("bad promise:", () => session.post("Nope.nope", {})); - -let genericCount = 0; -let specificCount = 0; -let firstGeneric = ""; -let firstSpecific = ""; -session.on("inspectorNotification", (message: { method?: string }) => { - genericCount++; - firstGeneric ||= message?.method || ""; -}); -session.on("Runtime.consoleAPICalled", (message: { method?: string }) => { - specificCount++; - firstSpecific ||= message?.method || ""; -}); - -const enableResult = await session.post("Runtime.enable", {}); -console.log("enable promise:", Object.keys(enableResult || {}).length); -await session.post("Runtime.evaluate", { expression: "console.log(\"promise-session-event\")" }); -await new Promise((resolve) => setTimeout(resolve, 20)); -console.log("events:", genericCount > 0, specificCount > 0, firstGeneric, firstSpecific); - -await reportAsync("disconnect:", () => session.disconnect()); -await reportAsync("post after disconnect:", () => session.post("Runtime.evaluate", {})); diff --git a/test-parity/node-suite/inspector-promises/surface/constructor.ts b/test-parity/node-suite/inspector-promises/surface/constructor.ts new file mode 100644 index 0000000000..1a0ab2287f --- /dev/null +++ b/test-parity/node-suite/inspector-promises/surface/constructor.ts @@ -0,0 +1,22 @@ +import { Session } from "node:inspector/promises"; + +try { + Session(); + console.log("unexpected call"); +} catch (error) { + const cause = error as { name?: string; message?: string }; + console.log( + "without new:", + cause.name, + cause.message?.includes("cannot be invoked without 'new'"), + ); +} +const session = new Session("ignored" as never); +try { + console.log("extra argument:", session instanceof Session); + session.connect(); + const value = await session.post("Runtime.evaluate", { expression: "1" }); + console.log("usable:", value.result.value); +} finally { + session.disconnect(); +} diff --git a/test-parity/node-suite/inspector-promises/surface/exports.ts b/test-parity/node-suite/inspector-promises/surface/exports.ts new file mode 100644 index 0000000000..2d0f74aec9 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/surface/exports.ts @@ -0,0 +1,26 @@ +import promisesDefault, * as promisesNamespace from "node:inspector/promises"; +import callbackDefault, * as callbackNamespace from "node:inspector"; + +console.log("default:", promisesNamespace.default === promisesDefault); +console.log( + "keys:", + Reflect.ownKeys(promisesDefault).join(",") === + Reflect.ownKeys(callbackDefault).join(","), + Reflect.ownKeys(promisesDefault).join(","), +); +for (const key of Reflect.ownKeys(callbackDefault)) { + console.log(String(key), promisesDefault[key] === callbackDefault[key]); +} +console.log( + "namespace sessions:", + promisesNamespace.Session === promisesDefault.Session, + callbackNamespace.Session === callbackDefault.Session, +); +console.log( + "descriptors:", + Reflect.ownKeys(promisesDefault).every((key) => { + const descriptor = Object.getOwnPropertyDescriptor(promisesDefault, key); + return descriptor?.enumerable && descriptor.writable && + descriptor.configurable; + }), +); diff --git a/test-parity/node-suite/inspector-promises/surface/inherited-receivers.ts b/test-parity/node-suite/inspector-promises/surface/inherited-receivers.ts new file mode 100644 index 0000000000..7560a69862 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/surface/inherited-receivers.ts @@ -0,0 +1,11 @@ +import { Session } from "node:inspector/promises"; + +for (const name of ["connect", "disconnect"] as const) { + try { + Session.prototype[name].call({}); + console.log(name, "unexpected"); + } catch (error) { + const cause = error as { name?: string; message?: string }; + console.log(name, cause.name, cause.message?.includes("#connection")); + } +} diff --git a/test-parity/node-suite/inspector-promises/surface/post-descriptor.ts b/test-parity/node-suite/inspector-promises/surface/post-descriptor.ts new file mode 100644 index 0000000000..764cdcab06 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/surface/post-descriptor.ts @@ -0,0 +1,20 @@ +import { Session } from "node:inspector/promises"; + +const descriptor = Object.getOwnPropertyDescriptor(Session.prototype, "post"); +console.log( + "descriptor:", + descriptor?.enumerable, + descriptor?.writable, + descriptor?.configurable, +); +console.log( + "function:", + Session.prototype.post.name, + Session.prototype.post.length, + Object.prototype.toString.call(Session.prototype.post), +); +console.log( + "own:", + Object.hasOwn(Session.prototype, "post"), + Object.hasOwn(Session.prototype, "connect"), +); diff --git a/test-parity/node-suite/inspector-promises/surface/post-receiver.ts b/test-parity/node-suite/inspector-promises/surface/post-receiver.ts new file mode 100644 index 0000000000..501fe86947 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/surface/post-receiver.ts @@ -0,0 +1,17 @@ +import { Session } from "node:inspector/promises"; + +let returnedPromise = false; +try { + const pending = Session.prototype.post.call({}, "Runtime.enable"); + returnedPromise = pending instanceof Promise; + await pending; + console.log("unexpected resolution"); +} catch (error) { + const cause = error as { name?: string; message?: string }; + console.log( + "receiver:", + returnedPromise, + cause.name, + cause.message?.includes("#connection"), + ); +} diff --git a/test-parity/node-suite/inspector-promises/surface/session-class.ts b/test-parity/node-suite/inspector-promises/surface/session-class.ts new file mode 100644 index 0000000000..fcf8d6d9a6 --- /dev/null +++ b/test-parity/node-suite/inspector-promises/surface/session-class.ts @@ -0,0 +1,25 @@ +import { Session as PromiseSession } from "node:inspector/promises"; +import { Session as CallbackSession } from "node:inspector"; + +const session = new PromiseSession(); +console.log("distinct:", PromiseSession !== CallbackSession); +console.log( + "subclass:", + Object.getPrototypeOf(PromiseSession) === CallbackSession, + Object.getPrototypeOf(PromiseSession.prototype) === CallbackSession.prototype, +); +console.log( + "instances:", + session instanceof PromiseSession, + session instanceof CallbackSession, +); +console.log( + "methods:", + PromiseSession.prototype.connect === CallbackSession.prototype.connect, + PromiseSession.prototype.disconnect === CallbackSession.prototype.disconnect, + PromiseSession.prototype.post === CallbackSession.prototype.post, +); +console.log( + "own prototype:", + Reflect.ownKeys(PromiseSession.prototype).join(","), +); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 922c40b052..58a39a2137 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -5,9 +5,9 @@ "note": "Deterministic modules are floored at full pass. Timing/racy modules (http2, net, stream, diagnostics_channel, fs-promises) carry a small margin below observed pass so ordinary flake does not false-alarm; the guard still catches real regressions, which are large (e.g. dns 6->0, http 19->9). node_suite_run.normalize() scrubs environment-variant tokens (console.time hrtime durations, stack-trace frame lines) symmetrically before the stdout compare, so console is floored at full pass (119) on its deterministic content. http is verified 19/19 in isolation but the full-suite harness flakes to 17 under port contention, so it is floored at 17 (flake margin, not a regression); a real http break is a much larger drop. Floors were refreshed from a clean node-26 run at 2810/2863 (98.1%), then the deterministic child_process floor was measured independently at 43/53 on Node 26.5.0. The overall summary is the sum of the committed per-module floors." }, "overall": { - "pass": 2818, - "total": 2890, - "pct": 97.5 + "pass": 2820, + "total": 2919, + "pct": 96.6 }, "modules": { "assert": { @@ -99,8 +99,8 @@ "total": 3 }, "inspector-promises": { - "pass": 1, - "total": 1 + "pass": 3, + "total": 30 }, "module": { "pass": 28,