diff --git a/.changeset/d12-fake-inventory-gate.md b/.changeset/d12-fake-inventory-gate.md new file mode 100644 index 0000000000..ed385bf892 --- /dev/null +++ b/.changeset/d12-fake-inventory-gate.md @@ -0,0 +1,7 @@ +--- +--- + +test-only: the D12 honesty gate iterates the known-fake inventory — every +`CORE_FALLBACK_FACTORIES` product registered slot-by-slot must come out of +both discovery builders as `degraded`, never `available` (#3898 suggestion 4; +`cache`/`queue`/`job` had no per-slot pin before). Releases nothing. diff --git a/packages/objectql/src/protocol-discovery.test.ts b/packages/objectql/src/protocol-discovery.test.ts index d41f52d9da..1341c654ae 100644 --- a/packages/objectql/src/protocol-discovery.test.ts +++ b/packages/objectql/src/protocol-discovery.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; -import { createMemoryMetadata } from '@objectstack/core'; +import { createMemoryMetadata, CORE_FALLBACK_FACTORIES } from '@objectstack/core'; import { ObjectQL } from './engine.js'; describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => { @@ -219,6 +219,31 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => expect(discovery.services.data.message).toBeUndefined(); }); + // ── The class-wide gate (#3898): the fake INVENTORY, not spot checks ────── + // Same gate as the dispatcher's: every in-memory fallback the kernel can + // auto-register (CORE_FALLBACK_FACTORIES — the complete fake inventory now + // that plugin-dev's stub table is retired, ADR-0115, and the `_fallback` + // marker was eliminated rather than recognized, #4058 step 1) goes through + // THIS builder too and must never come out `available`. Table-driven so a + // new fallback is gated the day it is added; cache/queue/job had no + // per-slot pin here either. + it('reports every CORE_FALLBACK_FACTORIES product as degraded, never available (#3898)', async () => { + expect(Object.keys(CORE_FALLBACK_FACTORIES).length).toBeGreaterThan(0); + + for (const [slot, factory] of Object.entries(CORE_FALLBACK_FACTORIES)) { + const mockServices = new Map(); + mockServices.set(slot, factory()); + + protocol = new ObjectStackProtocolImplementation(engine, () => mockServices); + const reported = (await protocol.getDiscovery()).services[slot]; + + expect(reported, `services.${slot}`).toBeDefined(); + expect(reported.enabled, `services.${slot}.enabled`).toBe(true); + expect(reported.status, `services.${slot}.status`).toBe('degraded'); + expect(reported.message, `services.${slot}.message`).toBeTruthy(); + } + }); + // #3891 — the degraded ObjectQL fallback is retired. Analytics is an // ordinary optional service now: absent ⇒ unavailable, and the route must // NOT be advertised (an advertised route with no handler 404s — the exact diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index ecd5f2fda5..e17970c372 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -2653,6 +2653,38 @@ describe('HttpDispatcher', () => { expect(fromDispatcher.message).toBe(fromProtocol.message); expect(fromDispatcher.route).toBe(fromProtocol.route); }); + + // ── The class-wide gate (#3898): the fake INVENTORY, not spot checks ── + // + // Everything above pins one slot each. This iterates the actual list of + // in-memory fallbacks the kernel auto-registers — CORE_FALLBACK_FACTORIES + // is the complete fake inventory now that plugin-dev's stub table is + // retired (ADR-0115) and the third marker kind, `_fallback`, was + // eliminated rather than recognized (#4058 step 1) — and registers each + // product into its own slot: discovery must never call any of them + // `available`. Table-driven so the next fallback added to the table is + // gated the day it lands; this class of hole recurs with every new + // fallback. cache/queue/job had no per-slot pin before this — dropping + // their `svcAvailable(…, svc)` third argument, the exact #4130 + // regression shape, was test-invisible. + + it('reports every CORE_FALLBACK_FACTORIES product as degraded, never available (#3898)', async () => { + const { CORE_FALLBACK_FACTORIES } = await import('@objectstack/core'); + expect(Object.keys(CORE_FALLBACK_FACTORIES).length).toBeGreaterThan(0); + + for (const [slot, factory] of Object.entries(CORE_FALLBACK_FACTORIES)) { + const fallback = factory(); + (kernel as any).getService = vi.fn().mockImplementation((n: string) => (n === slot ? fallback : null)); + (kernel as any).services = new Map([[slot, fallback]]); + + const info = await dispatcher.getDiscoveryInfo('/api/v1'); + const reported = (info.services as Record)[slot]; + expect(reported, `services.${slot}`).toBeDefined(); + expect(reported.enabled, `services.${slot}.enabled`).toBe(true); + expect(reported.status, `services.${slot}.status`).toBe('degraded'); + expect(reported.message, `services.${slot}.message`).toBeTruthy(); + } + }); }); // ═══════════════════════════════════════════════════════════════