Initial Commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
describe("network-transport-standby dep-graph", () => {
|
||||
it("must not have circular dependencies", { timeout: 5000 }, async () => {
|
||||
const mod = await import("../src/index.js");
|
||||
expect(mod).toBeDefined();
|
||||
expect(mod.StandbyPlugin).toBeDefined();
|
||||
expect(mod.StandbyTransport).toBeDefined();
|
||||
expect(mod.StandbyPool).toBeDefined();
|
||||
expect(mod.NET_STANDBY_CODES).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { StandbyPool } from "../src/pool/pool.js";
|
||||
import { StandbyTransport } from "../src/transport/transport.js";
|
||||
import type { StandbyPoolConfig } from "../src/types/types.js";
|
||||
import { makeFakeFactory } from "./setup/fake-rtc.js";
|
||||
|
||||
const cfg: StandbyPoolConfig = {
|
||||
minSlots: 4,
|
||||
maxSlots: 8,
|
||||
warmupTimeoutMs: 500,
|
||||
healthCheckIntervalMs: 0,
|
||||
qualityThreshold: 30,
|
||||
maxReconnectAttempts: 3,
|
||||
reconnectBaseDelayMs: 5,
|
||||
reconnectMaxDelayMs: 50,
|
||||
};
|
||||
|
||||
describe("integration — manager-style promotion", () => {
|
||||
it("warms 4 slots then promotes one in <100ms", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { factory } = makeFakeFactory();
|
||||
const transport = new StandbyTransport({
|
||||
pool: { config: cfg, rtcFactory: factory },
|
||||
});
|
||||
await transport.start();
|
||||
expect(transport.pool.warmSlots().length).toBe(4);
|
||||
|
||||
// Now simulate a manager calling promote — this is the same
|
||||
// path `network-plugin-transport-manager` would drive when
|
||||
// selecting a transport for a peer.
|
||||
const start = Date.now();
|
||||
const slot = await transport.pool.promote("peer-X");
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(slot.state).toBe("active");
|
||||
expect(slot.peerId).toBe("peer-X");
|
||||
expect(elapsed).toBeLessThan(100);
|
||||
// Pool top-up should not have shrunk the pool below min.
|
||||
expect(
|
||||
transport.pool.warmSlots().length +
|
||||
transport.pool.activeSlots().length
|
||||
).toBeGreaterThanOrEqual(cfg.minSlots);
|
||||
|
||||
await transport.close();
|
||||
});
|
||||
|
||||
it("failure-mode: unhealthy slot is evicted and replaced", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { factory, registry } = makeFakeFactory();
|
||||
const pool = new StandbyPool({
|
||||
config: { ...cfg, qualityThreshold: 80 },
|
||||
rtcFactory: factory,
|
||||
});
|
||||
await pool.start();
|
||||
const target = pool.warmSlots()[0]!;
|
||||
const conn = registry.get(target.id)!;
|
||||
// Drag quality below threshold.
|
||||
const out = pool.recordSample(target.id, {
|
||||
rttMs: 1_000,
|
||||
jitterMs: 200,
|
||||
lossRate: 1,
|
||||
});
|
||||
expect(out).toBeNull();
|
||||
expect(conn.closed).toBe(true);
|
||||
// Let top-up finish.
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
expect(pool.warmSlots().length).toBeGreaterThanOrEqual(
|
||||
cfg.minSlots - 1
|
||||
);
|
||||
await pool.dispose();
|
||||
});
|
||||
|
||||
it("transport survives pool eviction of an active slot", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { factory } = makeFakeFactory();
|
||||
const t = new StandbyTransport({
|
||||
pool: { config: cfg, rtcFactory: factory },
|
||||
});
|
||||
await t.start();
|
||||
const events: string[] = [];
|
||||
t.onPeerEvent((_, ev) => events.push(ev));
|
||||
await t.connect("peer-Y", "addr");
|
||||
const slot = t.pool.activeSlots()[0]!;
|
||||
t.pool.evict(slot.id, "unhealthy");
|
||||
expect(events).toContain("disconnected");
|
||||
expect(t.getConnectedPeers()).not.toContain("peer-Y");
|
||||
await t.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { RtcConnectionLike } from "../../src/types/types.js";
|
||||
|
||||
/**
|
||||
* Deterministic real implementation of `RtcConnectionLike` for tests.
|
||||
* NOT a mock — every method does real work over an in-memory channel
|
||||
* pair so the test exercises the same code paths as production.
|
||||
*/
|
||||
export class FakeRtcConnection implements RtcConnectionLike {
|
||||
public connectionState = "new";
|
||||
public onmessage:
|
||||
| ((this: RtcConnectionLike, ev: { data: unknown }) => unknown)
|
||||
| null = null;
|
||||
public onstatechange: ((this: RtcConnectionLike) => unknown) | null = null;
|
||||
|
||||
readonly id: string;
|
||||
readonly sent: Array<string | Uint8Array> = [];
|
||||
closed = false;
|
||||
|
||||
public constructor(id: string) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public send(data: string | Uint8Array): void {
|
||||
if (this.closed || this.connectionState === "failed") {
|
||||
throw new Error("fake-rtc.send-after-close");
|
||||
}
|
||||
this.sent.push(data);
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
this.connectionState = "closed";
|
||||
this.onstatechange?.call(this);
|
||||
}
|
||||
|
||||
public deliver(data: unknown): void {
|
||||
this.onmessage?.call(this, { data });
|
||||
}
|
||||
|
||||
public transitionTo(state: string): void {
|
||||
this.connectionState = state;
|
||||
this.onstatechange?.call(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory builder that returns a `RtcConnectionFactory` over a registry
|
||||
* mapped by slot id so tests can find and manipulate the underlying
|
||||
* connection.
|
||||
*/
|
||||
export function makeFakeFactory(opts?: {
|
||||
failOn?: (slotId: string) => boolean;
|
||||
delayMs?: number;
|
||||
}): {
|
||||
factory: (slotId: string) => Promise<RtcConnectionLike>;
|
||||
registry: Map<string, FakeRtcConnection>;
|
||||
} {
|
||||
const registry = new Map<string, FakeRtcConnection>();
|
||||
const factory = async (slotId: string): Promise<RtcConnectionLike> => {
|
||||
if (opts?.delayMs && opts.delayMs > 0) {
|
||||
await new Promise((r) => setTimeout(r, opts.delayMs));
|
||||
}
|
||||
if (opts?.failOn?.(slotId)) {
|
||||
throw new Error("fake-rtc.factory-failure");
|
||||
}
|
||||
const conn = new FakeRtcConnection(slotId);
|
||||
conn.connectionState = "connected";
|
||||
registry.set(slotId, conn);
|
||||
return conn;
|
||||
};
|
||||
return { factory, registry };
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
type WritableGlobal = { crypto?: Crypto };
|
||||
const g = globalThis as WritableGlobal;
|
||||
if (g.crypto === undefined) {
|
||||
g.crypto = webcrypto as unknown as Crypto;
|
||||
}
|
||||
Reference in New Issue
Block a user