[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
429
electron/plugins/streamRouter.test.cjs
Normal file
429
electron/plugins/streamRouter.test.cjs
Normal file
@@ -0,0 +1,429 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const test = require("node:test");
|
||||
|
||||
const { PluginStreamRouter } = require("./streamRouter.cjs");
|
||||
|
||||
test("incoming stream credit is returned only after the consumer releases a chunk", async () => {
|
||||
const sent = [];
|
||||
const received = [];
|
||||
const router = new PluginStreamRouter({
|
||||
send(message) { sent.push(message); },
|
||||
onIncomingStream({ bind }) {
|
||||
bind({
|
||||
async onChunk(chunk, release) {
|
||||
received.push(chunk.value);
|
||||
assert.equal(sent.length, 0);
|
||||
release();
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
await router.accept({ frame: { streamId: "input", sequence: 0, kind: "open", windowBytes: 1024 } });
|
||||
const contract = await import("@netcatty/plugin-contract");
|
||||
await router.accept({
|
||||
frame: {
|
||||
streamId: "input",
|
||||
sequence: 1,
|
||||
kind: "chunk",
|
||||
data: contract.createJsonStreamChunk({ value: 1 }),
|
||||
},
|
||||
});
|
||||
assert.deepEqual(received, [{ value: 1 }]);
|
||||
assert.deepEqual(sent[0].frame, {
|
||||
streamId: "input",
|
||||
sequence: 0,
|
||||
kind: "windowUpdate",
|
||||
creditBytes: contract.createJsonStreamChunk({ value: 1 }).byteLength,
|
||||
});
|
||||
});
|
||||
|
||||
test("incoming binary streams return their declared byte credit after release", async () => {
|
||||
const sent = [];
|
||||
const received = [];
|
||||
const router = new PluginStreamRouter({
|
||||
send(message) { sent.push(message); },
|
||||
onIncomingStream({ bind }) {
|
||||
bind({
|
||||
onChunk(chunk, release) {
|
||||
received.push([...chunk.bytes]);
|
||||
release();
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const contract = await import("@netcatty/plugin-contract");
|
||||
const bytes = new Uint8Array([1, 2, 3, 4]);
|
||||
|
||||
await router.accept({ frame: { streamId: "binary", sequence: 0, kind: "open", windowBytes: 1024 } });
|
||||
await router.accept({
|
||||
frame: {
|
||||
streamId: "binary",
|
||||
sequence: 1,
|
||||
kind: "chunk",
|
||||
data: contract.createBase64StreamChunk(bytes),
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(received, [[1, 2, 3, 4]]);
|
||||
assert.deepEqual(sent[0].frame, {
|
||||
streamId: "binary",
|
||||
sequence: 0,
|
||||
kind: "windowUpdate",
|
||||
creditBytes: 4,
|
||||
});
|
||||
assert.equal(Number.isSafeInteger(sent[0].frame.creditBytes), true);
|
||||
});
|
||||
|
||||
test("outgoing stream applies byte backpressure and ordered credit updates", async () => {
|
||||
const sent = [];
|
||||
const router = new PluginStreamRouter({ send(message) { sent.push(message); } });
|
||||
const stream = await router.openOutgoing("output", 1024);
|
||||
const source = new Uint8Array(1024);
|
||||
source[0] = 7;
|
||||
await stream.write(source);
|
||||
source[0] = 99;
|
||||
assert.equal(new Uint8Array(sent[1].transfer)[0], 7);
|
||||
let secondResolved = false;
|
||||
const second = stream.write(new Uint8Array(1)).then(() => { secondResolved = true; });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(secondResolved, false);
|
||||
await router.accept({
|
||||
frame: { streamId: "output", sequence: 0, kind: "windowUpdate", creditBytes: 1 },
|
||||
});
|
||||
await second;
|
||||
assert.equal(sent.filter((message) => message.frame.kind === "chunk").length, 2);
|
||||
await assert.rejects(
|
||||
router.accept({ frame: { streamId: "output", sequence: 0, kind: "windowUpdate", creditBytes: 1 } }),
|
||||
/Out-of-order/,
|
||||
);
|
||||
await assert.rejects(
|
||||
router.accept({ frame: { streamId: "output", sequence: 2, kind: "windowUpdate", creditBytes: 1 } }),
|
||||
/Out-of-order/,
|
||||
);
|
||||
});
|
||||
|
||||
test("outgoing JSON stream chunks preserve the exact contract shape", async () => {
|
||||
const sent = [];
|
||||
const router = new PluginStreamRouter({ send(message) { sent.push(message); } });
|
||||
const stream = await router.openOutgoing("json-output", 1024);
|
||||
const value = { nested: ["value", 1] };
|
||||
const contract = await import("@netcatty/plugin-contract");
|
||||
|
||||
await stream.write(value);
|
||||
|
||||
assert.deepEqual(sent[1].frame.data, contract.createJsonStreamChunk(value));
|
||||
assert.equal(Object.hasOwn(sent[1].frame.data, "transfer"), false);
|
||||
assert.equal(Object.hasOwn(sent[1], "transfer"), false);
|
||||
});
|
||||
|
||||
test("ended outgoing streams accept final receive credit before retiring", async () => {
|
||||
const sent = [];
|
||||
const router = new PluginStreamRouter({ send(message) { sent.push(message); } });
|
||||
const stream = await router.openOutgoing("ended-output", 1024);
|
||||
const contract = await import("@netcatty/plugin-contract");
|
||||
const chunk = contract.createJsonStreamChunk({ final: true });
|
||||
await stream.write({ final: true });
|
||||
|
||||
stream.end();
|
||||
assert.equal(sent.at(-1).frame.kind, "end");
|
||||
assert.equal(router.outgoing.has("ended-output"), true);
|
||||
await assert.rejects(stream.write({ late: true }), /closed/);
|
||||
|
||||
await router.accept({
|
||||
frame: {
|
||||
streamId: "ended-output",
|
||||
sequence: 0,
|
||||
kind: "windowUpdate",
|
||||
creditBytes: chunk.byteLength,
|
||||
},
|
||||
});
|
||||
assert.equal(router.outgoing.has("ended-output"), false);
|
||||
});
|
||||
|
||||
test("unhandled incoming streams are cancelled immediately", async () => {
|
||||
const sent = [];
|
||||
const router = new PluginStreamRouter({ send(message) { sent.push(message); } });
|
||||
await router.accept({ frame: { streamId: "unhandled", sequence: 0, kind: "open", windowBytes: 1024 } });
|
||||
assert.equal(sent[0].frame.kind, "cancel");
|
||||
});
|
||||
|
||||
test("a stalled incoming owner is aborted and cannot block later streams", async () => {
|
||||
const sent = [];
|
||||
const signals = [];
|
||||
const closed = [];
|
||||
let calls = 0;
|
||||
const router = new PluginStreamRouter({
|
||||
openTimeoutMs: 10,
|
||||
send(message) { sent.push(message); },
|
||||
onIncomingStream({ bind, signal }) {
|
||||
calls += 1;
|
||||
signals.push(signal);
|
||||
if (calls === 1) {
|
||||
bind({ onChunk() {}, onClose(reason) { closed.push(reason); } });
|
||||
return new Promise(() => {});
|
||||
}
|
||||
bind({ onChunk() {} });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
await router.accept({
|
||||
frame: { streamId: "stalled", sequence: 0, kind: "open", windowBytes: 1024 },
|
||||
});
|
||||
assert.equal(signals[0].aborted, true);
|
||||
assert.equal(closed[0], signals[0].reason);
|
||||
assert.equal(sent[0].frame.kind, "cancel");
|
||||
assert.equal(router.incoming.has("stalled"), false);
|
||||
|
||||
await router.accept({
|
||||
frame: { streamId: "next", sequence: 0, kind: "open", windowBytes: 1024 },
|
||||
});
|
||||
assert.equal(router.incoming.has("next"), true);
|
||||
});
|
||||
|
||||
test("incoming streams require an explicit accept and a chunk handler", async () => {
|
||||
const sent = [];
|
||||
const router = new PluginStreamRouter({
|
||||
send(message) { sent.push(message); },
|
||||
onIncomingStream() {},
|
||||
});
|
||||
await router.accept({ frame: { streamId: "implicit", sequence: 0, kind: "open", windowBytes: 1024 } });
|
||||
assert.equal(sent[0].frame.kind, "cancel");
|
||||
|
||||
const invalid = new PluginStreamRouter({
|
||||
send() {},
|
||||
onIncomingStream({ bind }) {
|
||||
bind({});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
invalid.accept({ frame: { streamId: "invalid", sequence: 0, kind: "open", windowBytes: 1024 } }),
|
||||
/onChunk handler is required/,
|
||||
);
|
||||
assert.equal(invalid.incoming.size, 0);
|
||||
});
|
||||
|
||||
test("owner-selection failures close an already-bound incoming stream", async () => {
|
||||
const sent = [];
|
||||
const closed = [];
|
||||
const router = new PluginStreamRouter({
|
||||
send(message) { sent.push(message); },
|
||||
onIncomingStream({ bind }) {
|
||||
bind({ onChunk() {}, onClose(reason) { closed.push(reason); } });
|
||||
throw new Error("owner setup failed");
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
router.accept({ frame: { streamId: "failed-owner", sequence: 0, kind: "open", windowBytes: 1024 } }),
|
||||
/owner setup failed/,
|
||||
);
|
||||
assert.equal(sent[0].frame.kind, "cancel");
|
||||
assert.match(closed[0].message, /owner setup failed/);
|
||||
assert.equal(router.incoming.size, 0);
|
||||
});
|
||||
|
||||
test("stream envelopes reject extra fields and accessors before state changes", async () => {
|
||||
const router = new PluginStreamRouter({ send() {} });
|
||||
await assert.rejects(router.accept({
|
||||
frame: { streamId: "extra", sequence: 0, kind: "open", windowBytes: 1024 },
|
||||
unexpected: true,
|
||||
}), /unknown properties/);
|
||||
const accessor = {};
|
||||
Object.defineProperty(accessor, "frame", {
|
||||
enumerable: true,
|
||||
get() { throw new Error("accessor invoked"); },
|
||||
});
|
||||
await assert.rejects(router.accept(accessor), /data properties/);
|
||||
});
|
||||
|
||||
test("peer cancellation closes only the matching outgoing stream", async () => {
|
||||
const sent = [];
|
||||
const router = new PluginStreamRouter({ send(message) { sent.push(message); } });
|
||||
const stream = await router.openOutgoing("cancelled", 1024);
|
||||
await router.accept({
|
||||
frame: { streamId: "cancelled", sequence: 1, kind: "cancel" },
|
||||
});
|
||||
await assert.rejects(stream.write(new Uint8Array([1])), /closed/);
|
||||
const other = await router.openOutgoing("other", 1024);
|
||||
await other.write(new Uint8Array([2]));
|
||||
assert.equal(sent.at(-1).frame.streamId, "other");
|
||||
});
|
||||
|
||||
test("transport failure while opening a stream releases its identity", async () => {
|
||||
const router = new PluginStreamRouter({
|
||||
send() { throw new Error("transport closed"); },
|
||||
});
|
||||
await assert.rejects(router.openOutgoing("failed-open", 1024), /transport closed/);
|
||||
assert.equal(router.outgoing.size, 0);
|
||||
});
|
||||
|
||||
test("transport failure while returning credit closes the incoming stream", async () => {
|
||||
const closed = [];
|
||||
let releaseChunk;
|
||||
const router = new PluginStreamRouter({
|
||||
send() { throw new Error("transport closed"); },
|
||||
onIncomingStream({ bind }) {
|
||||
bind({
|
||||
onChunk(_chunk, release) { releaseChunk = release; },
|
||||
onClose(reason) { closed.push(reason); },
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const contract = await import("@netcatty/plugin-contract");
|
||||
await router.accept({ frame: { streamId: "failed-credit", sequence: 0, kind: "open", windowBytes: 1024 } });
|
||||
await router.accept({
|
||||
frame: {
|
||||
streamId: "failed-credit",
|
||||
sequence: 1,
|
||||
kind: "chunk",
|
||||
data: contract.createJsonStreamChunk({ value: 1 }),
|
||||
},
|
||||
});
|
||||
assert.throws(() => releaseChunk(), /transport closed/);
|
||||
assert.equal(router.incoming.size, 0);
|
||||
assert.equal(closed.length, 1);
|
||||
assert.match(closed[0].message, /transport closed/);
|
||||
});
|
||||
|
||||
test("incoming stream completion waits for asynchronous owner cleanup", async () => {
|
||||
const sent = [];
|
||||
let ownerSignal;
|
||||
let releaseCleanup;
|
||||
const cleanup = new Promise((resolve) => { releaseCleanup = resolve; });
|
||||
let cleanupFinished = false;
|
||||
const router = new PluginStreamRouter({
|
||||
send(message) { sent.push(message); },
|
||||
onIncomingStream(stream) {
|
||||
ownerSignal = stream.signal;
|
||||
stream.bind({
|
||||
onChunk() {},
|
||||
async onClose() {
|
||||
await cleanup;
|
||||
cleanupFinished = true;
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
await router.accept({ frame: { streamId: "cleanup", sequence: 0, kind: "open", windowBytes: 1_024 } });
|
||||
let settled = false;
|
||||
const ending = router.accept({ frame: { streamId: "cleanup", sequence: 1, kind: "end" } })
|
||||
.then(() => { settled = true; });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(settled, false);
|
||||
assert.equal(ownerSignal.aborted, true);
|
||||
assert.match(ownerSignal.reason.message, /stream end/);
|
||||
releaseCleanup();
|
||||
await ending;
|
||||
assert.equal(cleanupFinished, true);
|
||||
assert.deepEqual(sent, []);
|
||||
});
|
||||
|
||||
test("router shutdown contains rejected asynchronous owner cleanup", async () => {
|
||||
const router = new PluginStreamRouter({
|
||||
send() {},
|
||||
onIncomingStream(stream) {
|
||||
stream.bind({
|
||||
onChunk() {},
|
||||
async onClose() { throw new Error("cleanup failed"); },
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
await router.accept({ frame: { streamId: "shutdown-cleanup", sequence: 0, kind: "open", windowBytes: 1_024 } });
|
||||
router.close();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
});
|
||||
|
||||
test("transport failure rejects a write and closes its outgoing stream", async () => {
|
||||
let sends = 0;
|
||||
const router = new PluginStreamRouter({
|
||||
send() {
|
||||
sends += 1;
|
||||
if (sends > 1) throw new Error("transport closed");
|
||||
},
|
||||
});
|
||||
const stream = await router.openOutgoing("failed-write", 1024);
|
||||
await assert.rejects(stream.write(new Uint8Array([1])), /transport closed/);
|
||||
assert.equal(router.outgoing.size, 0);
|
||||
await assert.rejects(stream.write(new Uint8Array([2])), /closed/);
|
||||
});
|
||||
|
||||
test("transport failure while cancelling rejects every backpressured write", async () => {
|
||||
let failSends = false;
|
||||
const router = new PluginStreamRouter({
|
||||
send() {
|
||||
if (failSends) throw new Error("transport closed");
|
||||
},
|
||||
});
|
||||
const stream = await router.openOutgoing("failed-cancel", 1024);
|
||||
await stream.write(new Uint8Array(1024));
|
||||
const queued = stream.write(new Uint8Array([1]));
|
||||
failSends = true;
|
||||
assert.throws(() => stream.cancel(), /transport closed/);
|
||||
await assert.rejects(queued, /cancel/);
|
||||
assert.equal(router.outgoing.size, 0);
|
||||
});
|
||||
|
||||
test("closing a router invalidates retained incoming release callbacks", async () => {
|
||||
const sent = [];
|
||||
let releaseChunk;
|
||||
let finishChunk;
|
||||
const chunkBlocked = new Promise((resolve) => { finishChunk = resolve; });
|
||||
const closed = [];
|
||||
let ownerSignal;
|
||||
const router = new PluginStreamRouter({
|
||||
send(message) { sent.push(message); },
|
||||
onIncomingStream({ bind, signal }) {
|
||||
ownerSignal = signal;
|
||||
bind({
|
||||
onChunk(_chunk, release) {
|
||||
releaseChunk = release;
|
||||
return chunkBlocked;
|
||||
},
|
||||
onClose(reason) { closed.push(reason); },
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const contract = await import("@netcatty/plugin-contract");
|
||||
await router.accept({ frame: { streamId: "closing", sequence: 0, kind: "open", windowBytes: 1024 } });
|
||||
const accepting = router.accept({
|
||||
frame: {
|
||||
streamId: "closing",
|
||||
sequence: 1,
|
||||
kind: "chunk",
|
||||
data: contract.createJsonStreamChunk({ value: 1 }),
|
||||
},
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
const closeError = new Error("runtime closed");
|
||||
router.close(closeError);
|
||||
assert.equal(ownerSignal.aborted, true);
|
||||
assert.equal(ownerSignal.reason, closeError);
|
||||
releaseChunk();
|
||||
finishChunk();
|
||||
await accepting;
|
||||
assert.deepEqual(sent, []);
|
||||
assert.deepEqual(closed, [closeError]);
|
||||
});
|
||||
|
||||
test("oversized binary writes fail before consuming stream credit", async () => {
|
||||
const sent = [];
|
||||
const router = new PluginStreamRouter({ send(message) { sent.push(message); } });
|
||||
const contract = await import("@netcatty/plugin-contract");
|
||||
const stream = await router.openOutgoing("oversized", contract.PLUGIN_STREAM_MAX_WINDOW_BYTES);
|
||||
await assert.rejects(
|
||||
stream.write(new Uint8Array(contract.PLUGIN_STREAM_MAX_CHUNK_BYTES + 1)),
|
||||
/chunk exceeds/,
|
||||
);
|
||||
assert.equal(sent.length, 1);
|
||||
assert.equal(router.outgoing.get("oversized").availableBytes, contract.PLUGIN_STREAM_MAX_WINDOW_BYTES);
|
||||
});
|
||||
Reference in New Issue
Block a user