Skip to content

Commit 4d8b44e

Browse files
committed
test(dev): zero-warning-boot regression guards + registry log-level env seam (#3420)
Follow-ups so the startup log stays at zero warnings and the debug output is discoverable: 1. Regression guards for all three noise sources: - password: examples assert every generic (non-better-auth) `password` field affirms `ackPlaintextMasking` (showcase no-startup-warnings.test.ts; crm smoke.test.ts — future-proofs an example with none today). - better-auth: auth-manager.mcp-oauth.test.ts asserts oauthProvider is wired with `silenceWarnings.oauthAuthServerConfig: true`. - registry: registry-log-level.test.ts asserts the re-register / package- overwrite lines are silent at the default `info` level and only emit via console.debug at `debug` (never console.warn). 2. Other examples: verified only showcase declared a generic password field; the better-auth and registry fixes are framework-level so crm/todo inherit them. crm now carries the same password guard. (todo ships no password fields and uses the custom `objectstack test` runner, so no vitest guard is added there.) 3. better-auth double-print root cause: the notice fires in oauth-provider's `init(ctx)`, run once per betterAuth() construction; auth is (re)built more than once at boot (initial lazy build, then a rebuild after boot-time auth settings apply — applyConfigPatch nulls the cached instance). silenceWarnings gates the emitter itself, so it is covered on every build path. Corrected the inaccurate inline comment accordingly. 4. Registry debug discoverability: add a `logLevel` option to SchemaRegistryOptions resolved from `OS_REGISTRY_LOG` (unknown → info), so a developer can surface the debug-gated housekeeping with OS_REGISTRY_LOG=debug. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TXHXXHnquzVUjeQYPs9bmy
1 parent 69eea6c commit 4d8b44e

6 files changed

Lines changed: 177 additions & 5 deletions

File tree

examples/app-crm/test/smoke.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,23 @@ describe('app-crm minimal metadata bundle', () => {
9595
expect(rules.some((r) => r.type === 'owner')).toBe(true);
9696
});
9797

98+
// #3420 — official examples must boot warning-free. A generic (non-better-auth)
99+
// `password` field trips the ADR-0100 author-time warning unless it affirms
100+
// intent with `ackPlaintextMasking: true`. crm ships none today; this guard
101+
// fails loudly if one is ever added without the acknowledgment.
102+
it('has no un-acknowledged generic password fields (#3420)', () => {
103+
const offenders: string[] = [];
104+
for (const obj of (stack.objects ?? []) as any[]) {
105+
if (obj?.managedBy === 'better-auth') continue;
106+
for (const [fieldName, def] of Object.entries((obj?.fields ?? {}) as Record<string, any>)) {
107+
if (def?.type === 'password' && def?.ackPlaintextMasking !== true) {
108+
offenders.push(`${obj.name}.${fieldName}`);
109+
}
110+
}
111+
}
112+
expect(offenders, `un-acknowledged generic password field(s): ${offenders.join(', ')}`).toEqual([]);
113+
});
114+
98115
});
99116

