@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
package/tests/views.test.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { mkdirSync, readFileSync, symlinkSync } from 'node:fs';
|
|
|
4
4
|
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
|
-
import { walkManifests } from '../src/assembly/walk-manifests.js';
|
|
8
|
-
import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas } from '../src/views/integration-schemas.js';
|
|
7
|
+
import { walkManifests, type WalkManifestsResult } from '../src/assembly/walk-manifests.js';
|
|
8
|
+
import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas, loadServiceFieldRegistry } from '../src/views/integration-schemas.js';
|
|
9
|
+
import { resolveServiceDisplay } from '../src/views/context.js';
|
|
9
10
|
import type { ViewSnapshot } from '../src/views/context.js';
|
|
10
11
|
import { URL_METHOD_MAP } from './naming-alignment.test.js';
|
|
11
12
|
import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
|
|
@@ -41,11 +42,12 @@ function harnessWithContracts(
|
|
|
41
42
|
'packages/module-parking/tbox.module.json',
|
|
42
43
|
JSON.stringify({
|
|
43
44
|
schemaVersion: 1,
|
|
44
|
-
|
|
45
|
+
id: 'module-parking',
|
|
45
46
|
version: '0.1.0',
|
|
46
47
|
kind: 'business',
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
declare: {
|
|
49
|
+
owns: [{ service: 'parking.query' }],
|
|
50
|
+
consumes: [{ service: 'parking.query', optional: false, name: '车位查询' }],
|
|
49
51
|
},
|
|
50
52
|
dependencies: { modules: [] },
|
|
51
53
|
env: [],
|
|
@@ -61,7 +63,7 @@ function harnessWithContracts(
|
|
|
61
63
|
{ id: 'mall-sh-01', name: '上海店' },
|
|
62
64
|
],
|
|
63
65
|
defaultInstanceId: 'mall-bj-01',
|
|
64
|
-
|
|
66
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
65
67
|
services: {
|
|
66
68
|
'parking.query': { provider: { provider: 'my-custom-api', credentialType: '-' }, implementation: 'my-custom-parking@1' },
|
|
67
69
|
'auth.alipay-login': {},
|
|
@@ -103,9 +105,12 @@ function layerHarness(): {
|
|
|
103
105
|
].join(' '),
|
|
104
106
|
});
|
|
105
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 });
|
|
106
111
|
const schemas = {
|
|
107
|
-
query: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
|
|
108
|
-
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: '车场组' } } },
|
|
109
114
|
};
|
|
110
115
|
// v4.6 joycity 代数 fixture:R1(baseUrl 双无 enum / env 同集异序 / envMixed 混布)/
|
|
111
116
|
// R2(options 深相等 / meta 异构)/ R3(lotId string≠integer / timeout number≠integer / untyped 无 type)/
|
|
@@ -114,14 +119,14 @@ function layerHarness(): {
|
|
|
114
119
|
query: {
|
|
115
120
|
type: 'object',
|
|
116
121
|
properties: {
|
|
117
|
-
baseUrl: { type: 'string', format: 'uri' },
|
|
118
|
-
lotId: { type: 'string' },
|
|
119
|
-
env: { type: 'string', enum: ['dev', 'staging', 'prod'] },
|
|
120
|
-
envMixed: { type: 'string', enum: ['a', 'b'] },
|
|
121
|
-
meta: { type: 'object', properties: { region: { type: 'string' } } },
|
|
122
|
-
options: { type: 'object', properties: { x: { type: 'string' } } },
|
|
123
|
-
untyped: { description: 'x' },
|
|
124
|
-
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' },
|
|
125
130
|
},
|
|
126
131
|
required: ['baseUrl', 'lotId'],
|
|
127
132
|
},
|
|
@@ -129,15 +134,15 @@ function layerHarness(): {
|
|
|
129
134
|
type: 'object',
|
|
130
135
|
additionalProperties: false,
|
|
131
136
|
properties: {
|
|
132
|
-
apiKey: { type: 'string' },
|
|
133
|
-
baseUrl: { type: 'string' },
|
|
134
|
-
lotId: { type: 'integer' },
|
|
135
|
-
env: { type: 'string', enum: ['prod', 'dev', 'staging'] },
|
|
136
|
-
envMixed: { type: 'string' },
|
|
137
|
-
meta: { type: 'object', properties: { zone: { type: 'number' } } },
|
|
138
|
-
options: { type: 'object', properties: { x: { type: 'string' } } },
|
|
139
|
-
untyped: { type: 'string' },
|
|
140
|
-
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' },
|
|
141
146
|
},
|
|
142
147
|
required: ['apiKey', 'options'],
|
|
143
148
|
},
|
|
@@ -147,10 +152,10 @@ function layerHarness(): {
|
|
|
147
152
|
'packages/provider-w/tbox.module.json',
|
|
148
153
|
JSON.stringify({
|
|
149
154
|
schemaVersion: 1,
|
|
150
|
-
|
|
155
|
+
id: 'provider-w',
|
|
151
156
|
version: '0.1.0',
|
|
152
157
|
kind: 'business',
|
|
153
|
-
|
|
158
|
+
declare: {
|
|
154
159
|
providers: {
|
|
155
160
|
slots: [
|
|
156
161
|
{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
|
|
@@ -170,10 +175,10 @@ function layerHarness(): {
|
|
|
170
175
|
'packages/provider-j/tbox.module.json',
|
|
171
176
|
JSON.stringify({
|
|
172
177
|
schemaVersion: 1,
|
|
173
|
-
|
|
178
|
+
id: 'provider-j',
|
|
174
179
|
version: '0.1.0',
|
|
175
180
|
kind: 'business',
|
|
176
|
-
|
|
181
|
+
declare: {
|
|
177
182
|
providers: {
|
|
178
183
|
slots: [
|
|
179
184
|
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
@@ -193,10 +198,10 @@ function layerHarness(): {
|
|
|
193
198
|
'packages/provider-p/tbox.module.json',
|
|
194
199
|
JSON.stringify({
|
|
195
200
|
schemaVersion: 1,
|
|
196
|
-
|
|
201
|
+
id: 'provider-p',
|
|
197
202
|
version: '0.1.0',
|
|
198
203
|
kind: 'business',
|
|
199
|
-
|
|
204
|
+
declare: {
|
|
200
205
|
providers: {
|
|
201
206
|
slots: [{ service: 'parking.payment', provider: 'partial', implementation: 'partial-payment@1', credentialType: 'partial-c' }],
|
|
202
207
|
},
|
|
@@ -210,10 +215,13 @@ function layerHarness(): {
|
|
|
210
215
|
'packages/module-parking/tbox.module.json',
|
|
211
216
|
JSON.stringify({
|
|
212
217
|
schemaVersion: 1,
|
|
213
|
-
|
|
218
|
+
id: 'module-parking',
|
|
214
219
|
version: '0.1.0',
|
|
215
220
|
kind: 'business',
|
|
216
|
-
|
|
221
|
+
declare: {
|
|
222
|
+
owns: [{ service: 'parking.query' }, { service: 'parking.payment' }],
|
|
223
|
+
consumes: [{ service: 'parking.query', optional: false }, { service: 'parking.payment', optional: true }],
|
|
224
|
+
},
|
|
217
225
|
dependencies: { modules: [] },
|
|
218
226
|
env: [],
|
|
219
227
|
}),
|
|
@@ -225,7 +233,7 @@ function layerHarness(): {
|
|
|
225
233
|
provider: 'wanda',
|
|
226
234
|
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
227
235
|
defaultInstanceId: 'mall-bj-01',
|
|
228
|
-
|
|
236
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
229
237
|
services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
|
|
230
238
|
}),
|
|
231
239
|
);
|
|
@@ -258,7 +266,8 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
258
266
|
expect(Object.keys(appView)).toEqual(['services', 'integration']);
|
|
259
267
|
const mod = await h.tk.loadModule('module-parking');
|
|
260
268
|
// v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
|
|
261
|
-
|
|
269
|
+
// v7 词汇统一:模块展示名 name 前置(恒发——兜底 id)
|
|
270
|
+
expect(Object.keys(mod)).toEqual(['name', 'description', 'ownedServices', 'integration', 'binding', 'configEditable', 'services']);
|
|
262
271
|
const svc = await h.tk.loadService('parking.query');
|
|
263
272
|
expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
|
|
264
273
|
// 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
|
|
@@ -279,7 +288,7 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
279
288
|
});
|
|
280
289
|
|
|
281
290
|
it('O3 键集 = 服务词汇 ∪ 已配置;required echo(OR 聚合 + 平台 false)', async () => {
|
|
282
|
-
// vocabulary 空(无契约包 fixture
|
|
291
|
+
// vocabulary 空(无契约包 fixture 物化 slots——退化态回归锁;词汇在场态见 B1 族 T-O3)
|
|
283
292
|
const batch = await h.tk.loadServiceResolutions();
|
|
284
293
|
expect(Object.keys(batch.resolutions).sort()).toEqual(['auth.alipay-login', 'parking.query']);
|
|
285
294
|
// parking.query 由 module-parking demand required:0 → required true
|
|
@@ -511,8 +520,8 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
|
|
|
511
520
|
'packages/provider-multi/tbox.module.json',
|
|
512
521
|
JSON.stringify({
|
|
513
522
|
schemaVersion: 1,
|
|
514
|
-
|
|
515
|
-
|
|
523
|
+
id: 'provider-multi',
|
|
524
|
+
declare: {
|
|
516
525
|
providers: {
|
|
517
526
|
slots: [
|
|
518
527
|
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
@@ -583,8 +592,11 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
|
|
|
583
592
|
// api.md §5 表
|
|
584
593
|
'CREDENTIAL_FILE_MISSING', 'CREDENTIAL_TYPE_MISMATCH', 'IMPL_WITHOUT_PROVIDER', 'INSTANCE_SUPPLY_UNMATCHED',
|
|
585
594
|
'SCHEMA_FILE_MISSING', 'DOMAIN_CONFIG_UNUSED', 'DOMAIN_CONFIG_INVALID', 'INSTANCE_KEY_NOT_REGISTERED',
|
|
586
|
-
'REALM_SWITCH_HINT', 'WILDCARD_ALIGNMENT', 'VENDOR_NOT_INSTALLED', 'BINDING_UNRESOLVED',
|
|
595
|
+
'REALM_SWITCH_HINT', 'WILDCARD_ALIGNMENT', 'VENDOR_NOT_INSTALLED', 'BINDING_UNRESOLVED',
|
|
587
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',
|
|
588
600
|
// 谓词头映射表 additive(检查族编号映射:#3/#4/#5 内联/#13/v4)
|
|
589
601
|
'SLOT_NOT_DECLARED', 'SLOT_NOT_CONFIGURED', 'INLINE_PROVIDER_INVALID', 'MULTI_INSTANCE_NO_DEFAULT', 'INSTANCE_DISABLED',
|
|
590
602
|
]);
|
|
@@ -634,18 +646,18 @@ describe('A2 错误面(P-4 notes 优先 / P-5 畸形包 503)', () => {
|
|
|
634
646
|
});
|
|
635
647
|
});
|
|
636
648
|
|
|
637
|
-
describe('D34
|
|
638
|
-
it('D1
|
|
649
|
+
describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单数重写)', () => {
|
|
650
|
+
it('D1 单槽回归:ownedServices/binding/configEditable 装配 + integration 集成位原值', async () => {
|
|
639
651
|
const h2 = harnessWithContracts();
|
|
640
652
|
const mod = await h2.tk.loadModule('module-parking');
|
|
641
|
-
expect(mod.
|
|
653
|
+
expect(mod.ownedServices).toEqual(['parking.query']);
|
|
642
654
|
expect(mod.configEditable).toBe(true);
|
|
643
|
-
expect(mod.
|
|
655
|
+
expect(mod.binding).toEqual({ provider: 'wanda', layer: 'module' });
|
|
644
656
|
expect(mod.integration).toEqual({ provider: 'wanda' });
|
|
645
657
|
h2.app.dispose();
|
|
646
658
|
});
|
|
647
659
|
|
|
648
|
-
it('D2
|
|
660
|
+
it('D2 多域归属合法化(v6 正例化——跨域 owns 单键落位):binding 单值作用全 owned + PUT 全量替换', async () => {
|
|
649
661
|
const app = createTempApp('tbox-d2-');
|
|
650
662
|
invalidateContractsResolver();
|
|
651
663
|
const store = joinStore(app.appDir, 'contracts');
|
|
@@ -656,33 +668,30 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
|
|
|
656
668
|
'packages/module-multi/tbox.module.json',
|
|
657
669
|
JSON.stringify({
|
|
658
670
|
schemaVersion: 1,
|
|
659
|
-
|
|
671
|
+
id: 'module-multi',
|
|
660
672
|
version: '0.1.0',
|
|
661
673
|
kind: 'business',
|
|
662
|
-
|
|
674
|
+
declare: { owns: [{ service: 'parking.query' }, { service: 'member.account' }], consumes: [{ service: 'parking.query', optional: false }] },
|
|
663
675
|
dependencies: { modules: [] },
|
|
664
676
|
env: [],
|
|
665
677
|
}),
|
|
666
678
|
);
|
|
667
679
|
const tk = createAppToolkit(app.appDir);
|
|
668
680
|
const mod = await tk.loadModule('module-multi');
|
|
669
|
-
expect(mod.
|
|
681
|
+
expect(mod.ownedServices).toEqual(['member.account', 'parking.query']);
|
|
670
682
|
expect(mod.integration).toBeNull();
|
|
671
|
-
expect(mod.configEditable).toBe(
|
|
672
|
-
expect(mod.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
await
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
message: expect.stringContaining('跨 2 个域(member、parking)'),
|
|
680
|
-
});
|
|
681
|
-
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' });
|
|
682
691
|
app.dispose();
|
|
683
692
|
});
|
|
684
693
|
|
|
685
|
-
it('D3
|
|
694
|
+
it('D3 零归属模块:ownedServices [] + configEditable false + PUT 400(zero-ownership)', async () => {
|
|
686
695
|
const app = createTempApp('tbox-d3-');
|
|
687
696
|
invalidateContractsResolver();
|
|
688
697
|
const store = joinStore(app.appDir, 'contracts');
|
|
@@ -693,63 +702,95 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
|
|
|
693
702
|
'packages/module-zero/tbox.module.json',
|
|
694
703
|
JSON.stringify({
|
|
695
704
|
schemaVersion: 1,
|
|
696
|
-
|
|
705
|
+
id: 'module-zero',
|
|
697
706
|
version: '0.1.0',
|
|
698
707
|
kind: 'business',
|
|
699
|
-
|
|
708
|
+
declare: {},
|
|
700
709
|
dependencies: { modules: [] },
|
|
701
710
|
env: [],
|
|
702
711
|
}),
|
|
703
712
|
);
|
|
704
713
|
const tk = createAppToolkit(app.appDir);
|
|
705
714
|
const mod = await tk.loadModule('module-zero');
|
|
706
|
-
expect(mod.
|
|
707
|
-
expect(mod.
|
|
715
|
+
expect(mod.ownedServices).toEqual([]);
|
|
716
|
+
expect(mod.binding).toEqual({ provider: null, layer: null });
|
|
708
717
|
expect(mod.integration).toBeNull();
|
|
709
718
|
await expect(tk.writeModuleIntegration('module-zero', { provider: 'x' })).rejects.toMatchObject({
|
|
710
719
|
code: 'VALIDATION_FAILED',
|
|
711
720
|
httpStatus: 400,
|
|
712
|
-
message: expect.stringContaining('
|
|
721
|
+
message: expect.stringContaining('未声明任何 declare.owns'),
|
|
713
722
|
});
|
|
714
723
|
app.dispose();
|
|
715
724
|
});
|
|
716
725
|
|
|
717
|
-
it('D4
|
|
726
|
+
it('D4 v6 互斥归属:双模块 owns 同服务 → OWNERSHIP_CONFLICT(先声明胜出)+ 双方可写(归属冲突≠写禁)', async () => {
|
|
718
727
|
const app = createTempApp('tbox-d4-');
|
|
719
728
|
invalidateContractsResolver();
|
|
720
729
|
const store = joinStore(app.appDir, 'contracts');
|
|
721
730
|
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
722
731
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
723
|
-
const manifest = (
|
|
732
|
+
const manifest = (id: string, service: string): string =>
|
|
724
733
|
JSON.stringify({
|
|
725
734
|
schemaVersion: 1,
|
|
726
|
-
|
|
735
|
+
id,
|
|
727
736
|
version: '0.1.0',
|
|
728
737
|
kind: 'business',
|
|
729
|
-
|
|
738
|
+
declare: { owns: [{ service }], consumes: [{ service, optional: false }] },
|
|
730
739
|
dependencies: { modules: [] },
|
|
731
740
|
env: [],
|
|
732
741
|
});
|
|
733
742
|
writeAppFile(app.appDir, 'packages/module-a/tbox.module.json', manifest('module-a', 'parking.alpha'));
|
|
734
|
-
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'));
|
|
735
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 写禁)
|
|
736
754
|
const a = await tk.loadModule('module-a');
|
|
737
755
|
const b = await tk.loadModule('module-b');
|
|
738
|
-
expect(a.configEditable).toBe(
|
|
739
|
-
expect(b.configEditable).toBe(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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'),
|
|
744
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']);
|
|
745
782
|
app.dispose();
|
|
746
783
|
});
|
|
747
784
|
|
|
748
|
-
it('D5 死键扫尾:
|
|
785
|
+
it('D5 死键扫尾:providerBindings/moduleDomainWritable/domain-binding 全链删除(源文件内容断言)', () => {
|
|
749
786
|
const read = (rel: string): string => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8');
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
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');
|
|
753
794
|
});
|
|
754
795
|
});
|
|
755
796
|
|
|
@@ -843,7 +884,7 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
843
884
|
const h2 = layerHarness();
|
|
844
885
|
const mod = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas'] });
|
|
845
886
|
expect(mod.integrationSchemas).toEqual({
|
|
846
|
-
provider: 'wanda', //
|
|
887
|
+
provider: 'wanda', // resolveModuleBinding 单源(modules['module-parking'])
|
|
847
888
|
services: {
|
|
848
889
|
'parking.query': { configSchema: h2.schemas.query },
|
|
849
890
|
'parking.payment': { configSchema: h2.schemas.parking },
|
|
@@ -872,7 +913,7 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
872
913
|
'config/integrations.json',
|
|
873
914
|
JSON.stringify({
|
|
874
915
|
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
875
|
-
|
|
916
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
876
917
|
services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
|
|
877
918
|
}),
|
|
878
919
|
);
|
|
@@ -901,10 +942,10 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
901
942
|
'packages/module-multi/tbox.module.json',
|
|
902
943
|
JSON.stringify({
|
|
903
944
|
schemaVersion: 1,
|
|
904
|
-
|
|
945
|
+
id: 'module-multi',
|
|
905
946
|
version: '0.1.0',
|
|
906
947
|
kind: 'business',
|
|
907
|
-
|
|
948
|
+
declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
|
|
908
949
|
dependencies: { modules: [] },
|
|
909
950
|
env: [],
|
|
910
951
|
}),
|
|
@@ -928,10 +969,10 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
928
969
|
'packages/provider-custom/tbox.module.json',
|
|
929
970
|
JSON.stringify({
|
|
930
971
|
schemaVersion: 1,
|
|
931
|
-
|
|
972
|
+
id: 'provider-custom',
|
|
932
973
|
version: '0.1.0',
|
|
933
974
|
kind: 'business',
|
|
934
|
-
|
|
975
|
+
declare: { providers: { slots: [] }, consumes: [], owns: [] },
|
|
935
976
|
dependencies: { modules: [] },
|
|
936
977
|
env: [],
|
|
937
978
|
}),
|
|
@@ -949,8 +990,8 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
949
990
|
join('node_modules', '@tbox.cn', 'app-provider-mock', 'tbox.module.json'),
|
|
950
991
|
JSON.stringify({
|
|
951
992
|
schemaVersion: 1,
|
|
952
|
-
|
|
953
|
-
|
|
993
|
+
id: 'provider-mock',
|
|
994
|
+
declare: {
|
|
954
995
|
providers: { slots: [{ service: 'parking.query', provider: 'mock', implementation: 'mock-parking@1', credentialType: '-' }] },
|
|
955
996
|
},
|
|
956
997
|
}),
|
|
@@ -968,11 +1009,11 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
968
1009
|
expect(walk.modules.map((m) => m.id).sort()).toEqual(['module-parking', 'provider-custom', 'vendor-mock']);
|
|
969
1010
|
// 详情 200 保持(装包仍合法——零域空态;写通道零域由 D34 守卫拦)
|
|
970
1011
|
const custom = await h2.tk.loadModule('provider-custom');
|
|
971
|
-
expect(custom.
|
|
1012
|
+
expect(custom.ownedServices).toEqual([]);
|
|
972
1013
|
expect(custom.configEditable).toBe(false);
|
|
973
1014
|
expect(custom.services).toEqual([]);
|
|
974
1015
|
const mock = await h2.tk.loadModule('vendor-mock');
|
|
975
|
-
expect(mock.
|
|
1016
|
+
expect(mock.ownedServices).toEqual([]);
|
|
976
1017
|
// 供给轴不受影响:provider-mock 槽位照常进 catalog(清单排除不伤聚合)
|
|
977
1018
|
const providers = await h2.tk.loadProviders();
|
|
978
1019
|
expect(providers.providers.map((p) => p.provider)).toContain('mock');
|
|
@@ -1007,10 +1048,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1007
1048
|
'packages/provider-w/tbox.module.json',
|
|
1008
1049
|
JSON.stringify({
|
|
1009
1050
|
schemaVersion: 1,
|
|
1010
|
-
|
|
1051
|
+
id: 'provider-w',
|
|
1011
1052
|
version: '0.1.0',
|
|
1012
1053
|
kind: 'business',
|
|
1013
|
-
|
|
1054
|
+
declare: {
|
|
1014
1055
|
providers: {
|
|
1015
1056
|
slots: [
|
|
1016
1057
|
{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
|
|
@@ -1029,10 +1070,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1029
1070
|
'packages/provider-multi/tbox.module.json',
|
|
1030
1071
|
JSON.stringify({
|
|
1031
1072
|
schemaVersion: 1,
|
|
1032
|
-
|
|
1073
|
+
id: 'provider-multi',
|
|
1033
1074
|
version: '0.1.0',
|
|
1034
1075
|
kind: 'business',
|
|
1035
|
-
|
|
1076
|
+
declare: {
|
|
1036
1077
|
providers: {
|
|
1037
1078
|
slots: [
|
|
1038
1079
|
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
@@ -1052,10 +1093,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1052
1093
|
'packages/module-parking/tbox.module.json',
|
|
1053
1094
|
JSON.stringify({
|
|
1054
1095
|
schemaVersion: 1,
|
|
1055
|
-
|
|
1096
|
+
id: 'module-parking',
|
|
1056
1097
|
version: '0.1.0',
|
|
1057
1098
|
kind: 'business',
|
|
1058
|
-
|
|
1099
|
+
declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
|
|
1059
1100
|
dependencies: { modules: [] },
|
|
1060
1101
|
env: [],
|
|
1061
1102
|
}),
|
|
@@ -1137,12 +1178,12 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1137
1178
|
|
|
1138
1179
|
it('E6 域级联(分支①)+ ghost 域厂商诚实态', async () => {
|
|
1139
1180
|
const h = eHarness();
|
|
1140
|
-
writeIntegrations(h.app.appDir, {
|
|
1181
|
+
writeIntegrations(h.app.appDir, { modules: { 'module-parking': { provider: 'wanda' } }, services: { 'parking.query': {} } });
|
|
1141
1182
|
const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
1142
1183
|
expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
|
|
1143
1184
|
// ghost 域厂商(catalog 外)→ configSchema null(诊断归谓词 #11——富化非门控)
|
|
1144
1185
|
const h2 = eHarness();
|
|
1145
|
-
writeIntegrations(h2.app.appDir, {
|
|
1186
|
+
writeIntegrations(h2.app.appDir, { modules: { 'module-parking': { provider: 'ghost-vendor' } }, services: { 'parking.query': {} } });
|
|
1146
1187
|
const ghost = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
1147
1188
|
expect(ghost.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
|
|
1148
1189
|
h.app.dispose();
|
|
@@ -1214,10 +1255,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1214
1255
|
});
|
|
1215
1256
|
|
|
1216
1257
|
it('E14 malformed → 整字段 null(不落域级联——分支回归锚)', async () => {
|
|
1217
|
-
// ① 坏对象(缺 credentialType)——配
|
|
1258
|
+
// ① 坏对象(缺 credentialType)——配 modules 佐证未走模块级联
|
|
1218
1259
|
const h = eHarness();
|
|
1219
1260
|
writeIntegrations(h.app.appDir, {
|
|
1220
|
-
|
|
1261
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1221
1262
|
services: { 'parking.query': { provider: { provider: 'my-api' } } },
|
|
1222
1263
|
});
|
|
1223
1264
|
const bad = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
@@ -1225,7 +1266,7 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1225
1266
|
// ② provider: null(谓词 #5 同判 INLINE_PROVIDER_INVALID——坏形态非缺席)
|
|
1226
1267
|
const h2 = eHarness();
|
|
1227
1268
|
writeIntegrations(h2.app.appDir, {
|
|
1228
|
-
|
|
1269
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1229
1270
|
services: { 'parking.query': { provider: null } },
|
|
1230
1271
|
});
|
|
1231
1272
|
const nul = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
@@ -1237,7 +1278,7 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1237
1278
|
it('E15 provider 空串 → 走域级联(非 null)', async () => {
|
|
1238
1279
|
const h = eHarness();
|
|
1239
1280
|
writeIntegrations(h.app.appDir, {
|
|
1240
|
-
|
|
1281
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1241
1282
|
services: { 'parking.query': { provider: '' } },
|
|
1242
1283
|
});
|
|
1243
1284
|
const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
@@ -1262,15 +1303,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1262
1303
|
unsupplied: [],
|
|
1263
1304
|
additionalPropertiesAllowed: false,
|
|
1264
1305
|
fields: [
|
|
1265
|
-
{ key: 'baseUrl', schema: { type: 'string', format: 'uri' }, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1266
|
-
{ key: 'lotId', schema: null, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1267
|
-
{ key: 'env', schema: { type: 'string', enum: ['dev', 'staging', 'prod'] }, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1268
|
-
{ key: 'envMixed', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1269
|
-
{ key: 'meta', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1270
|
-
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string' } } }, sources: [src('parking.query', false), src('parking.payment', true)] },
|
|
1271
|
-
{ key: 'untyped', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1272
|
-
{ key: 'timeout', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1273
|
-
{ 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)] },
|
|
1274
1315
|
],
|
|
1275
1316
|
};
|
|
1276
1317
|
|
|
@@ -1281,15 +1322,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1281
1322
|
unsupplied: [],
|
|
1282
1323
|
additionalPropertiesAllowed: false,
|
|
1283
1324
|
fields: [
|
|
1284
|
-
{ key: 'apiKey', schema: { type: 'string' }, sources: [src('parking.payment', true)] },
|
|
1285
|
-
{ key: 'baseUrl', schema: { type: 'string' }, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1286
|
-
{ key: 'lotId', schema: null, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1287
|
-
{ key: 'env', schema: { type: 'string', enum: ['prod', 'dev', 'staging'] }, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1288
|
-
{ key: 'envMixed', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1289
|
-
{ key: 'meta', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1290
|
-
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string' } } }, sources: [src('parking.payment', true), src('parking.query', false)] },
|
|
1291
|
-
{ key: 'untyped', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1292
|
-
{ 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)] },
|
|
1293
1334
|
],
|
|
1294
1335
|
};
|
|
1295
1336
|
|
|
@@ -1299,15 +1340,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1299
1340
|
/** E4:module 轴 moduleProvider=ghost-x(未知厂商——非 null 空集 + 全 demand 回填,demand 序) */
|
|
1300
1341
|
const E4 = { provider: 'ghost-x', schemaless: [], unsupplied: ['parking.query', 'parking.payment'], additionalPropertiesAllowed: true, fields: [] };
|
|
1301
1342
|
|
|
1302
|
-
/** E5:实际态(无轴参,wanda;scope = [query, payment]
|
|
1343
|
+
/** E5:实际态(无轴参,wanda;scope = [query, payment])——parkLotId 未标记 global 被表单收窄(G 批负例) */
|
|
1303
1344
|
const E5 = {
|
|
1304
1345
|
provider: 'wanda',
|
|
1305
1346
|
schemaless: [],
|
|
1306
1347
|
unsupplied: [],
|
|
1307
1348
|
additionalPropertiesAllowed: true,
|
|
1308
1349
|
fields: [
|
|
1309
|
-
{ key: 'baseUrl', schema: { type: 'string', format: 'uri' }, sources: [src('parking.query', true)] },
|
|
1310
|
-
{ 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)] },
|
|
1311
1352
|
],
|
|
1312
1353
|
};
|
|
1313
1354
|
|
|
@@ -1389,7 +1430,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1389
1430
|
|
|
1390
1431
|
it('S5a-8/9 单源直通 + required 聚合(some;conflict 照常聚合)', () => {
|
|
1391
1432
|
const single = reg({ 'a.one': { configSchema: { type: 'object', properties: { only: { type: 'string', enum: ['z'] } } } } });
|
|
1392
|
-
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)] });
|
|
1393
1434
|
const some = reg({
|
|
1394
1435
|
'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string' } }, required: ['k'] } },
|
|
1395
1436
|
'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
|
|
@@ -1445,7 +1486,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1445
1486
|
const appInc = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
|
|
1446
1487
|
expect(appInc.integrationFieldRegistry).toEqual(E2);
|
|
1447
1488
|
expect(appInc.integrationFieldRegistry?.fields[0].key).toBe('apiKey'); // 足迹字母序(vs E1 首键 baseUrl)
|
|
1448
|
-
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 先)
|
|
1449
1490
|
const partial = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: 'partial' });
|
|
1450
1491
|
expect(partial.integrationFieldRegistry).toEqual(E3);
|
|
1451
1492
|
const actual = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'] });
|
|
@@ -1483,7 +1524,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1483
1524
|
const actJoy = await h2.tk.loadModule('module-parking', { include: both });
|
|
1484
1525
|
expect(actJoy.integrationFieldRegistry).toEqual(hypJoy.integrationFieldRegistry);
|
|
1485
1526
|
expect(actJoy.integrationSchemas).toEqual(hypJoy.integrationSchemas);
|
|
1486
|
-
// ② 跟随应用级(root.provider=wanda)≡
|
|
1527
|
+
// ② 跟随应用级(root.provider=wanda)≡ 清模块位(N1 删 modules['module-parking'] → root 兜底)后实读——非平凡:清位前实际态 = E1
|
|
1487
1528
|
const hypFollow = await h2.tk.loadModule('module-parking', { include: both, moduleProvider: 'wanda' });
|
|
1488
1529
|
expect(hypFollow.integrationFieldRegistry).toEqual(E5);
|
|
1489
1530
|
await h2.tk.writeModuleIntegration('module-parking', {});
|
|
@@ -1496,7 +1537,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1496
1537
|
h2.app.dispose();
|
|
1497
1538
|
});
|
|
1498
1539
|
|
|
1499
|
-
it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(
|
|
1540
|
+
it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(modules/services 不受扰)', async () => {
|
|
1500
1541
|
const h2 = layerHarness();
|
|
1501
1542
|
const hyp = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
|
|
1502
1543
|
const cur = await h2.tk.loadApp();
|
|
@@ -1555,7 +1596,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1555
1596
|
moduleProvider: 'joycity',
|
|
1556
1597
|
});
|
|
1557
1598
|
expect(v.integration).toEqual({ provider: 'wanda' });
|
|
1558
|
-
expect(v.
|
|
1599
|
+
expect(v.binding).toEqual({ provider: 'wanda', layer: 'module' });
|
|
1559
1600
|
expect(v.configEditable).toBe(true);
|
|
1560
1601
|
expect(v.integrationFieldRegistry?.provider).toBe('joycity'); // 表单层重定向
|
|
1561
1602
|
expect(v.integrationSchemas?.provider).toBe('joycity');
|
|
@@ -1571,7 +1612,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1571
1612
|
'config/integrations.json',
|
|
1572
1613
|
JSON.stringify({
|
|
1573
1614
|
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
1574
|
-
|
|
1615
|
+
modules: { 'module-parking': { provider: 'joycity' } },
|
|
1575
1616
|
services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
|
|
1576
1617
|
}),
|
|
1577
1618
|
);
|
|
@@ -1581,7 +1622,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1581
1622
|
).toBeNull();
|
|
1582
1623
|
h3.app.dispose();
|
|
1583
1624
|
|
|
1584
|
-
// 可写未绑定(无
|
|
1625
|
+
// 可写未绑定(无 modules 位无 root.provider——无轴参 → null)
|
|
1585
1626
|
const h4 = layerHarness();
|
|
1586
1627
|
writeAppFile(
|
|
1587
1628
|
h4.app.appDir,
|
|
@@ -1597,29 +1638,39 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1597
1638
|
expect(unbound.configEditable).toBe(true);
|
|
1598
1639
|
h4.app.dispose();
|
|
1599
1640
|
|
|
1600
|
-
// D34
|
|
1641
|
+
// v6 形态(D34 三形态退役):零归属 → null(带 moduleProvider 仍 null——不豁免);
|
|
1642
|
+
// 多域/跨域归属合法化 → 非 null(override 可计算;unsupplied 回填非供给归属槽)
|
|
1601
1643
|
const d34 = createTempApp('tbox-v46-d34-');
|
|
1602
1644
|
invalidateContractsResolver();
|
|
1603
1645
|
const store34 = joinStore(d34.appDir, 'contracts');
|
|
1604
1646
|
writeFakeContractsPackage(store34, { version: '0.9.0' });
|
|
1605
1647
|
linkContracts(d34.appDir, store34, { subdir: 'apps/server' });
|
|
1606
1648
|
const need = (service: string): { service: string; optional: boolean } => ({ service, optional: false });
|
|
1607
|
-
const manifest34 = (
|
|
1608
|
-
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: [] });
|
|
1609
1651
|
writeAppFile(d34.appDir, 'packages/module-multi/tbox.module.json', manifest34('module-multi', [need('parking.query'), need('member.account')]));
|
|
1610
1652
|
writeAppFile(
|
|
1611
1653
|
d34.appDir,
|
|
1612
1654
|
'packages/module-zero/tbox.module.json',
|
|
1613
|
-
JSON.stringify({ schemaVersion: 1,
|
|
1655
|
+
JSON.stringify({ schemaVersion: 1, id: 'module-zero', version: '0.1.0', kind: 'business', declare: {}, dependencies: { modules: [] }, env: [] }),
|
|
1614
1656
|
);
|
|
1615
1657
|
writeAppFile(d34.appDir, 'packages/module-a/tbox.module.json', manifest34('module-a', [need('parking.alpha')]));
|
|
1616
1658
|
writeAppFile(d34.appDir, 'packages/module-b/tbox.module.json', manifest34('module-b', [need('parking.beta')]));
|
|
1617
1659
|
const tk34 = createAppToolkit(d34.appDir);
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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: [] });
|
|
1623
1674
|
d34.dispose();
|
|
1624
1675
|
});
|
|
1625
1676
|
|
|
@@ -1680,7 +1731,13 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1680
1731
|
const props = c.properties;
|
|
1681
1732
|
const declared = props && typeof props === 'object' && !Array.isArray(props) ? Object.keys(props) : []; // 同款守卫
|
|
1682
1733
|
const rev = registry.fields.filter((f) => f.sources.some((x) => x.service === s)).map((f) => f.key);
|
|
1683
|
-
|
|
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());
|
|
1684
1741
|
}
|
|
1685
1742
|
for (const s of registry.schemaless) expect(layer.services[s]?.configSchema).toBeNull();
|
|
1686
1743
|
// unsupplied ≡ demand − scope 键集
|
|
@@ -1690,6 +1747,39 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1690
1747
|
h2.app.dispose();
|
|
1691
1748
|
});
|
|
1692
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
|
+
|
|
1693
1783
|
it('S8 app 轴:unsupplied ≡ [](结构性:足迹 ⊆ 供给)', async () => {
|
|
1694
1784
|
const h2 = layerHarness();
|
|
1695
1785
|
const v = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
|
|
@@ -1697,3 +1787,199 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1697
1787
|
h2.app.dispose();
|
|
1698
1788
|
});
|
|
1699
1789
|
});
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* B1 展示名链(模块 meta / 词汇 meta 单源 / resolveServiceDisplay 兜底链):
|
|
1793
|
+
* 兜底链 = 词汇 meta(slotMeta)> demand meta(catalog.services)> service id(逐字段回落)。
|
|
1794
|
+
* fixture:词汇 meta 停车查询 vs demand meta 车位查询——断言词汇胜出(防漂移锁)。
|
|
1795
|
+
*/
|
|
1796
|
+
describe('B1 展示名链', () => {
|
|
1797
|
+
/** 展示名 fixture 基座:词汇物化(3 槽 2 meta)+ 停车模块(带 meta + demand meta)+ demo 厂商 */
|
|
1798
|
+
function displayHarness(): Harness {
|
|
1799
|
+
const app = createTempApp('tbox-display-');
|
|
1800
|
+
invalidateContractsResolver();
|
|
1801
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
1802
|
+
writeFakeContractsPackage(store, {
|
|
1803
|
+
version: '0.9.0',
|
|
1804
|
+
slots: ['parking.query', 'auth.alipay-login', 'mall.info'],
|
|
1805
|
+
meta: {
|
|
1806
|
+
'parking.query': { name: '停车查询', description: '费用报价、车场信息、可享权益' },
|
|
1807
|
+
'auth.alipay-login': { name: '支付宝登录' },
|
|
1808
|
+
},
|
|
1809
|
+
});
|
|
1810
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
1811
|
+
mkdirDeep(join(app.appDir, 'packages', 'module-parking'));
|
|
1812
|
+
writeAppFile(
|
|
1813
|
+
app.appDir,
|
|
1814
|
+
'packages/module-parking/tbox.module.json',
|
|
1815
|
+
JSON.stringify({
|
|
1816
|
+
schemaVersion: 1,
|
|
1817
|
+
id: 'module-parking',
|
|
1818
|
+
version: '0.1.0',
|
|
1819
|
+
kind: 'business',
|
|
1820
|
+
name: '停车缴费',
|
|
1821
|
+
description: '车辆查询与绑定、停车费用查询、缴费支付与开票',
|
|
1822
|
+
declare: {
|
|
1823
|
+
owns: [{ service: 'parking.query' }],
|
|
1824
|
+
// demand meta name=车位查询——应被词汇 meta 停车查询压过(词汇主源)
|
|
1825
|
+
consumes: [{ service: 'parking.query', optional: false, name: '车位查询' }],
|
|
1826
|
+
},
|
|
1827
|
+
dependencies: { modules: [] },
|
|
1828
|
+
env: [],
|
|
1829
|
+
}),
|
|
1830
|
+
);
|
|
1831
|
+
mkdirDeep(join(app.appDir, 'packages', 'provider-demo'));
|
|
1832
|
+
writeAppFile(
|
|
1833
|
+
app.appDir,
|
|
1834
|
+
'packages/provider-demo/tbox.module.json',
|
|
1835
|
+
JSON.stringify({
|
|
1836
|
+
schemaVersion: 1,
|
|
1837
|
+
id: 'provider-demo',
|
|
1838
|
+
version: '0.1.0',
|
|
1839
|
+
kind: 'third-party',
|
|
1840
|
+
name: '万达',
|
|
1841
|
+
description: '万达厂商集成',
|
|
1842
|
+
declare: {
|
|
1843
|
+
providers: {
|
|
1844
|
+
slots: [{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'apikey' }],
|
|
1845
|
+
},
|
|
1846
|
+
},
|
|
1847
|
+
dependencies: { modules: [] },
|
|
1848
|
+
env: [],
|
|
1849
|
+
}),
|
|
1850
|
+
);
|
|
1851
|
+
const tk = createAppToolkit(app.appDir);
|
|
1852
|
+
return { app, tk };
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
it('T1 resolveServiceDisplay 三态:词汇 > demand > id;逐字段回落(词汇仅 description 时 demand title 可达)', () => {
|
|
1856
|
+
const walk = {
|
|
1857
|
+
slotMeta: {
|
|
1858
|
+
'a.b': { name: '甲', description: '甲描述' },
|
|
1859
|
+
'c.d': { description: '只有描述' },
|
|
1860
|
+
},
|
|
1861
|
+
catalog: {
|
|
1862
|
+
services: {
|
|
1863
|
+
'a.b': { name: '乙', description: '乙描述' },
|
|
1864
|
+
'c.d': { name: '丙' },
|
|
1865
|
+
},
|
|
1866
|
+
},
|
|
1867
|
+
} as unknown as WalkManifestsResult;
|
|
1868
|
+
// 词汇全量在场 → 词汇胜出
|
|
1869
|
+
expect(resolveServiceDisplay(walk, 'a.b')).toEqual({ name: '甲', description: '甲描述' });
|
|
1870
|
+
// 词汇仅 description → title 回落 demand、description 取词汇(逐字段回落)
|
|
1871
|
+
expect(resolveServiceDisplay(walk, 'c.d')).toEqual({ name: '丙', description: '只有描述' });
|
|
1872
|
+
// 双缺席 → demand
|
|
1873
|
+
expect(resolveServiceDisplay(walk, 'e.f')).toEqual({ name: 'e.f' });
|
|
1874
|
+
// 双双缺席 → id 兜底(title 恒有值)
|
|
1875
|
+
const bare = { slotMeta: {}, catalog: { services: {} } } as unknown as WalkManifestsResult;
|
|
1876
|
+
expect(resolveServiceDisplay(bare, 'x.y')).toEqual({ name: 'x.y' });
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
it('T2 loadModulesView:模块 meta 填充;name 兜底', async () => {
|
|
1880
|
+
const h2 = displayHarness();
|
|
1881
|
+
const v = await h2.tk.loadModules();
|
|
1882
|
+
const parking = v.modules.find((m) => m.id === 'module-parking');
|
|
1883
|
+
expect(parking?.name).toBe('停车缴费');
|
|
1884
|
+
expect(parking?.description).toBe('车辆查询与绑定、停车费用查询、缴费支付与开票');
|
|
1885
|
+
// 无 meta 模块(provider 家族不在清单——用第二基座验证兜底)
|
|
1886
|
+
const h3 = harnessWithContracts();
|
|
1887
|
+
const bare = (await h3.tk.loadModules()).modules.find((m) => m.id === 'module-parking');
|
|
1888
|
+
expect(bare?.name).toBe('module-parking');
|
|
1889
|
+
expect(bare?.description).toBe('');
|
|
1890
|
+
h3.app.dispose();
|
|
1891
|
+
h2.app.dispose();
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
it('T3 loadModuleDetailView:meta 恒发;services[] 词汇优先(demand 车位查询被词汇停车查询压过——防漂移锁)', async () => {
|
|
1895
|
+
const h2 = displayHarness();
|
|
1896
|
+
const v = await h2.tk.loadModule('module-parking');
|
|
1897
|
+
expect(v.name).toBe('停车缴费');
|
|
1898
|
+
expect(v.description).toContain('停车费用查询');
|
|
1899
|
+
const svc = v.services.find((s) => s.service === 'parking.query');
|
|
1900
|
+
expect(svc?.name).toBe('停车查询');
|
|
1901
|
+
expect(svc?.description).toBe('费用报价、车场信息、可享权益');
|
|
1902
|
+
expect(svc?.required).toBe(true);
|
|
1903
|
+
h2.app.dispose();
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
it('T-O3 词汇在场键集:resolutions 键集 = 词汇(退化态 = 既有 O3——无 slots 物化基座)', async () => {
|
|
1907
|
+
const h2 = displayHarness();
|
|
1908
|
+
const batch = await h2.tk.loadServiceResolutions();
|
|
1909
|
+
expect(Object.keys(batch.resolutions).sort()).toEqual(['auth.alipay-login', 'mall.info', 'parking.query']);
|
|
1910
|
+
h2.app.dispose();
|
|
1911
|
+
});
|
|
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
|
+
|
|
1954
|
+
it('T4 loadAppView 平台条目:title 走链恒发;词汇无 meta 槽 = id 兜底', async () => {
|
|
1955
|
+
const h2 = displayHarness();
|
|
1956
|
+
const v = await h2.tk.loadApp();
|
|
1957
|
+
const byKey = Object.fromEntries(v.services.map((s) => [s.service, s]));
|
|
1958
|
+
// 平台条目 = 词汇 − 占用(parking.query 被模块占用)
|
|
1959
|
+
expect(Object.keys(byKey).sort()).toEqual(['auth.alipay-login', 'mall.info']);
|
|
1960
|
+
expect(byKey['auth.alipay-login']?.name).toBe('支付宝登录');
|
|
1961
|
+
expect(byKey['mall.info']?.name).toBe('mall.info');
|
|
1962
|
+
expect(byKey['auth.alipay-login']?.required).toBe(false);
|
|
1963
|
+
h2.app.dispose();
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
it('T5 loadServiceDetailView:词汇优先 + id 兜底恒有值', async () => {
|
|
1967
|
+
const h2 = displayHarness();
|
|
1968
|
+
expect((await h2.tk.loadService('parking.query')).name).toBe('停车查询');
|
|
1969
|
+
expect((await h2.tk.loadService('mall.info')).name).toBe('mall.info');
|
|
1970
|
+
h2.app.dispose();
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
it('T6 厂商 title 传播(walk 层首声明)+ /providers/:p services[] 走链', async () => {
|
|
1974
|
+
const h2 = displayHarness();
|
|
1975
|
+
const list = await h2.tk.loadProviders();
|
|
1976
|
+
const wanda = list.providers.find((p) => p.provider === 'wanda');
|
|
1977
|
+
expect(wanda?.name).toBe('万达');
|
|
1978
|
+
expect(wanda?.description).toBe('万达厂商集成');
|
|
1979
|
+
const detail = await h2.tk.loadProvider('wanda');
|
|
1980
|
+
expect(detail.name).toBe('万达');
|
|
1981
|
+
expect(detail.services['parking.query']?.name).toBe('停车查询');
|
|
1982
|
+
expect(detail.services['parking.query']?.description).toBe('费用报价、车场信息、可享权益');
|
|
1983
|
+
h2.app.dispose();
|
|
1984
|
+
});
|
|
1985
|
+
});
|