@teamscala/service-boot 0.23.379 → 0.23.380
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
|
@@ -19,9 +19,21 @@ import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
|
19
19
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
20
20
|
import { getServerClient } from "./page-server-orpc/server-client.ts";
|
|
21
21
|
import * as v from "valibot";
|
|
22
|
+
import type { BlockSchemaKey } from "@teamscala/db/registry/generated/block-schema";
|
|
22
23
|
|
|
23
24
|
const AgentConfigSchema = v.object({ agentId: v.string() });
|
|
24
25
|
|
|
26
|
+
// THE PUSHED BLOCK NAME IS AN IMPORTED IDENTITY, NEVER A SPELLED STRING.
|
|
27
|
+
// discover-blocks keys the Puck registry off block_schema.slug alone
|
|
28
|
+
// (`components[row.slug] = …`), so a type that is not a live slug resolves to
|
|
29
|
+
// nothing — and because injection is fail-soft, the page renders unchanged with
|
|
30
|
+
// no error. That is exactly how the bubble went missing from every gated
|
|
31
|
+
// proposal and agreement: the block was consolidated to ResourceLauncher and
|
|
32
|
+
// these injectors kept pushing their pre-consolidation names. BlockSchemaKey is
|
|
33
|
+
// the generated union of live slugs, so annotating it here makes a stale name a
|
|
34
|
+
// COMPILE error instead of a silent absence (identity-is-imported-never-spelled).
|
|
35
|
+
const LAUNCHER_BLOCK: BlockSchemaKey = "ResourceLauncher";
|
|
36
|
+
|
|
25
37
|
/** Serialise the entire page content tree as text for the GPT context. */
|
|
26
38
|
async function buildProposalContext(resolved: Record<string, unknown>): Promise<string> {
|
|
27
39
|
const proposal = resolved.proposal as Record<string, unknown> | undefined;
|
|
@@ -119,7 +131,7 @@ export async function injectProposalChatLauncher(
|
|
|
119
131
|
try {
|
|
120
132
|
const runtimeContext = await buildProposalContext(resolved);
|
|
121
133
|
(page.content as Array<Record<string, unknown>>).push({
|
|
122
|
-
type:
|
|
134
|
+
type: LAUNCHER_BLOCK,
|
|
123
135
|
props: {
|
|
124
136
|
id: "proposal-chat-launcher",
|
|
125
137
|
agentId,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Regression test for the block NAME the chat injectors push.
|
|
7
|
+
*
|
|
8
|
+
* THE DEFECT THIS PINS (found 2026-08-19): the floating assistant bubble was
|
|
9
|
+
* silently absent on every gated proposal and agreement. discover-blocks keys
|
|
10
|
+
* the Puck component registry off `block_schema.slug` and nothing else
|
|
11
|
+
* (`components[row.slug] = …`), so a pushed block `type` must EQUAL an active
|
|
12
|
+
* slug. The launcher block was consolidated to the single `ResourceLauncher`
|
|
13
|
+
* row, but the injectors still pushed the pre-consolidation names —
|
|
14
|
+
* `ResourceChatLauncher` here and `ChatLauncherBlock` in proposal-chat-inject.
|
|
15
|
+
* Neither resolves, and because injection is deliberately fail-soft the page
|
|
16
|
+
* rendered unchanged with no error: the classic shape of
|
|
17
|
+
* `a-deleted-producer-leaves-its-consumers-executing`.
|
|
18
|
+
*
|
|
19
|
+
* Asserting the literal is the WEAK half of the fix — it catches a regression
|
|
20
|
+
* of this exact string, not a future rename of the block. The strong half is
|
|
21
|
+
* single-sourcing the name so it cannot be spelled at all
|
|
22
|
+
* (`identity-is-imported-never-spelled`); until that lands, this test is what
|
|
23
|
+
* stands between the bubble and another silent disappearance.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
27
|
+
|
|
28
|
+
/** The ONE active launcher block_schema slug — the Puck registry key. */
|
|
29
|
+
const REGISTERED_LAUNCHER_SLUG = "ResourceLauncher";
|
|
30
|
+
|
|
31
|
+
mock.module("@teamscala/db/registry/load-config", () => ({
|
|
32
|
+
loadRegistryConfig: async () => ({ agentId: "agent-1" }),
|
|
33
|
+
}));
|
|
34
|
+
mock.module("./page-server-orpc/server-client.ts", () => ({
|
|
35
|
+
getServerClient: () => ({}),
|
|
36
|
+
}));
|
|
37
|
+
mock.module("@teamscala/logger/app-loggers", () => ({
|
|
38
|
+
getAppLogger: () => ({ error: () => {} }),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const { injectResourceChatLauncher } = await import("./resource-chat-inject.ts");
|
|
42
|
+
const { injectProposalChatLauncher } = await import("./proposal-chat-inject.ts");
|
|
43
|
+
|
|
44
|
+
function makeResolved(resourceKey: string): Record<string, unknown> {
|
|
45
|
+
return {
|
|
46
|
+
[resourceKey]: { organisation_id: "org-1", name: "Demo" },
|
|
47
|
+
brand: { primaryColor: "#123456" },
|
|
48
|
+
page: { puck_data: { root: { props: {} }, zones: {}, content: [] } },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pushedBlocks(resolved: Record<string, unknown>) {
|
|
53
|
+
const pd = (resolved.page as { puck_data: { content: Array<{ type: string }> } }).puck_data;
|
|
54
|
+
return pd.content;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("chat injectors — pushed block name resolves to a registered block", () => {
|
|
58
|
+
test("the generic resource injector pushes the registered launcher slug", async () => {
|
|
59
|
+
const resolved = makeResolved("agreement");
|
|
60
|
+
await injectResourceChatLauncher(resolved, "viewer@example.com", {
|
|
61
|
+
resourceKey: "agreement",
|
|
62
|
+
agentConfigSlug: "agreement-assistant-agent",
|
|
63
|
+
title: "Agreement Assistant",
|
|
64
|
+
launchLabel: "Ask about this agreement",
|
|
65
|
+
});
|
|
66
|
+
const blocks = pushedBlocks(resolved);
|
|
67
|
+
expect(blocks).toHaveLength(1);
|
|
68
|
+
expect(blocks[0].type).toBe(REGISTERED_LAUNCHER_SLUG);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("the proposal injector pushes the registered launcher slug", async () => {
|
|
72
|
+
const resolved = makeResolved("proposal");
|
|
73
|
+
await injectProposalChatLauncher(resolved, "viewer@example.com");
|
|
74
|
+
const blocks = pushedBlocks(resolved);
|
|
75
|
+
expect(blocks).toHaveLength(1);
|
|
76
|
+
expect(blocks[0].type).toBe(REGISTERED_LAUNCHER_SLUG);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("neither injector pushes a pre-consolidation launcher name", async () => {
|
|
80
|
+
const a = makeResolved("agreement");
|
|
81
|
+
await injectResourceChatLauncher(a, "viewer@example.com", {
|
|
82
|
+
resourceKey: "agreement",
|
|
83
|
+
agentConfigSlug: "agreement-assistant-agent",
|
|
84
|
+
title: "Agreement Assistant",
|
|
85
|
+
launchLabel: "Ask about this agreement",
|
|
86
|
+
});
|
|
87
|
+
const p = makeResolved("proposal");
|
|
88
|
+
await injectProposalChatLauncher(p, "viewer@example.com");
|
|
89
|
+
|
|
90
|
+
const dead = ["ResourceChatLauncher", "ChatLauncherBlock"];
|
|
91
|
+
for (const blocks of [pushedBlocks(a), pushedBlocks(p)]) {
|
|
92
|
+
for (const block of blocks) {
|
|
93
|
+
expect(dead).not.toContain(block.type);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -24,15 +24,28 @@ import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
|
24
24
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
25
25
|
import { getServerClient } from "./page-server-orpc/server-client.ts";
|
|
26
26
|
import * as v from "valibot";
|
|
27
|
+
import type { BlockSchemaKey } from "@teamscala/db/registry/generated/block-schema";
|
|
28
|
+
import type { ConfigKey } from "@teamscala/db/registry/generated/config";
|
|
27
29
|
|
|
28
30
|
const AgentConfigSchema = v.object({ agentId: v.string() });
|
|
29
31
|
|
|
32
|
+
// THE PUSHED BLOCK NAME IS AN IMPORTED IDENTITY, NEVER A SPELLED STRING.
|
|
33
|
+
// discover-blocks keys the Puck registry off block_schema.slug alone
|
|
34
|
+
// (`components[row.slug] = …`), so a type that is not a live slug resolves to
|
|
35
|
+
// nothing — and because injection is fail-soft, the page renders unchanged with
|
|
36
|
+
// no error. That is exactly how the bubble went missing from every gated
|
|
37
|
+
// proposal and agreement: the block was consolidated to ResourceLauncher and
|
|
38
|
+
// these injectors kept pushing their pre-consolidation names. BlockSchemaKey is
|
|
39
|
+
// the generated union of live slugs, so annotating it here makes a stale name a
|
|
40
|
+
// COMPILE error instead of a silent absence (identity-is-imported-never-spelled).
|
|
41
|
+
const LAUNCHER_BLOCK: BlockSchemaKey = "ResourceLauncher";
|
|
42
|
+
|
|
30
43
|
/** Per-resource-type injection config — the only thing that varies. */
|
|
31
44
|
export interface ResourceChatOptions {
|
|
32
45
|
/** Key in `resolved` holding the record (e.g. "proposal", "agreement"). */
|
|
33
46
|
resourceKey: string;
|
|
34
47
|
/** `config` row slug carrying { agentId } (e.g. "agreement-assistant-agent"). */
|
|
35
|
-
agentConfigSlug:
|
|
48
|
+
agentConfigSlug: ConfigKey;
|
|
36
49
|
/** Org id to scope the chat. If omitted, read from resolved[resourceKey].organisation_id
|
|
37
50
|
* (proposals); agreements pass it explicitly (org isn't on the resolved record). */
|
|
38
51
|
organisationId?: string;
|
|
@@ -145,7 +158,7 @@ export async function injectResourceChatLauncher(
|
|
|
145
158
|
try {
|
|
146
159
|
const runtimeContext = await buildResourceContext(resolved, opts.resourceKey);
|
|
147
160
|
(page.content as Array<Record<string, unknown>>).push({
|
|
148
|
-
type:
|
|
161
|
+
type: LAUNCHER_BLOCK,
|
|
149
162
|
props: {
|
|
150
163
|
id: `${opts.resourceKey}-chat-launcher`,
|
|
151
164
|
agentId,
|