@tbox.cn/app-toolkit 0.3.0 → 0.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/dist/chunk-WUULQDOV.js +1 -0
- package/dist/contracts-resolver-PS2UH2G6.js +1 -0
- package/dist/index.d.ts +382 -129
- package/dist/index.js +9 -8
- package/package.json +2 -2
- package/src/assembly/provider-catalog.ts +1 -1
- package/src/assembly/resolve-key.ts +75 -0
- package/src/assembly/walk-manifests.ts +216 -96
- package/src/core/contracts-expected.ts +2 -2
- package/src/core/contracts-resolver.ts +12 -1
- package/src/core/env-expansion.ts +14 -12
- package/src/core/module-schema.ts +88 -12
- package/src/dto.ts +53 -25
- package/src/factory.ts +62 -18
- package/src/index.ts +18 -7
- package/src/integrations/evaluate-app.ts +41 -0
- package/src/integrations/mock-bindings.ts +1 -1
- package/src/integrations/module-binding.ts +60 -0
- package/src/integrations/predicate-io.ts +26 -5
- package/src/integrations/predicate.ts +97 -57
- package/src/integrations/prune-bindings.ts +205 -0
- package/src/integrations/write.ts +43 -27
- package/src/views/app.ts +8 -4
- package/src/views/context.ts +26 -7
- package/src/views/integration-schemas.ts +61 -17
- package/src/views/modules.ts +45 -27
- package/src/views/providers.ts +13 -1
- package/src/views/service-detail.ts +5 -3
- package/src/views/service-resolutions.ts +14 -13
- package/tests/credentials-view.test.ts +2 -2
- package/tests/demo-app.ts +34 -5
- package/tests/dev-manifest-catalog.test.ts +86 -6
- package/tests/file-cache.test.ts +5 -5
- package/tests/module-schema.test.ts +117 -16
- package/tests/resolve-key.test.ts +154 -0
- package/tests/vendor-schema-keywords.test.ts +86 -0
- package/tests/views.test.ts +425 -139
- package/tests/write-core.test.ts +83 -0
- package/dist/chunk-KPD3LUH4.js +0 -1
- package/dist/contracts-resolver-E4SECBOG.js +0 -1
- package/src/integrations/domain-binding.ts +0 -81
|
@@ -7,37 +7,67 @@ import {
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* module-schema(descriptor zod 单源)迁移改造验证(C2-5 + C1 P6 归位):
|
|
10
|
-
* - demand meta additive(
|
|
10
|
+
* - demand meta additive(serviceConsumeContribSchema 增 name?/description?——P6)
|
|
11
11
|
* - resource type 放宽(z.enum(RESOURCE_TYPE_IDS) → z.string()——词汇校验移谓词)
|
|
12
12
|
* - 安全钉子 H9b(provider 字符集 pattern)
|
|
13
13
|
* - 既有无 meta 素材全过(additive 等价)
|
|
14
|
+
* - v7 词汇统一(标识/展示名分轴):id/name 翻转 + 四象限 legacy 矩阵
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
const BASE_MANIFEST = {
|
|
17
18
|
schemaVersion: 1,
|
|
18
|
-
|
|
19
|
+
id: 'module-sample',
|
|
19
20
|
version: '0.1.0',
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
describe('module-schema(descriptor zod 单源)', () => {
|
|
23
|
-
it('
|
|
24
|
+
it('v6 N1 旧键定向拒:顶层 contributes → ok:false + 三路径升级指引(非泛化 strict 报错)', () => {
|
|
25
|
+
const r = parseModuleDescriptor({ ...BASE_MANIFEST, contributes: { services: [] } });
|
|
26
|
+
expect(r.ok).toBe(false);
|
|
27
|
+
if (r.ok) return;
|
|
28
|
+
expect(r.errors[0]).toContain('`contributes` 已更名 `declare`');
|
|
29
|
+
expect(r.errors[0]).toContain('module update --source');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('v6 N1 旧键定向拒:declare.services → ok:false + consumes 指引', () => {
|
|
33
|
+
const r = parseModuleDescriptor({ ...BASE_MANIFEST, declare: { services: [{ service: 'x.y' }] } });
|
|
34
|
+
expect(r.ok).toBe(false);
|
|
35
|
+
if (r.ok) return;
|
|
36
|
+
expect(r.errors[0]).toContain('`declare.services` 已更名 `declare.consumes`');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('v6 owns 薄形状:strict 单键(元数据键拒——S1 防双数组漂移);owns/consumes 双键并存合法', () => {
|
|
40
|
+
const thin = parseModuleDescriptor({ ...BASE_MANIFEST, declare: { owns: [{ service: 'a.b', optional: true } as never] } });
|
|
41
|
+
expect(thin.ok).toBe(false); // owns 元素 strict——元数据归 consumes
|
|
42
|
+
const both = parseModuleDescriptor({
|
|
43
|
+
...BASE_MANIFEST,
|
|
44
|
+
declare: { owns: [{ service: 'a.b' }], consumes: [{ service: 'a.b', optional: false, name: 'T' }] },
|
|
45
|
+
});
|
|
46
|
+
expect(both.ok).toBe(true);
|
|
47
|
+
if (both.ok) {
|
|
48
|
+
expect(both.value.declare.owns).toEqual([{ service: 'a.b' }]);
|
|
49
|
+
expect(both.value.declare.consumes[0].name).toBe('T');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('P6 demand meta additive:services 元素带 name/description 通过;缺席仍合法', () => {
|
|
24
54
|
const withMeta = parseModuleDescriptor({
|
|
25
55
|
...BASE_MANIFEST,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ service: 'parking.query', optional: false,
|
|
56
|
+
declare: {
|
|
57
|
+
consumes: [
|
|
58
|
+
{ service: 'parking.query', optional: false, name: '车位查询', description: '查询车位状态' },
|
|
29
59
|
],
|
|
30
60
|
},
|
|
31
61
|
});
|
|
32
62
|
expect(withMeta.ok).toBe(true);
|
|
33
63
|
if (withMeta.ok) {
|
|
34
|
-
expect(withMeta.value.
|
|
35
|
-
expect(withMeta.value.
|
|
64
|
+
expect(withMeta.value.declare.consumes[0].name).toBe('车位查询');
|
|
65
|
+
expect(withMeta.value.declare.consumes[0].description).toBe('查询车位状态');
|
|
36
66
|
}
|
|
37
67
|
// 既有无 meta 素材全过(additive 等价)
|
|
38
68
|
const withoutMeta = parseModuleDescriptor({
|
|
39
69
|
...BASE_MANIFEST,
|
|
40
|
-
|
|
70
|
+
declare: { consumes: [{ service: 'parking.query' }] },
|
|
41
71
|
});
|
|
42
72
|
expect(withoutMeta.ok).toBe(true);
|
|
43
73
|
});
|
|
@@ -45,7 +75,7 @@ describe('module-schema(descriptor zod 单源)', () => {
|
|
|
45
75
|
it('resource type 放宽:任意 string 通过(词汇校验移谓词经 resolver 取词表)', () => {
|
|
46
76
|
const result = parseModuleDescriptor({
|
|
47
77
|
...BASE_MANIFEST,
|
|
48
|
-
|
|
78
|
+
declare: { resources: [{ id: 'kb-1', type: 'knowledge' }, { id: 'kb-2', type: 'future-type' }] },
|
|
49
79
|
});
|
|
50
80
|
expect(result.ok).toBe(true);
|
|
51
81
|
});
|
|
@@ -53,14 +83,14 @@ describe('module-schema(descriptor zod 单源)', () => {
|
|
|
53
83
|
it('H9b 安全钉子:provider 名含路径敌对字符 → 拒绝;合法字符集通过', () => {
|
|
54
84
|
const evil = parseModuleDescriptor({
|
|
55
85
|
...BASE_MANIFEST,
|
|
56
|
-
|
|
86
|
+
declare: {
|
|
57
87
|
providers: { slots: [{ service: 'a.b', provider: '../etc', implementation: 'x@1', credentialType: '-' }] },
|
|
58
88
|
},
|
|
59
89
|
});
|
|
60
90
|
expect(evil.ok).toBe(false);
|
|
61
91
|
const good = parseModuleDescriptor({
|
|
62
92
|
...BASE_MANIFEST,
|
|
63
|
-
|
|
93
|
+
declare: {
|
|
64
94
|
providers: { slots: [{ service: 'a.b', provider: 'wanda', implementation: 'wanda-a@1', credentialType: '-' }] },
|
|
65
95
|
},
|
|
66
96
|
});
|
|
@@ -72,13 +102,84 @@ describe('module-schema(descriptor zod 单源)', () => {
|
|
|
72
102
|
expect(SAFE_NAME_PATTERN.test('-lead')).toBe(false);
|
|
73
103
|
});
|
|
74
104
|
|
|
75
|
-
it('零贡献合法(provider-custom 空包形态):
|
|
105
|
+
it('零贡献合法(provider-custom 空包形态):declare 全 default', () => {
|
|
76
106
|
const parsed = moduleDescriptorSchema.safeParse(BASE_MANIFEST);
|
|
77
107
|
expect(parsed.success).toBe(true);
|
|
78
108
|
if (parsed.success) {
|
|
79
|
-
expect(parsed.data.
|
|
80
|
-
expect(parsed.data.
|
|
81
|
-
expect(parsed.data.
|
|
109
|
+
expect(parsed.data.declare.providers.slots).toEqual([]);
|
|
110
|
+
expect(parsed.data.declare.consumes).toEqual([]);
|
|
111
|
+
expect(parsed.data.declare.resources).toEqual([]);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('v7 B1 模块 meta:name = 展示名(id = 标识键);缺席仍合法(回退归视图层)', () => {
|
|
116
|
+
const withMeta = parseModuleDescriptor({ ...BASE_MANIFEST, name: '停车缴费', description: '车辆查询与绑定、停车费用查询、缴费支付与开票' });
|
|
117
|
+
expect(withMeta.ok).toBe(true);
|
|
118
|
+
if (withMeta.ok) {
|
|
119
|
+
expect(withMeta.value.id).toBe('module-sample');
|
|
120
|
+
expect(withMeta.value.name).toBe('停车缴费');
|
|
121
|
+
expect(withMeta.value.description).toContain('停车费用查询');
|
|
122
|
+
}
|
|
123
|
+
const withoutMeta = moduleDescriptorSchema.safeParse(BASE_MANIFEST);
|
|
124
|
+
expect(withoutMeta.success).toBe(true);
|
|
125
|
+
if (withoutMeta.success) {
|
|
126
|
+
expect(withoutMeta.data.name).toBeUndefined();
|
|
127
|
+
expect(withoutMeta.data.description).toBeUndefined();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── v7 词汇统一:legacy 四象限矩阵(旧态/半迁移定向拒;v7 两形态放行)──
|
|
132
|
+
|
|
133
|
+
it('v7 legacy ①:v6 旧态(name 标识 + title 展示、无 id)→ 定向拒 + legacy 标志 + 双键指引', () => {
|
|
134
|
+
const r = parseModuleDescriptor({
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
name: 'module-old',
|
|
137
|
+
title: '旧模块',
|
|
138
|
+
version: '0.1.0',
|
|
139
|
+
declare: { consumes: [{ service: 'a.b' }] },
|
|
140
|
+
});
|
|
141
|
+
expect(r.ok).toBe(false);
|
|
142
|
+
if (r.ok) return;
|
|
143
|
+
expect(r.legacy).toBe(true);
|
|
144
|
+
const joined = r.errors.join('\n');
|
|
145
|
+
expect(joined).toContain('标识键 `name` 已更名 `id`');
|
|
146
|
+
expect(joined).toContain('展示名键 `title` 已更名 `name`');
|
|
147
|
+
expect(joined).toContain('module update --source');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('v7 legacy ②:v6 最小旧态(仅 name 无 title)→ 定向拒 + legacy 标志', () => {
|
|
151
|
+
const r = parseModuleDescriptor({ schemaVersion: 1, name: 'module-old', version: '0.1.0' });
|
|
152
|
+
expect(r.ok).toBe(false);
|
|
153
|
+
if (r.ok) return;
|
|
154
|
+
expect(r.legacy).toBe(true);
|
|
155
|
+
expect(r.errors[0]).toContain('标识键 `name` 已更名 `id`');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('v7 legacy ③:半迁移(有 id 残留 title)→ 定向拒(title→name)', () => {
|
|
159
|
+
const r = parseModuleDescriptor({ ...BASE_MANIFEST, title: '残留展示名' });
|
|
160
|
+
expect(r.ok).toBe(false);
|
|
161
|
+
if (r.ok) return;
|
|
162
|
+
expect(r.legacy).toBe(true);
|
|
163
|
+
expect(r.errors[0]).toContain('展示名键 `title` 已更名 `name`');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('v7 legacy ④:declare.consumes[].title 残留(根级已翻转)→ 定向拒', () => {
|
|
167
|
+
const r = parseModuleDescriptor({
|
|
168
|
+
...BASE_MANIFEST,
|
|
169
|
+
declare: { consumes: [{ service: 'a.b', title: '旧槽名' }] },
|
|
170
|
+
});
|
|
171
|
+
expect(r.ok).toBe(false);
|
|
172
|
+
if (r.ok) return;
|
|
173
|
+
expect(r.legacy).toBe(true);
|
|
174
|
+
expect(r.errors[0]).toContain('展示名键 `title` 已更名 `name`');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('v7 完整形态:id + name(展示名)+ description 并存合法(同名键歧义以 id 在场性消解)', () => {
|
|
178
|
+
const r = parseModuleDescriptor({ ...BASE_MANIFEST, name: '示例模块' });
|
|
179
|
+
expect(r.ok).toBe(true);
|
|
180
|
+
if (r.ok) {
|
|
181
|
+
expect(r.value.id).toBe('module-sample');
|
|
182
|
+
expect(r.value.name).toBe('示例模块');
|
|
82
183
|
}
|
|
83
184
|
});
|
|
84
185
|
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveModuleKey, resolveServiceKey } from '../src/assembly/resolve-key.js';
|
|
3
|
+
import { AppToolkitError } from '../src/core/errors.js';
|
|
4
|
+
import type { WalkManifestsResult, WalkModuleInfo } from '../src/assembly/walk-manifests.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* resolve-key(双键寻址 helper——v7 词汇统一):歧义矩阵单测(纯参数式——walk 手工构造)。
|
|
8
|
+
* 语义锁:id 精确优先 / 展示名唯一命中 / 多命中 404 带候选 / 零命中 404 /
|
|
9
|
+
* 供给足迹进服务键集(O6)/ configuredKeys 注入。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function mod(id: string, name?: string): WalkModuleInfo {
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
dir: `/x/${id}`,
|
|
16
|
+
pkg: `@app/${id}`,
|
|
17
|
+
descriptor: {
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
id,
|
|
20
|
+
...(name !== undefined ? { name } : {}),
|
|
21
|
+
version: '0.1.0',
|
|
22
|
+
kind: 'business',
|
|
23
|
+
risk: { level: 'low', writeBoundary: 'source' },
|
|
24
|
+
distribution: { defaultMode: 'codegen' },
|
|
25
|
+
declare: {
|
|
26
|
+
handlers: [], tools: [], cards: [], routes: [], pages: [], tabs: [],
|
|
27
|
+
providers: { slots: [] }, owns: [], consumes: [], resources: [],
|
|
28
|
+
},
|
|
29
|
+
dependencies: { modules: [] },
|
|
30
|
+
env: [],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function walk(overrides: Partial<WalkManifestsResult> = {}): WalkManifestsResult {
|
|
36
|
+
return {
|
|
37
|
+
modules: [],
|
|
38
|
+
catalog: { providers: {}, services: {}, credentialTypes: {} },
|
|
39
|
+
serviceSchemas: {},
|
|
40
|
+
vocabulary: [],
|
|
41
|
+
slotMeta: {},
|
|
42
|
+
ownership: {},
|
|
43
|
+
declarationIssues: [],
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('resolveModuleKey(模块双键)', () => {
|
|
49
|
+
it('id 精确命中优先(即使展示名域存在同串冲突)', () => {
|
|
50
|
+
const w = walk({
|
|
51
|
+
modules: [mod('module-a', '模块X'), mod('module-b', 'module-a')],
|
|
52
|
+
});
|
|
53
|
+
expect(resolveModuleKey(w, 'module-a').id).toBe('module-a');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('唯一展示名命中 → 返回该模块', () => {
|
|
57
|
+
const w = walk({ modules: [mod('module-a', '停车缴费'), mod('module-b')] });
|
|
58
|
+
expect(resolveModuleKey(w, '停车缴费').id).toBe('module-a');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('展示名缺省模块:name 兜底 = id(可按 id 命中兜底分支)', () => {
|
|
62
|
+
const w = walk({ modules: [mod('module-b')] });
|
|
63
|
+
expect(resolveModuleKey(w, 'module-b').id).toBe('module-b');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('多展示名命中 → 404 MODULE_NOT_FOUND + 候选列表 + 改用 id 指引', () => {
|
|
67
|
+
const w = walk({ modules: [mod('module-a', '同名'), mod('module-b', '同名')] });
|
|
68
|
+
try {
|
|
69
|
+
resolveModuleKey(w, '同名');
|
|
70
|
+
expect.unreachable('应 throw');
|
|
71
|
+
} catch (e) {
|
|
72
|
+
expect(e).toBeInstanceOf(AppToolkitError);
|
|
73
|
+
const err = e as AppToolkitError;
|
|
74
|
+
expect(err.code).toBe('MODULE_NOT_FOUND');
|
|
75
|
+
expect(err.httpStatus).toBe(404);
|
|
76
|
+
expect(err.message).toContain('id=module-a');
|
|
77
|
+
expect(err.message).toContain('id=module-b');
|
|
78
|
+
expect(err.message).toContain('改用模块 id');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('零命中 → 404 MODULE_NOT_FOUND', () => {
|
|
83
|
+
expect(() => resolveModuleKey(walk({ modules: [mod('module-a')] }), 'ghost')).toThrowError(AppToolkitError);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('resolveServiceKey(服务双键)', () => {
|
|
88
|
+
it('词汇键精确命中(vocabulary)', () => {
|
|
89
|
+
const w = walk({ vocabulary: ['parking.query'] });
|
|
90
|
+
expect(resolveServiceKey(w, [], 'parking.query')).toBe('parking.query');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('demand 侧命中(catalog.services)', () => {
|
|
94
|
+
const w = walk({ catalog: { providers: {}, services: { 'auth.x': { optional: true, defaults: [], owners: [] } }, credentialTypes: {} } });
|
|
95
|
+
expect(resolveServiceKey(w, [], 'auth.x')).toBe('auth.x');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('供给足迹命中(纯供给无词汇/demand——O6 多 impl 形态)', () => {
|
|
99
|
+
const w = walk({
|
|
100
|
+
catalog: {
|
|
101
|
+
providers: { mock: { 'mock-p@1': { provider: 'mock', implementation: 'mock-p@1', credentialType: '-', owner: '@app/provider-mock', services: ['parking.query'] } } },
|
|
102
|
+
services: {},
|
|
103
|
+
credentialTypes: {},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
expect(resolveServiceKey(w, [], 'parking.query')).toBe('parking.query');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('configuredKeys 注入命中(已配置未声明槽)', () => {
|
|
110
|
+
expect(resolveServiceKey(walk(), ['legacy.slot'], 'legacy.slot')).toBe('legacy.slot');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('唯一展示名命中(slotMeta 词汇 meta 源)', () => {
|
|
114
|
+
const w = walk({
|
|
115
|
+
vocabulary: ['parking.query', 'parking.payment'],
|
|
116
|
+
slotMeta: { 'parking.query': { name: '停车查询' } },
|
|
117
|
+
});
|
|
118
|
+
expect(resolveServiceKey(w, [], '停车查询')).toBe('parking.query');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('展示名 demand meta 源命中(词汇缺席回落)', () => {
|
|
122
|
+
const w = walk({
|
|
123
|
+
vocabulary: ['auth.x'],
|
|
124
|
+
catalog: { providers: {}, services: { 'auth.x': { optional: true, defaults: [], name: '登录', owners: [] } }, credentialTypes: {} },
|
|
125
|
+
});
|
|
126
|
+
expect(resolveServiceKey(w, [], '登录')).toBe('auth.x');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('多展示名命中 → 404 SERVICE_NOT_FOUND + 候选列表', () => {
|
|
130
|
+
const w = walk({
|
|
131
|
+
vocabulary: ['a.x', 'b.x'],
|
|
132
|
+
slotMeta: { 'a.x': { name: '同名' }, 'b.x': { name: '同名' } },
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
resolveServiceKey(w, [], '同名');
|
|
136
|
+
expect.unreachable('应 throw');
|
|
137
|
+
} catch (e) {
|
|
138
|
+
const err = e as AppToolkitError;
|
|
139
|
+
expect(err.code).toBe('SERVICE_NOT_FOUND');
|
|
140
|
+
expect(err.httpStatus).toBe(404);
|
|
141
|
+
expect(err.message).toContain('a.x');
|
|
142
|
+
expect(err.message).toContain('b.x');
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('零命中 → 404 SERVICE_NOT_FOUND', () => {
|
|
147
|
+
expect(() => resolveServiceKey(walk({ vocabulary: ['a.x'] }), [], 'ghost')).toThrowError(AppToolkitError);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('原型键不放行(hasOwn 判据——"toString"/"constructor" 非自有键)', () => {
|
|
151
|
+
expect(() => resolveServiceKey(walk(), [], 'toString')).toThrowError(AppToolkitError);
|
|
152
|
+
expect(() => resolveServiceKey(walk(), [], 'constructor')).toThrowError(AppToolkitError);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -22,6 +22,10 @@ const SUPPORTED: ReadonlySet<string> = new Set([
|
|
|
22
22
|
'format', 'minLength', 'maxLength', 'additionalProperties', '$schema',
|
|
23
23
|
// 结构性(只读降级 + 值保全——支持集含之,出现不红)
|
|
24
24
|
'oneOf', 'anyOf', '$ref', 'items', 'patternProperties',
|
|
25
|
+
// G 批(v6 全局字段声明):属性级 x-tbox-global 旗标——表单收窄消费(app/module 轴过滤),
|
|
26
|
+
// 渲染器不解释(toolkit buildIntegrationFieldRegistry 收集);Ajv compile 位若未来启用,
|
|
27
|
+
// 支持集须含本键(当前 config schema 零 compile 位——文档纪律见 integrations-v6 Note)
|
|
28
|
+
'x-tbox-global',
|
|
25
29
|
]);
|
|
26
30
|
|
|
27
31
|
/** 递归走已知 subschema 位,收集 schema 节点自身键(properties 值下的键 = 字段名,不属关键词) */
|
|
@@ -98,4 +102,86 @@ describe('vendor schema 关键词矩阵 ⊆ 渲染器支持集(D32 表单引
|
|
|
98
102
|
expect(used.has(k), `基线关键词 ${k} 未被提取到——提取器或目录布局变化,人工核对`).toBe(true);
|
|
99
103
|
}
|
|
100
104
|
});
|
|
105
|
+
|
|
106
|
+
it('B5 展示名 tripwire:全文件 properties.*.title 必在(新字段裸键名结构性不可上线)', () => {
|
|
107
|
+
const files = schemaFiles();
|
|
108
|
+
expect(files.length).toBeGreaterThan(5);
|
|
109
|
+
for (const { pkg, rel } of files) {
|
|
110
|
+
const doc = JSON.parse(readFileSync(rel, 'utf8')) as { properties?: Record<string, unknown> };
|
|
111
|
+
for (const [key, sub] of Object.entries(doc.properties ?? {})) {
|
|
112
|
+
const hasTitle = !!sub && typeof sub === 'object' && typeof (sub as { title?: unknown }).title === 'string';
|
|
113
|
+
expect(hasTitle, `${pkg} 字段 "${key}" 缺 title——集成配置表单展示名必填(B5 防线;纯名词,长尾注记移 description)`).toBe(true);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('G 批 x-tbox-global 标记结构断言(integrations-v6 全局字段声明)', () => {
|
|
120
|
+
function globalKeysOf(rel: string): string[] {
|
|
121
|
+
const doc = JSON.parse(readFileSync(rel, 'utf8')) as {
|
|
122
|
+
properties?: Record<string, { 'x-tbox-global'?: unknown }>;
|
|
123
|
+
};
|
|
124
|
+
return Object.entries(doc.properties ?? {})
|
|
125
|
+
.filter(([, v]) => v && typeof v === 'object' && v['x-tbox-global'] === true)
|
|
126
|
+
.map(([k]) => k);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it('标记值恒布尔 true 且标记键 ∈ properties(结构形状 tripwire)', () => {
|
|
130
|
+
for (const { pkg, rel } of schemaFiles()) {
|
|
131
|
+
const doc = JSON.parse(readFileSync(rel, 'utf8')) as {
|
|
132
|
+
properties?: Record<string, Record<string, unknown>>;
|
|
133
|
+
};
|
|
134
|
+
for (const [k, sub] of Object.entries(doc.properties ?? {})) {
|
|
135
|
+
if (sub && 'x-tbox-global' in sub) {
|
|
136
|
+
expect(sub['x-tbox-global'], `${pkg} ${k} 标记值恒 true(布尔旗标非枚举位)`).toBe(true);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('mocktest 期望集锁⑥:member/parking/promotion 全局标记键恒等(消费面 S8 互证基线)', () => {
|
|
143
|
+
// 2026-09-20 拆分:config schema 供给面随 provider-mocktest 迁移(键集不变)
|
|
144
|
+
const base = join(fileURLToPath(new URL('../../..', import.meta.url)), 'modules', 'provider-mocktest', 'schemas');
|
|
145
|
+
expect(globalKeysOf(join(base, 'member.config.json')).sort()).toEqual(['cardName', 'initialPoints', 'levelName']);
|
|
146
|
+
expect(globalKeysOf(join(base, 'parking.config.json'))).toEqual(['parkName']);
|
|
147
|
+
expect(globalKeysOf(join(base, 'promotion.config.json')).sort()).toEqual(['activityTitle', 'couponTitle']);
|
|
148
|
+
expect(globalKeysOf(join(base, 'shopping.config.json'))).toEqual([]); // 零声明负例
|
|
149
|
+
expect(globalKeysOf(join(base, 'mall-info.config.json'))).toEqual([]); // 零声明负例
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('素材对账锁:.real-env 全局层键 ⊆ 对应厂商声明并集(mock/mocktest/wanda/joycity 四档)', () => {
|
|
153
|
+
const repoRoot = fileURLToPath(new URL('../../..', import.meta.url));
|
|
154
|
+
const collect = (vendor: string): Set<string> => {
|
|
155
|
+
const out = new Set<string>();
|
|
156
|
+
for (const f of schemaFiles().filter((x) => x.pkg === `provider-${vendor}`)) {
|
|
157
|
+
for (const k of globalKeysOf(f.rel)) out.add(k);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
};
|
|
161
|
+
const globalLayerKeys = (vendor: string): Set<string> => {
|
|
162
|
+
const ig = JSON.parse(readFileSync(join(repoRoot, '.real-env', 'config', vendor, 'integrations.json'), 'utf8')) as {
|
|
163
|
+
config?: Record<string, unknown>;
|
|
164
|
+
configByInstance?: Record<string, Record<string, unknown>>;
|
|
165
|
+
modules?: Record<string, { config?: Record<string, unknown> }>;
|
|
166
|
+
};
|
|
167
|
+
const out = new Set<string>();
|
|
168
|
+
for (const k of Object.keys(ig.config ?? {})) out.add(k);
|
|
169
|
+
for (const layer of Object.values(ig.configByInstance ?? {})) {
|
|
170
|
+
for (const k of Object.keys(layer)) out.add(k);
|
|
171
|
+
}
|
|
172
|
+
for (const node of Object.values(ig.modules ?? {})) {
|
|
173
|
+
for (const k of Object.keys(node.config ?? {})) out.add(k);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
};
|
|
177
|
+
// mock 保留 = 空集 tripwire(公开包 0.5.0 零 schema 后任何 config 键混回 mock 档即红);
|
|
178
|
+
// mocktest 承接 schema 供给面并续接对账
|
|
179
|
+
for (const vendor of ['mock', 'mocktest', 'wanda', 'joycity']) {
|
|
180
|
+
const declared = collect(vendor);
|
|
181
|
+
const used = globalLayerKeys(vendor);
|
|
182
|
+
for (const k of used) {
|
|
183
|
+
expect(declared.has(k), `${vendor} 档全局层键 "${k}" ∉ schema x-tbox-global 声明并集——补标记或改素材(对账锁)`).toBe(true);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
101
187
|
});
|