Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions packages/client/src/auth-route-ledger-coverage.test.ts
Original file line numberDiff line numberDiff line change
@@ -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<unknown>((o, k) => (o == null ? o : (o as Record<string, unknown>)[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');
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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([]);

Expand Down
8 changes: 6 additions & 2 deletions scripts/check-cross-package-test-inputs.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
//
Expand DownExpand Up@@ -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',
],
},
Expand Down
1 change: 1 addition & 0 deletions turbo.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"
]
},
Expand Down
Loading