minutework 0.1.31 → 0.1.33

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.
Files changed (168) hide show
  1. package/EXTERNAL_ALPHA.md +33 -33
  2. package/README.md +34 -34
  3. package/assets/claude-local/CLAUDE.md.template +12 -12
  4. package/assets/claude-local/skills/README.md +3 -1
  5. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +17 -4
  6. package/assets/claude-local/skills/capability-gap-reporting/SKILL.md +3 -3
  7. package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
  8. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +12 -5
  9. package/assets/claude-local/skills/layering-and-import-modes/SKILL.md +2 -2
  10. package/assets/claude-local/skills/openclaw-skill-importer/SKILL.md +2 -2
  11. package/assets/claude-local/skills/project-overview-and-strategy/SKILL.md +8 -8
  12. package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +11 -8
  13. package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +6 -5
  14. package/assets/claude-local/skills/vuilder-discovery-output-contract/SKILL.md +6 -6
  15. package/assets/claude-local/skills/workspace-guidance-refresh/SKILL.md +4 -4
  16. package/assets/templates/fastapi-sidecar/pyproject.toml +1 -1
  17. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/main.py +1 -1
  18. package/assets/templates/mobile-app/.env.example +4 -4
  19. package/assets/templates/mobile-app/AGENTS.md +3 -3
  20. package/assets/templates/mobile-app/README.md +10 -10
  21. package/assets/templates/mobile-app/app/(app)/_layout.tsx +2 -2
  22. package/assets/templates/mobile-app/app/(app)/index.tsx +2 -2
  23. package/assets/templates/mobile-app/app/(auth)/login.tsx +3 -3
  24. package/assets/templates/mobile-app/app/_layout.tsx +1 -1
  25. package/assets/templates/mobile-app/babel.config.js +1 -1
  26. package/assets/templates/mobile-app/eas.json +1 -1
  27. package/assets/templates/mobile-app/expo-env.d.ts +1 -1
  28. package/assets/templates/mobile-app/metro.config.js +2 -2
  29. package/assets/templates/mobile-app/package.json +1 -1
  30. package/assets/templates/mobile-app/src/mw/client.ts +3 -3
  31. package/assets/templates/mobile-app/src/mw/contracts.ts +2 -2
  32. package/assets/templates/mobile-app/src/mw/endpoints.ts +2 -2
  33. package/assets/templates/mobile-app/src/mw/env.ts +4 -4
  34. package/assets/templates/mobile-app/src/mw/session.ts +1 -1
  35. package/assets/templates/mobile-app/template.json +1 -1
  36. package/assets/templates/mobile-app/tools/template/validate-template.mjs +2 -2
  37. package/assets/templates/mobile-app/tsconfig.json +1 -1
  38. package/assets/templates/next-tenant-app/.env.example +1 -1
  39. package/assets/templates/next-tenant-app/README.md +26 -138
  40. package/assets/templates/next-tenant-app/package.json +1 -0
  41. package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
  42. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
  43. package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
  44. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
  45. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
  46. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
  47. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
  48. package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
  49. package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
  50. package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
  51. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
  52. package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
  53. package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
  54. package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
  55. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
  56. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
  57. package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
  58. package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
  59. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
  60. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
  61. package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +1 -1
  62. package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
  63. package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
  64. package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
  65. package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
  66. package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
  67. package/assets/templates/next-tenant-app/template.json +3 -3
  68. package/assets/templates/next-tenant-app/template.schema.json +1 -0
  69. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
  70. package/assets/templates/next-tenant-app/tools/template/with-public-site-fixture.mjs +2 -2
  71. package/bin/minutework.js +1 -1
  72. package/dist/agent.js +7 -7
  73. package/dist/agent.js.map +1 -1
  74. package/dist/auth.js +7 -7
  75. package/dist/auth.js.map +1 -1
  76. package/dist/compile.js +5 -5
  77. package/dist/config.js +6 -6
  78. package/dist/config.js.map +1 -1
  79. package/dist/deploy.js +7 -7
  80. package/dist/deploy.js.map +1 -1
  81. package/dist/developer-client.js +2 -2
  82. package/dist/developer-client.js.map +1 -1
  83. package/dist/index.js +30 -30
  84. package/dist/index.js.map +1 -1
  85. package/dist/init.js +10 -10
  86. package/dist/init.js.map +1 -1
  87. package/dist/launcher.js +1 -1
  88. package/dist/launcher.js.map +1 -1
  89. package/dist/managed-engine.js +6 -6
  90. package/dist/managed-engine.js.map +1 -1
  91. package/dist/orchestrator-context.js +1 -1
  92. package/dist/orchestrator-context.js.map +1 -1
  93. package/dist/orchestrator.js +15 -15
  94. package/dist/orchestrator.js.map +1 -1
  95. package/dist/paths.js +1 -1
  96. package/dist/paths.js.map +1 -1
  97. package/dist/publish.js +3 -3
  98. package/dist/publish.js.map +1 -1
  99. package/dist/reporting.js +8 -8
  100. package/dist/reporting.js.map +1 -1
  101. package/dist/sandbox.js +5 -5
  102. package/dist/sandbox.js.map +1 -1
  103. package/dist/state.js +1 -1
  104. package/dist/state.js.map +1 -1
  105. package/dist/tokens.js +9 -9
  106. package/dist/tokens.js.map +1 -1
  107. package/dist/workspace-assets.js +6 -6
  108. package/dist/workspace-assets.js.map +1 -1
  109. package/dist/workspace.js +3 -3
  110. package/dist/workspace.js.map +1 -1
  111. package/package.json +3 -3
  112. package/vendor/workspace-mcp/context.d.ts +6 -6
  113. package/vendor/workspace-mcp/context.js +56 -56
  114. package/vendor/workspace-mcp/context.js.map +1 -1
  115. package/vendor/workspace-mcp/types.d.ts +4 -0
  116. package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
  117. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
  118. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
  119. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
  120. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
  121. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
  122. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
  123. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
  124. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
  125. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
  126. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
  127. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
  128. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
  129. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
  130. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
  131. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
  132. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
  133. package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
  134. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
  135. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
  136. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
  137. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
  138. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
  139. package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
  140. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
  141. package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
  142. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
  143. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
  144. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
  145. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
  146. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
  147. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
  148. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
  149. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
  150. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
  151. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
  152. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
  153. package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
  154. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
  155. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
  156. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
  157. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
  158. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
  159. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
  160. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
  161. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
  162. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
  163. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
  164. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
  165. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
  166. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
  167. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
  168. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
