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,687 @@
1
+ /**
2
+ * Group C (jui CLI), Group D (jsonui-doc CLI), and Group E (API discovery)
3
+ * tool wrappers. child_process is mocked — tests pin the exact CLI argv each
4
+ * tool builds and the JSON output contract each tool returns. No real jui or
5
+ * jsonui-doc binary is ever invoked.
6
+ */
7
+ import { join } from "path";
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+
10
+ const execFileMock = vi.hoisted(() => vi.fn());
11
+ vi.mock("child_process", () => ({ execFile: execFileMock }));
12
+ vi.mock("node:child_process", () => ({ execFile: execFileMock }));
13
+
14
+ import { ServerConfig } from "../src/config.js";
15
+ // Group C
16
+ import { register as registerJuiInit } from "../src/tools/jui/jui_init.js";
17
+ import { register as registerJuiGenerateProject } from "../src/tools/jui/jui_generate_project.js";
18
+ import { register as registerJuiGenerateScreen } from "../src/tools/jui/jui_generate_screen.js";
19
+ import { register as registerJuiGenerateConverter } from "../src/tools/jui/jui_generate_converter.js";
20
+ import { register as registerJuiBuild } from "../src/tools/jui/jui_build.js";
21
+ import { register as registerJuiVerify } from "../src/tools/jui/jui_verify.js";
22
+ import { register as registerJuiMigrateLayouts } from "../src/tools/jui/jui_migrate_layouts.js";
23
+ import { register as registerJuiSyncTool } from "../src/tools/jui/jui_sync_tool.js";
24
+ // Group D
25
+ import { register as registerDocInitSpec } from "../src/tools/doc/doc_init_spec.js";
26
+ import { register as registerDocInitComponent } from "../src/tools/doc/doc_init_component.js";
27
+ import { register as registerDocValidateSpec } from "../src/tools/doc/doc_validate_spec.js";
28
+ import { register as registerDocValidateComponent } from "../src/tools/doc/doc_validate_component.js";
29
+ import { register as registerDocGenerateSpec } from "../src/tools/doc/doc_generate_spec.js";
30
+ import { register as registerDocGenerateComponent } from "../src/tools/doc/doc_generate_component.js";
31
+ import { register as registerDocGenerateHtml } from "../src/tools/doc/doc_generate_html.js";
32
+ import { register as registerDocRulesInit } from "../src/tools/doc/doc_rules_init.js";
33
+ import { register as registerDocRulesShow } from "../src/tools/doc/doc_rules_show.js";
34
+ // Group E
35
+ import { register as registerListApiSpecs } from "../src/tools/api/list_api_specs.js";
36
+ import { register as registerListApiModels } from "../src/tools/api/list_api_models.js";
37
+ import { register as registerPreviewApiModelSync } from "../src/tools/api/preview_api_model_sync.js";
38
+
39
+ import {
40
+ cleanupTempDirs,
41
+ createToolHarness,
42
+ makeFixtureProject,
43
+ makeTempDir,
44
+ type ToolHarness,
45
+ } from "./helpers.js";
46
+
47
+ type ExecCallback = (error: any, stdout: string, stderr: string) => void;
48
+
49
+ interface RecordedCall {
50
+ command: string;
51
+ args: string[];
52
+ options: Record<string, any>;
53
+ }
54
+
55
+ let recorded: RecordedCall[];
56
+ let nextResponse: { error?: any; stdout?: string; stderr?: string };
57
+ let harness: ToolHarness;
58
+ let projectDir: string;
59
+
60
+ beforeEach(() => {
61
+ delete process.env.JUI_PROJECT_DIR;
62
+ recorded = [];
63
+ nextResponse = {};
64
+ execFileMock.mockReset();
65
+ execFileMock.mockImplementation(
66
+ (command: string, args: string[], options: any, callback: ExecCallback) => {
67
+ recorded.push({ command, args, options });
68
+ callback(
69
+ nextResponse.error ?? null,
70
+ nextResponse.stdout ?? "",
71
+ nextResponse.stderr ?? ""
72
+ );
73
+ }
74
+ );
75
+
76
+ projectDir = makeTempDir("project");
77
+ makeFixtureProject(projectDir);
78
+
79
+ harness = createToolHarness();
80
+ const config = new ServerConfig();
81
+ registerJuiInit(harness.server, config);
82
+ registerJuiGenerateProject(harness.server, config);
83
+ registerJuiGenerateScreen(harness.server, config);
84
+ registerJuiGenerateConverter(harness.server, config);
85
+ registerJuiBuild(harness.server, config);
86
+ registerJuiVerify(harness.server, config);
87
+ registerJuiMigrateLayouts(harness.server, config);
88
+ registerJuiSyncTool(harness.server, config);
89
+ registerDocInitSpec(harness.server, config);
90
+ registerDocInitComponent(harness.server, config);
91
+ registerDocValidateSpec(harness.server, config);
92
+ registerDocValidateComponent(harness.server, config);
93
+ registerDocGenerateSpec(harness.server, config);
94
+ registerDocGenerateComponent(harness.server, config);
95
+ registerDocGenerateHtml(harness.server, config);
96
+ registerDocRulesInit(harness.server, config);
97
+ registerDocRulesShow(harness.server, config);
98
+ registerListApiSpecs(harness.server, config);
99
+ registerListApiModels(harness.server, config);
100
+ registerPreviewApiModelSync(harness.server, config);
101
+ });
102
+
103
+ afterEach(() => {
104
+ delete process.env.JUI_PROJECT_DIR;
105
+ cleanupTempDirs();
106
+ });
107
+
108
+ function failWithCode(code: number, stdout = "", stderr = ""): void {
109
+ nextResponse = {
110
+ error: Object.assign(new Error(`exit ${code}`), { code }),
111
+ stdout,
112
+ stderr,
113
+ };
114
+ }
115
+
116
+ // ----- Group C: jui CLI -----------------------------------------------------
117
+
118
+ describe("jui_build", () => {
119
+ it("runs `jui build` in the project dir with a 300s timeout", async () => {
120
+ nextResponse = { stdout: "built" };
121
+ const text = await harness.call("jui_build", { project_dir: projectDir });
122
+ expect(recorded).toEqual([
123
+ expect.objectContaining({
124
+ command: "jui",
125
+ args: ["build"],
126
+ options: expect.objectContaining({ cwd: projectDir, timeout: 300_000 }),
127
+ }),
128
+ ]);
129
+ expect(JSON.parse(text)).toEqual({ success: true, output: "built" });
130
+ });
131
+
132
+ it("maps clean + platform params to CLI flags", async () => {
133
+ await harness.call("jui_build", {
134
+ clean: true,
135
+ platform: "ios",
136
+ project_dir: projectDir,
137
+ });
138
+ expect(recorded[0].args).toEqual(["build", "--clean", "--ios-only"]);
139
+
140
+ await harness.call("jui_build", { platform: "android", project_dir: projectDir });
141
+ expect(recorded[1].args).toEqual(["build", "--android-only"]);
142
+
143
+ await harness.call("jui_build", { platform: "web", project_dir: projectDir });
144
+ expect(recorded[2].args).toEqual(["build", "--web-only"]);
145
+ });
146
+
147
+ it("reports CLI failure via the formatResult contract", async () => {
148
+ failWithCode(1, "log output", "build failed");
149
+ const parsed = JSON.parse(
150
+ await harness.call("jui_build", { project_dir: projectDir })
151
+ );
152
+ expect(parsed).toEqual({
153
+ success: false,
154
+ output: "log output",
155
+ errors: "build failed",
156
+ });
157
+ });
158
+
159
+ it("does not spawn anything when no project dir is configured", async () => {
160
+ const text = await harness.call("jui_build");
161
+ expect(text).toMatch(/^Error: No project directory specified/);
162
+ expect(execFileMock).not.toHaveBeenCalled();
163
+ });
164
+ });
165
+
166
+ describe("jui_init", () => {
167
+ it("builds the full init argv from params", async () => {
168
+ await harness.call("jui_init", {
169
+ project_name: "ExampleApp",
170
+ ios_path: "ios",
171
+ ios_mode: "swiftui",
172
+ android_path: "android",
173
+ android_mode: "compose",
174
+ package_name: "com.example.app",
175
+ web_path: "web",
176
+ project_dir: projectDir,
177
+ });
178
+ expect(recorded[0].command).toBe("jui");
179
+ expect(recorded[0].args).toEqual([
180
+ "init",
181
+ "--project-name",
182
+ "ExampleApp",
183
+ "--ios",
184
+ "ios",
185
+ "--ios-mode",
186
+ "swiftui",
187
+ "--android",
188
+ "android",
189
+ "--android-mode",
190
+ "compose",
191
+ "--package-name",
192
+ "com.example.app",
193
+ "--web",
194
+ "web",
195
+ ]);
196
+ });
197
+ });
198
+
199
+ describe("jui_generate_project", () => {
200
+ it("maps spec_file / force / skip_layout / dry_run / platform flags", async () => {
201
+ await harness.call("jui_generate_project", {
202
+ spec_file: "login.spec.json",
203
+ force: true,
204
+ skip_layout: true,
205
+ dry_run: true,
206
+ platform: "web",
207
+ project_dir: projectDir,
208
+ });
209
+ expect(recorded[0].args).toEqual([
210
+ "generate",
211
+ "project",
212
+ "--file",
213
+ "login.spec.json",
214
+ "--force",
215
+ "--skip-layout",
216
+ "--dry-run",
217
+ "--web-only",
218
+ ]);
219
+ expect(recorded[0].options.timeout).toBe(120_000);
220
+ });
221
+ });
222
+
223
+ describe("jui_generate_screen", () => {
224
+ it("passes all screen names positionally", async () => {
225
+ await harness.call("jui_generate_screen", {
226
+ names: ["LoginScreen", "RegisterScreen"],
227
+ project_dir: projectDir,
228
+ });
229
+ expect(recorded[0].args).toEqual([
230
+ "generate",
231
+ "screen",
232
+ "LoginScreen",
233
+ "RegisterScreen",
234
+ ]);
235
+ });
236
+
237
+ it("appends --display-name for a single screen", async () => {
238
+ await harness.call("jui_generate_screen", {
239
+ names: ["LoginScreen"],
240
+ display_name: "Login",
241
+ project_dir: projectDir,
242
+ });
243
+ expect(recorded[0].args).toEqual([
244
+ "generate",
245
+ "screen",
246
+ "LoginScreen",
247
+ "--display-name",
248
+ "Login",
249
+ ]);
250
+ });
251
+ });
252
+
253
+ describe("jui_generate_converter", () => {
254
+ it("maps direct-mode params to flags", async () => {
255
+ await harness.call("jui_generate_converter", {
256
+ name: "ExampleCard",
257
+ attributes: "title:String,count:Int",
258
+ container: true,
259
+ skip_existing: true,
260
+ project_dir: projectDir,
261
+ });
262
+ expect(recorded[0].args).toEqual([
263
+ "generate",
264
+ "converter",
265
+ "ExampleCard",
266
+ "--attributes",
267
+ "title:String,count:Int",
268
+ "--container",
269
+ "--skip-existing",
270
+ ]);
271
+ });
272
+
273
+ it("maps spec-mode params to flags", async () => {
274
+ await harness.call("jui_generate_converter", {
275
+ from_spec: "docs/components/json/example_card.component.json",
276
+ all: true,
277
+ project_dir: projectDir,
278
+ });
279
+ expect(recorded[0].args).toEqual([
280
+ "generate",
281
+ "converter",
282
+ "--from",
283
+ "docs/components/json/example_card.component.json",
284
+ "--all",
285
+ ]);
286
+ });
287
+ });
288
+
289
+ describe("jui_verify", () => {
290
+ it("reports no diffs on exit 0", async () => {
291
+ nextResponse = { stdout: "all layouts match" };
292
+ const parsed = JSON.parse(
293
+ await harness.call("jui_verify", { project_dir: projectDir })
294
+ );
295
+ expect(parsed).toEqual({
296
+ success: true,
297
+ has_diffs: false,
298
+ report: "all layouts match",
299
+ });
300
+ });
301
+
302
+ it("reports has_diffs=true on non-zero exit and passes flags", async () => {
303
+ failWithCode(1, "3 screens differ", "warning");
304
+ const parsed = JSON.parse(
305
+ await harness.call("jui_verify", {
306
+ spec_file: "login.spec.json",
307
+ detail: true,
308
+ fail_on_diff: true,
309
+ platform: "ios",
310
+ project_dir: projectDir,
311
+ })
312
+ );
313
+ expect(recorded[0].args).toEqual([
314
+ "verify",
315
+ "--file",
316
+ "login.spec.json",
317
+ "--detail",
318
+ "--fail-on-diff",
319
+ "--platform",
320
+ "ios",
321
+ ]);
322
+ expect(parsed).toEqual({
323
+ success: false,
324
+ has_diffs: true,
325
+ report: "3 screens differ",
326
+ errors: "warning",
327
+ });
328
+ });
329
+ });
330
+
331
+ describe("jui_migrate_layouts", () => {
332
+ it("maps source_platform and dry_run", async () => {
333
+ await harness.call("jui_migrate_layouts", {
334
+ source_platform: "android",
335
+ dry_run: true,
336
+ project_dir: projectDir,
337
+ });
338
+ expect(recorded[0].args).toEqual([
339
+ "migrate-layouts",
340
+ "--from",
341
+ "android",
342
+ "--dry-run",
343
+ ]);
344
+ });
345
+ });
346
+
347
+ describe("jui_sync_tool", () => {
348
+ it("maps platform / prune / dry_run / from", async () => {
349
+ await harness.call("jui_sync_tool", {
350
+ platform: "web",
351
+ prune: true,
352
+ dry_run: true,
353
+ from: "/opt/jsonui-cli",
354
+ project_dir: projectDir,
355
+ });
356
+ expect(recorded[0].args).toEqual([
357
+ "sync_tool",
358
+ "--platform",
359
+ "web",
360
+ "--prune",
361
+ "--dry-run",
362
+ "--from",
363
+ "/opt/jsonui-cli",
364
+ ]);
365
+ });
366
+ });
367
+
368
+ // ----- Group D: jsonui-doc CLI ----------------------------------------------
369
+
370
+ describe("doc_validate_spec", () => {
371
+ it("resolves a bare filename into spec_directory from jui.config.json", async () => {
372
+ await harness.call("doc_validate_spec", {
373
+ file: "login.spec.json",
374
+ project_dir: projectDir,
375
+ });
376
+ expect(recorded[0].command).toBe("jsonui-doc");
377
+ expect(recorded[0].args).toEqual([
378
+ "validate",
379
+ "spec",
380
+ join(projectDir, "docs/screens/json", "login.spec.json"),
381
+ ]);
382
+ expect(recorded[0].options.cwd).toBe(projectDir);
383
+ });
384
+
385
+ it("leaves paths containing a separator untouched", async () => {
386
+ await harness.call("doc_validate_spec", {
387
+ file: "custom/dir/login.spec.json",
388
+ project_dir: projectDir,
389
+ });
390
+ expect(recorded[0].args[2]).toBe("custom/dir/login.spec.json");
391
+ });
392
+
393
+ it("falls back to the project root when jui.config.json is unreadable", async () => {
394
+ const bare = makeTempDir("bare-project");
395
+ await harness.call("doc_validate_spec", {
396
+ file: "login.spec.json",
397
+ project_dir: bare,
398
+ });
399
+ expect(recorded[0].args[2]).toBe(join(bare, "login.spec.json"));
400
+ });
401
+
402
+ it("maps exit code to success/is_valid", async () => {
403
+ failWithCode(1, "2 errors found", "invalid enum value");
404
+ const parsed = JSON.parse(
405
+ await harness.call("doc_validate_spec", {
406
+ file: "login.spec.json",
407
+ project_dir: projectDir,
408
+ })
409
+ );
410
+ expect(parsed).toEqual({
411
+ success: false,
412
+ is_valid: false,
413
+ output: "2 errors found",
414
+ errors: "invalid enum value",
415
+ });
416
+ });
417
+
418
+ it("reports is_valid=true on exit 0", async () => {
419
+ nextResponse = { stdout: "Valid!" };
420
+ const parsed = JSON.parse(
421
+ await harness.call("doc_validate_spec", {
422
+ file: "login.spec.json",
423
+ project_dir: projectDir,
424
+ })
425
+ );
426
+ expect(parsed.success).toBe(true);
427
+ expect(parsed.is_valid).toBe(true);
428
+ });
429
+ });
430
+
431
+ describe("doc_validate_component", () => {
432
+ it("resolves a bare filename into component_spec_directory", async () => {
433
+ await harness.call("doc_validate_component", {
434
+ file: "example_card.component.json",
435
+ project_dir: projectDir,
436
+ });
437
+ expect(recorded[0].args).toEqual([
438
+ "validate",
439
+ "component",
440
+ join(projectDir, "docs/components/json", "example_card.component.json"),
441
+ ]);
442
+ });
443
+ });
444
+
445
+ describe("doc_generate_spec", () => {
446
+ it("resolves bare filenames and maps output/format flags", async () => {
447
+ await harness.call("doc_generate_spec", {
448
+ file: "login.spec.json",
449
+ output: "docs/screens/html",
450
+ format: "markdown",
451
+ project_dir: projectDir,
452
+ });
453
+ expect(recorded[0].args).toEqual([
454
+ "generate",
455
+ "spec",
456
+ join(projectDir, "docs/screens/json", "login.spec.json"),
457
+ "-o",
458
+ "docs/screens/html",
459
+ "--format",
460
+ "markdown",
461
+ ]);
462
+ });
463
+ });
464
+
465
+ describe("doc_generate_component", () => {
466
+ it("resolves bare filenames into component_spec_directory", async () => {
467
+ await harness.call("doc_generate_component", {
468
+ file: "example_card.component.json",
469
+ project_dir: projectDir,
470
+ });
471
+ expect(recorded[0].args).toEqual([
472
+ "generate",
473
+ "component",
474
+ join(projectDir, "docs/components/json", "example_card.component.json"),
475
+ ]);
476
+ });
477
+ });
478
+
479
+ describe("doc_generate_html", () => {
480
+ it("maps input dir, output dir, and title", async () => {
481
+ await harness.call("doc_generate_html", {
482
+ input_dir: "tests/screens",
483
+ output_dir: "docs/tests/html",
484
+ title: "Example Test Docs",
485
+ project_dir: projectDir,
486
+ });
487
+ expect(recorded[0].args).toEqual([
488
+ "generate",
489
+ "html",
490
+ "tests/screens",
491
+ "-o",
492
+ "docs/tests/html",
493
+ "-t",
494
+ "Example Test Docs",
495
+ ]);
496
+ });
497
+
498
+ it("maps apps, docs_dirs, figma_dir, layouts_dir, and with_checks to CLI flags", async () => {
499
+ await harness.call("doc_generate_html", {
500
+ input_dir: "tests",
501
+ output_dir: "docs/html",
502
+ title: "Whole Site",
503
+ apps: ["user:docs/user", "admin:docs/admin"],
504
+ docs_dirs: ["docs/backend", "docs/requirements"],
505
+ figma_dir: "docs/figma",
506
+ layouts_dir: "Layouts",
507
+ with_checks: true,
508
+ project_dir: projectDir,
509
+ });
510
+ expect(recorded[0].args).toEqual([
511
+ "generate",
512
+ "html",
513
+ "tests",
514
+ "-o",
515
+ "docs/html",
516
+ "-t",
517
+ "Whole Site",
518
+ "--app",
519
+ "user:docs/user",
520
+ "--app",
521
+ "admin:docs/admin",
522
+ "-d",
523
+ "docs/backend",
524
+ "-d",
525
+ "docs/requirements",
526
+ "-fig",
527
+ "docs/figma",
528
+ "--layouts-dir",
529
+ "Layouts",
530
+ "--with-checks",
531
+ ]);
532
+ });
533
+
534
+ it("omits all optional multi-app flags when not provided", async () => {
535
+ await harness.call("doc_generate_html", {
536
+ input_dir: "tests",
537
+ project_dir: projectDir,
538
+ });
539
+ expect(recorded[0].args).toEqual(["generate", "html", "tests"]);
540
+ });
541
+ });
542
+
543
+ describe("doc_init_spec", () => {
544
+ it("maps name, display name, output dir, and file path", async () => {
545
+ await harness.call("doc_init_spec", {
546
+ name: "LearnHelloWorld",
547
+ display_name: "Hello World",
548
+ output_dir: "docs/screens/json",
549
+ file_path: "learn/hello-world.spec.json",
550
+ project_dir: projectDir,
551
+ });
552
+ expect(recorded[0].args).toEqual([
553
+ "init",
554
+ "spec",
555
+ "LearnHelloWorld",
556
+ "-d",
557
+ "Hello World",
558
+ "-o",
559
+ "docs/screens/json",
560
+ "-f",
561
+ "learn/hello-world.spec.json",
562
+ ]);
563
+ });
564
+ });
565
+
566
+ describe("doc_init_component", () => {
567
+ it("maps name, display name, and category", async () => {
568
+ await harness.call("doc_init_component", {
569
+ name: "ExampleCard",
570
+ display_name: "Example Card",
571
+ category: "card",
572
+ project_dir: projectDir,
573
+ });
574
+ expect(recorded[0].args).toEqual([
575
+ "init",
576
+ "component",
577
+ "ExampleCard",
578
+ "-d",
579
+ "Example Card",
580
+ "-c",
581
+ "card",
582
+ ]);
583
+ });
584
+ });
585
+
586
+ describe("doc_rules_init / doc_rules_show", () => {
587
+ it("doc_rules_init maps flutter and output dir", async () => {
588
+ await harness.call("doc_rules_init", {
589
+ flutter: true,
590
+ output_dir: "config",
591
+ project_dir: projectDir,
592
+ });
593
+ expect(recorded[0].args).toEqual(["rules", "init", "--flutter", "-o", "config"]);
594
+ });
595
+
596
+ it("doc_rules_show maps the search directory", async () => {
597
+ await harness.call("doc_rules_show", {
598
+ directory: "config",
599
+ project_dir: projectDir,
600
+ });
601
+ expect(recorded[0].args).toEqual(["rules", "show", "-d", "config"]);
602
+ });
603
+ });
604
+
605
+ // ----- Group E: API model discovery ------------------------------------------
606
+
607
+ describe("list_api_specs", () => {
608
+ it("passes the CLI's JSON stdout through verbatim (trimmed)", async () => {
609
+ nextResponse = { stdout: '{"specs":[{"file":"api.yaml"}]}\n' };
610
+ const text = await harness.call("list_api_specs", { project_dir: projectDir });
611
+ expect(recorded[0].args).toEqual(["ls", "api-specs", "--json"]);
612
+ expect(text).toBe('{"specs":[{"file":"api.yaml"}]}');
613
+ });
614
+
615
+ it("surfaces stderr (then stdout, then a fallback) on failure", async () => {
616
+ failWithCode(2, "", "api_directory not configured");
617
+ expect(
618
+ await harness.call("list_api_specs", { project_dir: projectDir })
619
+ ).toBe("api_directory not configured");
620
+
621
+ failWithCode(2, "some stdout", "");
622
+ expect(
623
+ await harness.call("list_api_specs", { project_dir: projectDir })
624
+ ).toBe("some stdout");
625
+
626
+ failWithCode(2, "", "");
627
+ expect(
628
+ await harness.call("list_api_specs", { project_dir: projectDir })
629
+ ).toBe("jui ls api-specs failed");
630
+ });
631
+ });
632
+
633
+ describe("list_api_models", () => {
634
+ it("adds --platform when restricted", async () => {
635
+ nextResponse = { stdout: '{"models":[]}' };
636
+ const text = await harness.call("list_api_models", {
637
+ platform: "ios",
638
+ project_dir: projectDir,
639
+ });
640
+ expect(recorded[0].args).toEqual([
641
+ "ls",
642
+ "api-models",
643
+ "--json",
644
+ "--platform",
645
+ "ios",
646
+ ]);
647
+ expect(text).toBe('{"models":[]}');
648
+ });
649
+
650
+ it("surfaces stderr on failure", async () => {
651
+ failWithCode(1, "", "no swagger found");
652
+ expect(
653
+ await harness.call("list_api_models", { project_dir: projectDir })
654
+ ).toBe("no swagger found");
655
+ });
656
+ });
657
+
658
+ describe("preview_api_model_sync", () => {
659
+ it("runs the dry-run generate and returns stdout even on halt (non-zero exit)", async () => {
660
+ failWithCode(1, '{"halt":"oneOf without discriminator"}', "halted");
661
+ const text = await harness.call("preview_api_model_sync", {
662
+ platform: "web",
663
+ project_dir: projectDir,
664
+ });
665
+ expect(recorded[0].args).toEqual([
666
+ "g",
667
+ "api",
668
+ "--dry-run",
669
+ "--json",
670
+ "--platform",
671
+ "web",
672
+ ]);
673
+ expect(text).toBe('{"halt":"oneOf without discriminator"}');
674
+ });
675
+
676
+ it("falls back to stderr, then a placeholder, when stdout is empty", async () => {
677
+ failWithCode(1, "", "swagger parse error");
678
+ expect(
679
+ await harness.call("preview_api_model_sync", { project_dir: projectDir })
680
+ ).toBe("swagger parse error");
681
+
682
+ nextResponse = { stdout: "", stderr: "" };
683
+ expect(
684
+ await harness.call("preview_api_model_sync", { project_dir: projectDir })
685
+ ).toBe("preview returned no output");
686
+ });
687
+ });