From 2de8c3ef71485f4254a8210bb53eb46ea5a02205 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 15:31:30 +0000 Subject: [PATCH] test(client): resolve every AUTH_ROUTE_LEDGER client name against a real client Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4 --- .../src/auth-route-ledger-coverage.test.ts | 86 +++++++++++++++++++ .../src/auth-route-ledger.conformance.test.ts | 8 ++ scripts/check-cross-package-test-inputs.mjs | 8 +- turbo.json | 1 + 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 packages/client/src/auth-route-ledger-coverage.test.ts diff --git a/packages/client/src/auth-route-ledger-coverage.test.ts b/packages/client/src/auth-route-ledger-coverage.test.ts new file mode 100644 index 0000000000..7f6de1f00d --- /dev/null +++ b/packages/client/src/auth-route-ledger-coverage.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auth route-ledger ↔ client-surface conformance (#11359) — the client half of + * the guard whose server half lives in + * `packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts`. + * Same contract as the three ledger guards next door + * (`route-ledger-coverage.test.ts`, `rest-route-ledger-coverage.test.ts`, + * `service-route-ledger-coverage.test.ts`): every auth ledger entry that names + * a client method must resolve to a real function on an instantiated client. + * + * WHY THIS ARRIVED LAST, AND WHAT WAS UNGUARDED UNTIL IT DID. The server half + * has always carried a test called "every `sdk` entry names its client + * method", and read literally it checks only that the FIELD IS NON-EMPTY: + * `filter((e) => e.disposition === 'sdk' && !e.client)`. `!e.client` is + * falsy-on-absent, so a row spelled `client: 'auth.setInitialPasword'` — or one + * naming a method later renamed or deleted — passed it unchanged. The other + * five ledgers each had a client-side half resolving the name against a real + * client; `AUTH_ROUTE_LEDGER` is the LARGEST of them (56 rows, 54 `sdk`) and + * had none. That is the direction #3528 shipped through, quoted in + * `route-ledger-coverage.test.ts`'s own header: "the ledger equivalent of the + * day would have said `resume → automation.resume` while no such method + * existed." + * + * Every row resolved on the day this landed, so this file was green from its + * first run — which is exactly why it carries the discriminating leg below. A + * guard that passes because the invariant holds and a guard that passes because + * it asserts nothing are indistinguishable from outside, and that + * indistinguishability IS the defect being closed here. + * + * The ledger is imported as a relative SOURCE file deliberately: it is pure + * data (no imports — the module says so in its own header, "it must stay + * import-free"), and a client→plugin-auth package edge for it would be + * backwards — the plugin is where the routes are mounted, so the ledger lives + * there, and each package verifies its own half. `packages/client` already + * reads this exact file the same way from `client-url-conformance.test.ts` and + * `route-ledger-response-schema.test.ts`, so no new package edge and no new + * cross-package input radius is created by this guard. + */ + +import { describe, it, expect } from 'vitest'; +import { ObjectStackClient } from './index'; +import { AUTH_ROUTE_LEDGER } from '../../plugins/plugin-auth/src/auth-route-ledger'; + +describe('auth route ledger ↔ @objectstack/client surface', () => { + const client = new ObjectStackClient({ baseUrl: 'http://localhost:9' }); + + const resolve = (path: string): unknown => + path.split('.').reduce((o, k) => (o == null ? o : (o as Record)[k]), client); + + // Rows carrying a client name, which is NOT the same set as the `sdk` rows: a + // `disabled` row deliberately keeps its `client` (#7735 — `auth.deleteUser` + // still exists on the SDK and still builds that URL, so erasing the name + // would hide the fact the row records). Both are claims about the client + // surface, so both are resolved here — matching `e.client != null` in the + // three sibling guards rather than filtering on disposition. + const named = AUTH_ROUTE_LEDGER.filter((e) => e.client != null); + + it('every auth ledger entry naming a client method resolves to a real function', () => { + const broken = named + .filter((e) => typeof resolve(e.client!) !== 'function') + .map((e) => `${e.route} → client.${e.client}`); + expect( + broken, + `auth ledger entries claiming a client method that does not exist: ${broken.join('; ')}`, + ).toEqual([]); + }); + + it('the resolver discriminates, over a population that is really there (guard the guard)', () => { + // The assertion above is `toEqual([])` over a filtered list, which is the + // shape that passes loudest when it is measuring nothing. Three controls, + // all derived from the ledger rather than hardcoded so they cannot rot into + // prose: the population is the ledger's real size, a name taken FROM it + // resolves, and that same name corrupted does NOT. The third is the + // permanent form of this card's ablation — the typo'd `client:` value the + // server-side non-emptiness check waves through. + expect(named.length).toBeGreaterThan(50); + + const sample = named[0]!.client!; + expect(typeof resolve(sample), `${sample} should resolve on a real client`).toBe('function'); + expect( + typeof resolve(`${sample}__no_such_method`), + 'a name that does not exist must NOT resolve — otherwise the guard above cannot fail', + ).not.toBe('function'); + }); +}); diff --git a/packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts b/packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts index d4c9cb0dff..f9026bcdb3 100644 --- a/packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts +++ b/packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts @@ -150,6 +150,14 @@ describe('auth route ledger ↔ the live better-auth route table', () => { describe('auth route ledger hygiene', () => { it('every `sdk` entry names its client method; every non-sdk entry carries a rationale', () => { + // PRESENCE only — `!e.client` is falsy-on-absent, so this cannot tell a + // real method name from a typo'd or renamed one, and nothing here can: + // resolving the name needs an ObjectStackClient, and plugin-auth has no + // dependency on the client (nor should it — the edge would be backwards). + // That half is the client-side guard added by #11359 — + // `packages/client/src/auth-route-ledger-coverage.test.ts` — which imports + // this ledger as a relative source file and asserts every name resolves to + // a real function. Read the two together before trusting this test's title. const sdkWithout = AUTH_ROUTE_LEDGER.filter((e) => e.disposition === 'sdk' && !e.client).map((e) => e.route); expect(sdkWithout, 'sdk-disposition entries missing a client method name').toEqual([]); diff --git a/scripts/check-cross-package-test-inputs.mjs b/scripts/check-cross-package-test-inputs.mjs index 9b230bcdce..c3e5e16b74 100644 --- a/scripts/check-cross-package-test-inputs.mjs +++ b/scripts/check-cross-package-test-inputs.mjs @@ -431,14 +431,17 @@ export const CROSS_PACKAGE_TEST_INPUTS = { '@objectstack/client': { // The first entry this gate DERIVED from import specifiers rather than from // a path-shaped read (#10452), and the reason that half was worth building: - // five tests here import six sibling packages' route ledgers directly by + // six tests here import six sibling packages' route ledgers directly by // relative specifier, and nothing had ever declared any of them. // src/client-url-conformance.test.ts and src/route-ledger-response-schema.test.ts // import runtime, rest, service-storage, service-i18n and plugin-auth; // src/route-ledger-coverage.test.ts imports runtime; // src/rest-route-ledger-coverage.test.ts imports rest; // src/service-route-ledger-coverage.test.ts imports the three services, - // service-datasource among them. + // service-datasource among them; + // src/auth-route-ledger-coverage.test.ts imports plugin-auth (#11359) — + // the sixth ledger's client half, added last and reading a file the + // five globs below already carried, so it widened no radius. // Each asserts this client's URL builders still agree with the ledger the // server side publishes, so a ledger edit changes the verdict by design. // @@ -475,6 +478,7 @@ export const CROSS_PACKAGE_TEST_INPUTS = { 'packages/services/service-storage/src/storage-route-ledger.conformance.test.ts', 'packages/services/service-i18n/src/i18n-route-ledger.conformance.test.ts', 'packages/services/service-datasource/src/datasource-route-ledger.conformance.test.ts', + 'packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts', 'scripts/check-route-envelope.mjs', ], }, diff --git a/turbo.json b/turbo.json index 909fb72877..1bc881050c 100644 --- a/turbo.json +++ b/turbo.json @@ -100,6 +100,7 @@ "$TURBO_ROOT$/packages/services/service-storage/src/storage-route-ledger.conformance.test.ts", "$TURBO_ROOT$/packages/services/service-i18n/src/i18n-route-ledger.conformance.test.ts", "$TURBO_ROOT$/packages/services/service-datasource/src/datasource-route-ledger.conformance.test.ts", + "$TURBO_ROOT$/packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts", "$TURBO_ROOT$/scripts/check-route-envelope.mjs" ] },