@tbox.cn/app-toolkit 0.4.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 +315 -144
- package/dist/index.js +9 -9
- 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 +138 -85
- package/src/core/contracts-expected.ts +2 -2
- package/src/core/contracts-resolver.ts +12 -1
- package/src/core/module-schema.ts +87 -14
- package/src/dto.ts +48 -33
- package/src/factory.ts +62 -18
- package/src/index.ts +14 -7
- 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 +33 -14
- package/src/integrations/write.ts +43 -27
- package/src/views/app.ts +4 -4
- package/src/views/context.ts +11 -12
- package/src/views/integration-schemas.ts +61 -17
- package/src/views/modules.ts +29 -23
- package/src/views/providers.ts +5 -5
- package/src/views/service-detail.ts +1 -1
- package/src/views/service-resolutions.ts +14 -13
- package/tests/credentials-view.test.ts +2 -2
- package/tests/demo-app.ts +11 -6
- package/tests/dev-manifest-catalog.test.ts +14 -14
- package/tests/file-cache.test.ts +5 -5
- package/tests/module-schema.test.ts +106 -20
- package/tests/resolve-key.test.ts +154 -0
- package/tests/vendor-schema-keywords.test.ts +74 -0
- package/tests/views.test.ts +296 -166
- 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
package/tests/views.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { createAppToolkit, type AppToolkit } from '../src/factory.js';
|
|
6
6
|
import { invalidateContractsResolver } from '../src/core/contracts-resolver.js';
|
|
7
7
|
import { walkManifests, type WalkManifestsResult } from '../src/assembly/walk-manifests.js';
|
|
8
|
-
import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas } from '../src/views/integration-schemas.js';
|
|
8
|
+
import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas, loadServiceFieldRegistry } from '../src/views/integration-schemas.js';
|
|
9
9
|
import { resolveServiceDisplay } from '../src/views/context.js';
|
|
10
10
|
import type { ViewSnapshot } from '../src/views/context.js';
|
|
11
11
|
import { URL_METHOD_MAP } from './naming-alignment.test.js';
|
|
@@ -42,11 +42,12 @@ function harnessWithContracts(
|
|
|
42
42
|
'packages/module-parking/tbox.module.json',
|
|
43
43
|
JSON.stringify({
|
|
44
44
|
schemaVersion: 1,
|
|
45
|
-
|
|
45
|
+
id: 'module-parking',
|
|
46
46
|
version: '0.1.0',
|
|
47
47
|
kind: 'business',
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
declare: {
|
|
49
|
+
owns: [{ service: 'parking.query' }],
|
|
50
|
+
consumes: [{ service: 'parking.query', optional: false, name: '车位查询' }],
|
|
50
51
|
},
|
|
51
52
|
dependencies: { modules: [] },
|
|
52
53
|
env: [],
|
|
@@ -62,7 +63,7 @@ function harnessWithContracts(
|
|
|
62
63
|
{ id: 'mall-sh-01', name: '上海店' },
|
|
63
64
|
],
|
|
64
65
|
defaultInstanceId: 'mall-bj-01',
|
|
65
|
-
|
|
66
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
66
67
|
services: {
|
|
67
68
|
'parking.query': { provider: { provider: 'my-custom-api', credentialType: '-' }, implementation: 'my-custom-parking@1' },
|
|
68
69
|
'auth.alipay-login': {},
|
|
@@ -104,9 +105,12 @@ function layerHarness(): {
|
|
|
104
105
|
].join(' '),
|
|
105
106
|
});
|
|
106
107
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
108
|
+
// G 批:fixture 全字段标 x-tbox-global(表单收窄过滤后 E1/E2/E5 字段面逐字不变);
|
|
109
|
+
// wanda parking.json 另含未标记键 parkLotId(P16 负例——layer.properties 在场、registry.fields 不收)
|
|
110
|
+
const g = (sub: Record<string, unknown>): Record<string, unknown> => ({ ...sub, 'x-tbox-global': true });
|
|
107
111
|
const schemas = {
|
|
108
|
-
query: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
|
|
109
|
-
parking: { type: 'object', properties: { mallId: { type: 'string' } } },
|
|
112
|
+
query: { type: 'object', properties: { baseUrl: g({ type: 'string', format: 'uri' }) }, required: ['baseUrl'] },
|
|
113
|
+
parking: { type: 'object', properties: { mallId: g({ type: 'string' }), parkLotId: { type: 'string', title: '车场组' } } },
|
|
110
114
|
};
|
|
111
115
|
// v4.6 joycity 代数 fixture:R1(baseUrl 双无 enum / env 同集异序 / envMixed 混布)/
|
|
112
116
|
// R2(options 深相等 / meta 异构)/ R3(lotId string≠integer / timeout number≠integer / untyped 无 type)/
|
|
@@ -115,14 +119,14 @@ function layerHarness(): {
|
|
|
115
119
|
query: {
|
|
116
120
|
type: 'object',
|
|
117
121
|
properties: {
|
|
118
|
-
baseUrl: { type: 'string', format: 'uri' },
|
|
119
|
-
lotId: { type: 'string' },
|
|
120
|
-
env: { type: 'string', enum: ['dev', 'staging', 'prod'] },
|
|
121
|
-
envMixed: { type: 'string', enum: ['a', 'b'] },
|
|
122
|
-
meta: { type: 'object', properties: { region: { type: 'string' } } },
|
|
123
|
-
options: { type: 'object', properties: { x: { type: 'string' } } },
|
|
124
|
-
untyped: { description: 'x' },
|
|
125
|
-
timeout: { type: 'number' },
|
|
122
|
+
baseUrl: { 'x-tbox-global': true, type: 'string', format: 'uri' },
|
|
123
|
+
lotId: { 'x-tbox-global': true, type: 'string' },
|
|
124
|
+
env: { 'x-tbox-global': true, type: 'string', enum: ['dev', 'staging', 'prod'] },
|
|
125
|
+
envMixed: { 'x-tbox-global': true, type: 'string', enum: ['a', 'b'] },
|
|
126
|
+
meta: { 'x-tbox-global': true, type: 'object', properties: { region: { 'x-tbox-global': true, type: 'string' } } },
|
|
127
|
+
options: { 'x-tbox-global': true, type: 'object', properties: { x: { 'x-tbox-global': true, type: 'string' } } },
|
|
128
|
+
untyped: { 'x-tbox-global': true, description: 'x' },
|
|
129
|
+
timeout: { 'x-tbox-global': true, type: 'number' },
|
|
126
130
|
},
|
|
127
131
|
required: ['baseUrl', 'lotId'],
|
|
128
132
|
},
|
|
@@ -130,15 +134,15 @@ function layerHarness(): {
|
|
|
130
134
|
type: 'object',
|
|
131
135
|
additionalProperties: false,
|
|
132
136
|
properties: {
|
|
133
|
-
apiKey: { type: 'string' },
|
|
134
|
-
baseUrl: { type: 'string' },
|
|
135
|
-
lotId: { type: 'integer' },
|
|
136
|
-
env: { type: 'string', enum: ['prod', 'dev', 'staging'] },
|
|
137
|
-
envMixed: { type: 'string' },
|
|
138
|
-
meta: { type: 'object', properties: { zone: { type: 'number' } } },
|
|
139
|
-
options: { type: 'object', properties: { x: { type: 'string' } } },
|
|
140
|
-
untyped: { type: 'string' },
|
|
141
|
-
timeout: { type: 'integer' },
|
|
137
|
+
apiKey: { 'x-tbox-global': true, type: 'string' },
|
|
138
|
+
baseUrl: { 'x-tbox-global': true, type: 'string' },
|
|
139
|
+
lotId: { 'x-tbox-global': true, type: 'integer' },
|
|
140
|
+
env: { 'x-tbox-global': true, type: 'string', enum: ['prod', 'dev', 'staging'] },
|
|
141
|
+
envMixed: { 'x-tbox-global': true, type: 'string' },
|
|
142
|
+
meta: { 'x-tbox-global': true, type: 'object', properties: { zone: { 'x-tbox-global': true, type: 'number' } } },
|
|
143
|
+
options: { 'x-tbox-global': true, type: 'object', properties: { x: { 'x-tbox-global': true, type: 'string' } } },
|
|
144
|
+
untyped: { 'x-tbox-global': true, type: 'string' },
|
|
145
|
+
timeout: { 'x-tbox-global': true, type: 'integer' },
|
|
142
146
|
},
|
|
143
147
|
required: ['apiKey', 'options'],
|
|
144
148
|
},
|
|
@@ -148,10 +152,10 @@ function layerHarness(): {
|
|
|
148
152
|
'packages/provider-w/tbox.module.json',
|
|
149
153
|
JSON.stringify({
|
|
150
154
|
schemaVersion: 1,
|
|
151
|
-
|
|
155
|
+
id: 'provider-w',
|
|
152
156
|
version: '0.1.0',
|
|
153
157
|
kind: 'business',
|
|
154
|
-
|
|
158
|
+
declare: {
|
|
155
159
|
providers: {
|
|
156
160
|
slots: [
|
|
157
161
|
{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
|
|
@@ -171,10 +175,10 @@ function layerHarness(): {
|
|
|
171
175
|
'packages/provider-j/tbox.module.json',
|
|
172
176
|
JSON.stringify({
|
|
173
177
|
schemaVersion: 1,
|
|
174
|
-
|
|
178
|
+
id: 'provider-j',
|
|
175
179
|
version: '0.1.0',
|
|
176
180
|
kind: 'business',
|
|
177
|
-
|
|
181
|
+
declare: {
|
|
178
182
|
providers: {
|
|
179
183
|
slots: [
|
|
180
184
|
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
@@ -194,10 +198,10 @@ function layerHarness(): {
|
|
|
194
198
|
'packages/provider-p/tbox.module.json',
|
|
195
199
|
JSON.stringify({
|
|
196
200
|
schemaVersion: 1,
|
|
197
|
-
|
|
201
|
+
id: 'provider-p',
|
|
198
202
|
version: '0.1.0',
|
|
199
203
|
kind: 'business',
|
|
200
|
-
|
|
204
|
+
declare: {
|
|
201
205
|
providers: {
|
|
202
206
|
slots: [{ service: 'parking.payment', provider: 'partial', implementation: 'partial-payment@1', credentialType: 'partial-c' }],
|
|
203
207
|
},
|
|
@@ -211,10 +215,13 @@ function layerHarness(): {
|
|
|
211
215
|
'packages/module-parking/tbox.module.json',
|
|
212
216
|
JSON.stringify({
|
|
213
217
|
schemaVersion: 1,
|
|
214
|
-
|
|
218
|
+
id: 'module-parking',
|
|
215
219
|
version: '0.1.0',
|
|
216
220
|
kind: 'business',
|
|
217
|
-
|
|
221
|
+
declare: {
|
|
222
|
+
owns: [{ service: 'parking.query' }, { service: 'parking.payment' }],
|
|
223
|
+
consumes: [{ service: 'parking.query', optional: false }, { service: 'parking.payment', optional: true }],
|
|
224
|
+
},
|
|
218
225
|
dependencies: { modules: [] },
|
|
219
226
|
env: [],
|
|
220
227
|
}),
|
|
@@ -226,7 +233,7 @@ function layerHarness(): {
|
|
|
226
233
|
provider: 'wanda',
|
|
227
234
|
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
228
235
|
defaultInstanceId: 'mall-bj-01',
|
|
229
|
-
|
|
236
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
230
237
|
services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
|
|
231
238
|
}),
|
|
232
239
|
);
|
|
@@ -259,8 +266,8 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
259
266
|
expect(Object.keys(appView)).toEqual(['services', 'integration']);
|
|
260
267
|
const mod = await h.tk.loadModule('module-parking');
|
|
261
268
|
// v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
|
|
262
|
-
//
|
|
263
|
-
expect(Object.keys(mod)).toEqual(['
|
|
269
|
+
// v7 词汇统一:模块展示名 name 前置(恒发——兜底 id)
|
|
270
|
+
expect(Object.keys(mod)).toEqual(['name', 'description', 'ownedServices', 'integration', 'binding', 'configEditable', 'services']);
|
|
264
271
|
const svc = await h.tk.loadService('parking.query');
|
|
265
272
|
expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
|
|
266
273
|
// 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
|
|
@@ -513,8 +520,8 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
|
|
|
513
520
|
'packages/provider-multi/tbox.module.json',
|
|
514
521
|
JSON.stringify({
|
|
515
522
|
schemaVersion: 1,
|
|
516
|
-
|
|
517
|
-
|
|
523
|
+
id: 'provider-multi',
|
|
524
|
+
declare: {
|
|
518
525
|
providers: {
|
|
519
526
|
slots: [
|
|
520
527
|
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
@@ -585,8 +592,11 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
|
|
|
585
592
|
// api.md §5 表
|
|
586
593
|
'CREDENTIAL_FILE_MISSING', 'CREDENTIAL_TYPE_MISMATCH', 'IMPL_WITHOUT_PROVIDER', 'INSTANCE_SUPPLY_UNMATCHED',
|
|
587
594
|
'SCHEMA_FILE_MISSING', 'DOMAIN_CONFIG_UNUSED', 'DOMAIN_CONFIG_INVALID', 'INSTANCE_KEY_NOT_REGISTERED',
|
|
588
|
-
'REALM_SWITCH_HINT', 'WILDCARD_ALIGNMENT', 'VENDOR_NOT_INSTALLED', 'BINDING_UNRESOLVED',
|
|
595
|
+
'REALM_SWITCH_HINT', 'WILDCARD_ALIGNMENT', 'VENDOR_NOT_INSTALLED', 'BINDING_UNRESOLVED',
|
|
589
596
|
'RESOURCE_TYPE_UNKNOWN', 'RESOURCE_NOT_DECLARED',
|
|
597
|
+
// v6 additive(api.md §5 表已收口):MODULE_CONFIG_DEAD / #17 GLOBAL_FIELD_UNDECLARED;
|
|
598
|
+
// v6 退役:DOMAIN_KEY_UNMATCHED(#14——domains 键退场)
|
|
599
|
+
'MODULE_CONFIG_DEAD', 'GLOBAL_FIELD_UNDECLARED',
|
|
590
600
|
// 谓词头映射表 additive(检查族编号映射:#3/#4/#5 内联/#13/v4)
|
|
591
601
|
'SLOT_NOT_DECLARED', 'SLOT_NOT_CONFIGURED', 'INLINE_PROVIDER_INVALID', 'MULTI_INSTANCE_NO_DEFAULT', 'INSTANCE_DISABLED',
|
|
592
602
|
]);
|
|
@@ -636,18 +646,18 @@ describe('A2 错误面(P-4 notes 优先 / P-5 畸形包 503)', () => {
|
|
|
636
646
|
});
|
|
637
647
|
});
|
|
638
648
|
|
|
639
|
-
describe('D34
|
|
640
|
-
it('D1
|
|
649
|
+
describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单数重写)', () => {
|
|
650
|
+
it('D1 单槽回归:ownedServices/binding/configEditable 装配 + integration 集成位原值', async () => {
|
|
641
651
|
const h2 = harnessWithContracts();
|
|
642
652
|
const mod = await h2.tk.loadModule('module-parking');
|
|
643
|
-
expect(mod.
|
|
653
|
+
expect(mod.ownedServices).toEqual(['parking.query']);
|
|
644
654
|
expect(mod.configEditable).toBe(true);
|
|
645
|
-
expect(mod.
|
|
655
|
+
expect(mod.binding).toEqual({ provider: 'wanda', layer: 'module' });
|
|
646
656
|
expect(mod.integration).toEqual({ provider: 'wanda' });
|
|
647
657
|
h2.app.dispose();
|
|
648
658
|
});
|
|
649
659
|
|
|
650
|
-
it('D2
|
|
660
|
+
it('D2 多域归属合法化(v6 正例化——跨域 owns 单键落位):binding 单值作用全 owned + PUT 全量替换', async () => {
|
|
651
661
|
const app = createTempApp('tbox-d2-');
|
|
652
662
|
invalidateContractsResolver();
|
|
653
663
|
const store = joinStore(app.appDir, 'contracts');
|
|
@@ -658,33 +668,30 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
|
|
|
658
668
|
'packages/module-multi/tbox.module.json',
|
|
659
669
|
JSON.stringify({
|
|
660
670
|
schemaVersion: 1,
|
|
661
|
-
|
|
671
|
+
id: 'module-multi',
|
|
662
672
|
version: '0.1.0',
|
|
663
673
|
kind: 'business',
|
|
664
|
-
|
|
674
|
+
declare: { owns: [{ service: 'parking.query' }, { service: 'member.account' }], consumes: [{ service: 'parking.query', optional: false }] },
|
|
665
675
|
dependencies: { modules: [] },
|
|
666
676
|
env: [],
|
|
667
677
|
}),
|
|
668
678
|
);
|
|
669
679
|
const tk = createAppToolkit(app.appDir);
|
|
670
680
|
const mod = await tk.loadModule('module-multi');
|
|
671
|
-
expect(mod.
|
|
681
|
+
expect(mod.ownedServices).toEqual(['member.account', 'parking.query']);
|
|
672
682
|
expect(mod.integration).toBeNull();
|
|
673
|
-
expect(mod.configEditable).toBe(
|
|
674
|
-
expect(mod.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
await
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
message: expect.stringContaining('跨 2 个域(member、parking)'),
|
|
682
|
-
});
|
|
683
|
-
await expect(tk.writeModuleIntegration('module-multi', {})).rejects.toMatchObject({ code: 'VALIDATION_FAILED' });
|
|
683
|
+
expect(mod.configEditable).toBe(true);
|
|
684
|
+
expect(mod.binding).toEqual({ provider: null, layer: null });
|
|
685
|
+
// v6 写通道:多域归属合法——模块级 provider 单值落 modules[module-multi]
|
|
686
|
+
const r = await tk.writeModuleIntegration('module-multi', { provider: 'wanda' });
|
|
687
|
+
expect(r.written).toBe(true);
|
|
688
|
+
const mod2 = await tk.loadModule('module-multi');
|
|
689
|
+
expect(mod2.integration).toEqual({ provider: 'wanda' });
|
|
690
|
+
expect(mod2.binding).toEqual({ provider: 'wanda', layer: 'module' });
|
|
684
691
|
app.dispose();
|
|
685
692
|
});
|
|
686
693
|
|
|
687
|
-
it('D3
|
|
694
|
+
it('D3 零归属模块:ownedServices [] + configEditable false + PUT 400(zero-ownership)', async () => {
|
|
688
695
|
const app = createTempApp('tbox-d3-');
|
|
689
696
|
invalidateContractsResolver();
|
|
690
697
|
const store = joinStore(app.appDir, 'contracts');
|
|
@@ -695,63 +702,95 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
|
|
|
695
702
|
'packages/module-zero/tbox.module.json',
|
|
696
703
|
JSON.stringify({
|
|
697
704
|
schemaVersion: 1,
|
|
698
|
-
|
|
705
|
+
id: 'module-zero',
|
|
699
706
|
version: '0.1.0',
|
|
700
707
|
kind: 'business',
|
|
701
|
-
|
|
708
|
+
declare: {},
|
|
702
709
|
dependencies: { modules: [] },
|
|
703
710
|
env: [],
|
|
704
711
|
}),
|
|
705
712
|
);
|
|
706
713
|
const tk = createAppToolkit(app.appDir);
|
|
707
714
|
const mod = await tk.loadModule('module-zero');
|
|
708
|
-
expect(mod.
|
|
709
|
-
expect(mod.
|
|
715
|
+
expect(mod.ownedServices).toEqual([]);
|
|
716
|
+
expect(mod.binding).toEqual({ provider: null, layer: null });
|
|
710
717
|
expect(mod.integration).toBeNull();
|
|
711
718
|
await expect(tk.writeModuleIntegration('module-zero', { provider: 'x' })).rejects.toMatchObject({
|
|
712
719
|
code: 'VALIDATION_FAILED',
|
|
713
720
|
httpStatus: 400,
|
|
714
|
-
message: expect.stringContaining('
|
|
721
|
+
message: expect.stringContaining('未声明任何 declare.owns'),
|
|
715
722
|
});
|
|
716
723
|
app.dispose();
|
|
717
724
|
});
|
|
718
725
|
|
|
719
|
-
it('D4
|
|
726
|
+
it('D4 v6 互斥归属:双模块 owns 同服务 → OWNERSHIP_CONFLICT(先声明胜出)+ 双方可写(归属冲突≠写禁)', async () => {
|
|
720
727
|
const app = createTempApp('tbox-d4-');
|
|
721
728
|
invalidateContractsResolver();
|
|
722
729
|
const store = joinStore(app.appDir, 'contracts');
|
|
723
730
|
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
724
731
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
725
|
-
const manifest = (
|
|
732
|
+
const manifest = (id: string, service: string): string =>
|
|
726
733
|
JSON.stringify({
|
|
727
734
|
schemaVersion: 1,
|
|
728
|
-
|
|
735
|
+
id,
|
|
729
736
|
version: '0.1.0',
|
|
730
737
|
kind: 'business',
|
|
731
|
-
|
|
738
|
+
declare: { owns: [{ service }], consumes: [{ service, optional: false }] },
|
|
732
739
|
dependencies: { modules: [] },
|
|
733
740
|
env: [],
|
|
734
741
|
});
|
|
735
742
|
writeAppFile(app.appDir, 'packages/module-a/tbox.module.json', manifest('module-a', 'parking.alpha'));
|
|
736
|
-
writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b', 'parking.
|
|
743
|
+
writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b', 'parking.alpha'));
|
|
737
744
|
const tk = createAppToolkit(app.appDir);
|
|
745
|
+
const walk = tk.walkManifests();
|
|
746
|
+
// 先声明胜出(packages/ 目录字母序:module-a 先)
|
|
747
|
+
expect(walk.ownership['parking.alpha']).toBe('module-a');
|
|
748
|
+
// N4 执法分层:冲突 = declarationIssues error(doctor 面向第三方组合态执法;写通道不拒——归属冲突非写禁)
|
|
749
|
+
const conflict = walk.declarationIssues.find((i) => i.code === 'OWNERSHIP_CONFLICT');
|
|
750
|
+
expect(conflict).toBeDefined();
|
|
751
|
+
expect(conflict!.severity).toBe('error');
|
|
752
|
+
expect(conflict!.message).toContain('module-b');
|
|
753
|
+
// 双方 configEditable 均 true(owns>0 唯一判据——v6 无 shared-key 写禁)
|
|
738
754
|
const a = await tk.loadModule('module-a');
|
|
739
755
|
const b = await tk.loadModule('module-b');
|
|
740
|
-
expect(a.configEditable).toBe(
|
|
741
|
-
expect(b.configEditable).toBe(
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
756
|
+
expect(a.configEditable).toBe(true);
|
|
757
|
+
expect(b.configEditable).toBe(true);
|
|
758
|
+
app.dispose();
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('D6 偏斜态 503 第四分支(P1 标记门):v5 contracts 假件(无 OWNERSHIP_BINDING_V6)+ v6 toolkit → 503', async () => {
|
|
762
|
+
const app = createTempApp('tbox-d6-');
|
|
763
|
+
invalidateContractsResolver();
|
|
764
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
765
|
+
writeFakeContractsPackage(store, { version: '0.9.0', withMarker: false });
|
|
766
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
767
|
+
writeAppFile(app.appDir, 'packages/module-parking/tbox.module.json', JSON.stringify({
|
|
768
|
+
schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
|
|
769
|
+
declare: { owns: [{ service: 'parking.query' }] },
|
|
770
|
+
}));
|
|
771
|
+
const tk = createAppToolkit(app.appDir);
|
|
772
|
+
// 写路径门控(求值轴同此门——assertContractsUsable 单点)
|
|
773
|
+
await expect(tk.writeModuleIntegration('module-parking', { provider: 'wanda' })).rejects.toMatchObject({
|
|
774
|
+
code: 'CONTRACTS_NOT_RESOLVED',
|
|
775
|
+
httpStatus: 503,
|
|
776
|
+
message: expect.stringContaining('OWNERSHIP_BINDING_V6'),
|
|
746
777
|
});
|
|
778
|
+
await expect(tk.loadServiceResolutions()).rejects.toMatchObject({ code: 'CONTRACTS_NOT_RESOLVED', httpStatus: 503 });
|
|
779
|
+
// 静态端点照常(永不 503——门控 policy at edge 只拦求值/写/migrate)
|
|
780
|
+
const mod = await tk.loadModule('module-parking');
|
|
781
|
+
expect(mod.ownedServices).toEqual(['parking.query']);
|
|
747
782
|
app.dispose();
|
|
748
783
|
});
|
|
749
784
|
|
|
750
|
-
it('D5 死键扫尾:
|
|
785
|
+
it('D5 死键扫尾:providerBindings/moduleDomainWritable/domain-binding 全链删除(源文件内容断言)', () => {
|
|
751
786
|
const read = (rel: string): string => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8');
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
expect(read('../src/
|
|
787
|
+
const exists = (rel: string): boolean => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8').length >= 0;
|
|
788
|
+
void exists;
|
|
789
|
+
expect(read('../src/views/modules.ts')).not.toContain('providerBindings:');
|
|
790
|
+
expect(read('../src/integrations/write.ts')).not.toContain('moduleDomain');
|
|
791
|
+
expect(read('../src/index.ts')).not.toContain('domain-binding');
|
|
792
|
+
// v6 新面在场(正向锚)
|
|
793
|
+
expect(read('../src/integrations/module-binding.ts')).toContain('moduleOwnershipWritable');
|
|
755
794
|
});
|
|
756
795
|
});
|
|
757
796
|
|
|
@@ -845,7 +884,7 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
845
884
|
const h2 = layerHarness();
|
|
846
885
|
const mod = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas'] });
|
|
847
886
|
expect(mod.integrationSchemas).toEqual({
|
|
848
|
-
provider: 'wanda', //
|
|
887
|
+
provider: 'wanda', // resolveModuleBinding 单源(modules['module-parking'])
|
|
849
888
|
services: {
|
|
850
889
|
'parking.query': { configSchema: h2.schemas.query },
|
|
851
890
|
'parking.payment': { configSchema: h2.schemas.parking },
|
|
@@ -874,7 +913,7 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
874
913
|
'config/integrations.json',
|
|
875
914
|
JSON.stringify({
|
|
876
915
|
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
877
|
-
|
|
916
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
878
917
|
services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
|
|
879
918
|
}),
|
|
880
919
|
);
|
|
@@ -903,10 +942,10 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
903
942
|
'packages/module-multi/tbox.module.json',
|
|
904
943
|
JSON.stringify({
|
|
905
944
|
schemaVersion: 1,
|
|
906
|
-
|
|
945
|
+
id: 'module-multi',
|
|
907
946
|
version: '0.1.0',
|
|
908
947
|
kind: 'business',
|
|
909
|
-
|
|
948
|
+
declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
|
|
910
949
|
dependencies: { modules: [] },
|
|
911
950
|
env: [],
|
|
912
951
|
}),
|
|
@@ -930,10 +969,10 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
930
969
|
'packages/provider-custom/tbox.module.json',
|
|
931
970
|
JSON.stringify({
|
|
932
971
|
schemaVersion: 1,
|
|
933
|
-
|
|
972
|
+
id: 'provider-custom',
|
|
934
973
|
version: '0.1.0',
|
|
935
974
|
kind: 'business',
|
|
936
|
-
|
|
975
|
+
declare: { providers: { slots: [] }, consumes: [], owns: [] },
|
|
937
976
|
dependencies: { modules: [] },
|
|
938
977
|
env: [],
|
|
939
978
|
}),
|
|
@@ -951,8 +990,8 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
951
990
|
join('node_modules', '@tbox.cn', 'app-provider-mock', 'tbox.module.json'),
|
|
952
991
|
JSON.stringify({
|
|
953
992
|
schemaVersion: 1,
|
|
954
|
-
|
|
955
|
-
|
|
993
|
+
id: 'provider-mock',
|
|
994
|
+
declare: {
|
|
956
995
|
providers: { slots: [{ service: 'parking.query', provider: 'mock', implementation: 'mock-parking@1', credentialType: '-' }] },
|
|
957
996
|
},
|
|
958
997
|
}),
|
|
@@ -970,11 +1009,11 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
970
1009
|
expect(walk.modules.map((m) => m.id).sort()).toEqual(['module-parking', 'provider-custom', 'vendor-mock']);
|
|
971
1010
|
// 详情 200 保持(装包仍合法——零域空态;写通道零域由 D34 守卫拦)
|
|
972
1011
|
const custom = await h2.tk.loadModule('provider-custom');
|
|
973
|
-
expect(custom.
|
|
1012
|
+
expect(custom.ownedServices).toEqual([]);
|
|
974
1013
|
expect(custom.configEditable).toBe(false);
|
|
975
1014
|
expect(custom.services).toEqual([]);
|
|
976
1015
|
const mock = await h2.tk.loadModule('vendor-mock');
|
|
977
|
-
expect(mock.
|
|
1016
|
+
expect(mock.ownedServices).toEqual([]);
|
|
978
1017
|
// 供给轴不受影响:provider-mock 槽位照常进 catalog(清单排除不伤聚合)
|
|
979
1018
|
const providers = await h2.tk.loadProviders();
|
|
980
1019
|
expect(providers.providers.map((p) => p.provider)).toContain('mock');
|
|
@@ -1009,10 +1048,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1009
1048
|
'packages/provider-w/tbox.module.json',
|
|
1010
1049
|
JSON.stringify({
|
|
1011
1050
|
schemaVersion: 1,
|
|
1012
|
-
|
|
1051
|
+
id: 'provider-w',
|
|
1013
1052
|
version: '0.1.0',
|
|
1014
1053
|
kind: 'business',
|
|
1015
|
-
|
|
1054
|
+
declare: {
|
|
1016
1055
|
providers: {
|
|
1017
1056
|
slots: [
|
|
1018
1057
|
{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
|
|
@@ -1031,10 +1070,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1031
1070
|
'packages/provider-multi/tbox.module.json',
|
|
1032
1071
|
JSON.stringify({
|
|
1033
1072
|
schemaVersion: 1,
|
|
1034
|
-
|
|
1073
|
+
id: 'provider-multi',
|
|
1035
1074
|
version: '0.1.0',
|
|
1036
1075
|
kind: 'business',
|
|
1037
|
-
|
|
1076
|
+
declare: {
|
|
1038
1077
|
providers: {
|
|
1039
1078
|
slots: [
|
|
1040
1079
|
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
@@ -1054,10 +1093,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1054
1093
|
'packages/module-parking/tbox.module.json',
|
|
1055
1094
|
JSON.stringify({
|
|
1056
1095
|
schemaVersion: 1,
|
|
1057
|
-
|
|
1096
|
+
id: 'module-parking',
|
|
1058
1097
|
version: '0.1.0',
|
|
1059
1098
|
kind: 'business',
|
|
1060
|
-
|
|
1099
|
+
declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
|
|
1061
1100
|
dependencies: { modules: [] },
|
|
1062
1101
|
env: [],
|
|
1063
1102
|
}),
|
|
@@ -1139,12 +1178,12 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1139
1178
|
|
|
1140
1179
|
it('E6 域级联(分支①)+ ghost 域厂商诚实态', async () => {
|
|
1141
1180
|
const h = eHarness();
|
|
1142
|
-
writeIntegrations(h.app.appDir, {
|
|
1181
|
+
writeIntegrations(h.app.appDir, { modules: { 'module-parking': { provider: 'wanda' } }, services: { 'parking.query': {} } });
|
|
1143
1182
|
const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
1144
1183
|
expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
|
|
1145
1184
|
// ghost 域厂商(catalog 外)→ configSchema null(诊断归谓词 #11——富化非门控)
|
|
1146
1185
|
const h2 = eHarness();
|
|
1147
|
-
writeIntegrations(h2.app.appDir, {
|
|
1186
|
+
writeIntegrations(h2.app.appDir, { modules: { 'module-parking': { provider: 'ghost-vendor' } }, services: { 'parking.query': {} } });
|
|
1148
1187
|
const ghost = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
1149
1188
|
expect(ghost.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
|
|
1150
1189
|
h.app.dispose();
|
|
@@ -1216,10 +1255,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1216
1255
|
});
|
|
1217
1256
|
|
|
1218
1257
|
it('E14 malformed → 整字段 null(不落域级联——分支回归锚)', async () => {
|
|
1219
|
-
// ① 坏对象(缺 credentialType)——配
|
|
1258
|
+
// ① 坏对象(缺 credentialType)——配 modules 佐证未走模块级联
|
|
1220
1259
|
const h = eHarness();
|
|
1221
1260
|
writeIntegrations(h.app.appDir, {
|
|
1222
|
-
|
|
1261
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1223
1262
|
services: { 'parking.query': { provider: { provider: 'my-api' } } },
|
|
1224
1263
|
});
|
|
1225
1264
|
const bad = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
@@ -1227,7 +1266,7 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1227
1266
|
// ② provider: null(谓词 #5 同判 INLINE_PROVIDER_INVALID——坏形态非缺席)
|
|
1228
1267
|
const h2 = eHarness();
|
|
1229
1268
|
writeIntegrations(h2.app.appDir, {
|
|
1230
|
-
|
|
1269
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1231
1270
|
services: { 'parking.query': { provider: null } },
|
|
1232
1271
|
});
|
|
1233
1272
|
const nul = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
@@ -1239,7 +1278,7 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1239
1278
|
it('E15 provider 空串 → 走域级联(非 null)', async () => {
|
|
1240
1279
|
const h = eHarness();
|
|
1241
1280
|
writeIntegrations(h.app.appDir, {
|
|
1242
|
-
|
|
1281
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1243
1282
|
services: { 'parking.query': { provider: '' } },
|
|
1244
1283
|
});
|
|
1245
1284
|
const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
@@ -1264,15 +1303,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1264
1303
|
unsupplied: [],
|
|
1265
1304
|
additionalPropertiesAllowed: false,
|
|
1266
1305
|
fields: [
|
|
1267
|
-
{ key: 'baseUrl', schema: { type: 'string', format: 'uri' }, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1268
|
-
{ key: 'lotId', schema: null, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1269
|
-
{ key: 'env', schema: { type: 'string', enum: ['dev', 'staging', 'prod'] }, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1270
|
-
{ key: 'envMixed', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1271
|
-
{ key: 'meta', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1272
|
-
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string' } } }, sources: [src('parking.query', false), src('parking.payment', true)] },
|
|
1273
|
-
{ key: 'untyped', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1274
|
-
{ key: 'timeout', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1275
|
-
{ key: 'apiKey', schema: { type: 'string' }, sources: [src('parking.payment', true)] },
|
|
1306
|
+
{ key: 'baseUrl', schema: { type: 'string', format: 'uri', 'x-tbox-global': true }, global: true, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1307
|
+
{ key: 'lotId', schema: null, global: true, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1308
|
+
{ key: 'env', schema: { type: 'string', enum: ['dev', 'staging', 'prod'], 'x-tbox-global': true }, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1309
|
+
{ key: 'envMixed', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1310
|
+
{ key: 'meta', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1311
|
+
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string', 'x-tbox-global': true } }, 'x-tbox-global': true }, global: true, sources: [src('parking.query', false), src('parking.payment', true)] },
|
|
1312
|
+
{ key: 'untyped', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1313
|
+
{ key: 'timeout', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1314
|
+
{ key: 'apiKey', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', true)] },
|
|
1276
1315
|
],
|
|
1277
1316
|
};
|
|
1278
1317
|
|
|
@@ -1283,15 +1322,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1283
1322
|
unsupplied: [],
|
|
1284
1323
|
additionalPropertiesAllowed: false,
|
|
1285
1324
|
fields: [
|
|
1286
|
-
{ key: 'apiKey', schema: { type: 'string' }, sources: [src('parking.payment', true)] },
|
|
1287
|
-
{ key: 'baseUrl', schema: { type: 'string' }, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1288
|
-
{ key: 'lotId', schema: null, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1289
|
-
{ key: 'env', schema: { type: 'string', enum: ['prod', 'dev', 'staging'] }, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1290
|
-
{ key: 'envMixed', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1291
|
-
{ key: 'meta', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1292
|
-
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string' } } }, sources: [src('parking.payment', true), src('parking.query', false)] },
|
|
1293
|
-
{ key: 'untyped', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1294
|
-
{ key: 'timeout', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1325
|
+
{ key: 'apiKey', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', true)] },
|
|
1326
|
+
{ key: 'baseUrl', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1327
|
+
{ key: 'lotId', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1328
|
+
{ key: 'env', schema: { type: 'string', enum: ['prod', 'dev', 'staging'], 'x-tbox-global': true }, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1329
|
+
{ key: 'envMixed', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1330
|
+
{ key: 'meta', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1331
|
+
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string', 'x-tbox-global': true } }, 'x-tbox-global': true }, global: true, sources: [src('parking.payment', true), src('parking.query', false)] },
|
|
1332
|
+
{ key: 'untyped', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1333
|
+
{ key: 'timeout', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1295
1334
|
],
|
|
1296
1335
|
};
|
|
1297
1336
|
|
|
@@ -1301,15 +1340,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1301
1340
|
/** E4:module 轴 moduleProvider=ghost-x(未知厂商——非 null 空集 + 全 demand 回填,demand 序) */
|
|
1302
1341
|
const E4 = { provider: 'ghost-x', schemaless: [], unsupplied: ['parking.query', 'parking.payment'], additionalPropertiesAllowed: true, fields: [] };
|
|
1303
1342
|
|
|
1304
|
-
/** E5:实际态(无轴参,wanda;scope = [query, payment]
|
|
1343
|
+
/** E5:实际态(无轴参,wanda;scope = [query, payment])——parkLotId 未标记 global 被表单收窄(G 批负例) */
|
|
1305
1344
|
const E5 = {
|
|
1306
1345
|
provider: 'wanda',
|
|
1307
1346
|
schemaless: [],
|
|
1308
1347
|
unsupplied: [],
|
|
1309
1348
|
additionalPropertiesAllowed: true,
|
|
1310
1349
|
fields: [
|
|
1311
|
-
{ key: 'baseUrl', schema: { type: 'string', format: 'uri' }, sources: [src('parking.query', true)] },
|
|
1312
|
-
{ key: 'mallId', schema: { type: 'string' }, sources: [src('parking.payment', false)] },
|
|
1350
|
+
{ key: 'baseUrl', schema: { type: 'string', format: 'uri', 'x-tbox-global': true }, global: true, sources: [src('parking.query', true)] },
|
|
1351
|
+
{ key: 'mallId', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', false)] },
|
|
1313
1352
|
],
|
|
1314
1353
|
};
|
|
1315
1354
|
|
|
@@ -1391,7 +1430,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1391
1430
|
|
|
1392
1431
|
it('S5a-8/9 单源直通 + required 聚合(some;conflict 照常聚合)', () => {
|
|
1393
1432
|
const single = reg({ 'a.one': { configSchema: { type: 'object', properties: { only: { type: 'string', enum: ['z'] } } } } });
|
|
1394
|
-
expect(single.fields[0]).toEqual({ key: 'only', schema: { type: 'string', enum: ['z'] }, sources: [src('a.one', false)] });
|
|
1433
|
+
expect(single.fields[0]).toEqual({ key: 'only', schema: { type: 'string', enum: ['z'] }, global: false, sources: [src('a.one', false)] });
|
|
1395
1434
|
const some = reg({
|
|
1396
1435
|
'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string' } }, required: ['k'] } },
|
|
1397
1436
|
'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
|
|
@@ -1447,7 +1486,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1447
1486
|
const appInc = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
|
|
1448
1487
|
expect(appInc.integrationFieldRegistry).toEqual(E2);
|
|
1449
1488
|
expect(appInc.integrationFieldRegistry?.fields[0].key).toBe('apiKey'); // 足迹字母序(vs E1 首键 baseUrl)
|
|
1450
|
-
expect(appInc.integrationFieldRegistry?.fields[1].schema).toEqual({ type: 'string' }); // 首源漂移(payment 先)
|
|
1489
|
+
expect(appInc.integrationFieldRegistry?.fields[1].schema).toEqual({ type: 'string', 'x-tbox-global': true }); // 首源漂移(payment 先)
|
|
1451
1490
|
const partial = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: 'partial' });
|
|
1452
1491
|
expect(partial.integrationFieldRegistry).toEqual(E3);
|
|
1453
1492
|
const actual = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'] });
|
|
@@ -1485,7 +1524,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1485
1524
|
const actJoy = await h2.tk.loadModule('module-parking', { include: both });
|
|
1486
1525
|
expect(actJoy.integrationFieldRegistry).toEqual(hypJoy.integrationFieldRegistry);
|
|
1487
1526
|
expect(actJoy.integrationSchemas).toEqual(hypJoy.integrationSchemas);
|
|
1488
|
-
// ② 跟随应用级(root.provider=wanda)≡
|
|
1527
|
+
// ② 跟随应用级(root.provider=wanda)≡ 清模块位(N1 删 modules['module-parking'] → root 兜底)后实读——非平凡:清位前实际态 = E1
|
|
1489
1528
|
const hypFollow = await h2.tk.loadModule('module-parking', { include: both, moduleProvider: 'wanda' });
|
|
1490
1529
|
expect(hypFollow.integrationFieldRegistry).toEqual(E5);
|
|
1491
1530
|
await h2.tk.writeModuleIntegration('module-parking', {});
|
|
@@ -1498,7 +1537,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1498
1537
|
h2.app.dispose();
|
|
1499
1538
|
});
|
|
1500
1539
|
|
|
1501
|
-
it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(
|
|
1540
|
+
it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(modules/services 不受扰)', async () => {
|
|
1502
1541
|
const h2 = layerHarness();
|
|
1503
1542
|
const hyp = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
|
|
1504
1543
|
const cur = await h2.tk.loadApp();
|
|
@@ -1557,7 +1596,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1557
1596
|
moduleProvider: 'joycity',
|
|
1558
1597
|
});
|
|
1559
1598
|
expect(v.integration).toEqual({ provider: 'wanda' });
|
|
1560
|
-
expect(v.
|
|
1599
|
+
expect(v.binding).toEqual({ provider: 'wanda', layer: 'module' });
|
|
1561
1600
|
expect(v.configEditable).toBe(true);
|
|
1562
1601
|
expect(v.integrationFieldRegistry?.provider).toBe('joycity'); // 表单层重定向
|
|
1563
1602
|
expect(v.integrationSchemas?.provider).toBe('joycity');
|
|
@@ -1573,7 +1612,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1573
1612
|
'config/integrations.json',
|
|
1574
1613
|
JSON.stringify({
|
|
1575
1614
|
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
1576
|
-
|
|
1615
|
+
modules: { 'module-parking': { provider: 'joycity' } },
|
|
1577
1616
|
services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
|
|
1578
1617
|
}),
|
|
1579
1618
|
);
|
|
@@ -1583,7 +1622,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1583
1622
|
).toBeNull();
|
|
1584
1623
|
h3.app.dispose();
|
|
1585
1624
|
|
|
1586
|
-
// 可写未绑定(无
|
|
1625
|
+
// 可写未绑定(无 modules 位无 root.provider——无轴参 → null)
|
|
1587
1626
|
const h4 = layerHarness();
|
|
1588
1627
|
writeAppFile(
|
|
1589
1628
|
h4.app.appDir,
|
|
@@ -1599,29 +1638,39 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1599
1638
|
expect(unbound.configEditable).toBe(true);
|
|
1600
1639
|
h4.app.dispose();
|
|
1601
1640
|
|
|
1602
|
-
// D34
|
|
1641
|
+
// v6 形态(D34 三形态退役):零归属 → null(带 moduleProvider 仍 null——不豁免);
|
|
1642
|
+
// 多域/跨域归属合法化 → 非 null(override 可计算;unsupplied 回填非供给归属槽)
|
|
1603
1643
|
const d34 = createTempApp('tbox-v46-d34-');
|
|
1604
1644
|
invalidateContractsResolver();
|
|
1605
1645
|
const store34 = joinStore(d34.appDir, 'contracts');
|
|
1606
1646
|
writeFakeContractsPackage(store34, { version: '0.9.0' });
|
|
1607
1647
|
linkContracts(d34.appDir, store34, { subdir: 'apps/server' });
|
|
1608
1648
|
const need = (service: string): { service: string; optional: boolean } => ({ service, optional: false });
|
|
1609
|
-
const manifest34 = (
|
|
1610
|
-
JSON.stringify({ schemaVersion: 1,
|
|
1649
|
+
const manifest34 = (id: string, services: Array<{ service: string; optional: boolean }>): string =>
|
|
1650
|
+
JSON.stringify({ schemaVersion: 1, id, version: '0.1.0', kind: 'business', declare: { owns: services.map((x) => ({ service: x.service })), consumes: services }, dependencies: { modules: [] }, env: [] });
|
|
1611
1651
|
writeAppFile(d34.appDir, 'packages/module-multi/tbox.module.json', manifest34('module-multi', [need('parking.query'), need('member.account')]));
|
|
1612
1652
|
writeAppFile(
|
|
1613
1653
|
d34.appDir,
|
|
1614
1654
|
'packages/module-zero/tbox.module.json',
|
|
1615
|
-
JSON.stringify({ schemaVersion: 1,
|
|
1655
|
+
JSON.stringify({ schemaVersion: 1, id: 'module-zero', version: '0.1.0', kind: 'business', declare: {}, dependencies: { modules: [] }, env: [] }),
|
|
1616
1656
|
);
|
|
1617
1657
|
writeAppFile(d34.appDir, 'packages/module-a/tbox.module.json', manifest34('module-a', [need('parking.alpha')]));
|
|
1618
1658
|
writeAppFile(d34.appDir, 'packages/module-b/tbox.module.json', manifest34('module-b', [need('parking.beta')]));
|
|
1619
1659
|
const tk34 = createAppToolkit(d34.appDir);
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1660
|
+
// 零归属:null(唯一 null 形态——override 不豁免)
|
|
1661
|
+
const zero = await tk34.loadModule('module-zero', { include: ['integrationFieldRegistry', 'integrationSchemas'], moduleProvider: 'joycity' });
|
|
1662
|
+
expect(zero.integrationFieldRegistry).toBeNull();
|
|
1663
|
+
expect(zero.integrationSchemas).toBeNull();
|
|
1664
|
+
expect(zero.configEditable).toBe(false);
|
|
1665
|
+
// 多域归属:可写(owns>0)——override joycity 供给 parking.query(member.account → unsupplied)
|
|
1666
|
+
const multi = await tk34.loadModule('module-multi', { include: ['integrationFieldRegistry', 'integrationSchemas'], moduleProvider: 'joycity' });
|
|
1667
|
+
expect(multi.configEditable).toBe(true);
|
|
1668
|
+
expect(multi.integrationSchemas?.provider).toBe('joycity');
|
|
1669
|
+
// d34 fixture 无 provider 包 → joycity 零供给 → 双 owned 全回填(manifest owns 序)
|
|
1670
|
+
expect(multi.integrationFieldRegistry?.unsupplied).toEqual(['parking.query', 'member.account']);
|
|
1671
|
+
// 跨域归属:scope = owns ∩ 供给 = [](parking.alpha 非 joycity 供给)→ 非 null 空集 + unsupplied 全回填
|
|
1672
|
+
const a = await tk34.loadModule('module-a', { include: ['integrationFieldRegistry'], moduleProvider: 'joycity' });
|
|
1673
|
+
expect(a.integrationFieldRegistry).toEqual({ provider: 'joycity', schemaless: [], unsupplied: ['parking.alpha'], additionalPropertiesAllowed: true, fields: [] });
|
|
1625
1674
|
d34.dispose();
|
|
1626
1675
|
});
|
|
1627
1676
|
|
|
@@ -1682,7 +1731,13 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1682
1731
|
const props = c.properties;
|
|
1683
1732
|
const declared = props && typeof props === 'object' && !Array.isArray(props) ? Object.keys(props) : []; // 同款守卫
|
|
1684
1733
|
const rev = registry.fields.filter((f) => f.sources.some((x) => x.service === s)).map((f) => f.key);
|
|
1685
|
-
|
|
1734
|
+
// G 批收窄互证:表单键 ⊆ 声明键,且声明侧标记键 ≡ 表单键(未标记键落 ExtraFields——不进 fields)
|
|
1735
|
+
const marked = declared.filter((k) => {
|
|
1736
|
+
const sub = (props as Record<string, Record<string, unknown>>)[k];
|
|
1737
|
+
return !!sub && sub['x-tbox-global'] === true;
|
|
1738
|
+
});
|
|
1739
|
+
expect(rev.every((k) => declared.includes(k)), s).toBe(true);
|
|
1740
|
+
expect([...marked].sort(), s).toEqual([...rev].sort());
|
|
1686
1741
|
}
|
|
1687
1742
|
for (const s of registry.schemaless) expect(layer.services[s]?.configSchema).toBeNull();
|
|
1688
1743
|
// unsupplied ≡ demand − scope 键集
|
|
@@ -1692,6 +1747,39 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1692
1747
|
h2.app.dispose();
|
|
1693
1748
|
});
|
|
1694
1749
|
|
|
1750
|
+
it('G 批收窄负例(P16 复合可见面):未标记键在 layer.properties 在场、registry.fields 不收(wanda 实际态 parkLotId)', async () => {
|
|
1751
|
+
const h2 = layerHarness();
|
|
1752
|
+
const v = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas', 'integrationFieldRegistry'] });
|
|
1753
|
+
const layer = v.integrationSchemas!;
|
|
1754
|
+
const registry = v.integrationFieldRegistry!;
|
|
1755
|
+
expect(layer.provider).toBe('wanda');
|
|
1756
|
+
const parkingProps = (layer.services['parking.payment'].configSchema as Record<string, Record<string, unknown>>).properties as Record<string, Record<string, unknown>>;
|
|
1757
|
+
expect(parkingProps.mallId['x-tbox-global']).toBe(true);
|
|
1758
|
+
expect(parkingProps.parkLotId).toBeDefined(); // 原始投影在场
|
|
1759
|
+
expect(parkingProps.parkLotId['x-tbox-global']).toBeUndefined();
|
|
1760
|
+
expect(registry.fields.some((f) => f.key === 'parkLotId')).toBe(false); // 表单收窄不收
|
|
1761
|
+
expect(registry.fields.some((f) => f.key === 'mallId')).toBe(true);
|
|
1762
|
+
// app 轴同律(足迹含 query——baseUrl 标记在场)
|
|
1763
|
+
const appV = await h2.tk.loadApp({ include: ['integrationSchemas', 'integrationFieldRegistry'] });
|
|
1764
|
+
expect(appV.integrationFieldRegistry!.fields.some((f) => f.key === 'baseUrl')).toBe(true);
|
|
1765
|
+
h2.app.dispose();
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
it('G 批服务轴不过滤锁:loadServiceFieldRegistry 全字段带旗标(unmarked → global:false 在场)', async () => {
|
|
1769
|
+
const h2 = layerHarness();
|
|
1770
|
+
const snap = (await import('../src/views/context.js')).loadViewSnapshotSync({ appDir: h2.app.appDir }, await h2.tk.resolveContracts());
|
|
1771
|
+
const reg = loadServiceFieldRegistry(snap, 'parking.payment');
|
|
1772
|
+
expect(reg).not.toBeNull();
|
|
1773
|
+
const keys = reg!.fields.map((f) => f.key).sort();
|
|
1774
|
+
expect(keys).toEqual(['mallId', 'parkLotId']); // 服务轴全量(未标记也入列——旗标 false 如实)
|
|
1775
|
+
const lot = reg!.fields.find((f) => f.key === 'parkLotId')!;
|
|
1776
|
+
expect(lot.global).toBe(false);
|
|
1777
|
+
expect(lot.schema).toEqual({ type: 'string', title: '车场组' }); // 无标记污染
|
|
1778
|
+
const mall = reg!.fields.find((f) => f.key === 'mallId')!;
|
|
1779
|
+
expect(mall.global).toBe(true);
|
|
1780
|
+
h2.app.dispose();
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1695
1783
|
it('S8 app 轴:unsupplied ≡ [](结构性:足迹 ⊆ 供给)', async () => {
|
|
1696
1784
|
const h2 = layerHarness();
|
|
1697
1785
|
const v = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
|
|
@@ -1715,8 +1803,8 @@ describe('B1 展示名链', () => {
|
|
|
1715
1803
|
version: '0.9.0',
|
|
1716
1804
|
slots: ['parking.query', 'auth.alipay-login', 'mall.info'],
|
|
1717
1805
|
meta: {
|
|
1718
|
-
'parking.query': {
|
|
1719
|
-
'auth.alipay-login': {
|
|
1806
|
+
'parking.query': { name: '停车查询', description: '费用报价、车场信息、可享权益' },
|
|
1807
|
+
'auth.alipay-login': { name: '支付宝登录' },
|
|
1720
1808
|
},
|
|
1721
1809
|
});
|
|
1722
1810
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
@@ -1726,14 +1814,15 @@ describe('B1 展示名链', () => {
|
|
|
1726
1814
|
'packages/module-parking/tbox.module.json',
|
|
1727
1815
|
JSON.stringify({
|
|
1728
1816
|
schemaVersion: 1,
|
|
1729
|
-
|
|
1817
|
+
id: 'module-parking',
|
|
1730
1818
|
version: '0.1.0',
|
|
1731
1819
|
kind: 'business',
|
|
1732
|
-
|
|
1820
|
+
name: '停车缴费',
|
|
1733
1821
|
description: '车辆查询与绑定、停车费用查询、缴费支付与开票',
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1822
|
+
declare: {
|
|
1823
|
+
owns: [{ service: 'parking.query' }],
|
|
1824
|
+
// demand meta name=车位查询——应被词汇 meta 停车查询压过(词汇主源)
|
|
1825
|
+
consumes: [{ service: 'parking.query', optional: false, name: '车位查询' }],
|
|
1737
1826
|
},
|
|
1738
1827
|
dependencies: { modules: [] },
|
|
1739
1828
|
env: [],
|
|
@@ -1745,12 +1834,12 @@ describe('B1 展示名链', () => {
|
|
|
1745
1834
|
'packages/provider-demo/tbox.module.json',
|
|
1746
1835
|
JSON.stringify({
|
|
1747
1836
|
schemaVersion: 1,
|
|
1748
|
-
|
|
1837
|
+
id: 'provider-demo',
|
|
1749
1838
|
version: '0.1.0',
|
|
1750
1839
|
kind: 'third-party',
|
|
1751
|
-
|
|
1840
|
+
name: '万达',
|
|
1752
1841
|
description: '万达厂商集成',
|
|
1753
|
-
|
|
1842
|
+
declare: {
|
|
1754
1843
|
providers: {
|
|
1755
1844
|
slots: [{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'apikey' }],
|
|
1756
1845
|
},
|
|
@@ -1766,37 +1855,37 @@ describe('B1 展示名链', () => {
|
|
|
1766
1855
|
it('T1 resolveServiceDisplay 三态:词汇 > demand > id;逐字段回落(词汇仅 description 时 demand title 可达)', () => {
|
|
1767
1856
|
const walk = {
|
|
1768
1857
|
slotMeta: {
|
|
1769
|
-
'a.b': {
|
|
1858
|
+
'a.b': { name: '甲', description: '甲描述' },
|
|
1770
1859
|
'c.d': { description: '只有描述' },
|
|
1771
1860
|
},
|
|
1772
1861
|
catalog: {
|
|
1773
1862
|
services: {
|
|
1774
|
-
'a.b': {
|
|
1775
|
-
'c.d': {
|
|
1863
|
+
'a.b': { name: '乙', description: '乙描述' },
|
|
1864
|
+
'c.d': { name: '丙' },
|
|
1776
1865
|
},
|
|
1777
1866
|
},
|
|
1778
1867
|
} as unknown as WalkManifestsResult;
|
|
1779
1868
|
// 词汇全量在场 → 词汇胜出
|
|
1780
|
-
expect(resolveServiceDisplay(walk, 'a.b')).toEqual({
|
|
1869
|
+
expect(resolveServiceDisplay(walk, 'a.b')).toEqual({ name: '甲', description: '甲描述' });
|
|
1781
1870
|
// 词汇仅 description → title 回落 demand、description 取词汇(逐字段回落)
|
|
1782
|
-
expect(resolveServiceDisplay(walk, 'c.d')).toEqual({
|
|
1871
|
+
expect(resolveServiceDisplay(walk, 'c.d')).toEqual({ name: '丙', description: '只有描述' });
|
|
1783
1872
|
// 双缺席 → demand
|
|
1784
|
-
expect(resolveServiceDisplay(walk, 'e.f')).toEqual({
|
|
1873
|
+
expect(resolveServiceDisplay(walk, 'e.f')).toEqual({ name: 'e.f' });
|
|
1785
1874
|
// 双双缺席 → id 兜底(title 恒有值)
|
|
1786
1875
|
const bare = { slotMeta: {}, catalog: { services: {} } } as unknown as WalkManifestsResult;
|
|
1787
|
-
expect(resolveServiceDisplay(bare, 'x.y')).toEqual({
|
|
1876
|
+
expect(resolveServiceDisplay(bare, 'x.y')).toEqual({ name: 'x.y' });
|
|
1788
1877
|
});
|
|
1789
1878
|
|
|
1790
1879
|
it('T2 loadModulesView:模块 meta 填充;name 兜底', async () => {
|
|
1791
1880
|
const h2 = displayHarness();
|
|
1792
1881
|
const v = await h2.tk.loadModules();
|
|
1793
1882
|
const parking = v.modules.find((m) => m.id === 'module-parking');
|
|
1794
|
-
expect(parking?.
|
|
1883
|
+
expect(parking?.name).toBe('停车缴费');
|
|
1795
1884
|
expect(parking?.description).toBe('车辆查询与绑定、停车费用查询、缴费支付与开票');
|
|
1796
1885
|
// 无 meta 模块(provider 家族不在清单——用第二基座验证兜底)
|
|
1797
1886
|
const h3 = harnessWithContracts();
|
|
1798
1887
|
const bare = (await h3.tk.loadModules()).modules.find((m) => m.id === 'module-parking');
|
|
1799
|
-
expect(bare?.
|
|
1888
|
+
expect(bare?.name).toBe('module-parking');
|
|
1800
1889
|
expect(bare?.description).toBe('');
|
|
1801
1890
|
h3.app.dispose();
|
|
1802
1891
|
h2.app.dispose();
|
|
@@ -1805,10 +1894,10 @@ describe('B1 展示名链', () => {
|
|
|
1805
1894
|
it('T3 loadModuleDetailView:meta 恒发;services[] 词汇优先(demand 车位查询被词汇停车查询压过——防漂移锁)', async () => {
|
|
1806
1895
|
const h2 = displayHarness();
|
|
1807
1896
|
const v = await h2.tk.loadModule('module-parking');
|
|
1808
|
-
expect(v.
|
|
1897
|
+
expect(v.name).toBe('停车缴费');
|
|
1809
1898
|
expect(v.description).toContain('停车费用查询');
|
|
1810
1899
|
const svc = v.services.find((s) => s.service === 'parking.query');
|
|
1811
|
-
expect(svc?.
|
|
1900
|
+
expect(svc?.name).toBe('停车查询');
|
|
1812
1901
|
expect(svc?.description).toBe('费用报价、车场信息、可享权益');
|
|
1813
1902
|
expect(svc?.required).toBe(true);
|
|
1814
1903
|
h2.app.dispose();
|
|
@@ -1821,22 +1910,63 @@ describe('B1 展示名链', () => {
|
|
|
1821
1910
|
h2.app.dispose();
|
|
1822
1911
|
});
|
|
1823
1912
|
|
|
1913
|
+
// ── v7 双键寻址(factory 级——resolve-key 单点经七方法入口;id 优先/展示名兜底/多命中 404)──
|
|
1914
|
+
|
|
1915
|
+
it('v7-DK1 模块双键:展示名「停车缴费」≡ id module-parking(同一视图);读侧 id 恒优先', async () => {
|
|
1916
|
+
const h2 = displayHarness();
|
|
1917
|
+
const byName = await h2.tk.loadModule('停车缴费');
|
|
1918
|
+
const byId = await h2.tk.loadModule('module-parking');
|
|
1919
|
+
expect(byName.name).toBe(byId.name);
|
|
1920
|
+
expect(byName.ownedServices).toEqual(byId.ownedServices);
|
|
1921
|
+
h2.app.dispose();
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
it('v7-DK2 服务双键:展示名「停车查询」→ parking.query(词汇 meta 源);含 include 富化同键', async () => {
|
|
1925
|
+
const h2 = displayHarness();
|
|
1926
|
+
const byName = await h2.tk.loadService('停车查询');
|
|
1927
|
+
expect(byName.service).toBe('parking.query');
|
|
1928
|
+
expect(byName.name).toBe('停车查询');
|
|
1929
|
+
h2.app.dispose();
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
it('v7-DK3 多展示名命中 → 404 带候选(两模块同名;fail-visible 不静默取首)', async () => {
|
|
1933
|
+
const app = createTempApp('tbox-dk3-');
|
|
1934
|
+
invalidateContractsResolver();
|
|
1935
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
1936
|
+
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
1937
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
1938
|
+
const manifest = (id: string): string =>
|
|
1939
|
+
JSON.stringify({ schemaVersion: 1, id, name: '同名模块', version: '0.1.0', kind: 'business', declare: {}, dependencies: { modules: [] }, env: [] });
|
|
1940
|
+
writeAppFile(app.appDir, 'packages/module-a/tbox.module.json', manifest('module-a'));
|
|
1941
|
+
writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b'));
|
|
1942
|
+
const tk = createAppToolkit(app.appDir);
|
|
1943
|
+
await expect(tk.loadModule('同名模块')).rejects.toMatchObject({
|
|
1944
|
+
code: 'MODULE_NOT_FOUND',
|
|
1945
|
+
httpStatus: 404,
|
|
1946
|
+
message: expect.stringContaining('id=module-a'),
|
|
1947
|
+
});
|
|
1948
|
+
await expect(tk.loadModule('同名模块')).rejects.toMatchObject({
|
|
1949
|
+
message: expect.stringContaining('改用模块 id'),
|
|
1950
|
+
});
|
|
1951
|
+
app.dispose();
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1824
1954
|
it('T4 loadAppView 平台条目:title 走链恒发;词汇无 meta 槽 = id 兜底', async () => {
|
|
1825
1955
|
const h2 = displayHarness();
|
|
1826
1956
|
const v = await h2.tk.loadApp();
|
|
1827
1957
|
const byKey = Object.fromEntries(v.services.map((s) => [s.service, s]));
|
|
1828
1958
|
// 平台条目 = 词汇 − 占用(parking.query 被模块占用)
|
|
1829
1959
|
expect(Object.keys(byKey).sort()).toEqual(['auth.alipay-login', 'mall.info']);
|
|
1830
|
-
expect(byKey['auth.alipay-login']?.
|
|
1831
|
-
expect(byKey['mall.info']?.
|
|
1960
|
+
expect(byKey['auth.alipay-login']?.name).toBe('支付宝登录');
|
|
1961
|
+
expect(byKey['mall.info']?.name).toBe('mall.info');
|
|
1832
1962
|
expect(byKey['auth.alipay-login']?.required).toBe(false);
|
|
1833
1963
|
h2.app.dispose();
|
|
1834
1964
|
});
|
|
1835
1965
|
|
|
1836
1966
|
it('T5 loadServiceDetailView:词汇优先 + id 兜底恒有值', async () => {
|
|
1837
1967
|
const h2 = displayHarness();
|
|
1838
|
-
expect((await h2.tk.loadService('parking.query')).
|
|
1839
|
-
expect((await h2.tk.loadService('mall.info')).
|
|
1968
|
+
expect((await h2.tk.loadService('parking.query')).name).toBe('停车查询');
|
|
1969
|
+
expect((await h2.tk.loadService('mall.info')).name).toBe('mall.info');
|
|
1840
1970
|
h2.app.dispose();
|
|
1841
1971
|
});
|
|
1842
1972
|
|
|
@@ -1844,11 +1974,11 @@ describe('B1 展示名链', () => {
|
|
|
1844
1974
|
const h2 = displayHarness();
|
|
1845
1975
|
const list = await h2.tk.loadProviders();
|
|
1846
1976
|
const wanda = list.providers.find((p) => p.provider === 'wanda');
|
|
1847
|
-
expect(wanda?.
|
|
1977
|
+
expect(wanda?.name).toBe('万达');
|
|
1848
1978
|
expect(wanda?.description).toBe('万达厂商集成');
|
|
1849
1979
|
const detail = await h2.tk.loadProvider('wanda');
|
|
1850
|
-
expect(detail.
|
|
1851
|
-
expect(detail.services['parking.query']?.
|
|
1980
|
+
expect(detail.name).toBe('万达');
|
|
1981
|
+
expect(detail.services['parking.query']?.name).toBe('停车查询');
|
|
1852
1982
|
expect(detail.services['parking.query']?.description).toBe('费用报价、车场信息、可享权益');
|
|
1853
1983
|
h2.app.dispose();
|
|
1854
1984
|
});
|