jui-tools-mcp-server 2.5.0

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 (172) hide show
  1. package/.github/workflows/test.yml +35 -0
  2. package/README.md +148 -0
  3. package/data/attribute_definitions.json +4248 -0
  4. package/data/attribute_semantics.json +137 -0
  5. package/data/binding_semantics.json +115 -0
  6. package/data/component_metadata.json +769 -0
  7. package/data/coverage.json +114 -0
  8. package/data/screen_identity.json +350 -0
  9. package/dist/cli_runner.d.ts +10 -0
  10. package/dist/cli_runner.js +33 -0
  11. package/dist/config.d.ts +19 -0
  12. package/dist/config.js +67 -0
  13. package/dist/data/component_metadata.d.ts +30 -0
  14. package/dist/data/component_metadata.js +498 -0
  15. package/dist/data/derived.d.ts +198 -0
  16. package/dist/data/derived.js +293 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +130 -0
  19. package/dist/spec_loader.d.ts +110 -0
  20. package/dist/spec_loader.js +445 -0
  21. package/dist/tools/api/list_api_models.d.ts +10 -0
  22. package/dist/tools/api/list_api_models.js +37 -0
  23. package/dist/tools/api/list_api_specs.d.ts +19 -0
  24. package/dist/tools/api/list_api_specs.js +42 -0
  25. package/dist/tools/api/preview_api_model_sync.d.ts +11 -0
  26. package/dist/tools/api/preview_api_model_sync.js +34 -0
  27. package/dist/tools/context/get_project_config.d.ts +3 -0
  28. package/dist/tools/context/get_project_config.js +20 -0
  29. package/dist/tools/context/list_component_specs.d.ts +3 -0
  30. package/dist/tools/context/list_component_specs.js +35 -0
  31. package/dist/tools/context/list_layouts.d.ts +3 -0
  32. package/dist/tools/context/list_layouts.js +118 -0
  33. package/dist/tools/context/list_screen_specs.d.ts +3 -0
  34. package/dist/tools/context/list_screen_specs.js +37 -0
  35. package/dist/tools/context/read_layout_file.d.ts +3 -0
  36. package/dist/tools/context/read_layout_file.js +25 -0
  37. package/dist/tools/context/read_spec_file.d.ts +3 -0
  38. package/dist/tools/context/read_spec_file.js +27 -0
  39. package/dist/tools/doc/doc_generate_component.d.ts +3 -0
  40. package/dist/tools/doc/doc_generate_component.js +38 -0
  41. package/dist/tools/doc/doc_generate_html.d.ts +3 -0
  42. package/dist/tools/doc/doc_generate_html.js +46 -0
  43. package/dist/tools/doc/doc_generate_spec.d.ts +3 -0
  44. package/dist/tools/doc/doc_generate_spec.js +38 -0
  45. package/dist/tools/doc/doc_init_component.d.ts +3 -0
  46. package/dist/tools/doc/doc_init_component.js +30 -0
  47. package/dist/tools/doc/doc_init_spec.d.ts +3 -0
  48. package/dist/tools/doc/doc_init_spec.js +33 -0
  49. package/dist/tools/doc/doc_rules_init.d.ts +3 -0
  50. package/dist/tools/doc/doc_rules_init.js +25 -0
  51. package/dist/tools/doc/doc_rules_show.d.ts +3 -0
  52. package/dist/tools/doc/doc_rules_show.js +21 -0
  53. package/dist/tools/doc/doc_validate_component.d.ts +3 -0
  54. package/dist/tools/doc/doc_validate_component.js +39 -0
  55. package/dist/tools/doc/doc_validate_spec.d.ts +3 -0
  56. package/dist/tools/doc/doc_validate_spec.js +40 -0
  57. package/dist/tools/jui/jui_build.d.ts +3 -0
  58. package/dist/tools/jui/jui_build.js +31 -0
  59. package/dist/tools/jui/jui_generate_converter.d.ts +3 -0
  60. package/dist/tools/jui/jui_generate_converter.js +43 -0
  61. package/dist/tools/jui/jui_generate_project.d.ts +3 -0
  62. package/dist/tools/jui/jui_generate_project.js +43 -0
  63. package/dist/tools/jui/jui_generate_screen.d.ts +3 -0
  64. package/dist/tools/jui/jui_generate_screen.js +22 -0
  65. package/dist/tools/jui/jui_init.d.ts +3 -0
  66. package/dist/tools/jui/jui_init.js +42 -0
  67. package/dist/tools/jui/jui_migrate_layouts.d.ts +3 -0
  68. package/dist/tools/jui/jui_migrate_layouts.js +25 -0
  69. package/dist/tools/jui/jui_sync_tool.d.ts +3 -0
  70. package/dist/tools/jui/jui_sync_tool.js +33 -0
  71. package/dist/tools/jui/jui_verify.d.ts +3 -0
  72. package/dist/tools/jui/jui_verify.js +43 -0
  73. package/dist/tools/spec/get_binding_rules.d.ts +3 -0
  74. package/dist/tools/spec/get_binding_rules.js +8 -0
  75. package/dist/tools/spec/get_data_source.d.ts +3 -0
  76. package/dist/tools/spec/get_data_source.js +41 -0
  77. package/dist/tools/spec/get_modifier_order.d.ts +3 -0
  78. package/dist/tools/spec/get_modifier_order.js +14 -0
  79. package/dist/tools/spec/get_platform_mapping.d.ts +3 -0
  80. package/dist/tools/spec/get_platform_mapping.js +20 -0
  81. package/dist/tools/spec/get_screen_identity.d.ts +3 -0
  82. package/dist/tools/spec/get_screen_identity.js +22 -0
  83. package/dist/tools/spec/lookup_attribute.d.ts +3 -0
  84. package/dist/tools/spec/lookup_attribute.js +14 -0
  85. package/dist/tools/spec/lookup_component.d.ts +3 -0
  86. package/dist/tools/spec/lookup_component.js +15 -0
  87. package/dist/tools/spec/search_components.d.ts +3 -0
  88. package/dist/tools/spec/search_components.js +14 -0
  89. package/dist/tools/test/test_artifacts_pull.d.ts +3 -0
  90. package/dist/tools/test/test_artifacts_pull.js +48 -0
  91. package/dist/tools/test/test_artifacts_status.d.ts +3 -0
  92. package/dist/tools/test/test_artifacts_status.js +28 -0
  93. package/dist/tools/test/test_generate_description.d.ts +3 -0
  94. package/dist/tools/test/test_generate_description.js +27 -0
  95. package/dist/tools/test/test_generate_flow.d.ts +3 -0
  96. package/dist/tools/test/test_generate_flow.js +29 -0
  97. package/dist/tools/test/test_generate_screen.d.ts +3 -0
  98. package/dist/tools/test/test_generate_screen.js +29 -0
  99. package/dist/tools/test/test_mock_generate.d.ts +3 -0
  100. package/dist/tools/test/test_mock_generate.js +64 -0
  101. package/dist/tools/test/test_report.d.ts +3 -0
  102. package/dist/tools/test/test_report.js +26 -0
  103. package/dist/tools/test/test_validate.d.ts +3 -0
  104. package/dist/tools/test/test_validate.js +43 -0
  105. package/docs/design.md +227 -0
  106. package/install.sh +90 -0
  107. package/package.json +24 -0
  108. package/scripts/fetch-definitions.js +101 -0
  109. package/scripts/fetch-manifest.js +60 -0
  110. package/src/cli_runner.ts +55 -0
  111. package/src/config.ts +88 -0
  112. package/src/data/derived.ts +305 -0
  113. package/src/index.ts +157 -0
  114. package/src/spec_loader.ts +591 -0
  115. package/src/tools/api/list_api_models.ts +44 -0
  116. package/src/tools/api/list_api_specs.ts +53 -0
  117. package/src/tools/api/preview_api_model_sync.ts +41 -0
  118. package/src/tools/context/get_project_config.ts +27 -0
  119. package/src/tools/context/list_component_specs.ts +44 -0
  120. package/src/tools/context/list_layouts.ts +131 -0
  121. package/src/tools/context/list_screen_specs.ts +46 -0
  122. package/src/tools/context/read_layout_file.ts +35 -0
  123. package/src/tools/context/read_spec_file.ts +38 -0
  124. package/src/tools/doc/doc_generate_component.ts +43 -0
  125. package/src/tools/doc/doc_generate_html.ts +40 -0
  126. package/src/tools/doc/doc_generate_spec.ts +43 -0
  127. package/src/tools/doc/doc_init_component.ts +32 -0
  128. package/src/tools/doc/doc_init_spec.ts +37 -0
  129. package/src/tools/doc/doc_rules_init.ts +29 -0
  130. package/src/tools/doc/doc_rules_show.ts +27 -0
  131. package/src/tools/doc/doc_validate_component.ts +47 -0
  132. package/src/tools/doc/doc_validate_spec.ts +48 -0
  133. package/src/tools/jui/jui_build.ts +31 -0
  134. package/src/tools/jui/jui_generate_converter.ts +41 -0
  135. package/src/tools/jui/jui_generate_project.ts +37 -0
  136. package/src/tools/jui/jui_generate_screen.ts +28 -0
  137. package/src/tools/jui/jui_init.ts +38 -0
  138. package/src/tools/jui/jui_migrate_layouts.ts +29 -0
  139. package/src/tools/jui/jui_sync_tool.ts +33 -0
  140. package/src/tools/jui/jui_verify.ts +43 -0
  141. package/src/tools/spec/get_binding_rules.ts +16 -0
  142. package/src/tools/spec/get_data_source.ts +53 -0
  143. package/src/tools/spec/get_modifier_order.ts +22 -0
  144. package/src/tools/spec/get_platform_mapping.ts +28 -0
  145. package/src/tools/spec/get_screen_identity.ts +31 -0
  146. package/src/tools/spec/lookup_attribute.ts +22 -0
  147. package/src/tools/spec/lookup_component.ts +23 -0
  148. package/src/tools/spec/search_components.ts +22 -0
  149. package/src/tools/test/test_artifacts_pull.ts +47 -0
  150. package/src/tools/test/test_artifacts_status.ts +34 -0
  151. package/src/tools/test/test_generate_description.ts +33 -0
  152. package/src/tools/test/test_generate_flow.ts +33 -0
  153. package/src/tools/test/test_generate_screen.ts +33 -0
  154. package/src/tools/test/test_mock_generate.ts +58 -0
  155. package/src/tools/test/test_report.ts +32 -0
  156. package/src/tools/test/test_validate.ts +43 -0
  157. package/tests/__snapshots__/derived.test.ts.snap +279 -0
  158. package/tests/bundled_snapshot.test.ts +155 -0
  159. package/tests/cli_runner.test.ts +120 -0
  160. package/tests/config.test.ts +212 -0
  161. package/tests/derived.test.ts +114 -0
  162. package/tests/helpers.ts +272 -0
  163. package/tests/setup.ts +27 -0
  164. package/tests/snapshot_completeness.test.ts +152 -0
  165. package/tests/spec_loader.test.ts +488 -0
  166. package/tests/tools_cli.test.ts +687 -0
  167. package/tests/tools_context.test.ts +274 -0
  168. package/tests/tools_spec.test.ts +225 -0
  169. package/tests/tools_test.test.ts +284 -0
  170. package/tsconfig.json +15 -0
  171. package/uninstall.sh +46 -0
  172. package/vitest.config.ts +15 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Shared fixtures and harnesses for the MCP server test suite.
