Fix hygiene: connect() rejection tests no longer leak a resolved transport
Found running the M1 mutation (bypasses the certificate-accept check) on
Yggdrasil under cgroup-limited CPU: three tests correctly failed their
assertion (`assert.rejects(connector.connect(...))` / `manager.connect(...)`
observed a resolve instead of a reject), but realtime-quic-consumer.test.js
then hung instead of exiting — "Promise resolution is still pending but the
event loop has already resolved". Each of those assertions discarded the
connect() call's return value on the assumption it would always reject, so
the transport that unexpectedly connected under the mutant was never
captured, never disposed: a live native connection, an active
onConnectionEvent registration and an outstanding runtime reference kept
the process alive after node:test had already recorded the failures. This
is the exact scenario the batch names by history ("Soul saw M1 and M3 fail
their assertion and then hang the file").
Added `assertRejectsAndDisposes()`, a drop-in replacement for
`assert.rejects()` that disposes whatever the promise resolves to before
failing the assertion, and used it at all eight call sites in this file
that assert a connect() call rejects (two direct connector.connect() calls,
six through the session manager). Verified on Yggdrasil: M1 re-run now
fails the same three assertions and the file exits on its own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
e943159a47
commit
d9a406f9ff
1 changed files with 34 additions and 8 deletions
|
|
@ -108,6 +108,32 @@ class RawQuicListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `assert.rejects`, but safe against a mutant or a real regression that
|
||||
* makes the promise resolve instead: a transport that unexpectedly connects
|
||||
* is disposed before the assertion fails, rather than left live with an open
|
||||
* native connection and an outstanding runtime reference. A bare
|
||||
* `assert.rejects(connector.connect(...))` hung the whole file exactly this
|
||||
* way under M1 (the certificate-accept mutation): three tests failed their
|
||||
* assertion correctly, but each left a connected, undisposed transport
|
||||
* behind, and Node's test runner refuses to exit while any promise or
|
||||
* native handle is still outstanding.
|
||||
*/
|
||||
async function assertRejectsAndDisposes(
|
||||
promise: Promise<{ dispose(): void }>,
|
||||
match?: RegExp,
|
||||
): Promise<void> {
|
||||
let resolved: { dispose(): void } | undefined;
|
||||
try {
|
||||
resolved = await promise;
|
||||
} catch (error) {
|
||||
if (match) assert.match(error instanceof Error ? error.message : String(error), match);
|
||||
return;
|
||||
}
|
||||
resolved.dispose();
|
||||
assert.fail("expected the promise to reject, but it resolved (the resolved value has been disposed)");
|
||||
}
|
||||
|
||||
function testFrame(overrides: Partial<CultMeshRealtimeFrame> = {}): CultMeshRealtimeFrame {
|
||||
return {
|
||||
channelId: "aetheria.entities",
|
||||
|
|
@ -162,7 +188,7 @@ test("connect refuses a wrong advertised pin", async (t) => {
|
|||
generation: "gen-1",
|
||||
};
|
||||
const connector = new CultMeshQuicRealtimeConnector();
|
||||
await assert.rejects(connector.connect(candidate, target), /certificate|rejected/i);
|
||||
await assertRejectsAndDisposes(connector.connect(candidate, target), /certificate|rejected/i);
|
||||
} finally {
|
||||
await listener.close();
|
||||
}
|
||||
|
|
@ -180,7 +206,7 @@ test("connect refuses a missing certificate pin when no validator is supplied",
|
|||
generation: "gen-1",
|
||||
};
|
||||
const connector = new CultMeshQuicRealtimeConnector();
|
||||
await assert.rejects(connector.connect(candidate, target), /certificate|rejected/i);
|
||||
await assertRejectsAndDisposes(connector.connect(candidate, target), /certificate|rejected/i);
|
||||
} finally {
|
||||
await listener.close();
|
||||
}
|
||||
|
|
@ -373,7 +399,7 @@ test("trust negative (a): an expired certificate is refused", async () => {
|
|||
trust,
|
||||
connectors: [unreachableConnector],
|
||||
});
|
||||
await assert.rejects(manager.connect(target), /valid|expired/i);
|
||||
await assertRejectsAndDisposes(manager.connect(target), /valid|expired/i);
|
||||
});
|
||||
|
||||
test("trust negative (b): an unsigned remote route is refused", async () => {
|
||||
|
|
@ -391,7 +417,7 @@ test("trust negative (b): an unsigned remote route is refused", async () => {
|
|||
trust,
|
||||
connectors: [unreachableConnector],
|
||||
});
|
||||
await assert.rejects(manager.connect(target), /Odin-signed/i);
|
||||
await assertRejectsAndDisposes(manager.connect(target), /Odin-signed/i);
|
||||
});
|
||||
|
||||
test("trust negative (c): a route signed by an untrusted Odin root is refused", async () => {
|
||||
|
|
@ -412,7 +438,7 @@ test("trust negative (c): a route signed by an untrusted Odin root is refused",
|
|||
trust,
|
||||
connectors: [unreachableConnector],
|
||||
});
|
||||
await assert.rejects(manager.connect(target), /not trusted/i);
|
||||
await assertRejectsAndDisposes(manager.connect(target), /not trusted/i);
|
||||
});
|
||||
|
||||
test("trust negative (d): a route that verifies but pins the wrong certificate is refused at the transport", async (t) => {
|
||||
|
|
@ -435,7 +461,7 @@ test("trust negative (d): a route that verifies but pins the wrong certificate i
|
|||
trust,
|
||||
connectors: [new CultMeshQuicRealtimeConnector()],
|
||||
});
|
||||
await assert.rejects(manager.connect(target), /certificate|rejected/i);
|
||||
await assertRejectsAndDisposes(manager.connect(target), /certificate|rejected/i);
|
||||
} finally {
|
||||
await listener.close();
|
||||
}
|
||||
|
|
@ -482,7 +508,7 @@ test("trust negative (e): a transport that fails isVerifiedFor is disposed and r
|
|||
connectors: [staleConnector],
|
||||
});
|
||||
|
||||
await assert.rejects(manager.connect(target), /did not prove|No realtime state path/i);
|
||||
await assertRejectsAndDisposes(manager.connect(target), /did not prove|No realtime state path/i);
|
||||
assert.equal(disposed, true, "the session manager must dispose a transport that fails isVerifiedFor");
|
||||
});
|
||||
|
||||
|
|
@ -558,7 +584,7 @@ test("trust negative (e2): a real transport whose generation no longer matches t
|
|||
connectors: [replayConnector],
|
||||
});
|
||||
|
||||
await assert.rejects(manager.connect(target), /did not prove|No realtime state path/i);
|
||||
await assertRejectsAndDisposes(manager.connect(target), /did not prove|No realtime state path/i);
|
||||
assert.ok(connectedTransport, "a real transport must have connected");
|
||||
// Real cleanup ran: receiveFrame on the disposed transport rejects.
|
||||
await assert.rejects(connectedTransport!.receiveFrame(), /disposed/i);
|
||||
|
|
|
|||
Loading…
Reference in a new issue