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,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLoader — 4-layer fallback resolution and in-memory spec model.
|
|
3
|
+
*
|
|
4
|
+
* Layer order under test:
|
|
5
|
+
* 1. $JSONUI_CLI_PATH 2. ./.jsonui-cli/ 3. ~/.jsonui-cli/ 4. bundled data/
|
|
6
|
+
*/
|
|
7
|
+
import { utimesSync } from "fs";
|
|
8
|
+
import { join, resolve, sep } from "path";
|
|
9
|
+
import { pathToFileURL } from "url";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
import { SpecLoader, mcpRootFromImportMetaUrl } from "../src/spec_loader.js";
|
|
13
|
+
import {
|
|
14
|
+
cleanupTempDirs,
|
|
15
|
+
makeBundledDataset,
|
|
16
|
+
makeCliDataset,
|
|
17
|
+
makeTempDir,
|
|
18
|
+
writeJson,
|
|
19
|
+
FIXTURE_ATTRIBUTE_DEFINITIONS,
|
|
20
|
+
FIXTURE_COMPONENT_METADATA,
|
|
21
|
+
FIXTURE_COVERAGE,
|
|
22
|
+
} from "./helpers.js";
|
|
23
|
+
|
|
24
|
+
const originalCwd = process.cwd();
|
|
25
|
+
|
|
26
|
+
/** An empty dir used as cwd so layer 2 never accidentally resolves. */
|
|
27
|
+
function neutralCwd(): string {
|
|
28
|
+
const dir = makeTempDir("cwd");
|
|
29
|
+
process.chdir(dir);
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Point layer 3 (~/.jsonui-cli) at a fresh empty home. */
|
|
34
|
+
function neutralHome(): string {
|
|
35
|
+
const home = makeTempDir("home");
|
|
36
|
+
process.env.HOME = home;
|
|
37
|
+
process.env.USERPROFILE = home;
|
|
38
|
+
return home;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
delete process.env.JSONUI_CLI_PATH;
|
|
43
|
+
neutralHome();
|
|
44
|
+
neutralCwd();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
process.chdir(originalCwd);
|
|
49
|
+
cleanupTempDirs();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ----- layer resolution ----------------------------------------------------
|
|
53
|
+
|
|
54
|
+
describe("SpecLoader 4-layer fallback", () => {
|
|
55
|
+
it("layer 1: resolves from $JSONUI_CLI_PATH when set", () => {
|
|
56
|
+
const cliRoot = makeTempDir("env-cli");
|
|
57
|
+
makeCliDataset(cliRoot);
|
|
58
|
+
process.env.JSONUI_CLI_PATH = cliRoot;
|
|
59
|
+
|
|
60
|
+
const loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
61
|
+
const source = loader.getDataSource();
|
|
62
|
+
|
|
63
|
+
expect(source.attributeDefinitions.layer).toBe("env");
|
|
64
|
+
expect(source.attributeDefinitions.path).toBe(
|
|
65
|
+
join(resolve(cliRoot), "shared", "core", "attribute_definitions.json")
|
|
66
|
+
);
|
|
67
|
+
expect(source.componentMetadata.layer).toBe("env");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("layer 2: resolves from ./.jsonui-cli/ in cwd when env layer is absent", () => {
|
|
71
|
+
const projectDir = makeTempDir("proj");
|
|
72
|
+
makeCliDataset(join(projectDir, ".jsonui-cli"));
|
|
73
|
+
process.chdir(projectDir);
|
|
74
|
+
|
|
75
|
+
const loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
76
|
+
const source = loader.getDataSource();
|
|
77
|
+
|
|
78
|
+
expect(source.attributeDefinitions.layer).toBe("cwd");
|
|
79
|
+
expect(source.componentMetadata.layer).toBe("cwd");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("layer 3: resolves from ~/.jsonui-cli/ when env + cwd layers are absent", () => {
|
|
83
|
+
const home = makeTempDir("home-with-cli");
|
|
84
|
+
makeCliDataset(join(home, ".jsonui-cli"));
|
|
85
|
+
process.env.HOME = home;
|
|
86
|
+
process.env.USERPROFILE = home;
|
|
87
|
+
|
|
88
|
+
const loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
89
|
+
const source = loader.getDataSource();
|
|
90
|
+
|
|
91
|
+
expect(source.attributeDefinitions.layer).toBe("home");
|
|
92
|
+
expect(source.componentMetadata.layer).toBe("home");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("layer 4: resolves from the bundled data/ snapshot as last resort", () => {
|
|
96
|
+
const mcpRoot = makeTempDir("mcp-root");
|
|
97
|
+
makeBundledDataset(mcpRoot);
|
|
98
|
+
|
|
99
|
+
const loader = new SpecLoader(mcpRoot);
|
|
100
|
+
const source = loader.getDataSource();
|
|
101
|
+
|
|
102
|
+
expect(source.attributeDefinitions.layer).toBe("bundled");
|
|
103
|
+
expect(source.attributeDefinitions.path).toBe(
|
|
104
|
+
join(mcpRoot, "data", "attribute_definitions.json")
|
|
105
|
+
);
|
|
106
|
+
expect(source.componentMetadata.layer).toBe("bundled");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("higher-priority layers win: env > cwd > home > bundled", () => {
|
|
110
|
+
const cliRoot = makeTempDir("env-cli");
|
|
111
|
+
makeCliDataset(cliRoot);
|
|
112
|
+
process.env.JSONUI_CLI_PATH = cliRoot;
|
|
113
|
+
|
|
114
|
+
const projectDir = makeTempDir("proj");
|
|
115
|
+
makeCliDataset(join(projectDir, ".jsonui-cli"));
|
|
116
|
+
process.chdir(projectDir);
|
|
117
|
+
|
|
118
|
+
const home = makeTempDir("home-with-cli");
|
|
119
|
+
makeCliDataset(join(home, ".jsonui-cli"));
|
|
120
|
+
process.env.HOME = home;
|
|
121
|
+
process.env.USERPROFILE = home;
|
|
122
|
+
|
|
123
|
+
const mcpRoot = makeTempDir("mcp-root");
|
|
124
|
+
makeBundledDataset(mcpRoot);
|
|
125
|
+
|
|
126
|
+
expect(
|
|
127
|
+
new SpecLoader(mcpRoot).getDataSource().attributeDefinitions.layer
|
|
128
|
+
).toBe("env");
|
|
129
|
+
|
|
130
|
+
delete process.env.JSONUI_CLI_PATH;
|
|
131
|
+
expect(
|
|
132
|
+
new SpecLoader(mcpRoot).getDataSource().attributeDefinitions.layer
|
|
133
|
+
).toBe("cwd");
|
|
134
|
+
|
|
135
|
+
process.chdir(makeTempDir("empty-cwd"));
|
|
136
|
+
expect(
|
|
137
|
+
new SpecLoader(mcpRoot).getDataSource().attributeDefinitions.layer
|
|
138
|
+
).toBe("home");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("a set-but-missing $JSONUI_CLI_PATH falls through to lower layers", () => {
|
|
142
|
+
process.env.JSONUI_CLI_PATH = makeTempDir("empty-env-cli");
|
|
143
|
+
const mcpRoot = makeTempDir("mcp-root");
|
|
144
|
+
makeBundledDataset(mcpRoot);
|
|
145
|
+
|
|
146
|
+
const source = new SpecLoader(mcpRoot).getDataSource();
|
|
147
|
+
expect(source.attributeDefinitions.layer).toBe("bundled");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("resolves each file independently (attrs from env, metadata from bundled)", () => {
|
|
151
|
+
const cliRoot = makeTempDir("env-cli");
|
|
152
|
+
makeCliDataset(cliRoot, { omitComponentMetadata: true });
|
|
153
|
+
process.env.JSONUI_CLI_PATH = cliRoot;
|
|
154
|
+
|
|
155
|
+
const mcpRoot = makeTempDir("mcp-root");
|
|
156
|
+
makeBundledDataset(mcpRoot);
|
|
157
|
+
|
|
158
|
+
const source = new SpecLoader(mcpRoot).getDataSource();
|
|
159
|
+
expect(source.attributeDefinitions.layer).toBe("env");
|
|
160
|
+
expect(source.componentMetadata.layer).toBe("bundled");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("throws a diagnostic error listing every tried layer when all 4 miss", () => {
|
|
164
|
+
process.env.JSONUI_CLI_PATH = makeTempDir("empty-env-cli");
|
|
165
|
+
const mcpRoot = makeTempDir("empty-mcp-root");
|
|
166
|
+
|
|
167
|
+
let error: Error | null = null;
|
|
168
|
+
try {
|
|
169
|
+
new SpecLoader(mcpRoot);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
error = e as Error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
expect(error).not.toBeNull();
|
|
175
|
+
expect(error!.message).toContain("jsonui-cli file not found");
|
|
176
|
+
expect(error!.message).toContain("[env]");
|
|
177
|
+
expect(error!.message).toContain("[cwd]");
|
|
178
|
+
expect(error!.message).toContain("[home]");
|
|
179
|
+
expect(error!.message).toContain("[bundled]");
|
|
180
|
+
expect(error!.message).toContain("JSONUI_CLI_PATH");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("omits the [env] candidate when $JSONUI_CLI_PATH is unset", () => {
|
|
184
|
+
const mcpRoot = makeTempDir("empty-mcp-root");
|
|
185
|
+
expect(() => new SpecLoader(mcpRoot)).toThrowError(/jsonui-cli file not found/);
|
|
186
|
+
try {
|
|
187
|
+
new SpecLoader(mcpRoot);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
expect((e as Error).message).not.toContain("[env]");
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ----- freshness -----------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
describe("SpecLoader freshness buckets", () => {
|
|
197
|
+
function loaderWithMtimeAge(days: number): SpecLoader {
|
|
198
|
+
const mcpRoot = makeTempDir("mcp-root");
|
|
199
|
+
makeBundledDataset(mcpRoot);
|
|
200
|
+
const when = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
201
|
+
for (const file of ["attribute_definitions.json", "component_metadata.json"]) {
|
|
202
|
+
utimesSync(join(mcpRoot, "data", file), when, when);
|
|
203
|
+
}
|
|
204
|
+
return new SpecLoader(mcpRoot);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
it("<= 30 days is fresh", () => {
|
|
208
|
+
const source = loaderWithMtimeAge(10).getDataSource();
|
|
209
|
+
expect(source.attributeDefinitions.freshness).toBe("fresh");
|
|
210
|
+
expect(source.componentMetadata.freshness).toBe("fresh");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("31-90 days is aging", () => {
|
|
214
|
+
expect(loaderWithMtimeAge(45).getDataSource().attributeDefinitions.freshness).toBe(
|
|
215
|
+
"aging"
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("> 90 days is stale", () => {
|
|
220
|
+
expect(loaderWithMtimeAge(120).getDataSource().attributeDefinitions.freshness).toBe(
|
|
221
|
+
"stale"
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("reports lastModified as ISO 8601", () => {
|
|
226
|
+
const source = loaderWithMtimeAge(10).getDataSource();
|
|
227
|
+
expect(source.attributeDefinitions.lastModified).toMatch(
|
|
228
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ----- spec model built from the fixture dataset ---------------------------
|
|
234
|
+
|
|
235
|
+
describe("SpecLoader spec model", () => {
|
|
236
|
+
let loader: SpecLoader;
|
|
237
|
+
|
|
238
|
+
beforeEach(() => {
|
|
239
|
+
const mcpRoot = makeTempDir("mcp-root");
|
|
240
|
+
makeBundledDataset(mcpRoot);
|
|
241
|
+
loader = new SpecLoader(mcpRoot);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("counts components (skipping _-prefixed keys and 'common')", () => {
|
|
245
|
+
const source = loader.getDataSource();
|
|
246
|
+
expect(source.componentCount).toBe(3); // Label, TextField, Button
|
|
247
|
+
expect(loader.listComponents().sort()).toEqual(["Button", "Label", "TextField"]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("counts common attributes, skipping _-prefixed keys", () => {
|
|
251
|
+
// width, padding, background, onClick, glowIntensity (not _meta)
|
|
252
|
+
expect(loader.getDataSource().commonAttributeCount).toBe(5);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("getComponent is case-insensitive", () => {
|
|
256
|
+
expect(loader.getComponent("label")?.name).toBe("Label");
|
|
257
|
+
expect(loader.getComponent("LABEL")?.name).toBe("Label");
|
|
258
|
+
expect(loader.getComponent("Nope")).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("resolves an `_alias_of` pointer section to the canonical component", () => {
|
|
262
|
+
// Flip is a B1-shape component alias of Button in the fixture, and is
|
|
263
|
+
// deliberately absent from every metadata `aliases` array (the
|
|
264
|
+
// Toggle -> Switch case): resolution must come from `_alias_of` alone,
|
|
265
|
+
// and no empty-attributes 'Flip' component may be registered.
|
|
266
|
+
expect(loader.getComponent("Flip")?.name).toBe("Button");
|
|
267
|
+
expect(loader.getComponent("flip")?.attributes).toHaveProperty("enabled");
|
|
268
|
+
expect(loader.listComponents()).not.toContain("Flip");
|
|
269
|
+
expect(loader.getDataSource().componentCount).toBe(3);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("expands aliases from component metadata (case-insensitive)", () => {
|
|
273
|
+
expect(loader.getComponent("Text")?.name).toBe("Label");
|
|
274
|
+
expect(loader.getComponent("input")?.name).toBe("TextField");
|
|
275
|
+
expect(loader.getComponent("EDITTEXT")?.name).toBe("TextField");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("skips _-prefixed attribute keys inside a component", () => {
|
|
279
|
+
const label = loader.getComponent("Label")!;
|
|
280
|
+
expect(Object.keys(label.attributes).sort()).toEqual(["fontSize", "text"]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("derives read-only bindingBehavior for binding attrs without a declared direction", () => {
|
|
284
|
+
const label = loader.getComponent("Label")!;
|
|
285
|
+
expect(label.bindingBehavior).toEqual({ text: { direction: "read-only" } });
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("derives two-way bindingBehavior when binding_direction is declared", () => {
|
|
289
|
+
const field = loader.getComponent("TextField")!;
|
|
290
|
+
expect(field.bindingBehavior.text).toEqual({ direction: "two-way" });
|
|
291
|
+
// hint is not a binding attr — no entry.
|
|
292
|
+
expect(field.bindingBehavior.hint).toBeUndefined();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("merges metadata platforms over the all-true default", () => {
|
|
296
|
+
const label = loader.getComponent("Label")!;
|
|
297
|
+
expect(label.platforms).toEqual({
|
|
298
|
+
swift_generated: true,
|
|
299
|
+
swift_dynamic: true,
|
|
300
|
+
kotlin_generated: true,
|
|
301
|
+
kotlin_dynamic: true,
|
|
302
|
+
react: false,
|
|
303
|
+
});
|
|
304
|
+
expect(label.description).toBe("Text display component");
|
|
305
|
+
expect(label.rules).toEqual(["Prefer fontColor over textColor"]);
|
|
306
|
+
expect(label.platformSpecific).toEqual({ swift: { renderer: "SwiftUI Text" } });
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("falls back to default metadata for components without a metadata entry", () => {
|
|
310
|
+
const button = loader.getComponent("Button")!;
|
|
311
|
+
expect(button.description).toBe("Button component");
|
|
312
|
+
expect(button.aliases).toEqual([]);
|
|
313
|
+
expect(button.platforms.react).toBe(true);
|
|
314
|
+
expect(button.rules).toEqual([]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("getComponentWithCommon attaches categorized common attributes", () => {
|
|
318
|
+
const withCommon = loader.getComponentWithCommon("Label");
|
|
319
|
+
expect(withCommon.commonAttributes.categories.sizing.width).toBeDefined();
|
|
320
|
+
expect(withCommon.commonAttributes.categories.spacing.padding).toBeDefined();
|
|
321
|
+
expect(withCommon.commonAttributes.categories.visual.background).toBeDefined();
|
|
322
|
+
expect(withCommon.commonAttributes.categories.other.glowIntensity).toBeDefined();
|
|
323
|
+
expect(loader.getComponentWithCommon("Nope")).toBeNull();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("getAttribute resolves common attributes with their category", () => {
|
|
327
|
+
const attr = loader.getAttribute("onClick");
|
|
328
|
+
expect(attr).toMatchObject({
|
|
329
|
+
name: "onClick",
|
|
330
|
+
category: "interaction",
|
|
331
|
+
scope: "common",
|
|
332
|
+
components: "all",
|
|
333
|
+
});
|
|
334
|
+
expect(attr.definition.type).toBe("binding");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("getAttribute resolves a single component-scoped attribute", () => {
|
|
338
|
+
const attr = loader.getAttribute("fontSize");
|
|
339
|
+
expect(attr).toMatchObject({
|
|
340
|
+
name: "fontSize",
|
|
341
|
+
scope: "component",
|
|
342
|
+
component: "Label",
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("getAttribute returns a matches array when several components define it", () => {
|
|
347
|
+
const attr = loader.getAttribute("text");
|
|
348
|
+
expect(attr.matches).toHaveLength(2);
|
|
349
|
+
const byComponent = Object.fromEntries(
|
|
350
|
+
attr.matches.map((m: any) => [m.component, m])
|
|
351
|
+
);
|
|
352
|
+
expect(byComponent.Label.bindingBehavior).toEqual({ direction: "read-only" });
|
|
353
|
+
expect(byComponent.TextField.bindingBehavior).toEqual({ direction: "two-way" });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("getAttribute returns null for unknown attributes", () => {
|
|
357
|
+
expect(loader.getAttribute("noSuchAttr")).toBeNull();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("getAttribute carries no implementationGaps without a coverage ledger", () => {
|
|
361
|
+
expect(loader.getAttribute("fontSize").implementationGaps).toBeUndefined();
|
|
362
|
+
expect(loader.getDataSource().coverage).toBeNull();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("searchComponents scores name > alias > description > attribute > rule and sorts desc", () => {
|
|
366
|
+
const results = loader.searchComponents("text");
|
|
367
|
+
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
368
|
+
// TextField matches by name (10) — must outrank Label (alias 8 at best).
|
|
369
|
+
expect(results[0].component).toBe("TextField");
|
|
370
|
+
const scores = results.map((r: any) => r.score);
|
|
371
|
+
expect(scores).toEqual([...scores].sort((a, b) => b - a));
|
|
372
|
+
|
|
373
|
+
const label = results.find((r: any) => r.component === "Label");
|
|
374
|
+
expect(label.matches).toContain("alias: Text");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("searchComponents matches common attributes with score 4", () => {
|
|
378
|
+
const results = loader.searchComponents("zebra"); // only in glowIntensity description
|
|
379
|
+
expect(results).toEqual([
|
|
380
|
+
{ component: "common", score: 4, matches: ["common.other.glowIntensity"] },
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("searchComponents returns [] when nothing matches", () => {
|
|
385
|
+
expect(loader.searchComponents("qqqqqq")).toEqual([]);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// ----- root helper ----------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
describe("mcpRootFromImportMetaUrl", () => {
|
|
392
|
+
it("returns the parent directory of the module's directory", () => {
|
|
393
|
+
const root = makeTempDir("pkg");
|
|
394
|
+
const moduleUrl = pathToFileURL(join(root, "dist", "index.js")).href;
|
|
395
|
+
expect(mcpRootFromImportMetaUrl(moduleUrl)).toBe(resolve(root));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("handles nested module paths", () => {
|
|
399
|
+
const root = makeTempDir("pkg");
|
|
400
|
+
const moduleUrl = pathToFileURL(
|
|
401
|
+
join(root, "dist", "tools", "spec", "x.js")
|
|
402
|
+
).href;
|
|
403
|
+
expect(mcpRootFromImportMetaUrl(moduleUrl)).toBe(
|
|
404
|
+
resolve(join(root, "dist", "tools"))
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Guard: fixture project dirs never collide with path separators oddities.
|
|
410
|
+
describe("fixture sanity", () => {
|
|
411
|
+
it("temp fixtures live outside the repo", () => {
|
|
412
|
+
const dir = makeTempDir("sanity");
|
|
413
|
+
expect(dir.includes(`${sep}jui-mcp-test-`)).toBe(true);
|
|
414
|
+
writeJson(join(dir, "x.json"), { ok: true });
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Coverage ledger (conformance/coverage.json): implementation-gap records
|
|
419
|
+
// attached to lookup results. The ledger tracks (component, attribute,
|
|
420
|
+
// platform) pairs that ARE declared but whose codegen does not consume the
|
|
421
|
+
// attribute — lookup_attribute is where an agent authoring a layout would
|
|
422
|
+
// otherwise trust the declaration blindly.
|
|
423
|
+
describe("SpecLoader coverage ledger", () => {
|
|
424
|
+
let loader: SpecLoader;
|
|
425
|
+
|
|
426
|
+
beforeEach(() => {
|
|
427
|
+
const cliRoot = makeTempDir("cli-with-coverage");
|
|
428
|
+
makeCliDataset(cliRoot, { coverage: FIXTURE_COVERAGE });
|
|
429
|
+
process.env.JSONUI_CLI_PATH = cliRoot;
|
|
430
|
+
loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("records the resolved ledger file in the data source", () => {
|
|
434
|
+
const coverage = loader.getDataSource().coverage;
|
|
435
|
+
expect(coverage).not.toBeNull();
|
|
436
|
+
expect(coverage!.path.endsWith(join("conformance", "coverage.json"))).toBe(
|
|
437
|
+
true
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("attaches implementationGaps to a component-scoped attribute", () => {
|
|
442
|
+
const attr = loader.getAttribute("fontSize");
|
|
443
|
+
expect(attr.component).toBe("Label");
|
|
444
|
+
expect(attr.implementationGaps).toEqual([
|
|
445
|
+
{
|
|
446
|
+
platform: "web",
|
|
447
|
+
reason: "unimplemented",
|
|
448
|
+
note: "test gap — web codegen does not read it",
|
|
449
|
+
},
|
|
450
|
+
]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("attaches implementationGaps to a common attribute", () => {
|
|
454
|
+
const attr = loader.getAttribute("onClick");
|
|
455
|
+
expect(attr.scope).toBe("common");
|
|
456
|
+
expect(attr.implementationGaps).toEqual([
|
|
457
|
+
{
|
|
458
|
+
platform: "android",
|
|
459
|
+
reason: "legacy",
|
|
460
|
+
note: "test gap — alias normalized before conversion",
|
|
461
|
+
},
|
|
462
|
+
]);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("leaves un-gapped attributes untouched", () => {
|
|
466
|
+
const attr = loader.getAttribute("text");
|
|
467
|
+
for (const m of attr.matches) {
|
|
468
|
+
expect(m.implementationGaps).toBeUndefined();
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// The ledger is served from the same in-memory cache as everything else,
|
|
473
|
+
// so an on-disk refresh MUST surface in getChangedSinceLoad — a tracked
|
|
474
|
+
// list that omits a file is the "silently stale" trap in a second costume.
|
|
475
|
+
it("counts the ledger among the files tracked by getChangedSinceLoad", () => {
|
|
476
|
+
expect(loader.getChangedSinceLoad()).toEqual([]);
|
|
477
|
+
|
|
478
|
+
const ledgerPath = loader.getDataSource().coverage!.path;
|
|
479
|
+
const attrPath = loader.getDataSource().attributeDefinitions.path;
|
|
480
|
+
const later = new Date(Date.now() + 5000);
|
|
481
|
+
utimesSync(ledgerPath, later, later);
|
|
482
|
+
utimesSync(attrPath, later, later);
|
|
483
|
+
|
|
484
|
+
const changed = loader.getChangedSinceLoad();
|
|
485
|
+
expect(changed).toContain(ledgerPath);
|
|
486
|
+
expect(changed).toContain(attrPath);
|
|
487
|
+
});
|
|
488
|
+
});
|