3
+ *
4
+ * Everything here is hermetic: fixture datasets are written to fresh temp
5
+ * directories, and the tool harness invokes tool handlers directly (after
6
+ * emulating the SDK-side Zod validation) without opening any transport.
7
+ */
8
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
9
+ import { tmpdir } from "os";
10
+ import { dirname, join } from "path";
11
+ import { z, type ZodRawShape } from "zod";
12
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+
14
+ // ----- temp dirs ----------------------------------------------------------
15
+
16
+ const createdDirs: string[] = [];
17
+
18
+ /** Create a throwaway temp directory, tracked for cleanupTempDirs(). */
19
+ export function makeTempDir(label = "fixture"): string {
20
+ const dir = mkdtempSync(join(tmpdir(), `jui-mcp-test-${label}-`));
21
+ createdDirs.push(dir);
22
+ return dir;
23
+ }
24
+
25
+ /** Remove every directory created via makeTempDir() (call in afterEach/afterAll). */
26
+ export function cleanupTempDirs(): void {
27
+ while (createdDirs.length > 0) {
28
+ const dir = createdDirs.pop()!;
29
+ rmSync(dir, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ export function writeJson(filePath: string, data: unknown): void {
34
+ mkdirSync(dirname(filePath), { recursive: true });
35
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
36
+ }
37
+
38
+ // ----- fixture jsonui-cli dataset ----------------------------------------
39
+
40
+ /**
41
+ * Minimal but representative attribute_definitions.json:
42
+ * - `_comment` top-level key (must be skipped)
43
+ * - `common` section with `_meta` (skipped), categorized + uncategorized keys
44
+ * - Label: binding attr WITHOUT binding_direction -> read-only
45
+ * - TextField: binding attr WITH binding_direction -> two-way
46
+ * - Button: no binding attrs, no metadata entry -> default metadata
47
+ */
48
+ export const FIXTURE_ATTRIBUTE_DEFINITIONS = {
49
+ _comment: "test fixture — not real data",
50
+ common: {
51
+ _meta: { version: "0.0.1" },
52
+ width: { type: ["string", "int"], description: "View width" },
53
+ padding: { type: "int", description: "Inner padding" },
54
+ background: { type: "string", description: "Background color" },
55
+ onClick: { type: "binding", description: "Tap handler binding" },
56
+ glowIntensity: { type: "float", description: "Uncategorized zebra attribute" },
57
+ },
58
+ Label: {
59
+ _internal: { note: "must be skipped" },
60
+ text: { type: ["string", "binding"], description: "Text content" },
61
+ fontSize: { type: "int", description: "Font size in points" },
62
+ },
63
+ TextField: {
64
+ text: {
65
+ type: ["string", "binding"],
66
+ description: "Text content (two-way)",
67
+ binding_direction: "two-way",
68
+ },
69
+ hint: { type: "string", description: "Placeholder hint" },
70
+ },
71
+ Button: {
72
+ onclick: { type: "string", description: "Selector-format tap handler" },
73
+ enabled: { type: ["boolean", "binding"], description: "Enabled state" },
74
+ },
75
+ // Component alias in the B1 pointer shape. Deliberately NOT listed in any
76
+ // metadata `aliases` array (the Toggle -> Switch case): the loader must
77
+ // resolve it from `_alias_of` alone instead of registering an
78
+ // empty-attributes component.
79
+ Flip: {
80
+ _alias_of: "Button",
81
+ _comment: "component alias fixture",
82
+ },
83
+ };
84
+
85
+ export const FIXTURE_COMPONENT_METADATA = {
86
+ _description: "test fixture metadata",
87
+ _schema: { version: 1 },
88
+ Label: {
89
+ description: "Text display component",
90
+ aliases: ["Text"],
91
+ platforms: { react: false },
92
+ platformSpecific: { swift: { renderer: "SwiftUI Text" } },
93
+ rules: ["Prefer fontColor over textColor"],
94
+ },
95
+ TextField: {
96
+ description: "Single-line text input",
97
+ aliases: ["EditText", "Input"],
98
+ },
99
+ // Button intentionally absent -> DEFAULT_METADATA path
100
+ };
101
+
102
+ export interface CliDatasetOptions {
103
+ attributeDefinitions?: unknown;
104
+ componentMetadata?: unknown;
105
+ /** Skip writing attribute_definitions.json (to test per-file fallback). */
106
+ omitAttributeDefinitions?: boolean;
107
+ /** Skip writing component_metadata.json (to test per-file fallback). */
108
+ omitComponentMetadata?: boolean;
109
+ /** Optional conformance/coverage.json fixture (implementation-gap ledger). */
110
+ coverage?: Record<string, unknown>;
111
+ }
112
+
113
+ /** Coverage-ledger fixture: one gap on Label.fontSize [web] + one on common.onClick [android]. */
114
+ export const FIXTURE_COVERAGE = {
115
+ schemaVersion: 1,
116
+ entries: [
117
+ {
118
+ component: "Label",
119
+ attribute: "fontSize",
120
+ platforms: ["web"],
121
+ reason: "unimplemented",
122
+ note: "test gap — web codegen does not read it",
123
+ },
124
+ {
125
+ component: "common",
126
+ attribute: "onClick",
127
+ platforms: ["android"],
128
+ reason: "legacy",
129
+ note: "test gap — alias normalized before conversion",
130
+ },
131
+ ],
132
+ };
133
+
134
+ /** Write a fixture jsonui-cli checkout layout: <root>/shared/core/*.json */
135
+ export function makeCliDataset(root: string, opts: CliDatasetOptions = {}): void {
136
+ const core = join(root, "shared", "core");
137
+ if (!opts.omitAttributeDefinitions) {
138
+ writeJson(
139
+ join(core, "attribute_definitions.json"),
140
+ opts.attributeDefinitions ?? FIXTURE_ATTRIBUTE_DEFINITIONS
141
+ );
142
+ }
143
+ if (!opts.omitComponentMetadata) {
144
+ writeJson(
145
+ join(core, "component_metadata.json"),
146
+ opts.componentMetadata ?? FIXTURE_COMPONENT_METADATA
147
+ );
148
+ }
149
+ if (opts.coverage) {
150
+ writeJson(join(root, "conformance", "coverage.json"), opts.coverage);
151
+ }
152
+ }
153
+
154
+ /** Write a fixture bundled snapshot layout: <mcpRoot>/data/*.json */
155
+ export function makeBundledDataset(root: string, opts: CliDatasetOptions = {}): void {
156
+ const dataDir = join(root, "data");
157
+ if (!opts.omitAttributeDefinitions) {
158
+ writeJson(
159
+ join(dataDir, "attribute_definitions.json"),
160
+ opts.attributeDefinitions ?? FIXTURE_ATTRIBUTE_DEFINITIONS
161
+ );
162
+ }
163
+ if (!opts.omitComponentMetadata) {
164
+ writeJson(
165
+ join(dataDir, "component_metadata.json"),
166
+ opts.componentMetadata ?? FIXTURE_COMPONENT_METADATA
167
+ );
168
+ }
169
+ }
170
+
171
+ // ----- fixture JsonUI project ---------------------------------------------
172
+
173
+ export const FIXTURE_PROJECT_CONFIG = {
174
+ project_name: "ExampleApp",
175
+ spec_directory: "docs/screens/json",
176
+ layouts_directory: "layouts",
177
+ styles_directory: "layouts/Styles",
178
+ images_directory: "images",
179
+ component_spec_directory: "docs/components/json",
180
+ strings_file: "strings.json",
181
+ type_map_file: ".jsonui-type-map.json",
182
+ platforms: { ios: { path: "ios", mode: "swiftui" } },
183
+ };
184
+
185
+ /**
186
+ * Create a minimal JsonUI project in `root`:
187
+ * jui.config.json, two screen specs (one intentionally corrupt),
188
+ * one component spec, nested layout files.
189
+ */
190
+ export function makeFixtureProject(root: string): void {
191
+ writeJson(join(root, "jui.config.json"), FIXTURE_PROJECT_CONFIG);
192
+
193
+ writeJson(join(root, "docs/screens/json/login.spec.json"), {
194
+ type: "screen_spec",
195
+ metadata: {
196
+ name: "Login",
197
+ displayName: "Login Screen",
198
+ layoutFile: "login",
199
+ },
200
+ });
201
+ // Corrupt JSON: list_screen_specs must degrade gracefully, not crash.
202
+ mkdirSync(join(root, "docs/screens/json"), { recursive: true });
203
+ writeFileSync(join(root, "docs/screens/json/broken.spec.json"), "{ not json !");
204
+
205
+ writeJson(join(root, "docs/components/json/example_card.component.json"), {
206
+ type: "component_spec",
207
+ metadata: { name: "ExampleCard", category: "card" },
208
+ });
209
+
210
+ writeJson(join(root, "layouts/login.json"), {
211
+ type: "View",
212
+ id: "login_root",
213
+ child: [{ type: "Label", text: "Welcome" }],
214
+ });
215
+ writeJson(join(root, "layouts/Styles/common.json"), {
216
+ defaultFontColor: "#333333",
217
+ });
218
+ // Non-JSON file must not be listed by list_layouts.
219
+ writeFileSync(join(root, "layouts/README.txt"), "not a layout");
220
+ }
221
+
222
+ // ----- MCP tool capture harness -------------------------------------------
223
+
224
+ export interface CapturedTool {
225
+ description: string;
226
+ schema: ZodRawShape;
227
+ handler: (params: Record<string, unknown>) => Promise<{
228
+ content: Array<{ type: string; text: string }>;
229
+ }>;
230
+ }
231
+
232
+ export interface ToolHarness {
233
+ server: McpServer;
234
+ tools: Map<string, CapturedTool>;
235
+ /**
236
+ * Invoke a registered tool. Params are validated through the captured Zod
237
+ * raw shape first, mirroring what the MCP SDK does before dispatch.
238
+ * Returns the text of the first content item.
239
+ */
240
+ call(name: string, params?: Record<string, unknown>): Promise<string>;
241
+ }
242
+
243
+ export function createToolHarness(): ToolHarness {
244
+ const tools = new Map<string, CapturedTool>();
245
+ const server = {
246
+ tool(
247
+ name: string,
248
+ description: string,
249
+ schema: ZodRawShape,
250
+ handler: CapturedTool["handler"]
251
+ ) {
252
+ tools.set(name, { description, schema, handler });
253
+ },
254
+ } as unknown as McpServer;
255
+
256
+ async function call(
257
+ name: string,
258
+ params: Record<string, unknown> = {}
259
+ ): Promise<string> {
260
+ const tool = tools.get(name);
261
+ if (!tool) {
262
+ throw new Error(
263
+ `Tool not registered: ${name}. Registered: ${[...tools.keys()].join(", ")}`
264
+ );
265
+ }
266
+ const parsed = z.object(tool.schema).parse(params);
267
+ const result = await tool.handler(parsed);
268
+ return result.content[0].text;
269
+ }
270
+
271
+ return { server, tools, call };
272
+ }
package/tests/setup.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Global test setup — makes every test hermetic by default.
3
+ *
4
+ * The server reads three ambient inputs that must never leak in from the
5
+ * machine running the tests:
6
+ *
7
+ * - `$JSONUI_CLI_PATH` (layer 1 of the SpecLoader fallback)
8
+ * - `$HOME` / `~/.jsonui-cli` (layer 3 — real installs exist on dev boxes)
9
+ * - `$JUI_PROJECT_DIR` (ServerConfig default project dir)
10
+ *
11
+ * We delete the env vars and point HOME at a fresh empty temp directory.
12
+ * Individual tests that exercise a specific layer re-set these explicitly.
13
+ *
14
+ * Note: process.cwd() (layer 2) is handled per-suite via process.chdir()
15
+ * because it can't be neutralized globally without breaking module paths.
16
+ */
17
+ import { mkdtempSync } from "fs";
18
+ import { tmpdir } from "os";
19
+ import { join } from "path";
20
+
21
+ delete process.env.JSONUI_CLI_PATH;
22
+ delete process.env.JUI_PROJECT_DIR;
23
+
24
+ const isolatedHome = mkdtempSync(join(tmpdir(), "jui-mcp-test-home-"));
25
+ process.env.HOME = isolatedHome;
26
+ // Windows fallback used by os.homedir(); harmless elsewhere.
27
+ process.env.USERPROFILE = isolatedHome;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Snapshot completeness — the structural guard against the "bundled but not
3
+ * auto-fetched" trap. binding_semantics.json fell into it (diagnosed, fixed,
4
+ * commented), then coverage.json fell into the very same hole. The root cause
5
+ * was that nothing enforced the invariant; this suite is that enforcement.
6
+ *
7
+ * Three surfaces must agree about the set of jsonui-cli data files:
8
+ *
9
+ * data/ (bundled) ⊆ fetch-manifest FILES ⊆ what SpecLoader reads
10
+ *
11
+ * A. Every file bundled in data/ is in the auto-fetch manifest — otherwise
12
+ * it only ever refreshes by hand and silently goes stale.
13
+ * B. Every manifest entry is actually read AND reported by the SpecLoader —
14
+ * otherwise postinstall fetches dead weight nobody serves, or serves a
15
+ * file get_data_source cannot account for. Checked on both layers:
16
+ * the bundled fallback (data/<local>) and a synthetic jsonui-cli
17
+ * checkout laid out from the manifest's <remote> paths, so a drifting
18
+ * remote path is caught too.
19
+ */
20
+ import { copyFileSync, mkdirSync, readdirSync } from "fs";
21
+ import { fileURLToPath } from "url";
22
+ import { basename, dirname, join, resolve } from "path";
23
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
24
+
25
+ import { SpecLoader, type DataSourceInfo } from "../src/spec_loader.js";
26
+ import { cleanupTempDirs, makeTempDir } from "./helpers.js";
27
+ // Side-effect-free manifest module: importing it must never trigger a fetch.
28
+ import { FILES } from "../scripts/fetch-manifest.js";
29
+
30
+ interface ManifestEntry {
31
+ remote: string;
32
+ local: string;
33
+ envOverride?: string;
34
+ }
35
+
36
+ const manifest = FILES as ManifestEntry[];
37
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
38
+ const originalCwd = process.cwd();
39
+
40
+ /** Every file the loader read, as reported by get_data_source. */
41
+ function reportedFiles(source: DataSourceInfo) {
42
+ return [
43
+ source.attributeDefinitions,
44
+ source.componentMetadata,
45
+ source.screenIdentity,
46
+ source.bindingSemantics,
47
+ source.attributeSemantics,
48
+ source.coverage,
49
+ ].filter((f): f is NonNullable<typeof f> => f != null);
50
+ }
51
+
52
+ let bundledLoader: SpecLoader;
53
+ let cliLoader: SpecLoader;
54
+ let cliRoot: string;
55
+
56
+ beforeAll(() => {
57
+ // Neutralize the env/cwd/home layers, then load once from this repo's
58
+ // committed data/ snapshot (same harness as bundled_snapshot.test.ts).
59
+ delete process.env.JSONUI_CLI_PATH;
60
+ const home = makeTempDir("home");
61
+ process.env.HOME = home;
62
+ process.env.USERPROFILE = home;
63
+ process.chdir(makeTempDir("cwd"));
64
+ bundledLoader = new SpecLoader(repoRoot);
65
+
66
+ // Synthetic jsonui-cli checkout built FROM the manifest: each entry's
67
+ // bundled snapshot copied to its <remote> path. Loading it through the env
68
+ // layer (against an empty MCP root, so nothing can fall back) proves the
69
+ // manifest's remote paths are exactly where the loader looks.
70
+ cliRoot = makeTempDir("cli-checkout");
71
+ for (const entry of manifest) {
72
+ const target = join(cliRoot, entry.remote);
73
+ mkdirSync(dirname(target), { recursive: true });
74
+ copyFileSync(join(repoRoot, "data", entry.local), target);
75
+ }
76
+ process.env.JSONUI_CLI_PATH = cliRoot;
77
+ cliLoader = new SpecLoader(makeTempDir("empty-mcp-root"));
78
+ delete process.env.JSONUI_CLI_PATH;
79
+ });
80
+
81
+ afterAll(() => {
82
+ process.chdir(originalCwd);
83
+ cleanupTempDirs();
84
+ });
85
+
86
+ describe("fetch manifest sanity", () => {
87
+ it("has unique local filenames and unique remote paths", () => {
88
+ const locals = manifest.map((f) => f.local);
89
+ const remotes = manifest.map((f) => f.remote);
90
+ expect(new Set(locals).size).toBe(locals.length);
91
+ expect(new Set(remotes).size).toBe(remotes.length);
92
+ });
93
+
94
+ it("keeps local names in sync with their remote basenames", () => {
95
+ for (const entry of manifest) {
96
+ expect(
97
+ basename(entry.remote),
98
+ `${entry.local} is fetched from ${entry.remote} — renaming in flight invites confusion`
99
+ ).toBe(entry.local);
100
+ }
101
+ });
102
+ });
103
+
104
+ describe("data/ ⊆ manifest (everything bundled is auto-fetched)", () => {
105
+ it("lists every data/ file in the fetch manifest", () => {
106
+ const fetched = new Set(manifest.map((f) => f.local));
107
+ const bundled = readdirSync(join(repoRoot, "data")).filter(
108
+ (name) => !name.startsWith(".") // .DS_Store and friends
109
+ );
110
+ expect(bundled.length).toBeGreaterThan(0);
111
+ for (const file of bundled) {
112
+ expect(
113
+ fetched,
114
+ `data/${file} is not in scripts/fetch-manifest.js FILES — ` +
115
+ `it will only ever refresh by hand and silently go stale ` +
116
+ `(the binding_semantics/coverage trap, third edition)`
117
+ ).toContain(file);
118
+ }
119
+ });
120
+ });
121
+
122
+ describe("manifest ⊆ SpecLoader (everything fetched is read and reported)", () => {
123
+ it("reads and reports every manifest file from the bundled layer", () => {
124
+ const reported = reportedFiles(bundledLoader.getDataSource());
125
+ for (const file of reported) {
126
+ expect(file.layer).toBe("bundled");
127
+ }
128
+ const reportedNames = reported.map((f) => basename(f.path));
129
+ for (const entry of manifest) {
130
+ expect(
131
+ reportedNames,
132
+ `${entry.local} is fetched into data/ but the SpecLoader never read it ` +
133
+ `(or does not report it via get_data_source)`
134
+ ).toContain(entry.local);
135
+ }
136
+ });
137
+
138
+ it("reads and reports every manifest file from a jsonui-cli checkout at its remote path", () => {
139
+ const reported = reportedFiles(cliLoader.getDataSource());
140
+ for (const file of reported) {
141
+ expect(file.layer).toBe("env");
142
+ }
143
+ const reportedPaths = reported.map((f) => f.path);
144
+ for (const entry of manifest) {
145
+ expect(
146
+ reportedPaths,
147
+ `${entry.remote} is where postinstall fetches ${entry.local} from, but the ` +
148
+ `SpecLoader resolves a different path inside a jsonui-cli checkout`
149
+ ).toContain(join(cliRoot, entry.remote));
150
+ }
151
+ });
152
+ });