100117
describe('Pipeline dashboard', () => {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import stack from '../objectstack.config.js';
5+
6+
/**
7+
* #3420 — the official examples must boot with ZERO warnings so users don't get
8+
* trained to ignore them. A generic (non-`better-auth`) `password` field trips a
9+
* non-fatal `ObjectSchema.create()` warning at author time (ADR-0100: plaintext
10+
* at rest, masked on read); the documented way to affirm intent is
11+
* `ackPlaintextMasking: true`. This guard fails if a new or edited object
12+
* reintroduces an un-acknowledged generic password field — keeping the showcase
13+
* boot log clean without silencing the diagnostic for real authors.
14+
*
15+
* The other two boot-noise sources from #3420 are framework-level and guarded in
16+
* their own packages: the better-auth `oauthAuthServerConfig` false positive
17+
* (@objectstack/plugin-auth auth-manager.mcp-oauth.test.ts) and the registry
18+
* re-register lines (@objectstack/objectql registry-log-level.test.ts).
19+
*/
20+
describe('showcase boots without ADR-0100 password warnings (#3420)', () => {
21+
it('every generic password field affirms ackPlaintextMasking', () => {
22+
const offenders: string[] = [];
23+
for (const obj of (stack.objects ?? []) as any[]) {
24+
if (obj?.managedBy === 'better-auth') continue;
25+
for (const [fieldName, def] of Object.entries((obj?.fields ?? {}) as Record<string, any>)) {
26+
if (def?.type === 'password' && def?.ackPlaintextMasking !== true) {
27+
offenders.push(`${obj.name}.${fieldName}`);
28+
}
29+
}
30+
}
31+
expect(offenders, `un-acknowledged generic password field(s): ${offenders.join(', ')}`).toEqual([]);
32+
});
33+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
4+
import { SchemaRegistry, REGISTRY_LOG_LEVELS } from './registry';
5+
6+
/**
7+
* #3420 — the registry's expected-but-noisy housekeeping (re-registering an
8+
* owned object, overwriting a package manifest on a rebuild / HMR / multi-project
9+
* seed-replay) must NOT reach a stock `os dev` boot log. It used to be emitted
10+
* via `console.warn` (always on) and looked like an error, though it is a normal
11+
* path. It is now emitted at `debug`, so it stays out of the default `info` level
12+
* but `OS_REGISTRY_LOG=debug` (or `{ logLevel: 'debug' }`) makes it discoverable.
13+
*
14+
* This is the regression guard: if either line ever regresses back to `warn`
15+
* (or to the always-on `log`/`console.log`), the "silent at info" assertions
16+
* fail — keeping the official examples' boot log clean.
17+
*/
18+
describe('SchemaRegistry log-level gating (#3420)', () => {
19+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
20+
const debug = vi.spyOn(console, 'debug').mockImplementation(() => {});
21+
beforeEach(() => { warn.mockClear(); debug.mockClear(); });
22+
afterEach(() => { delete process.env.OS_REGISTRY_LOG; });
23+
24+
const reRegisterSameOwner = (r: SchemaRegistry) => {
25+
r.registerObject({ name: 'sys_thing', fields: {} } as any, 'com.acme.app', 'sys', 'own');
26+
r.registerObject({ name: 'sys_thing', fields: {} } as any, 'com.acme.app', 'sys', 'own');
27+
};
28+
29+
it('at the default (info) level, re-registering an owned object is silent — no warn, no debug', () => {
30+
const r = new SchemaRegistry({ multiTenant: false });
31+
reRegisterSameOwner(r);
32+
expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('Re-registering owned object'));
33+
expect(debug).not.toHaveBeenCalledWith(expect.stringContaining('Re-registering owned object'));
34+
});
35+
36+
it('at debug level, the re-register line is emitted via console.debug (never console.warn)', () => {
37+
const r = new SchemaRegistry({ multiTenant: false, logLevel: 'debug' });
38+
reRegisterSameOwner(r);
39+
expect(debug).toHaveBeenCalledWith(expect.stringContaining('Re-registering owned object: sys_thing'));
40+
expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('Re-registering owned object'));
41+
});
42+
43+
it('overwriting a package manifest is debug-gated the same way', () => {
44+
const manifest = { id: 'com.test', name: 'Test', namespace: 'test', version: '1.0.0' } as any;
45+
46+
const info = new SchemaRegistry({ multiTenant: false });
47+
info.installPackage(manifest);
48+
info.installPackage(manifest); // same-package reload → "Overwriting package"
49+
expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('Overwriting package'));
50+
expect(debug).not.toHaveBeenCalledWith(expect.stringContaining('Overwriting package'));
51+
52+
debug.mockClear();
53+
const dbg = new SchemaRegistry({ multiTenant: false, logLevel: 'debug' });
54+
dbg.installPackage(manifest);
55+
dbg.installPackage(manifest);
56+
expect(debug).toHaveBeenCalledWith(expect.stringContaining('Overwriting package: com.test'));
57+
});
58+
59+
it('resolves the level from OS_REGISTRY_LOG when no explicit option is given', () => {
60+
process.env.OS_REGISTRY_LOG = 'debug';
61+
expect(new SchemaRegistry({ multiTenant: false }).logLevel).toBe('debug');
62+
});
63+
64+
it('falls back to info for an unrecognized OS_REGISTRY_LOG value', () => {
65+
process.env.OS_REGISTRY_LOG = 'chatty';
66+
expect(new SchemaRegistry({ multiTenant: false }).logLevel).toBe('info');
67+
});
68+
69+
it('an explicit logLevel option wins over the env var', () => {
70+
process.env.OS_REGISTRY_LOG = 'debug';
71+
expect(new SchemaRegistry({ multiTenant: false, logLevel: 'silent' }).logLevel).toBe('silent');
72+
});
73+
74+
it('exposes the full level vocabulary for validation', () => {
75+
expect(REGISTRY_LOG_LEVELS).toEqual(['debug', 'info', 'warn', 'error', 'silent']);
76+
});
77+
});

packages/objectql/src/registry.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ function mergeObjectDefinitions(base: ServiceObject, extension: Partial<ServiceO
121121
*/
122122
export type RegistryLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
123123

124+
/** All valid {@link RegistryLogLevel} values — used to validate `OS_REGISTRY_LOG`. */
125+
export const REGISTRY_LOG_LEVELS: readonly RegistryLogLevel[] = [
126+
'debug', 'info', 'warn', 'error', 'silent',
127+
];
128+
124129
/**
125130
* Construction options for {@link SchemaRegistry}.
126131
*/
@@ -172,6 +177,19 @@ export interface SchemaRegistryOptions {
172177
* package ids are always disambiguable by package-scoped resolution.)
173178
*/
174179
collisionPolicy?: 'error' | 'warn';
180+
181+
/**
182+
* Verbosity of the registry's own `[Registry] …` log lines. Sourced from the
183+
* `OS_REGISTRY_LOG` env var when not set explicitly (default `'info'`).
184+
*
185+
* The point of the env seam is discoverability (#3420): expected-but-noisy
186+
* housekeeping — re-registering an owned object / overwriting a package
187+
* manifest on a rebuild/HMR/seed-replay — is emitted at `'debug'`, so it stays
188+
* out of a stock `info` boot log but a developer chasing a registration issue
189+
* can surface it with `OS_REGISTRY_LOG=debug`. An unrecognized value falls
190+
* back to `'info'`.
191+
*/
192+
logLevel?: RegistryLogLevel;
175193
}
176194

177195
/**
@@ -529,6 +547,16 @@ export class SchemaRegistry {
529547
this.collisionPolicy =
530548
options.collisionPolicy ??
531549
((process.env.OS_METADATA_COLLISION ?? '').toLowerCase() === 'warn' ? 'warn' : 'error');
550+
551+
// #3420 — env-driven verbosity so debug-level registry housekeeping
552+
// (re-register / package overwrite) is discoverable without a code change.
553+
// Unrecognized OS_REGISTRY_LOG values fall back to the 'info' default.
554+
const envLevel = (process.env.OS_REGISTRY_LOG ?? '').toLowerCase();
555+
this._logLevel =
556+
options.logLevel ??
557+
(REGISTRY_LOG_LEVELS.includes(envLevel as RegistryLogLevel)
558+
? (envLevel as RegistryLogLevel)
559+
: this._logLevel);
532560
}
533561

534562
get logLevel(): RegistryLogLevel { return this._logLevel; }

packages/plugins/plugin-auth/src/auth-manager.mcp-oauth.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,16 @@ describe('oauthProvider plugin wiring (DCR + scopes + audiences)', () => {
302302
expect(opts.validAudiences).toContain('https://acme.example.com/api/v1/auth');
303303
});
304304

305+
it('silences the false-positive oauthAuthServerConfig warning (#3420)', async () => {
306+
// registerOidcDiscoveryRoutes mounts /.well-known/oauth-authorization-server
307+
// (and the /api/v1/auth path-insertion variant) at the issuer ROOT ourselves,
308+
// so better-auth's boot-time "Please ensure … exists" reminder is a false
309+
// positive. It must be silenced via the documented option, or the stock
310+
// showcase prints it (twice) on every `os dev`. Regression guard for the fix.
311+
const opts = await capturePluginOpts({ OS_MCP_SERVER_ENABLED: 'true' });
312+
expect(opts.silenceWarnings).toEqual({ oauthAuthServerConfig: true });
313+
});
314+
305315
it('OS_OIDC_DCR_ENABLED=false forces DCR off even with MCP on', async () => {
306316
const opts = await capturePluginOpts({ OS_MCP_SERVER_ENABLED: 'true', OS_OIDC_DCR_ENABLED: 'false' });
307317
expect(opts.allowDynamicClientRegistration).toBe(false);

packages/plugins/plugin-auth/src/auth-manager.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1853,11 +1853,18 @@ export class AuthManager {
18531853
// mount ourselves at the issuer ROOT (RFC 8414 §3 requires them there,
18541854
// not under the auth basePath) — registerOidcDiscoveryRoutes serves
18551855
// /.well-known/oauth-authorization-server AND the path-insertion variant
1856-
// (`…/api/v1/auth`) the notice names. Its boot-time "Please ensure …
1857-
// exists" reminder is therefore a false positive that fires on every
1858-
// stock example (twice — jwt+oauthProvider init runs it per instance);
1859-
// silence the one requirement we've already satisfied so an official
1860-
// dev boot stays warning-free (#3420).
1856+
// (`…/api/v1/auth`) the notice names. Its "Please ensure … exists"
1857+
// reminder is therefore a false positive on every stock example.
1858+
//
1859+
// #3420 root cause of the DOUBLE print: the notice fires in the
1860+
// oauth-provider plugin's `init(ctx)`, which better-auth runs once per
1861+
// `betterAuth()` construction — and the instance is built more than once
1862+
// at boot (an initial lazy build, then a rebuild once boot-time auth
1863+
// *settings* are applied — applyConfigPatch() nulls the cached instance
1864+
// so the next request rebuilds with the new policy). Gating the emitter
1865+
// here silences the one requirement we've already satisfied across every
1866+
// build path, independent of how many times auth is constructed, so an
1867+
// official dev boot stays warning-free.
18611868
silenceWarnings: { oauthAuthServerConfig: true },
18621869
// ── MCP OAuth track (#2698) ────────────────────────────────
18631870
// Coarse tool-family scopes for the platform's own MCP endpoint,

0 commit comments

Comments
 (0)