Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
// mirrors chunk A — see ../../src/transports/transport.types.ts header for spec link.
import { readdir, readFile, stat } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "@suaveplan/testing/runner";
const HERE = dirname(fileURLToPath(import.meta.url));
const TRANSPORTS = resolve(HERE, "..", "..", "src", "transports");
async function walk(dir: string): Promise<string[]> {
const out: string[] = [];
const entries = await readdir(dir);
for (const entry of entries) {
const full = join(dir, entry);
const st = await stat(full);
if (st.isDirectory()) {
out.push(...(await walk(full)));
} else if (full.endsWith(".ts") && !full.endsWith(".test.ts")) {
out.push(full);
}
}
return out;
}
describe("webrtc transport bundle purity", () => {
it("only the trystero adapter source references trystero", {
timeout: 5000,
}, async () => {
const files = await walk(TRANSPORTS);
expect(files.length).toBeGreaterThan(0);
const offenders: string[] = [];
for (const file of files) {
const text = await readFile(file, "utf8");
if (!text.includes("network-discovery-trystero")) continue;
if (file.includes(`${"trystero".toString()}/`)) continue;
offenders.push(file);
}
expect(offenders).toEqual([]);
});
it("webrtc and websocket adapters do not import trystero", {
timeout: 5000,
}, async () => {
const targets = [
join(TRANSPORTS, "webrtc", "webrtc-transport.ts"),
join(TRANSPORTS, "webrtc", "webrtc-transport.types.ts"),
join(TRANSPORTS, "webrtc", "index.ts"),
join(TRANSPORTS, "websocket", "websocket-transport.ts"),
join(TRANSPORTS, "websocket", "websocket-transport.types.ts"),
join(TRANSPORTS, "websocket", "index.ts"),
join(TRANSPORTS, "transport.types.ts"),
];
for (const file of targets) {
const text = await readFile(file, "utf8");
expect(text.includes("network-discovery-trystero")).toBe(false);
expect(text.includes('from "trystero"')).toBe(false);
}
});
it("trystero adapter uses a lazy loader, never a static import", {
timeout: 5000,
}, async () => {
const text = await readFile(
join(TRANSPORTS, "trystero", "trystero-transport.ts"),
"utf8"
);
const staticImport =
/^\s*import\s+[^;]*from\s+["']@suaveplan\/network-discovery-trystero["']/m;
expect(staticImport.test(text)).toBe(false);
expect(text.includes("config.loader")).toBe(true);
});
});

View File

@@ -0,0 +1,170 @@
import { expect, test } from "@playwright/test";
/**
* Transport-share two-browser sweep.
*
* Drives the package's transport-share workflow across two Playwright
* browser contexts wired together by a local signalling-channel fixture
* served by the harness page. The fixture is a real `BroadcastChannel`
* (cross-context) backed implementation — not a mock — so the wire
* envelopes round-trip through the same machinery that production would
* use across WebRTC data channels.
*
* The harness page at `/transport-share-harness` MUST expose:
* - `window.__share`: an object with
* - `becomeDriver(receiverId)`
* - `becomeReceiver(driverId)`
* - `driverPlay(time, offset)`
* - `driverSeek(seconds)`
* - `getRole()`
* - `getLastEnvelope()`
* - `disconnectDriver()`
* - DOM elements with `data-testid="role"` (text content === current role)
* and `data-testid="indicator"` (present iff `<SharedTransportIndicator>`
* is rendered).
*
* This spec is intentionally executable under the e2e turbo pipeline only;
* the harness binary is built per-PR by the e2e job (it lives in the
* monorepo's `apps/react-audio-stream-e2e` workspace).
*/
test.describe("transport-share — two-browser e2e", () => {
test("play on driver advances every receiver's transport", async ({
browser,
}) => {
const driverCtx = await browser.newContext();
const receiverCtx = await browser.newContext();
const driver = await driverCtx.newPage();
const receiver = await receiverCtx.newPage();
await driver.goto("/transport-share-harness?role=driver");
await receiver.goto("/transport-share-harness?role=receiver");
await receiver.evaluate(() => {
(
window as unknown as {
__share: { becomeReceiver(id: string): void };
}
).__share.becomeReceiver("driver");
});
await driver.evaluate(() => {
(
window as unknown as {
__share: { becomeDriver(id: string): void };
}
).__share.becomeDriver("receiver");
});
await driver.evaluate(() => {
(
window as unknown as {
__share: {
driverPlay(t: number, o: number): void;
};
}
).__share.driverPlay(0.5, 0);
});
await expect
.poll(
async () =>
await receiver.evaluate(() =>
(
window as unknown as {
__share: { getLastEnvelope(): unknown };
}
).__share.getLastEnvelope()
),
{ timeout: 5_000 }
)
.toEqual({ type: "play", time: 0.5, offset: 0 });
// The receiver MUST render the spec-mandated indicator.
await expect(receiver.locator('[data-testid="indicator"]')).toHaveText(
"Shared transport active"
);
await driverCtx.close();
await receiverCtx.close();
});
test("driver disconnect returns receiver to local", async ({ browser }) => {
const driverCtx = await browser.newContext();
const receiverCtx = await browser.newContext();
const driver = await driverCtx.newPage();
const receiver = await receiverCtx.newPage();
await driver.goto("/transport-share-harness?role=driver");
await receiver.goto("/transport-share-harness?role=receiver");
await receiver.evaluate(() => {
(
window as unknown as {
__share: { becomeReceiver(id: string): void };
}
).__share.becomeReceiver("driver");
});
await driver.evaluate(() => {
(
window as unknown as {
__share: { becomeDriver(id: string): void };
}
).__share.becomeDriver("receiver");
});
await expect(receiver.locator('[data-testid="role"]')).toHaveText(
"receiving"
);
await driver.evaluate(() => {
(
window as unknown as {
__share: { disconnectDriver(): void };
}
).__share.disconnectDriver();
});
await expect(receiver.locator('[data-testid="role"]')).toHaveText(
"local",
{ timeout: 5_000 }
);
await expect(receiver.locator('[data-testid="indicator"]')).toHaveCount(
0
);
await driverCtx.close();
await receiverCtx.close();
});
test("receiver controls no-op without throwing", async ({ browser }) => {
const receiverCtx = await browser.newContext();
const receiver = await receiverCtx.newPage();
await receiver.goto("/transport-share-harness?role=receiver");
await receiver.evaluate(() => {
(
window as unknown as {
__share: { becomeReceiver(id: string): void };
}
).__share.becomeReceiver("driver");
});
// notifySeek while receiving is the canonical receiver-controls-no-op
// path. It must complete without throwing AND must not appear on
// the wire (the local sent log stays empty).
const sent = await receiver.evaluate(() => {
(
window as unknown as {
__share: { driverSeek(s: number): unknown[] };
}
).__share.driverSeek(42);
return (
window as unknown as {
__share: { sent: unknown[] };
}
).__share.sent;
});
expect(sent).toEqual([]);
await receiverCtx.close();
});
});

View File

@@ -0,0 +1,79 @@
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const PKG_ROOT = resolve(__dirname, "..");
const DIST = join(PKG_ROOT, "dist");
/**
* Chunk-A runtime dependency allowlist. Chunks B/C/D may extend this
* (`@suaveplan/network-transport-websockets`, `@suaveplan/retry`, etc.);
* the parent spec's `tests/exports.test.ts` enforces the full union
* once every chunk has landed.
*/
const ALLOWED_RUNTIME_DEPS = new Set([
"@suaveplan/audio-fx-plugin-base",
"@suaveplan/audio-transport",
"@suaveplan/error",
"@suaveplan/events",
"@suaveplan/fsm",
"@suaveplan/network-transport-websockets",
"@suaveplan/opentelemetry",
"@suaveplan/react-audio",
"@suaveplan/retry",
"@suaveplan/types",
"@opentelemetry/api",
"tslib",
"zod",
"zustand",
]);
function walk(dir: string, out: string[] = []): string[] {
if (!existsSync(dir)) return out;
for (const name of readdirSync(dir)) {
const p = join(dir, name);
if (statSync(p).isDirectory()) walk(p, out);
else if (/\.(js|mjs|cjs)$/.test(name)) out.push(p);
}
return out;
}
describe("react-audio-stream purity gates", () => {
it("runtime dependency set matches the allowlist", {
timeout: 5000,
}, () => {
const pkg = JSON.parse(
readFileSync(join(PKG_ROOT, "package.json"), "utf8")
) as { dependencies?: Record<string, string> };
const deps = Object.keys(pkg.dependencies ?? {});
for (const dep of deps) {
expect(ALLOWED_RUNTIME_DEPS.has(dep)).toBe(true);
}
for (const allowed of ALLOWED_RUNTIME_DEPS) {
expect(deps.includes(allowed)).toBe(true);
}
});
it("dist references no audio context", { timeout: 30000 }, () => {
if (!existsSync(DIST)) {
// Skip pre-build — the package's own test stage runs before
// `vite build`; the CI pipeline reruns this after `turbo
// build` so the negative gate is enforced there.
return;
}
const files = walk(DIST);
const forbidden = [
/\bnew\s+AudioContext\b/,
/\bnew\s+OfflineAudioContext\b/,
/\binstanceof\s+AudioNode\b/,
/\bAudioWorkletNode\b/,
/\bregisterProcessor\b/,
];
for (const file of files) {
const text = readFileSync(file, "utf8");
for (const rx of forbidden) {
expect(rx.test(text)).toBe(false);
}
}
});
});