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.
- package/.github/workflows/test.yml +35 -0
- package/README.md +148 -0
- package/data/attribute_definitions.json +4248 -0
- package/data/attribute_semantics.json +137 -0
- package/data/binding_semantics.json +115 -0
- package/data/component_metadata.json +769 -0
- package/data/coverage.json +114 -0
- package/data/screen_identity.json +350 -0
- package/dist/cli_runner.d.ts +10 -0
- package/dist/cli_runner.js +33 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +67 -0
- package/dist/data/component_metadata.d.ts +30 -0
- package/dist/data/component_metadata.js +498 -0
- package/dist/data/derived.d.ts +198 -0
- package/dist/data/derived.js +293 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +130 -0
- package/dist/spec_loader.d.ts +110 -0
- package/dist/spec_loader.js +445 -0
- package/dist/tools/api/list_api_models.d.ts +10 -0
- package/dist/tools/api/list_api_models.js +37 -0
- package/dist/tools/api/list_api_specs.d.ts +19 -0
- package/dist/tools/api/list_api_specs.js +42 -0
- package/dist/tools/api/preview_api_model_sync.d.ts +11 -0
- package/dist/tools/api/preview_api_model_sync.js +34 -0
- package/dist/tools/context/get_project_config.d.ts +3 -0
- package/dist/tools/context/get_project_config.js +20 -0
- package/dist/tools/context/list_component_specs.d.ts +3 -0
- package/dist/tools/context/list_component_specs.js +35 -0
- package/dist/tools/context/list_layouts.d.ts +3 -0
- package/dist/tools/context/list_layouts.js +118 -0
- package/dist/tools/context/list_screen_specs.d.ts +3 -0
- package/dist/tools/context/list_screen_specs.js +37 -0
- package/dist/tools/context/read_layout_file.d.ts +3 -0
- package/dist/tools/context/read_layout_file.js +25 -0
- package/dist/tools/context/read_spec_file.d.ts +3 -0
- package/dist/tools/context/read_spec_file.js +27 -0
- package/dist/tools/doc/doc_generate_component.d.ts +3 -0
- package/dist/tools/doc/doc_generate_component.js +38 -0
- package/dist/tools/doc/doc_generate_html.d.ts +3 -0
- package/dist/tools/doc/doc_generate_html.js +46 -0
- package/dist/tools/doc/doc_generate_spec.d.ts +3 -0
- package/dist/tools/doc/doc_generate_spec.js +38 -0
- package/dist/tools/doc/doc_init_component.d.ts +3 -0
- package/dist/tools/doc/doc_init_component.js +30 -0
- package/dist/tools/doc/doc_init_spec.d.ts +3 -0
- package/dist/tools/doc/doc_init_spec.js +33 -0
- package/dist/tools/doc/doc_rules_init.d.ts +3 -0
- package/dist/tools/doc/doc_rules_init.js +25 -0
- package/dist/tools/doc/doc_rules_show.d.ts +3 -0
- package/dist/tools/doc/doc_rules_show.js +21 -0
- package/dist/tools/doc/doc_validate_component.d.ts +3 -0
- package/dist/tools/doc/doc_validate_component.js +39 -0
- package/dist/tools/doc/doc_validate_spec.d.ts +3 -0
- package/dist/tools/doc/doc_validate_spec.js +40 -0
- package/dist/tools/jui/jui_build.d.ts +3 -0
- package/dist/tools/jui/jui_build.js +31 -0
- package/dist/tools/jui/jui_generate_converter.d.ts +3 -0
- package/dist/tools/jui/jui_generate_converter.js +43 -0
- package/dist/tools/jui/jui_generate_project.d.ts +3 -0
- package/dist/tools/jui/jui_generate_project.js +43 -0
- package/dist/tools/jui/jui_generate_screen.d.ts +3 -0
- package/dist/tools/jui/jui_generate_screen.js +22 -0
- package/dist/tools/jui/jui_init.d.ts +3 -0
- package/dist/tools/jui/jui_init.js +42 -0
- package/dist/tools/jui/jui_migrate_layouts.d.ts +3 -0
- package/dist/tools/jui/jui_migrate_layouts.js +25 -0
- package/dist/tools/jui/jui_sync_tool.d.ts +3 -0
- package/dist/tools/jui/jui_sync_tool.js +33 -0
- package/dist/tools/jui/jui_verify.d.ts +3 -0
- package/dist/tools/jui/jui_verify.js +43 -0
- package/dist/tools/spec/get_binding_rules.d.ts +3 -0
- package/dist/tools/spec/get_binding_rules.js +8 -0
- package/dist/tools/spec/get_data_source.d.ts +3 -0
- package/dist/tools/spec/get_data_source.js +41 -0
- package/dist/tools/spec/get_modifier_order.d.ts +3 -0
- package/dist/tools/spec/get_modifier_order.js +14 -0
- package/dist/tools/spec/get_platform_mapping.d.ts +3 -0
- package/dist/tools/spec/get_platform_mapping.js +20 -0
- package/dist/tools/spec/get_screen_identity.d.ts +3 -0
- package/dist/tools/spec/get_screen_identity.js +22 -0
- package/dist/tools/spec/lookup_attribute.d.ts +3 -0
- package/dist/tools/spec/lookup_attribute.js +14 -0
- package/dist/tools/spec/lookup_component.d.ts +3 -0
- package/dist/tools/spec/lookup_component.js +15 -0
- package/dist/tools/spec/search_components.d.ts +3 -0
- package/dist/tools/spec/search_components.js +14 -0
- package/dist/tools/test/test_artifacts_pull.d.ts +3 -0
- package/dist/tools/test/test_artifacts_pull.js +48 -0
- package/dist/tools/test/test_artifacts_status.d.ts +3 -0
- package/dist/tools/test/test_artifacts_status.js +28 -0
- package/dist/tools/test/test_generate_description.d.ts +3 -0
- package/dist/tools/test/test_generate_description.js +27 -0
- package/dist/tools/test/test_generate_flow.d.ts +3 -0
- package/dist/tools/test/test_generate_flow.js +29 -0
- package/dist/tools/test/test_generate_screen.d.ts +3 -0
- package/dist/tools/test/test_generate_screen.js +29 -0
- package/dist/tools/test/test_mock_generate.d.ts +3 -0
- package/dist/tools/test/test_mock_generate.js +64 -0
- package/dist/tools/test/test_report.d.ts +3 -0
- package/dist/tools/test/test_report.js +26 -0
- package/dist/tools/test/test_validate.d.ts +3 -0
- package/dist/tools/test/test_validate.js +43 -0
- package/docs/design.md +227 -0
- package/install.sh +90 -0
- package/package.json +24 -0
- package/scripts/fetch-definitions.js +101 -0
- package/scripts/fetch-manifest.js +60 -0
- package/src/cli_runner.ts +55 -0
- package/src/config.ts +88 -0
- package/src/data/derived.ts +305 -0
- package/src/index.ts +157 -0
- package/src/spec_loader.ts +591 -0
- package/src/tools/api/list_api_models.ts +44 -0
- package/src/tools/api/list_api_specs.ts +53 -0
- package/src/tools/api/preview_api_model_sync.ts +41 -0
- package/src/tools/context/get_project_config.ts +27 -0
- package/src/tools/context/list_component_specs.ts +44 -0
- package/src/tools/context/list_layouts.ts +131 -0
- package/src/tools/context/list_screen_specs.ts +46 -0
- package/src/tools/context/read_layout_file.ts +35 -0
- package/src/tools/context/read_spec_file.ts +38 -0
- package/src/tools/doc/doc_generate_component.ts +43 -0
- package/src/tools/doc/doc_generate_html.ts +40 -0
- package/src/tools/doc/doc_generate_spec.ts +43 -0
- package/src/tools/doc/doc_init_component.ts +32 -0
- package/src/tools/doc/doc_init_spec.ts +37 -0
- package/src/tools/doc/doc_rules_init.ts +29 -0
- package/src/tools/doc/doc_rules_show.ts +27 -0
- package/src/tools/doc/doc_validate_component.ts +47 -0
- package/src/tools/doc/doc_validate_spec.ts +48 -0
- package/src/tools/jui/jui_build.ts +31 -0
- package/src/tools/jui/jui_generate_converter.ts +41 -0
- package/src/tools/jui/jui_generate_project.ts +37 -0
- package/src/tools/jui/jui_generate_screen.ts +28 -0
- package/src/tools/jui/jui_init.ts +38 -0
- package/src/tools/jui/jui_migrate_layouts.ts +29 -0
- package/src/tools/jui/jui_sync_tool.ts +33 -0
- package/src/tools/jui/jui_verify.ts +43 -0
- package/src/tools/spec/get_binding_rules.ts +16 -0
- package/src/tools/spec/get_data_source.ts +53 -0
- package/src/tools/spec/get_modifier_order.ts +22 -0
- package/src/tools/spec/get_platform_mapping.ts +28 -0
- package/src/tools/spec/get_screen_identity.ts +31 -0
- package/src/tools/spec/lookup_attribute.ts +22 -0
- package/src/tools/spec/lookup_component.ts +23 -0
- package/src/tools/spec/search_components.ts +22 -0
- package/src/tools/test/test_artifacts_pull.ts +47 -0
- package/src/tools/test/test_artifacts_status.ts +34 -0
- package/src/tools/test/test_generate_description.ts +33 -0
- package/src/tools/test/test_generate_flow.ts +33 -0
- package/src/tools/test/test_generate_screen.ts +33 -0
- package/src/tools/test/test_mock_generate.ts +58 -0
- package/src/tools/test/test_report.ts +32 -0
- package/src/tools/test/test_validate.ts +43 -0
- package/tests/__snapshots__/derived.test.ts.snap +279 -0
- package/tests/bundled_snapshot.test.ts +155 -0
- package/tests/cli_runner.test.ts +120 -0
- package/tests/config.test.ts +212 -0
- package/tests/derived.test.ts +114 -0
- package/tests/helpers.ts +272 -0
- package/tests/setup.ts +27 -0
- package/tests/snapshot_completeness.test.ts +152 -0
- package/tests/spec_loader.test.ts +488 -0
- package/tests/tools_cli.test.ts +687 -0
- package/tests/tools_context.test.ts +274 -0
- package/tests/tools_spec.test.ts +225 -0
- package/tests/tools_test.test.ts +284 -0
- package/tsconfig.json +15 -0
- package/uninstall.sh +46 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group B tools (project context) — exercised against a tmp-dir fixture
|
|
3
|
+
* project (jui.config.json + specs + layouts). No real project is touched.
|
|
4
|
+
*/
|
|
5
|
+
import { symlinkSync, writeFileSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
import { ServerConfig } from "../src/config.js";
|
|
10
|
+
import { register as registerGetProjectConfig } from "../src/tools/context/get_project_config.js";
|
|
11
|
+
import { register as registerListScreenSpecs } from "../src/tools/context/list_screen_specs.js";
|
|
12
|
+
import { register as registerListComponentSpecs } from "../src/tools/context/list_component_specs.js";
|
|
13
|
+
import { register as registerListLayouts } from "../src/tools/context/list_layouts.js";
|
|
14
|
+
import { register as registerReadSpecFile } from "../src/tools/context/read_spec_file.js";
|
|
15
|
+
import { register as registerReadLayoutFile } from "../src/tools/context/read_layout_file.js";
|
|
16
|
+
import {
|
|
17
|
+
cleanupTempDirs,
|
|
18
|
+
createToolHarness,
|
|
19
|
+
makeFixtureProject,
|
|
20
|
+
makeTempDir,
|
|
21
|
+
writeJson,
|
|
22
|
+
type ToolHarness,
|
|
23
|
+
} from "./helpers.js";
|
|
24
|
+
|
|
25
|
+
let harness: ToolHarness;
|
|
26
|
+
let projectDir: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
delete process.env.JUI_PROJECT_DIR;
|
|
30
|
+
projectDir = makeTempDir("project");
|
|
31
|
+
makeFixtureProject(projectDir);
|
|
32
|
+
|
|
33
|
+
harness = createToolHarness();
|
|
34
|
+
const config = new ServerConfig();
|
|
35
|
+
registerGetProjectConfig(harness.server, config);
|
|
36
|
+
registerListScreenSpecs(harness.server, config);
|
|
37
|
+
registerListComponentSpecs(harness.server, config);
|
|
38
|
+
registerListLayouts(harness.server, config);
|
|
39
|
+
registerReadSpecFile(harness.server, config);
|
|
40
|
+
registerReadLayoutFile(harness.server, config);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
delete process.env.JUI_PROJECT_DIR;
|
|
45
|
+
cleanupTempDirs();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("get_project_config", () => {
|
|
49
|
+
it("returns the resolved project dir and parsed config", async () => {
|
|
50
|
+
const result = JSON.parse(
|
|
51
|
+
await harness.call("get_project_config", { project_dir: projectDir })
|
|
52
|
+
);
|
|
53
|
+
expect(result.project_dir).toBe(projectDir);
|
|
54
|
+
expect(result.config.project_name).toBe("ExampleApp");
|
|
55
|
+
expect(result.config.layouts_directory).toBe("layouts");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("honors JUI_PROJECT_DIR when project_dir is omitted", async () => {
|
|
59
|
+
process.env.JUI_PROJECT_DIR = projectDir;
|
|
60
|
+
// ServerConfig captures the env at construction time — build a fresh one.
|
|
61
|
+
const envHarness = createToolHarness();
|
|
62
|
+
registerGetProjectConfig(envHarness.server, new ServerConfig());
|
|
63
|
+
const result = JSON.parse(await envHarness.call("get_project_config"));
|
|
64
|
+
expect(result.project_dir).toBe(projectDir);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns an Error text when no project dir is configured", async () => {
|
|
68
|
+
const text = await harness.call("get_project_config");
|
|
69
|
+
expect(text).toMatch(/^Error: No project directory specified/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns an Error text when jui.config.json is missing", async () => {
|
|
73
|
+
const bare = makeTempDir("bare");
|
|
74
|
+
const text = await harness.call("get_project_config", { project_dir: bare });
|
|
75
|
+
expect(text).toMatch(/^Error: jui\.config\.json not found/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns an Error text for a nonexistent project dir", async () => {
|
|
79
|
+
const text = await harness.call("get_project_config", {
|
|
80
|
+
project_dir: join(projectDir, "nope"),
|
|
81
|
+
});
|
|
82
|
+
expect(text).toMatch(/^Error: Project directory does not exist/);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("list_screen_specs", () => {
|
|
87
|
+
it("lists *.spec.json with extracted metadata", async () => {
|
|
88
|
+
const specs = JSON.parse(
|
|
89
|
+
await harness.call("list_screen_specs", { project_dir: projectDir })
|
|
90
|
+
);
|
|
91
|
+
const login = specs.find((s: any) => s.file === "login.spec.json");
|
|
92
|
+
expect(login).toEqual({
|
|
93
|
+
file: "login.spec.json",
|
|
94
|
+
name: "Login",
|
|
95
|
+
displayName: "Login Screen",
|
|
96
|
+
type: "screen_spec",
|
|
97
|
+
layoutFile: "login",
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("degrades gracefully for unparseable spec files", async () => {
|
|
102
|
+
const specs = JSON.parse(
|
|
103
|
+
await harness.call("list_screen_specs", { project_dir: projectDir })
|
|
104
|
+
);
|
|
105
|
+
const broken = specs.find((s: any) => s.file === "broken.spec.json");
|
|
106
|
+
expect(broken).toEqual({
|
|
107
|
+
file: "broken.spec.json",
|
|
108
|
+
name: "broken",
|
|
109
|
+
displayName: "",
|
|
110
|
+
type: "unknown",
|
|
111
|
+
layoutFile: null,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("reports a missing spec directory as a plain message", async () => {
|
|
116
|
+
const proj = makeTempDir("no-specs");
|
|
117
|
+
writeJson(join(proj, "jui.config.json"), {
|
|
118
|
+
project_name: "Empty",
|
|
119
|
+
spec_directory: "missing/specs",
|
|
120
|
+
});
|
|
121
|
+
const text = await harness.call("list_screen_specs", { project_dir: proj });
|
|
122
|
+
expect(text).toContain("Spec directory not found");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("list_component_specs", () => {
|
|
127
|
+
it("lists *.component.json with name and category", async () => {
|
|
128
|
+
const specs = JSON.parse(
|
|
129
|
+
await harness.call("list_component_specs", { project_dir: projectDir })
|
|
130
|
+
);
|
|
131
|
+
expect(specs).toEqual([
|
|
132
|
+
{
|
|
133
|
+
file: "example_card.component.json",
|
|
134
|
+
name: "ExampleCard",
|
|
135
|
+
category: "card",
|
|
136
|
+
},
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("list_layouts", () => {
|
|
142
|
+
it("lists .json files recursively with relative paths, skipping non-JSON", async () => {
|
|
143
|
+
const files = JSON.parse(
|
|
144
|
+
await harness.call("list_layouts", { project_dir: projectDir })
|
|
145
|
+
);
|
|
146
|
+
const paths = files
|
|
147
|
+
.map((f: any) => (typeof f === "string" ? f : f.layout))
|
|
148
|
+
.sort();
|
|
149
|
+
expect(paths).toEqual(["Styles/common.json", "login.json"]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("never classifies a non-layout subtree, however jui resolves", async () => {
|
|
153
|
+
const files = JSON.parse(
|
|
154
|
+
await harness.call("list_layouts", { project_dir: projectDir })
|
|
155
|
+
);
|
|
156
|
+
const byPath = new Map<string, any>(
|
|
157
|
+
files.map((f: any) => [typeof f === "string" ? f : f.layout, f])
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Holds whether or not `jui screens` is reachable: Styles/ is not a
|
|
161
|
+
// layout tree (canon: screenId.nonLayoutSubtrees), and a style file
|
|
162
|
+
// classified as a screen is how phantom screens got markers.
|
|
163
|
+
expect(byPath.get("Styles/common.json")).toBe("Styles/common.json");
|
|
164
|
+
|
|
165
|
+
// Classification comes from shelling out to `jui screens`, which is not
|
|
166
|
+
// installed everywhere (CI runs without it). When it IS available the
|
|
167
|
+
// shape is pinned; when it is not, the listing degrades to bare paths
|
|
168
|
+
// and that is the documented fallback — so assert the branch we are in
|
|
169
|
+
// rather than requiring the CLI.
|
|
170
|
+
const login = byPath.get("login.json");
|
|
171
|
+
if (typeof login === "string") {
|
|
172
|
+
expect(login).toBe("login.json");
|
|
173
|
+
} else {
|
|
174
|
+
expect(login).toMatchObject({
|
|
175
|
+
layout: "login.json",
|
|
176
|
+
screen: "login",
|
|
177
|
+
role: "screen",
|
|
178
|
+
roleReason: "default",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("read_spec_file", () => {
|
|
185
|
+
it("reads a screen spec from spec_directory", async () => {
|
|
186
|
+
const text = await harness.call("read_spec_file", {
|
|
187
|
+
file: "login.spec.json",
|
|
188
|
+
project_dir: projectDir,
|
|
189
|
+
});
|
|
190
|
+
expect(JSON.parse(text).metadata.name).toBe("Login");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("routes *.component.json to component_spec_directory", async () => {
|
|
194
|
+
const text = await harness.call("read_spec_file", {
|
|
195
|
+
file: "example_card.component.json",
|
|
196
|
+
project_dir: projectDir,
|
|
197
|
+
});
|
|
198
|
+
expect(JSON.parse(text).metadata.name).toBe("ExampleCard");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("reports missing files with the resolved path", async () => {
|
|
202
|
+
const text = await harness.call("read_spec_file", {
|
|
203
|
+
file: "ghost.spec.json",
|
|
204
|
+
project_dir: projectDir,
|
|
205
|
+
});
|
|
206
|
+
expect(text).toContain("File not found");
|
|
207
|
+
expect(text).toContain("ghost.spec.json");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("blocks ../ traversal out of the project", async () => {
|
|
211
|
+
// spec_directory is 3 levels deep — 4 ".." segments escape the project.
|
|
212
|
+
const text = await harness.call("read_spec_file", {
|
|
213
|
+
file: "../../../../outside-secret.json",
|
|
214
|
+
project_dir: projectDir,
|
|
215
|
+
});
|
|
216
|
+
expect(text).toMatch(/^Error: Path traversal detected/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("allows ../ segments that stay inside the project", async () => {
|
|
220
|
+
const text = await harness.call("read_spec_file", {
|
|
221
|
+
file: "../../components/json/example_card.component.json",
|
|
222
|
+
project_dir: projectDir,
|
|
223
|
+
});
|
|
224
|
+
// Ends in .component.json so it resolves from component_spec_directory,
|
|
225
|
+
// climbs to docs/, and lands back inside the project.
|
|
226
|
+
expect(JSON.parse(text).metadata.name).toBe("ExampleCard");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("read_layout_file", () => {
|
|
231
|
+
it("reads a top-level layout", async () => {
|
|
232
|
+
const text = await harness.call("read_layout_file", {
|
|
233
|
+
file: "login.json",
|
|
234
|
+
project_dir: projectDir,
|
|
235
|
+
});
|
|
236
|
+
expect(JSON.parse(text).id).toBe("login_root");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("reads nested layouts via relative paths", async () => {
|
|
240
|
+
const text = await harness.call("read_layout_file", {
|
|
241
|
+
file: "Styles/common.json",
|
|
242
|
+
project_dir: projectDir,
|
|
243
|
+
});
|
|
244
|
+
expect(JSON.parse(text).defaultFontColor).toBe("#333333");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("blocks ../ traversal out of the project", async () => {
|
|
248
|
+
const text = await harness.call("read_layout_file", {
|
|
249
|
+
file: "../../../../etc/hosts",
|
|
250
|
+
project_dir: projectDir,
|
|
251
|
+
});
|
|
252
|
+
expect(text).toMatch(/^Error: Path traversal detected/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("reports missing layouts as File not found", async () => {
|
|
256
|
+
const text = await harness.call("read_layout_file", {
|
|
257
|
+
file: "ghost.json",
|
|
258
|
+
project_dir: projectDir,
|
|
259
|
+
});
|
|
260
|
+
expect(text).toContain("File not found");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("blocks reading through a symlink that escapes the project", async () => {
|
|
264
|
+
const outside = makeTempDir("outside");
|
|
265
|
+
writeFileSync(join(outside, "secret.json"), '{"leaked":true}');
|
|
266
|
+
symlinkSync(outside, join(projectDir, "layouts", "external"));
|
|
267
|
+
const text = await harness.call("read_layout_file", {
|
|
268
|
+
file: "external/secret.json",
|
|
269
|
+
project_dir: projectDir,
|
|
270
|
+
});
|
|
271
|
+
expect(text).toMatch(/^Error: Path traversal detected/);
|
|
272
|
+
expect(text).not.toContain("leaked");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group A tools (component spec lookup) — input schema validation via the
|
|
3
|
+
* captured Zod shapes plus output contracts against a fixture SpecLoader.
|
|
4
|
+
*/
|
|
5
|
+
import { utimesSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { ZodError } from "zod";
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { SpecLoader } from "../src/spec_loader.js";
|
|
11
|
+
import { register as registerLookupComponent } from "../src/tools/spec/lookup_component.js";
|
|
12
|
+
import { register as registerLookupAttribute } from "../src/tools/spec/lookup_attribute.js";
|
|
13
|
+
import { register as registerSearchComponents } from "../src/tools/spec/search_components.js";
|
|
14
|
+
import { register as registerGetModifierOrder } from "../src/tools/spec/get_modifier_order.js";
|
|
15
|
+
import { register as registerGetBindingRules } from "../src/tools/spec/get_binding_rules.js";
|
|
16
|
+
import { register as registerGetPlatformMapping } from "../src/tools/spec/get_platform_mapping.js";
|
|
17
|
+
import { register as registerGetDataSource } from "../src/tools/spec/get_data_source.js";
|
|
18
|
+
import {
|
|
19
|
+
BINDING_RULES,
|
|
20
|
+
MODIFIER_ORDER,
|
|
21
|
+
PLATFORM_MAPPING,
|
|
22
|
+
} from "../src/data/derived.js";
|
|
23
|
+
import {
|
|
24
|
+
cleanupTempDirs,
|
|
25
|
+
createToolHarness,
|
|
26
|
+
makeBundledDataset,
|
|
27
|
+
makeTempDir,
|
|
28
|
+
type ToolHarness,
|
|
29
|
+
} from "./helpers.js";
|
|
30
|
+
|
|
31
|
+
const originalCwd = process.cwd();
|
|
32
|
+
|
|
33
|
+
function buildHarness(loader: SpecLoader): ToolHarness {
|
|
34
|
+
const harness = createToolHarness();
|
|
35
|
+
registerLookupComponent(harness.server, loader);
|
|
36
|
+
registerLookupAttribute(harness.server, loader);
|
|
37
|
+
registerSearchComponents(harness.server, loader);
|
|
38
|
+
registerGetModifierOrder(harness.server, loader);
|
|
39
|
+
registerGetBindingRules(harness.server, loader);
|
|
40
|
+
registerGetPlatformMapping(harness.server, loader);
|
|
41
|
+
registerGetDataSource(harness.server, loader);
|
|
42
|
+
return harness;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeFixtureLoader(opts: { staleDays?: number } = {}): SpecLoader {
|
|
46
|
+
const mcpRoot = makeTempDir("mcp-root");
|
|
47
|
+
makeBundledDataset(mcpRoot);
|
|
48
|
+
if (opts.staleDays) {
|
|
49
|
+
const when = new Date(Date.now() - opts.staleDays * 24 * 60 * 60 * 1000);
|
|
50
|
+
for (const f of ["attribute_definitions.json", "component_metadata.json"]) {
|
|
51
|
+
utimesSync(join(mcpRoot, "data", f), when, when);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return new SpecLoader(mcpRoot);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let harness: ToolHarness;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
delete process.env.JSONUI_CLI_PATH;
|
|
61
|
+
process.chdir(makeTempDir("cwd"));
|
|
62
|
+
harness = buildHarness(makeFixtureLoader());
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
process.chdir(originalCwd);
|
|
67
|
+
cleanupTempDirs();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("tool registration", () => {
|
|
71
|
+
it("registers the 7 Group A tools with descriptions", () => {
|
|
72
|
+
expect([...harness.tools.keys()].sort()).toEqual([
|
|
73
|
+
"get_binding_rules",
|
|
74
|
+
"get_data_source",
|
|
75
|
+
"get_modifier_order",
|
|
76
|
+
"get_platform_mapping",
|
|
77
|
+
"lookup_attribute",
|
|
78
|
+
"lookup_component",
|
|
79
|
+
"search_components",
|
|
80
|
+
]);
|
|
81
|
+
for (const tool of harness.tools.values()) {
|
|
82
|
+
expect(tool.description.length).toBeGreaterThan(10);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("lookup_component", () => {
|
|
88
|
+
it("returns the full spec plus commonAttributes as JSON", async () => {
|
|
89
|
+
const text = await harness.call("lookup_component", { name: "TextField" });
|
|
90
|
+
const spec = JSON.parse(text);
|
|
91
|
+
expect(spec.name).toBe("TextField");
|
|
92
|
+
expect(spec.attributes.text.binding_direction).toBe("two-way");
|
|
93
|
+
expect(spec.bindingBehavior.text.direction).toBe("two-way");
|
|
94
|
+
expect(spec.commonAttributes.categories.sizing.width).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("resolves aliases", async () => {
|
|
98
|
+
const spec = JSON.parse(
|
|
99
|
+
await harness.call("lookup_component", { name: "Input" })
|
|
100
|
+
);
|
|
101
|
+
expect(spec.name).toBe("TextField");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("lists available components when not found", async () => {
|
|
105
|
+
const text = await harness.call("lookup_component", { name: "Nope" });
|
|
106
|
+
expect(text).toContain("Component 'Nope' not found");
|
|
107
|
+
expect(text).toContain("Label");
|
|
108
|
+
expect(text).toContain("TextField");
|
|
109
|
+
expect(text).toContain("Button");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rejects a missing required 'name' param at the schema layer", async () => {
|
|
113
|
+
await expect(harness.call("lookup_component", {})).rejects.toThrow(ZodError);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("lookup_attribute", () => {
|
|
118
|
+
it("returns common attribute info as JSON", async () => {
|
|
119
|
+
const attr = JSON.parse(
|
|
120
|
+
await harness.call("lookup_attribute", { name: "onClick" })
|
|
121
|
+
);
|
|
122
|
+
expect(attr.scope).toBe("common");
|
|
123
|
+
expect(attr.category).toBe("interaction");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns a plain not-found message for unknown attributes", async () => {
|
|
127
|
+
const text = await harness.call("lookup_attribute", { name: "nope" });
|
|
128
|
+
expect(text).toBe("Attribute 'nope' not found.");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("search_components", () => {
|
|
133
|
+
it("returns scored results as JSON, best first", async () => {
|
|
134
|
+
const results = JSON.parse(
|
|
135
|
+
await harness.call("search_components", { query: "text" })
|
|
136
|
+
);
|
|
137
|
+
expect(results[0].component).toBe("TextField");
|
|
138
|
+
expect(results[0].score).toBeGreaterThan(0);
|
|
139
|
+
expect(results[0].matches.length).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("caps output at 20 results", async () => {
|
|
143
|
+
// Query matching many entries: every fixture component + common attrs
|
|
144
|
+
// is still < 20, so build a wide loader inline via a broad query and
|
|
145
|
+
// assert the slice contract holds structurally.
|
|
146
|
+
const results = JSON.parse(
|
|
147
|
+
await harness.call("search_components", { query: "t" })
|
|
148
|
+
);
|
|
149
|
+
expect(results.length).toBeLessThanOrEqual(20);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("returns a plain message when nothing matches", async () => {
|
|
153
|
+
const text = await harness.call("search_components", { query: "qqqq" });
|
|
154
|
+
expect(text).toBe("No results for 'qqqq'.");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("get_modifier_order", () => {
|
|
159
|
+
it("returns the full table when platform is omitted", async () => {
|
|
160
|
+
const result = JSON.parse(await harness.call("get_modifier_order"));
|
|
161
|
+
expect(result).toEqual(JSON.parse(JSON.stringify(MODIFIER_ORDER)));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns a single platform section", async () => {
|
|
165
|
+
const result = JSON.parse(
|
|
166
|
+
await harness.call("get_modifier_order", { platform: "swift" })
|
|
167
|
+
);
|
|
168
|
+
expect(result).toEqual(JSON.parse(JSON.stringify(MODIFIER_ORDER.swift)));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("rejects unknown platforms at the schema layer", async () => {
|
|
172
|
+
await expect(
|
|
173
|
+
harness.call("get_modifier_order", { platform: "flutter" })
|
|
174
|
+
).rejects.toThrow(ZodError);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("get_binding_rules", () => {
|
|
179
|
+
it("returns BINDING_RULES verbatim", async () => {
|
|
180
|
+
const result = JSON.parse(await harness.call("get_binding_rules"));
|
|
181
|
+
expect(result).toEqual(JSON.parse(JSON.stringify(BINDING_RULES)));
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("get_platform_mapping", () => {
|
|
186
|
+
it("returns the full mapping when category is omitted", async () => {
|
|
187
|
+
const result = JSON.parse(await harness.call("get_platform_mapping"));
|
|
188
|
+
expect(result).toEqual(JSON.parse(JSON.stringify(PLATFORM_MAPPING)));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns a single category", async () => {
|
|
192
|
+
const result = JSON.parse(
|
|
193
|
+
await harness.call("get_platform_mapping", { category: "textAlign" })
|
|
194
|
+
);
|
|
195
|
+
expect(result).toEqual(JSON.parse(JSON.stringify(PLATFORM_MAPPING.textAlign)));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("lists available categories for an unknown category", async () => {
|
|
199
|
+
const text = await harness.call("get_platform_mapping", {
|
|
200
|
+
category: "bogus",
|
|
201
|
+
});
|
|
202
|
+
expect(text).toContain("Category 'bogus' not found");
|
|
203
|
+
expect(text).toContain("textAlign");
|
|
204
|
+
expect(text).toContain("fontWeight");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("get_data_source", () => {
|
|
209
|
+
it("reports per-file layer, freshness, and counts", async () => {
|
|
210
|
+
const info = JSON.parse(await harness.call("get_data_source"));
|
|
211
|
+
expect(info.attributeDefinitions.layer).toBe("bundled");
|
|
212
|
+
expect(info.attributeDefinitions.freshness).toBe("fresh");
|
|
213
|
+
expect(info.componentMetadata.layer).toBe("bundled");
|
|
214
|
+
expect(info.componentCount).toBe(3);
|
|
215
|
+
expect(info.commonAttributeCount).toBe(5);
|
|
216
|
+
expect(info.hint).toBeUndefined();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("adds a re-fetch hint when any file is stale (> 90 days)", async () => {
|
|
220
|
+
const staleHarness = buildHarness(makeFixtureLoader({ staleDays: 120 }));
|
|
221
|
+
const info = JSON.parse(await staleHarness.call("get_data_source"));
|
|
222
|
+
expect(info.attributeDefinitions.freshness).toBe("stale");
|
|
223
|
+
expect(info.hint).toContain("90 days");
|
|
224
|
+
});
|
|
225
|
+
});
|