diff --git a/clients/web/src/test/core/auth/oauth-persist-browser-safe.test.ts b/clients/web/src/test/core/auth/oauth-persist-browser-safe.test.ts index b689adaa..3b3dc6cf 100644 --- a/clients/web/src/test/core/auth/oauth-persist-browser-safe.test.ts +++ b/clients/web/src/test/core/auth/oauth-persist-browser-safe.test.ts @@ -4,17 +4,35 @@ import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; /** - * Regression guard for the browser bundle. `core/auth/oauth-persist.ts` is - * imported by browser code (App.tsx -> RemoteOAuthStorage -> oauth-persist), - * so it must stay Node-free. It previously imported `store-io.js`, which pulls - * `node:fs` + `atomically` (-> `stubborn-fs` -> `node:process.getuid`) into the - * browser bundle and blanked the app at runtime. The Node-only file backend - * now lives in `core/auth/node/oauth-persist-file.ts`; the isomorphic module - * must only import the Node-free `store-serialize` helpers. + * Regression guard for the browser bundle. The browser's `RemoteOAuthStorage` + * (App.tsx -> lib/remoteOAuthStorage.ts -> remote/storage-remote.ts) chains + * through a handful of `core/auth` + `core/storage` modules, so ALL of them + * must stay Node-free. `oauth-persist.ts` previously value-imported + * `store-io.js`, which pulls `node:fs` + `atomically` (-> `stubborn-fs` -> + * `node:process.getuid`) into the browser bundle and blanked the app at + * runtime. The Node-only file backend now lives in + * `core/auth/node/oauth-persist-file.ts`. + * + * This guard is transitive: it scans every module reachable from + * `storage-remote.ts` (short of the `node/` boundary), so a forbidden import + * added one hop away — e.g. through `store-serialize.ts` or `store.ts` — is + * caught too, not just a direct import in `oauth-persist.ts`. */ const here = dirname(fileURLToPath(import.meta.url)); -const coreAuth = resolve(here, "../../../../../../core/auth"); +const coreDir = resolve(here, "../../../../../../core"); + +// Browser-reachable modules (value-import graph from storage-remote.ts, +// excluding the intentional `node/` backend which is never bundled). +const BROWSER_REACHABLE = [ + "auth/remote/storage-remote.ts", + "auth/oauth-storage.ts", + "auth/store.ts", + "auth/oauth-persist.ts", + "storage/store-serialize.ts", +]; + +const FORBIDDEN = [/(^|\/)store-io(\.js)?$/, /^node:/, /^atomically$/]; function importSpecifiers(source: string): string[] { const specs: string[] = []; @@ -26,22 +44,23 @@ function importSpecifiers(source: string): string[] { return specs; } -describe("oauth-persist.ts stays browser-safe", () => { - const source = readFileSync(resolve(coreAuth, "oauth-persist.ts"), "utf-8"); - const specs = importSpecifiers(source); +describe("browser-reachable OAuth storage modules stay Node-free", () => { + it.each(BROWSER_REACHABLE)( + "%s imports no Node built-ins, atomically, or store-io", + (relPath) => { + const source = readFileSync(resolve(coreDir, relPath), "utf-8"); + const offenders = importSpecifiers(source).filter((spec) => + FORBIDDEN.some((re) => re.test(spec)), + ); + expect(offenders).toEqual([]); + }, + ); - it("does not import the Node-only store-io module", () => { - expect(specs).not.toContain("../storage/store-io.js"); - }); - - it("does not import node: builtins or atomically", () => { - const forbidden = specs.filter( - (s) => s.startsWith("node:") || s === "atomically", + it("oauth-persist.ts sources its (de)serializers from store-serialize", () => { + const source = readFileSync( + resolve(coreDir, "auth/oauth-persist.ts"), + "utf-8", ); - expect(forbidden).toEqual([]); - }); - - it("gets its (de)serializers from the Node-free store-serialize module", () => { - expect(specs).toContain("../storage/store-serialize.js"); + expect(importSpecifiers(source)).toContain("../storage/store-serialize.js"); }); });