Skip to content

Commit cad773c

Browse files
os-zhuangclaude
andauthored
test(runtime,objectql): the D12 honesty gate iterates the fake inventory, not spot checks (#3898) (#4306)
The three holes #3898 inventoried are all closed on main — #4082 eliminated the `_fallback` marker by moving every kernel fallback onto the standard `__serviceInfo` descriptor, #4114 made both discovery builders compute the `metadata` slot, #4141 did the same for `data`. What remained was the issue's suggestion 4: a recurrence gate over the known-fake INVENTORY rather than per-slot spot checks. This adds that gate to both discovery builders' suites: iterate CORE_FALLBACK_FACTORIES (the complete fake inventory now that plugin-dev's stub table is retired, ADR-0115), register each product into its own slot, and assert discovery reports it `degraded` — never `available`. Table-driven, so the next fallback added to the table is gated the day it lands. cache/queue/job had no per-slot pin before this: dropping their `svcAvailable(…, svc)` third argument — the exact #4130 regression shape — was test-invisible. Verified the gate bites by simulating that regression (`services.cache.status: expected 'available' to be 'degraded'`). Closes #3898 Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 05154a1 commit cad773c

3 files changed

Lines changed: 65 additions & 1 deletion

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
---
3+
4+
test-only: the D12 honesty gate iterates the known-fake inventory — every
5+
`CORE_FALLBACK_FACTORIES` product registered slot-by-slot must come out of
6+
both discovery builders as `degraded`, never `available` (#3898 suggestion 4;
7+
`cache`/`queue`/`job` had no per-slot pin before). Releases nothing.

packages/objectql/src/protocol-discovery.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { describe, it, expect, beforeEach } from 'vitest';
44
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
5-
import { createMemoryMetadata } from '@objectstack/core';
5+
import { createMemoryMetadata, CORE_FALLBACK_FACTORIES } from '@objectstack/core';
66
import { ObjectQL } from './engine.js';
77

88
describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => {
@@ -219,6 +219,31 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>
219219
expect(discovery.services.data.message).toBeUndefined();
220220
});
221221

222+
// ── The class-wide gate (#3898): the fake INVENTORY, not spot checks ──────
223+
// Same gate as the dispatcher's: every in-memory fallback the kernel can
224+
// auto-register (CORE_FALLBACK_FACTORIES — the complete fake inventory now
225+
// that plugin-dev's stub table is retired, ADR-0115, and the `_fallback`
226+
// marker was eliminated rather than recognized, #4058 step 1) goes through
227+
// THIS builder too and must never come out `available`. Table-driven so a
228+
// new fallback is gated the day it is added; cache/queue/job had no
229+
// per-slot pin here either.
230+
it('reports every CORE_FALLBACK_FACTORIES product as degraded, never available (#3898)', async () => {
231+
expect(Object.keys(CORE_FALLBACK_FACTORIES).length).toBeGreaterThan(0);
232+
233+
for (const [slot, factory] of Object.entries(CORE_FALLBACK_FACTORIES)) {
234+
const mockServices = new Map<string, any>();
235+
mockServices.set(slot, factory());
236+
237+
protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
238+
const reported = (await protocol.getDiscovery()).services[slot];
239+
240+
expect(reported, `services.${slot}`).toBeDefined();
241+
expect(reported.enabled, `services.${slot}.enabled`).toBe(true);
242+
expect(reported.status, `services.${slot}.status`).toBe('degraded');
243+
expect(reported.message, `services.${slot}.message`).toBeTruthy();
244+
}
245+
});
246+
222247
// #3891 — the degraded ObjectQL fallback is retired. Analytics is an
223248
// ordinary optional service now: absent ⇒ unavailable, and the route must
224249
// NOT be advertised (an advertised route with no handler 404s — the exact

packages/runtime/src/http-dispatcher.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2653,6 +2653,38 @@ describe('HttpDispatcher', () => {
26532653
expect(fromDispatcher.message).toBe(fromProtocol.message);
26542654
expect(fromDispatcher.route).toBe(fromProtocol.route);
26552655
});
2656+
2657+
// ── The class-wide gate (#3898): the fake INVENTORY, not spot checks ──
2658+
//
2659+
// Everything above pins one slot each. This iterates the actual list of
2660+
// in-memory fallbacks the kernel auto-registers — CORE_FALLBACK_FACTORIES
2661+
// is the complete fake inventory now that plugin-dev's stub table is
2662+
// retired (ADR-0115) and the third marker kind, `_fallback`, was
2663+
// eliminated rather than recognized (#4058 step 1) — and registers each
2664+
// product into its own slot: discovery must never call any of them
2665+
// `available`. Table-driven so the next fallback added to the table is
2666+
// gated the day it lands; this class of hole recurs with every new
2667+
// fallback. cache/queue/job had no per-slot pin before this — dropping
2668+
// their `svcAvailable(…, svc)` third argument, the exact #4130
2669+
// regression shape, was test-invisible.
2670+
2671+
it('reports every CORE_FALLBACK_FACTORIES product as degraded, never available (#3898)', async () => {
2672+
const { CORE_FALLBACK_FACTORIES } = await import('@objectstack/core');
2673+
expect(Object.keys(CORE_FALLBACK_FACTORIES).length).toBeGreaterThan(0);
2674+
2675+
for (const [slot, factory] of Object.entries(CORE_FALLBACK_FACTORIES)) {
2676+
const fallback = factory();
2677+
(kernel as any).getService = vi.fn().mockImplementation((n: string) => (n === slot ? fallback : null));
2678+
(kernel as any).services = new Map([[slot, fallback]]);
2679+
2680+
const info = await dispatcher.getDiscoveryInfo('/api/v1');
2681+
const reported = (info.services as Record<string, any>)[slot];
2682+
expect(reported, `services.${slot}`).toBeDefined();
2683+
expect(reported.enabled, `services.${slot}.enabled`).toBe(true);
2684+
expect(reported.status, `services.${slot}.status`).toBe('degraded');
2685+
expect(reported.message, `services.${slot}.message`).toBeTruthy();
2686+
}
2687+
});
26562688
});
26572689

26582690
// ═══════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)