@teamscala/service-boot 0.23.382 → 0.23.383
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 +17 -1
- package/src/register-registry-page-route/captured-loader-config.test.ts +37 -0
- package/src/register-registry-page-route/captured-loader-config.ts +29 -0
- package/src/register-registry-page-route/resolve-page-data.test.ts +72 -0
- package/src/register-registry-page-route/resolve-page-data.ts +22 -1
- package/src/register-registry-page-route.ts +6 -1
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"name": "@teamscala/service-boot",
|
|
8
8
|
"type": "module",
|
|
9
|
-
"version": "0.23.
|
|
9
|
+
"version": "0.23.383",
|
|
10
10
|
"sideEffects": false,
|
|
11
11
|
"imports": {
|
|
12
12
|
"#service-boot/*": "./src/*",
|
|
@@ -190,6 +190,14 @@
|
|
|
190
190
|
"types": "./src/content-gate/access.d.ts",
|
|
191
191
|
"default": "./src/content-gate/access.ts"
|
|
192
192
|
},
|
|
193
|
+
"./content-gate/catalog": {
|
|
194
|
+
"types": "./src/content-gate/catalog.d.ts",
|
|
195
|
+
"default": "./src/content-gate/catalog.ts"
|
|
196
|
+
},
|
|
197
|
+
"./content-gate/catalog.ts": {
|
|
198
|
+
"types": "./src/content-gate/catalog.d.ts",
|
|
199
|
+
"default": "./src/content-gate/catalog.ts"
|
|
200
|
+
},
|
|
193
201
|
"./content-gate/config-types": {
|
|
194
202
|
"types": "./src/content-gate/config-types.d.ts",
|
|
195
203
|
"default": "./src/content-gate/config-types.ts"
|
|
@@ -722,6 +730,14 @@
|
|
|
722
730
|
"types": "./src/register-registry-page-route/ai-chat-prefetch.d.ts",
|
|
723
731
|
"default": "./src/register-registry-page-route/ai-chat-prefetch.ts"
|
|
724
732
|
},
|
|
733
|
+
"./register-registry-page-route/captured-loader-config": {
|
|
734
|
+
"types": "./src/register-registry-page-route/captured-loader-config.d.ts",
|
|
735
|
+
"default": "./src/register-registry-page-route/captured-loader-config.ts"
|
|
736
|
+
},
|
|
737
|
+
"./register-registry-page-route/captured-loader-config.ts": {
|
|
738
|
+
"types": "./src/register-registry-page-route/captured-loader-config.d.ts",
|
|
739
|
+
"default": "./src/register-registry-page-route/captured-loader-config.ts"
|
|
740
|
+
},
|
|
725
741
|
"./register-registry-page-route/graph-cache": {
|
|
726
742
|
"types": "./src/register-registry-page-route/graph-cache.d.ts",
|
|
727
743
|
"default": "./src/register-registry-page-route/graph-cache.ts"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* The boot log is the ONLY observable statement of what a running service
|
|
7
|
+
* captured from its resource_loader row (the 2026-08-18 evening-long bisect:
|
|
8
|
+
* a footer region added to the row never reached the booted process, and
|
|
9
|
+
* nothing anywhere said so). These tests pin the contract the line states —
|
|
10
|
+
* the captured source keys, the captured region slots, and the restart
|
|
11
|
+
* requirement — so the observability cannot silently regress to the old bare
|
|
12
|
+
* "registered route" line.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
15
|
+
import { describeCapturedLoaderConfig } from "./captured-loader-config.ts";
|
|
16
|
+
|
|
17
|
+
describe("describeCapturedLoaderConfig", () => {
|
|
18
|
+
test("names every captured source key and region slot, and states the restart contract", () => {
|
|
19
|
+
const line = describeCapturedLoaderConfig({
|
|
20
|
+
sources: [{ key: "brand" }, { key: "website" }, { key: "footer" }],
|
|
21
|
+
region_slots: ["header", "center", "footer"],
|
|
22
|
+
});
|
|
23
|
+
expect(line).toContain("brand");
|
|
24
|
+
expect(line).toContain("website");
|
|
25
|
+
expect(line).toContain("footer");
|
|
26
|
+
expect(line).toContain("header");
|
|
27
|
+
expect(line).toContain("center");
|
|
28
|
+
expect(line).toContain("captured at boot");
|
|
29
|
+
expect(line).toContain("restart");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("an absent list renders as none, never an empty bracket pair", () => {
|
|
33
|
+
const line = describeCapturedLoaderConfig({ sources: [{ key: "page" }] });
|
|
34
|
+
expect(line).toContain("region_slots [none]");
|
|
35
|
+
expect(line).not.toContain("region_slots []");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Summarises the resource_loader config a route captured at boot, for the
|
|
7
|
+
* route-registration log line. Registration reads the loader row ONCE into
|
|
8
|
+
* the route closure, so an edit to the row silently does not reach the
|
|
9
|
+
* running service (measured 2026-08-18: a footer region added to a report
|
|
10
|
+
* produced no footer and no diagnostic until a cold wake). Naming the
|
|
11
|
+
* captured shape in the boot log makes the row-vs-running-service gap a
|
|
12
|
+
* one-glance check in the service logs, and the restart contract is stated
|
|
13
|
+
* where the operator will be looking when the edit "doesn't work".
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Structural slice of the ResourceLoaderConfigSchema output this unit reads. */
|
|
17
|
+
interface LoaderConfigSlice {
|
|
18
|
+
sources?: { key: string }[];
|
|
19
|
+
region_slots?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function describeCapturedLoaderConfig(
|
|
23
|
+
loaderConfig: LoaderConfigSlice,
|
|
24
|
+
): string {
|
|
25
|
+
const sourceKeys =
|
|
26
|
+
loaderConfig.sources?.map((source) => source.key).join(", ") ?? "";
|
|
27
|
+
const regionSlots = loaderConfig.region_slots?.join(", ") ?? "";
|
|
28
|
+
return `captured at boot: sources [${sourceKeys || "none"}], region_slots [${regionSlots || "none"}] — resource_loader edits reach this service only after a restart`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Regression test for the silent region-slot vanish (measured 2026-08-18): a
|
|
7
|
+
* region the operator declared in resource_loader.region_slots whose source
|
|
8
|
+
* did not resolve was skipped with no log, no error and no signal anywhere —
|
|
9
|
+
* the page rendered minus a region while every other suspect checked out.
|
|
10
|
+
* These tests pin the observable contract: the omission is LOGGED naming the
|
|
11
|
+
* slot, once per slot per process (hot path never spams), and the resolved
|
|
12
|
+
* regions still render every slot that did resolve.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
const warn = mock((_message: string) => {});
|
|
17
|
+
mock.module("@teamscala/logger/app-loggers", () => ({
|
|
18
|
+
getAppLogger: () => ({ warn, info: () => {} }),
|
|
19
|
+
}));
|
|
20
|
+
mock.module("@teamscala/db/registry/load-config", () => ({
|
|
21
|
+
loadRegistryConfig: async () => ({}),
|
|
22
|
+
}));
|
|
23
|
+
mock.module("@teamscala/service-boot/page-server-orpc/server-client", () => ({
|
|
24
|
+
getServerClient: () => ({}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const { resolvedToPageData } = await import("./resolve-page-data.ts");
|
|
28
|
+
|
|
29
|
+
const request = (): Request =>
|
|
30
|
+
new Request("https://portal.example.com/site/org/dashboard");
|
|
31
|
+
|
|
32
|
+
function makeResolved(): Record<string, unknown> {
|
|
33
|
+
return {
|
|
34
|
+
website: { slug: "site", name: "Site" },
|
|
35
|
+
center: {
|
|
36
|
+
id: "p1",
|
|
37
|
+
name: "Dashboard",
|
|
38
|
+
puck_data: { root: { props: {} }, content: [] },
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("resolvedToPageData — unresolved region slots are observable", () => {
|
|
44
|
+
test("a declared slot whose source did not resolve is omitted AND warned by name", () => {
|
|
45
|
+
const pageData = resolvedToPageData(
|
|
46
|
+
makeResolved(),
|
|
47
|
+
{},
|
|
48
|
+
request(),
|
|
49
|
+
undefined,
|
|
50
|
+
["center", "footer"],
|
|
51
|
+
) as { regions?: { slot: string }[] };
|
|
52
|
+
const slots = (pageData.regions ?? []).map((region) => region.slot);
|
|
53
|
+
expect(slots).toContain("center");
|
|
54
|
+
expect(slots).not.toContain("footer");
|
|
55
|
+
expect(warn.mock.calls.length).toBe(1);
|
|
56
|
+
const message = String(warn.mock.calls[0][0]);
|
|
57
|
+
expect(message).toContain("footer");
|
|
58
|
+
expect(message).toContain("no source resolved");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the warn throttles to once per slot per process", () => {
|
|
62
|
+
const before = warn.mock.calls.length;
|
|
63
|
+
resolvedToPageData(
|
|
64
|
+
makeResolved(),
|
|
65
|
+
{},
|
|
66
|
+
request(),
|
|
67
|
+
undefined,
|
|
68
|
+
["center", "footer"],
|
|
69
|
+
);
|
|
70
|
+
expect(warn.mock.calls.length).toBe(before);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -81,6 +81,24 @@ interface RegionEntry {
|
|
|
81
81
|
metaDescription?: string | null;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// A region slot the operator DECLARED (resource_loader.region_slots) whose
|
|
85
|
+
// source did not resolve used to vanish with no signal anywhere — the page
|
|
86
|
+
// rendered minus a region while the page, source, attributes, template and
|
|
87
|
+
// tree builder all looked innocent (measured 2026-08-18: a footer region
|
|
88
|
+
// added to a report produced no footer and no diagnostic; the cause was the
|
|
89
|
+
// boot-time config capture named in the message). One warn per slot per
|
|
90
|
+
// process, throttled like the block-coverage guard in the caller, so the hot
|
|
91
|
+
// path never spams it.
|
|
92
|
+
const unresolvedRegionSlotsLogged = new Set<string>();
|
|
93
|
+
|
|
94
|
+
function warnUnresolvedRegionSlot(slot: string): void {
|
|
95
|
+
if (unresolvedRegionSlotsLogged.has(slot)) return;
|
|
96
|
+
unresolvedRegionSlotsLogged.add(slot);
|
|
97
|
+
getAppLogger().warn(
|
|
98
|
+
`[resolve-page-data] region slot "${slot}" is declared in the resource_loader config but no source resolved for it — the region is omitted from this render. A resource_loader edit reaches a booted service only after a restart.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
export function resolvedToPageData(
|
|
85
103
|
resolved: Record<string, unknown>,
|
|
86
104
|
meta: Record<string, unknown>,
|
|
@@ -133,7 +151,10 @@ export function resolvedToPageData(
|
|
|
133
151
|
const regions: RegionEntry[] = [];
|
|
134
152
|
for (const slot of regionSlots) {
|
|
135
153
|
const source = resolved[slot] as Record<string, unknown> | undefined;
|
|
136
|
-
if (!source)
|
|
154
|
+
if (!source) {
|
|
155
|
+
warnUnresolvedRegionSlot(slot);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
137
158
|
const puckData = extractPuckData(source) ?? EMPTY_PUCK_DATA;
|
|
138
159
|
applyLayoutDefaults(puckData, layoutDefaults);
|
|
139
160
|
regions.push({
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
readCachedGraph,
|
|
43
43
|
} from "./register-registry-page-route/graph-cache.ts";
|
|
44
44
|
import { resolvedToPageData } from "./register-registry-page-route/resolve-page-data.ts";
|
|
45
|
+
import { describeCapturedLoaderConfig } from "./register-registry-page-route/captured-loader-config.ts";
|
|
45
46
|
import {
|
|
46
47
|
buildMetaFromContext,
|
|
47
48
|
} from "./register-registry-page-route/meta-title.ts";
|
|
@@ -850,8 +851,12 @@ export async function registerRegistryPageRoute(
|
|
|
850
851
|
});
|
|
851
852
|
|
|
852
853
|
registeredPatterns.push(pattern);
|
|
854
|
+
// The loader config was captured at boot into the loadData closure — name
|
|
855
|
+
// the captured shape + the restart contract so a row edit that "doesn't
|
|
856
|
+
// work" on a running service is answered by this line, not an evening of
|
|
857
|
+
// bisecting (measured 2026-08-18).
|
|
853
858
|
log.info(
|
|
854
|
-
`[scala-install:${serviceSlug}] registered registry page route "${pattern}" → resource_loader "${loaderSlug}"`,
|
|
859
|
+
`[scala-install:${serviceSlug}] registered registry page route "${pattern}" → resource_loader "${loaderSlug}" (${describeCapturedLoaderConfig(loaderConfig)})`,
|
|
855
860
|
);
|
|
856
861
|
}
|
|
857
862
|
|