@teamscala/service-boot 0.23.385 → 0.23.386

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.385",
9
+ "version": "0.23.386",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -19,7 +19,9 @@
19
19
  import { afterEach, describe, expect, test } from "bun:test";
20
20
  import {
21
21
  getServerClient,
22
+ getServerClientCapabilityFault,
22
23
  isServerClientRegistered,
24
+ recordServerClientCapability,
23
25
  setServerClient,
24
26
  } from "./server-client.ts";
25
27
 
@@ -66,3 +68,45 @@ describe("page-server server client", () => {
66
68
  expect(isServerClientRegistered()).toBe(true);
67
69
  });
68
70
  });
71
+
72
+ describe("declared-vs-present capability probe (/health)", () => {
73
+ test("no fault when every declared model is servable", () => {
74
+ setServerClient({
75
+ assessments: { findFirst: async () => null },
76
+ assessment_responses: { findFirst: async () => null },
77
+ } as never);
78
+ recordServerClientCapability(["assessments", "assessment_responses"]);
79
+ expect(getServerClientCapabilityFault()).toBeNull();
80
+ });
81
+
82
+ test("names the models the client cannot serve", () => {
83
+ // THE OUTAGE, AS A HEALTH ANSWER. A client was registered and carried only
84
+ // the platform routers, so every process-existence signal was green while
85
+ // every data-backed page 500d. /health must be able to say this.
86
+ setServerClient({ puckPages: {}, userPreferences: {} } as never);
87
+ recordServerClientCapability(["assessments", "assessment_responses"]);
88
+ const fault = getServerClientCapabilityFault();
89
+ expect(fault).toContain("assessments");
90
+ expect(fault).toContain("assessment_responses");
91
+ });
92
+
93
+ test("a service declaring no models is never faulted", () => {
94
+ // Daemon/portal shells own no DB schema and are healthy with an empty client.
95
+ setServerClient({ puckPages: {} } as never);
96
+ recordServerClientCapability([]);
97
+ expect(getServerClientCapabilityFault()).toBeNull();
98
+ });
99
+
100
+ test("an unregistered client is itself the fault", () => {
101
+ setServerClient(null as never);
102
+ recordServerClientCapability(["assessments"]);
103
+ expect(getServerClientCapabilityFault()).toContain("no ORPC server client registered");
104
+ });
105
+
106
+ test("a model present without findFirst still counts as missing", () => {
107
+ // The generated router shape is what matters, not mere key presence.
108
+ setServerClient({ assessments: {} } as never);
109
+ recordServerClientCapability(["assessments"]);
110
+ expect(getServerClientCapabilityFault()).toContain("assessments");
111
+ });
112
+ });
@@ -55,3 +55,36 @@ export function setServerClient(
55
55
  ): void {
56
56
  serverClient = client;
57
57
  }
58
+
59
+ /**
60
+ * Declared-vs-present capability, recorded once at boot by
61
+ * registerPageServerClient and read by /health.
62
+ *
63
+ * The outage this exists for: a client WAS registered, so every
64
+ * process-existence signal was green, and it carried no CRUD models. Liveness
65
+ * must be a capability probe, never a registration check
66
+ * (`liveness-by-capability-not-process-existence`).
67
+ */
68
+ let capabilityFault: string | null = null;
69
+
70
+ /**
71
+ * Record whether the registered client can actually serve the models the
72
+ * service declares. Property lookups only — no I/O — so /health stays cheap.
73
+ */
74
+ export function recordServerClientCapability(declaredModels: readonly string[]): void {
75
+ if (!serverClient) {
76
+ capabilityFault = "no ORPC server client registered";
77
+ return;
78
+ }
79
+ const client = serverClient as unknown as Record<string, { findFirst?: unknown } | undefined>;
80
+ const missing = declaredModels.filter((m) => typeof client[m]?.findFirst !== "function");
81
+ capabilityFault =
82
+ missing.length === 0
83
+ ? null
84
+ : `generated client exposes no findFirst for ${missing.length} declared model(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", …" : ""}`;
85
+ }
86
+
87
+ /** The capability fault, or null when the service can serve its declared models. */
88
+ export function getServerClientCapabilityFault(): string | null {
89
+ return capabilityFault;
90
+ }
@@ -148,6 +148,9 @@ export async function registerPageServerClient(
148
148
  } as never,
149
149
  );
150
150
  mod.setServerClient(client);
151
+ // Record declared-vs-present capability for /health. Property lookups only.
152
+ (mod as unknown as { recordServerClientCapability?: (m: readonly string[]) => void })
153
+ .recordServerClientCapability?.(declaredModels);
151
154
  // State the model count. "registered" alone was true in the outage and told
152
155
  // nobody anything; a zero here is now visible in the boot log.
153
156
  log.info(