Files
WRNexusJS/packages/router/test/services-discovery.test.ts
ClintchizandClaude Opus 5 ce68803471 fix(rpc): close the service-collision fail-open and the fix-wave gaps
Critical:
- router: fail loudly (WRN-SERVICE-COLLISION) when two app/services files
  scan to the same service name, instead of silently letting directory-walk
  order pick a winner.

Important:
- server.ts: wrap a throwing input schema so its raw message cannot escape
  invoke(); returns RPC_INVALID and logs server-side instead.
- client.ts: race timeoutMs against transport.call so a stalled transport
  cannot hang the caller; rejects with a ServiceError(RPC_TRANSPORT).
- client.ts: the proxy returns undefined for undeclared properties (incl.
  then/catch/finally) instead of a function that throws, closing the
  await-client thenable trap.
- gateway.ts / rpc-dispatch.ts: import RPC_PATH_PREFIX / RPC_INTERNAL_HEADER
  from @wrnexus/rpc instead of hardcoding local copies.
- gateway.test.ts: cover the RPC-prefix edge block and internal-header
  stripping across casing variants.
- http.test.ts / client.test.ts: cover anonymous-call header omission, the
  internal marker, the retryable-status sweep, network/malformed/HTML
  failures, AbortSignal propagation, the timeout path, and timer cleanup.

Minor:
- transport.ts: Object.hasOwn for handler lookup; note the entry-only abort
  check.
- client.ts: wrap a missing/invalid WRNEXUS_RPC_SECRET as a ServiceError
  (RPC_IDENTITY) instead of a bare Error.
- rpc/package.json: drop the unused @wrnexus/authz dependency.
- server.ts: implement() now throws at construction time if a declared
  procedure has no own handler.

Verified: reverting the service-collision check and the client timeout race
each make their new test fail, then restore green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:10:20 +05:30

75 lines
2.7 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { buildRouter } from "../src/index.ts";
const root = join(import.meta.dir, ".tmp-services");
mkdirSync(root, { recursive: true });
function makeApp(): string {
const base = mkdtempSync(join(root, "app-"));
mkdirSync(join(base, "app", "pages"), { recursive: true });
return join(base, "app");
}
describe("service discovery", () => {
test("discovers source services and skips generated files", () => {
const appDir = makeApp();
const services = join(appDir, "services");
mkdirSync(services, { recursive: true });
writeFileSync(join(services, "billing.ts"), "export default {};");
writeFileSync(join(services, "types.gen.ts"), "export type T = string;");
expect(buildRouter(appDir).services.map((service) => service.name)).toEqual(["billing"]);
});
test("throws naming both files when two services collide on name", () => {
const appDir = makeApp();
const services = join(appDir, "services");
const nested = join(services, "legacy");
mkdirSync(nested, { recursive: true });
const top = join(services, "billing.ts");
const shadow = join(nested, "billing.ts");
writeFileSync(top, "export default {};");
writeFileSync(shadow, "export default {};");
expect(() => buildRouter(appDir)).toThrow(/WRN-SERVICE-COLLISION/);
try {
buildRouter(appDir);
} catch (error) {
const message = (error as Error).message;
expect(message).toContain(top);
expect(message).toContain(shadow);
}
});
test("unsafe service names are skipped with a warning", () => {
const appDir = makeApp();
const services = join(appDir, "services");
mkdirSync(services, { recursive: true });
writeFileSync(join(services, "bad name.ts"), "export default {};");
const originalWarn = console.warn;
let warned = false;
console.warn = (...args: unknown[]) => {
if (String(args[0]).includes("skipping service with unsafe name")) warned = true;
};
try {
expect(buildRouter(appDir).services).toEqual([]);
expect(warned).toBe(true);
} finally {
console.warn = originalWarn;
}
});
test("a missing services directory yields []", () => {
const appDir = makeApp();
expect(buildRouter(appDir).services).toEqual([]);
});
test("discovers .js service files", () => {
const appDir = makeApp();
const services = join(appDir, "services");
mkdirSync(services, { recursive: true });
writeFileSync(join(services, "reports.js"), "export default {};");
expect(buildRouter(appDir).services.map((service) => service.name)).toEqual(["reports"]);
});
});