forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathruntime-config.client.test.ts
More file actions
224 lines (212 loc) · 9.42 KB
/
Copy pathruntime-config.client.test.ts
File metadata and controls
224 lines (212 loc) · 9.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getRuntimeConfig } from "./runtime-config.client";
describe("client getRuntimeConfig (shell)", () => {
const originalWindow = globalThis.window;
beforeEach(() => {
// jsdom provides `window` by default in this vitest config — but a
// prior test that deleted `window` and failed before its restore
// would leave it undefined, and an unguarded dereference here turns
// that one failure into a cascade across the whole file. Restore
// first, then guard the delete.
(globalThis as { window?: Window }).window = originalWindow;
if (globalThis.window) {
delete (globalThis.window as Window & { __SHOWCASE_CONFIG__?: unknown })
.__SHOWCASE_CONFIG__;
}
});
afterEach(() => {
// Restore BEFORE the delete: if the test under teardown removed
// `window` (the SSR test), the unguarded dereference would throw
// inside the hook and mask the real failure.
(globalThis as { window?: Window }).window = originalWindow;
// Clean up the injected config HERE, not only in the next test's
// beforeEach — the last test in this file must not leak
// __SHOWCASE_CONFIG__ into other test files under worker reuse.
if (globalThis.window) {
delete (globalThis.window as Window & { __SHOWCASE_CONFIG__?: unknown })
.__SHOWCASE_CONFIG__;
}
});
it("returns the injected config", () => {
(window as Window & { __SHOWCASE_CONFIG__?: unknown }).__SHOWCASE_CONFIG__ =
{
baseUrl: "https://showcase.example.com",
posthogHost: "https://eu.i.posthog.com",
backendHostPattern: "showcase-{slug}-production.up.railway.app",
docsHost: "https://docs.showcase.copilotkit.ai",
};
expect(getRuntimeConfig()).toEqual({
baseUrl: "https://showcase.example.com",
posthogHost: "https://eu.i.posthog.com",
backendHostPattern: "showcase-{slug}-production.up.railway.app",
docsHost: "https://docs.showcase.copilotkit.ai",
});
});
it("throws when __SHOWCASE_CONFIG__ is missing (wiring bug)", () => {
expect(() => getRuntimeConfig()).toThrow(
/window\.__SHOWCASE_CONFIG__ is missing/,
);
});
it("throws when the injection ran with empty inputs (incomplete config)", () => {
// `!cfg` alone would accept a truthy-but-useless object — the
// fail-loud contract covers empty injected fields too. ALL FOUR
// URL-bearing fields are checked symmetrically (docsHost feeds
// docs links, posthogHost feeds capture — an empty value in either
// is the same wiring bug as an empty baseUrl).
for (const broken of [
{ baseUrl: "" },
{ backendHostPattern: "" },
{ docsHost: "" },
{ posthogHost: "" },
]) {
(
window as Window & { __SHOWCASE_CONFIG__?: unknown }
).__SHOWCASE_CONFIG__ = {
baseUrl: "https://showcase.example.com",
posthogHost: "https://eu.i.posthog.com",
backendHostPattern: "showcase-{slug}.example.com",
docsHost: "https://docs.showcase.copilotkit.ai",
...broken,
};
expect(
() => getRuntimeConfig(),
`empty ${Object.keys(broken)[0]} should throw`,
).toThrow(/__SHOWCASE_CONFIG__ is incomplete/);
}
});
it("throws (naming the field) when an injected field is not a string", () => {
// A layout bug injecting a number previously sailed through the
// truthiness check and exploded far from the cause (e.g. inside a
// consumer's replaceAll).
for (const [field, value] of [
["baseUrl", 42],
["backendHostPattern", null],
["docsHost", { url: "https://docs.example.com" }],
["posthogHost", 1],
] as const) {
(
window as Window & { __SHOWCASE_CONFIG__?: unknown }
).__SHOWCASE_CONFIG__ = {
baseUrl: "https://showcase.example.com",
posthogHost: "https://eu.i.posthog.com",
backendHostPattern: "showcase-{slug}.example.com",
docsHost: "https://docs.showcase.copilotkit.ai",
[field]: value,
};
expect(
() => getRuntimeConfig(),
`non-string ${field} should throw`,
).toThrow(new RegExp(`"${field}"`));
}
});
it("accepts a config without posthogKey (optional field)", () => {
// posthogKey is legitimately absent off-prod — it must NOT be part
// of the fail-loud required set.
(window as Window & { __SHOWCASE_CONFIG__?: unknown }).__SHOWCASE_CONFIG__ =
{
baseUrl: "https://showcase.example.com",
posthogHost: "https://eu.i.posthog.com",
backendHostPattern: "showcase-{slug}-production.up.railway.app",
docsHost: "https://docs.showcase.copilotkit.ai",
};
expect(getRuntimeConfig().posthogKey).toBeUndefined();
});
it("throws (naming posthogKey) when a PRESENT posthogKey is not a string", () => {
// posthogKey's absence exemption (legitimately unset off-prod) must
// not exempt wrong TYPES: a layout bug injecting a number would
// sail through and explode far from the cause in a capture consumer.
for (const bad of [42, null, { key: "phc_x" }]) {
// Full-replacement cast (not an intersection): the global Window
// augmentation types the field as RuntimeConfig, which would
// reject the deliberately-wrong posthogKey at compile time.
(
window as unknown as { __SHOWCASE_CONFIG__?: unknown }
).__SHOWCASE_CONFIG__ = {
baseUrl: "https://showcase.example.com",
posthogHost: "https://eu.i.posthog.com",
backendHostPattern: "showcase-{slug}-production.up.railway.app",
docsHost: "https://docs.showcase.copilotkit.ai",
posthogKey: bad,
};
expect(
() => getRuntimeConfig(),
`posthogKey ${JSON.stringify(bad)} should throw`,
).toThrow(/"posthogKey"/);
}
// A present STRING key still passes.
(window as Window & { __SHOWCASE_CONFIG__?: unknown }).__SHOWCASE_CONFIG__ =
{
baseUrl: "https://showcase.example.com",
posthogHost: "https://eu.i.posthog.com",
backendHostPattern: "showcase-{slug}-production.up.railway.app",
docsHost: "https://docs.showcase.copilotkit.ai",
posthogKey: "phc_x",
};
expect(getRuntimeConfig().posthogKey).toBe("phc_x");
});
it("throws (naming posthogKey) when a PRESENT posthogKey is an empty string", () => {
// The server reader can never produce "" (readEnvPair maps empty to
// undefined), so a present-but-empty key is the same wiring-bug
// class as a wrong type — NOT the legitimate absence case.
(
window as unknown as { __SHOWCASE_CONFIG__?: unknown }
).__SHOWCASE_CONFIG__ = {
baseUrl: "https://showcase.example.com",
posthogHost: "https://eu.i.posthog.com",
backendHostPattern: "showcase-{slug}-production.up.railway.app",
docsHost: "https://docs.showcase.copilotkit.ai",
posthogKey: "",
};
expect(() => getRuntimeConfig()).toThrow(/"posthogKey"/);
});
it("returns a frozen object so consumers cannot mutate the shared config", () => {
(window as Window & { __SHOWCASE_CONFIG__?: unknown }).__SHOWCASE_CONFIG__ =
{
baseUrl: "https://showcase.example.com",
posthogHost: "https://eu.i.posthog.com",
backendHostPattern: "showcase-{slug}-production.up.railway.app",
docsHost: "https://docs.showcase.copilotkit.ai",
};
const cfg = getRuntimeConfig();
expect(Object.isFrozen(cfg)).toBe(true);
// window.__SHOWCASE_CONFIG__ is a process-wide singleton; a strict-
// mode write must throw instead of silently changing it for everyone.
expect(() => {
(cfg as { baseUrl: string }).baseUrl = "https://evil.example.com";
}).toThrow(TypeError);
expect(getRuntimeConfig().baseUrl).toBe("https://showcase.example.com");
});
it("returns SSR sentinel placeholder when window is undefined", () => {
// Simulate SSR by stubbing window to undefined. "use client"
// component bodies execute on the server during SSR, so this reader
// MUST be SSR-safe (returns parseable-URL placeholders so
// `new URL()` in consumers doesn't throw) and NOT throw —
// otherwise the whole server-rendered HTML 500s. stubGlobal (not
// delete/reassign) per repo discipline: vitest restores the
// original even if an assertion throws mid-test (same pattern as
// runtime-url-wiring.test.ts).
vi.stubGlobal("window", undefined);
try {
const cfg = getRuntimeConfig();
// URL fields must be parseable.
expect(() => new URL(cfg.baseUrl)).not.toThrow();
expect(() => new URL(cfg.posthogHost)).not.toThrow();
expect(() => new URL(cfg.docsHost)).not.toThrow();
// Structural parity with the server reader: every real value is
// slashless (the server strips trailing slashes at every exit
// path), so the SSR placeholder must be slashless too — consumers
// string-compose against these values and must see ONE form.
expect(cfg.baseUrl).not.toMatch(/\/$/);
expect(cfg.posthogHost).not.toMatch(/\/$/);
expect(cfg.docsHost).not.toMatch(/\/$/);
// The host pattern is not a URL but must keep the {slug}
// placeholder so substitution still yields a syntactically
// valid (non-resolvable) host during SSR.
expect(cfg.backendHostPattern).toContain("{slug}");
// The placeholder is shared module state — must be frozen too.
expect(Object.isFrozen(cfg)).toBe(true);
} finally {
vi.unstubAllGlobals();
}
});
});