@@ -1,444 +0,0 @@
1
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
-
5
- import { afterEach, describe, expect, it, vi } from "vitest";
6
-
7
- import type {
8
- BlogEntrySummary,
9
- DocsEntrySummary,
10
- MarketingPageKey,
11
- PublicContentAdapter,
12
- PublicContentKind,
13
- } from "@/lib/content/contracts";
14
- import { publicSiteFixtureSnapshot } from "@/lib/content/__fixtures__/public-site-snapshot";
15
- import {
16
- MinuteWorkPublicSiteContentAdapter,
17
- PUBLIC_CONTENT_REVALIDATE_SECONDS,
18
- fetchPublicSiteSnapshotEnvelope,
19
- } from "@/lib/content/adapter.server";
20
-
21
- const originalEnv = { ...process.env };
22
-
23
- function jsonResponse(body: unknown, status = 200) {
24
- return new Response(JSON.stringify(body), {
25
- status,
26
- headers: {
27
- "content-type": "application/json",
28
- },
29
- });
30
- }
31
-
32
- function normalizeSlugParts(slugParts: readonly string[]) {
33
- return slugParts.map((part) => part.trim().toLowerCase()).filter(Boolean).join("/");
34
- }
35
-
36
- function getFixtureMarketingPage(pageKey: MarketingPageKey) {
37
- return (
38
- publicSiteFixtureSnapshot.marketingPages.find((page) => page.pageKey === pageKey) ??
39
- null
40
- );
41
- }
42
-
43
- function getFixtureEntry(kind: PublicContentKind, slugParts: readonly string[]) {
44
- return (
45
- publicSiteFixtureSnapshot[kind].find(
46
- (entry) => normalizeSlugParts(entry.slug) === normalizeSlugParts(slugParts),
47
- ) ?? null
48
- );
49
- }
50
-
51
- function toDocsEntrySummary(
52
- entry: typeof publicSiteFixtureSnapshot.docs[number],
53
- ): DocsEntrySummary {
54
- return {
55
- description: entry.description,
56
- eyebrow: entry.eyebrow,
57
- kind: entry.kind,
58
- publishedAt: entry.publishedAt,
59
- readingTime: entry.readingTime,
60
- slug: entry.slug,
61
- title: entry.title,
62
- };
63
- }
64
-
65
- function toBlogEntrySummary(
66
- entry: typeof publicSiteFixtureSnapshot.blog[number],
67
- ): BlogEntrySummary {
68
- return {
69
- description: entry.description,
70
- eyebrow: entry.eyebrow,
71
- kind: entry.kind,
72
- publishedAt: entry.publishedAt,
73
- readingTime: entry.readingTime,
74
- slug: entry.slug,
75
- title: entry.title,
76
- };
77
- }
78
-
79
- function getFixtureEntrySummaries(kind: "docs"): DocsEntrySummary[];
80
- function getFixtureEntrySummaries(kind: "blog"): BlogEntrySummary[];
81
- function getFixtureEntrySummaries(kind: PublicContentKind) {
82
- if (kind === "docs") {
83
- return publicSiteFixtureSnapshot.docs.map(toDocsEntrySummary);
84
- }
85
-
86
- return publicSiteFixtureSnapshot.blog.map(toBlogEntrySummary);
87
- }
88
-
89
- function createCustomAdapter(
90
- overrides: Partial<PublicContentAdapter> = {},
91
- ): PublicContentAdapter {
92
- return {
93
- async getSiteConfig() {
94
- return publicSiteFixtureSnapshot.site;
95
- },
96
- async getMarketingPage(pageKey) {
97
- return getFixtureMarketingPage(pageKey);
98
- },
99
- async listEntries(kind) {
100
- return kind === "docs"
101
- ? getFixtureEntrySummaries("docs")
102
- : getFixtureEntrySummaries("blog");
103
- },
104
- async getEntry(kind, slugParts) {
105
- return getFixtureEntry(kind, slugParts);
106
- },
107
- ...overrides,
108
- };
109
- }
110
-
111
- function createEnvelope(
112
- overrides: Partial<Awaited<ReturnType<typeof fetchPublicSiteSnapshotEnvelope>>> = {},
113
- ) {
114
- return {
115
- environment: "preview" as const,
116
- source_boundary: "runtime_preview" as const,
117
- snapshot: publicSiteFixtureSnapshot,
118
- ...overrides,
119
- };
120
- }
121
-
122
- function restoreProcessEnv() {
123
- for (const key of Object.keys(process.env)) {
124
- if (!(key in originalEnv)) {
125
- delete process.env[key];
126
- }
127
- }
128
-
129
- Object.assign(process.env, originalEnv);
130
- }
131
-
132
- function applyServerEnv(overrides: Record<string, string | undefined>) {
133
- restoreProcessEnv();
134
- process.env.MW_PLATFORM_BASE_URL = "http://127.0.0.1:8000";
135
- process.env.MW_PUBLIC_CONTENT_SOURCE = "minutework_cms";
136
- process.env.MW_CONTENT_API_TOKEN = "test-content-token";
137
- process.env.MW_PUBLIC_BASE_URL = "http://127.0.0.1:3000";
138
- process.env.MW_PUBLIC_SITE_PROPERTY_KEY = "main-site";
139
- process.env.MW_PUBLIC_SITE_ENV = "preview";
140
- process.env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE = "false";
141
-
142
- for (const [key, value] of Object.entries(overrides)) {
143
- if (value === undefined) {
144
- delete process.env[key];
145
- } else {
146
- process.env[key] = value;
147
- }
148
- }
149
- }
150
-
151
- describe("public content adapter", () => {
152
- afterEach(() => {
153
- restoreProcessEnv();
154
- vi.unstubAllGlobals();
155
- vi.resetModules();
156
- vi.doUnmock("@/lib/content/custom-adapter");
157
- });
158
-
159
- it("loads a public-site snapshot envelope through a cacheable bearer-authenticated fetch", async () => {
160
- const fetchMock = vi.fn().mockResolvedValue(jsonResponse(createEnvelope()));
161
- vi.stubGlobal("fetch", fetchMock);
162
-
163
- const envelope = await fetchPublicSiteSnapshotEnvelope({
164
- contentApiToken: "content-token",
165
- environment: "preview",
166
- platformBaseUrl: "https://platform.example.com",
167
- propertyKey: "main-site",
168
- });
169
-
170
- expect(envelope.snapshot.site.siteName).toBe("PandaWork Combined Starter");
171
- expect(fetchMock).toHaveBeenCalledWith(
172
- "https://platform.example.com/api/v1/developer/public-site/snapshots/main-site/?environment=preview",
173
- expect.objectContaining({
174
- headers: {
175
- accept: "application/json",
176
- authorization: "Bearer content-token",
177
- },
178
- next: {
179
- revalidate: PUBLIC_CONTENT_REVALIDATE_SECONDS,
180
- tags: [
181
- "public-site-content",
182
- "public-site-content:main-site",
183
- "public-site-content:preview",
184
- ],
185
- },
186
- }),
187
- );
188
- });
189
-
190
- it("rejects invalid environment and source-boundary combinations", async () => {
191
- const fetchMock = vi.fn().mockResolvedValue(
192
- jsonResponse(
193
- createEnvelope({
194
- environment: "live",
195
- source_boundary: "runtime_preview",
196
- }),
197
- ),
198
- );
199
- vi.stubGlobal("fetch", fetchMock);
200
-
201
- await expect(
202
- fetchPublicSiteSnapshotEnvelope({
203
- contentApiToken: "content-token",
204
- environment: "live",
205
- platformBaseUrl: "https://platform.example.com",
206
- propertyKey: "main-site",
207
- }),
208
- ).rejects.toThrow("published_live");
209
- });
210
-
211
- it("accepts runtime_live for live runtime-served publications", async () => {
212
- const fetchMock = vi.fn().mockResolvedValue(
213
- jsonResponse(
214
- createEnvelope({
215
- environment: "live",
216
- source_boundary: "runtime_live",
217
- }),
218
- ),
219
- );
220
- vi.stubGlobal("fetch", fetchMock);
221
-
222
- await expect(
223
- fetchPublicSiteSnapshotEnvelope({
224
- contentApiToken: "content-token",
225
- environment: "live",
226
- platformBaseUrl: "https://platform.example.com",
227
- propertyKey: "main-site",
228
- }),
229
- ).resolves.toMatchObject({
230
- environment: "live",
231
- source_boundary: "runtime_live",
232
- });
233
- });
234
-
235
- it("maps PandaWork public-site snapshots through the adapter interface", async () => {
236
- const adapter = new MinuteWorkPublicSiteContentAdapter(async () =>
237
- createEnvelope({
238
- environment: "live",
239
- source_boundary: "published_live",
240
- }),
241
- );
242
-
243
- await expect(adapter.getMarketingPage("home")).resolves.toEqual(
244
- publicSiteFixtureSnapshot.marketingPages[0],
245
- );
246
- await expect(adapter.listEntries("blog")).resolves.toEqual(
247
- publicSiteFixtureSnapshot.blog,
248
- );
249
- await expect(
250
- adapter.getEntry("blog", ["public-site-api-default"]),
251
- ).resolves.toEqual(publicSiteFixtureSnapshot.blog[0]);
252
- });
253
-
254
- it("allows empty marketing-page snapshots from the default gateway adapter", async () => {
255
- const adapter = new MinuteWorkPublicSiteContentAdapter(async () =>
256
- createEnvelope({
257
- snapshot: {
258
- ...publicSiteFixtureSnapshot,
259
- marketingPages: [],
260
- },
261
- }),
262
- );
263
-
264
- await expect(adapter.getMarketingPage("pricing")).resolves.toBeNull();
265
- });
266
-
267
- it("loads the default PandaWork public-site path when no custom adapter is provided", async () => {
268
- const fetchMock = vi.fn().mockResolvedValue(jsonResponse(createEnvelope()));
269
- vi.stubGlobal("fetch", fetchMock);
270
- applyServerEnv({});
271
- vi.doMock("@/lib/content/custom-adapter", () => ({
272
- resolveCustomPublicContentAdapter: () => null,
273
- }));
274
-
275
- const adapterModule = await import("./adapter.server");
276
-
277
- await expect(adapterModule.getSiteConfig()).resolves.toEqual(
278
- publicSiteFixtureSnapshot.site,
279
- );
280
- expect(fetchMock).toHaveBeenCalledTimes(1);
281
- });
282
-
283
- it("accepts valid custom adapter output through the shared contract wrapper", async () => {
284
- applyServerEnv({
285
- MW_PUBLIC_CONTENT_SOURCE: "custom",
286
- MW_CONTENT_API_TOKEN: undefined,
287
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
288
- });
289
- vi.doMock("@/lib/content/custom-adapter", () => ({
290
- resolveCustomPublicContentAdapter: () => createCustomAdapter(),
291
- }));
292
-
293
- const adapterModule = await import("./adapter.server");
294
-
295
- await expect(adapterModule.getMarketingPage("pricing")).resolves.toEqual(
296
- getFixtureMarketingPage("pricing"),
297
- );
298
- await expect(adapterModule.listEntries("blog")).resolves.toEqual(
299
- getFixtureEntrySummaries("blog"),
300
- );
301
- });
302
-
303
- it("requires an explicit adapter when MW_PUBLIC_CONTENT_SOURCE=custom", async () => {
304
- applyServerEnv({
305
- MW_PUBLIC_CONTENT_SOURCE: "custom",
306
- MW_CONTENT_API_TOKEN: undefined,
307
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
308
- });
309
- vi.doMock("@/lib/content/custom-adapter", () => ({
310
- resolveCustomPublicContentAdapter: () => null,
311
- }));
312
-
313
- const adapterModule = await import("./adapter.server");
314
-
315
- expect(() => adapterModule.getPublicContentAdapter()).toThrow(
316
- "MW_PUBLIC_CONTENT_SOURCE=custom requires resolveCustomPublicContentAdapter()",
317
- );
318
- });
319
-
320
- it("loads valid static JSON public content", async () => {
321
- const tempRoot = await mkdtemp(join(tmpdir(), "mw-public-content-"));
322
- const contentPath = join(tempRoot, "public-site.json");
323
-
324
- try {
325
- await writeFile(
326
- contentPath,
327
- JSON.stringify(publicSiteFixtureSnapshot),
328
- "utf8",
329
- );
330
- applyServerEnv({
331
- MW_PUBLIC_CONTENT_SOURCE: "static_json",
332
- MW_CONTENT_API_TOKEN: undefined,
333
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
334
- MW_STATIC_PUBLIC_CONTENT_PATH: contentPath,
335
- });
336
-
337
- const adapterModule = await import("./adapter.server");
338
-
339
- await expect(adapterModule.getMarketingPage("home")).resolves.toEqual(
340
- getFixtureMarketingPage("home"),
341
- );
342
- } finally {
343
- await rm(tempRoot, { force: true, recursive: true });
344
- }
345
- });
346
-
347
- it("rejects invalid static JSON public content", async () => {
348
- const tempRoot = await mkdtemp(join(tmpdir(), "mw-public-content-"));
349
- const contentPath = join(tempRoot, "public-site.json");
350
-
351
- try {
352
- await writeFile(contentPath, JSON.stringify({ site: {} }), "utf8");
353
- applyServerEnv({
354
- MW_PUBLIC_CONTENT_SOURCE: "static_json",
355
- MW_CONTENT_API_TOKEN: undefined,
356
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
357
- MW_STATIC_PUBLIC_CONTENT_PATH: contentPath,
358
- });
359
-
360
- const adapterModule = await import("./adapter.server");
361
-
362
- await expect(adapterModule.getSiteConfig()).rejects.toThrow(
363
- "Static public content is invalid",
364
- );
365
- } finally {
366
- await rm(tempRoot, { force: true, recursive: true });
367
- }
368
- });
369
-
370
- it("keeps public content disabled for private-only mode", async () => {
371
- applyServerEnv({
372
- MW_PUBLIC_CONTENT_SOURCE: "none",
373
- MW_CONTENT_API_TOKEN: undefined,
374
- MW_PUBLIC_BASE_URL: undefined,
375
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
376
- });
377
-
378
- const adapterModule = await import("./adapter.server");
379
-
380
- await expect(adapterModule.getSiteConfig()).rejects.toThrow(
381
- "Public content is disabled",
382
- );
383
- });
384
-
385
- it("rejects a custom adapter when a marketing page violates the canonical route contract", async () => {
386
- applyServerEnv({
387
- MW_PUBLIC_CONTENT_SOURCE: "custom",
388
- MW_CONTENT_API_TOKEN: undefined,
389
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
390
- });
391
- vi.doMock("@/lib/content/custom-adapter", () => ({
392
- resolveCustomPublicContentAdapter: () =>
393
- createCustomAdapter({
394
- async getMarketingPage(pageKey) {
395
- const page = getFixtureMarketingPage(pageKey);
396
-
397
- if (!page || pageKey !== "pricing") {
398
- return page;
399
- }
400
-
401
- return {
402
- ...page,
403
- path: "/plans",
404
- };
405
- },
406
- }),
407
- }));
408
-
409
- const adapterModule = await import("./adapter.server");
410
-
411
- await expect(adapterModule.getMarketingPage("pricing")).rejects.toThrow(
412
- '"pricing" marketing page must use the "/pricing" route.',
413
- );
414
- });
415
-
416
- it("rejects a custom adapter when blog summaries use multi-segment slugs", async () => {
417
- applyServerEnv({
418
- MW_PUBLIC_CONTENT_SOURCE: "custom",
419
- MW_CONTENT_API_TOKEN: undefined,
420
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
421
- });
422
- vi.doMock("@/lib/content/custom-adapter", () => ({
423
- resolveCustomPublicContentAdapter: () =>
424
- createCustomAdapter({
425
- async listEntries(kind) {
426
- if (kind !== "blog") {
427
- return getFixtureEntrySummaries("docs");
428
- }
429
-
430
- return publicSiteFixtureSnapshot.blog.map((entry) => ({
431
- ...toBlogEntrySummary(entry),
432
- slug: ["release", "v1"],
433
- })) as unknown as Awaited<ReturnType<PublicContentAdapter["listEntries"]>>;
434
- },
435
- }),
436
- }));
437
-
438
- const adapterModule = await import("./adapter.server");
439
-
440
- await expect(adapterModule.listEntries("blog")).rejects.toThrow(
441
- "Array must contain at most 1 element(s)",
442
- );
443
- });
444
- });