Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions test-parity/node-suite/inspector-promises/EVIDENCE.md
Original file line number Diff line number Diff line change
@@ -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.
111 changes: 111 additions & 0 deletions test-parity/node-suite/inspector-promises/README.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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<unknown> | 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,
);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
15 changes: 15 additions & 0 deletions test-parity/node-suite/inspector-promises/lifecycle/reconnect.ts
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Session } from "node:inspector/promises";

const session = new Session();
session.connect();
try {
const cases: Array<[string, () => Promise<unknown>]> = [
["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<unknown> | 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,
);
}
Comment thread
proggeramlug marked this conversation as resolved.
}
} finally {
session.disconnect();
}
Loading
Loading