@tbox.cn/app-toolkit 0.2.0 → 0.4.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.
@@ -4,6 +4,10 @@ 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, type WalkManifestsResult } from '../src/assembly/walk-manifests.js';
8
+ import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas } from '../src/views/integration-schemas.js';
9
+ import { resolveServiceDisplay } from '../src/views/context.js';
10
+ import type { ViewSnapshot } from '../src/views/context.js';
7
11
  import { URL_METHOD_MAP } from './naming-alignment.test.js';
8
12
  import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
9
13
 
@@ -76,6 +80,159 @@ function mkdirDeep(dir: string): void {
76
80
  mkdirSync(dir, { recursive: true });
77
81
  }
78
82
 
83
+ /** v4.4/v4.6 层级 schema 测试基座(模块作用域共享——S 系列 + D36/D37 族):wanda 供给两服务
84
+ * (单域 schema 文件)+ module-parking 单域双 demand。v4.6 additive:joycity 三 slot
85
+ * (query/payment 双 schema 同 type 部分重叠 + 冲突 key 全分支样本 + vehicle 无 schema)+
86
+ * partial 单 slot 无 schema——slot 不进 catalog.services(owners 仅 demand 触发)→ 既有断言键零触碰。 */
87
+ function layerHarness(): {
88
+ app: TempApp;
89
+ tk: AppToolkit;
90
+ schemas: { query: object; parking: object };
91
+ joycity: { query: object; payment: object };
92
+ } {
93
+ const app = createTempApp('tbox-v44-');
94
+ invalidateContractsResolver();
95
+ const store = joinStore(app.appDir, 'contracts');
96
+ writeFakeContractsPackage(store, {
97
+ version: '0.9.0',
98
+ resolveBody: [
99
+ 'const slot = integrations && integrations.services && integrations.services[service];',
100
+ 'if (!slot) return { status: "service-not-configured" };',
101
+ 'const prov = typeof slot.provider === "string" ? slot.provider : slot.provider && slot.provider.provider;',
102
+ 'if (!prov) return { status: "service-not-configured" };',
103
+ 'return { status: "ok", effective: { provider: prov, implementation: slot.implementation || "impl@1", config: slot.config || {} } };',
104
+ ].join(' '),
105
+ });
106
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
107
+ const schemas = {
108
+ query: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
109
+ parking: { type: 'object', properties: { mallId: { type: 'string' } } },
110
+ };
111
+ // v4.6 joycity 代数 fixture:R1(baseUrl 双无 enum / env 同集异序 / envMixed 混布)/
112
+ // R2(options 深相等 / meta 异构)/ R3(lotId string≠integer / timeout number≠integer / untyped 无 type)/
113
+ // required 聚合(query [baseUrl,lotId] / payment [apiKey,options])+ payment 属性序 apiKey 先在(双轴序判别)
114
+ const joycity = {
115
+ query: {
116
+ type: 'object',
117
+ 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' },
126
+ },
127
+ required: ['baseUrl', 'lotId'],
128
+ },
129
+ payment: {
130
+ type: 'object',
131
+ additionalProperties: false,
132
+ 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' },
142
+ },
143
+ required: ['apiKey', 'options'],
144
+ },
145
+ };
146
+ writeAppFile(
147
+ app.appDir,
148
+ 'packages/provider-w/tbox.module.json',
149
+ JSON.stringify({
150
+ schemaVersion: 1,
151
+ name: 'provider-w',
152
+ version: '0.1.0',
153
+ kind: 'business',
154
+ contributes: {
155
+ providers: {
156
+ slots: [
157
+ { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
158
+ { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
159
+ ],
160
+ },
161
+ },
162
+ dependencies: { modules: [] },
163
+ env: [],
164
+ }),
165
+ );
166
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.query));
167
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.parking));
168
+ // v4.6:provider-j(joycity——vehicle 无 configSchema:app 轴 schemaless / module 轴作用域差)+ provider-p(partial)
169
+ writeAppFile(
170
+ app.appDir,
171
+ 'packages/provider-j/tbox.module.json',
172
+ JSON.stringify({
173
+ schemaVersion: 1,
174
+ name: 'provider-j',
175
+ version: '0.1.0',
176
+ kind: 'business',
177
+ contributes: {
178
+ providers: {
179
+ slots: [
180
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
181
+ { service: 'parking.payment', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/payment.json' },
182
+ { service: 'parking.vehicle', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c' },
183
+ ],
184
+ },
185
+ },
186
+ dependencies: { modules: [] },
187
+ env: [],
188
+ }),
189
+ );
190
+ writeAppFile(app.appDir, 'packages/provider-j/schemas/query.json', JSON.stringify(joycity.query));
191
+ writeAppFile(app.appDir, 'packages/provider-j/schemas/payment.json', JSON.stringify(joycity.payment));
192
+ writeAppFile(
193
+ app.appDir,
194
+ 'packages/provider-p/tbox.module.json',
195
+ JSON.stringify({
196
+ schemaVersion: 1,
197
+ name: 'provider-p',
198
+ version: '0.1.0',
199
+ kind: 'business',
200
+ contributes: {
201
+ providers: {
202
+ slots: [{ service: 'parking.payment', provider: 'partial', implementation: 'partial-payment@1', credentialType: 'partial-c' }],
203
+ },
204
+ },
205
+ dependencies: { modules: [] },
206
+ env: [],
207
+ }),
208
+ );
209
+ writeAppFile(
210
+ app.appDir,
211
+ 'packages/module-parking/tbox.module.json',
212
+ JSON.stringify({
213
+ schemaVersion: 1,
214
+ name: 'module-parking',
215
+ version: '0.1.0',
216
+ kind: 'business',
217
+ contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'parking.payment', optional: true }] },
218
+ dependencies: { modules: [] },
219
+ env: [],
220
+ }),
221
+ );
222
+ writeAppFile(
223
+ app.appDir,
224
+ 'config/integrations.json',
225
+ JSON.stringify({
226
+ provider: 'wanda',
227
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
228
+ defaultInstanceId: 'mall-bj-01',
229
+ domains: { parking: { provider: 'wanda' } },
230
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
231
+ }),
232
+ );
233
+ return { app, tk: createAppToolkit(app.appDir), schemas, joycity };
234
+ }
235
+
79
236
  describe('views/工厂(O 矩阵核心族)', () => {
80
237
  let h: Harness;
81
238
 
@@ -102,7 +259,8 @@ describe('views/工厂(O 矩阵核心族)', () => {
102
259
  expect(Object.keys(appView)).toEqual(['services', 'integration']);
103
260
  const mod = await h.tk.loadModule('module-parking');
104
261
  // v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
105
- expect(Object.keys(mod)).toEqual(['domainKeys', 'integration', 'providerBindings', 'configEditable', 'services']);
262
+ // B1 additive:模块 meta title/description 前置(恒发——兜底 name/''
263
+ expect(Object.keys(mod)).toEqual(['title', 'description', 'domainKeys', 'integration', 'providerBindings', 'configEditable', 'services']);
106
264
  const svc = await h.tk.loadService('parking.query');
107
265
  expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
108
266
  // 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
@@ -123,7 +281,7 @@ describe('views/工厂(O 矩阵核心族)', () => {
123
281
  });
124
282
 
125
283
  it('O3 键集 = 服务词汇 ∪ 已配置;required echo(OR 聚合 + 平台 false)', async () => {
126
- // vocabulary 空(无契约包 fixture)→ 键集 = 已配置(parking.query + auth.alipay-login
284
+ // vocabulary 空(无契约包 fixture 物化 slots——退化态回归锁;词汇在场态见 B1 T-O3
127
285
  const batch = await h.tk.loadServiceResolutions();
128
286
  expect(Object.keys(batch.resolutions).sort()).toEqual(['auth.alipay-login', 'parking.query']);
129
287
  // parking.query 由 module-parking demand required:0 → required true
@@ -200,15 +358,15 @@ describe('views/工厂(O 矩阵核心族)', () => {
200
358
  expect(reread.integration).toEqual(node);
201
359
  });
202
360
 
203
- it('no-diff 深度等价 → written:false + restartScheduled:false;dryRun files []', async () => {
361
+ it('no-diff 深度等价 → written:false + restartScheduled:false;dryRun+diff files = 将触碰预览(D35)', async () => {
204
362
  const svc = await h.tk.loadService('parking.query');
205
363
  const result = await h.tk.writeServiceIntegration('parking.query', svc.integration ?? {});
206
364
  expect(result.written).toBe(false);
207
365
  expect(result.restartScheduled).toBe(false);
208
- // dryRun:复读管线不落盘
366
+ // dryRun:复读管线不落盘;body({provider:'mock'})与现值(inline custom API)实为 diff → files 预览
209
367
  const dry = await h.tk.writeServiceIntegration('parking.query', { provider: 'mock' }, { dryRun: true });
210
368
  expect(dry.written).toBe(false);
211
- expect(dry.files).toEqual([]);
369
+ expect(dry.files).toEqual(['config/integrations.json']);
212
370
  });
213
371
 
214
372
  it('写时自动初始化(R13 语义翻转提示)+ N1 清域(模块 {} 删域键)', async () => {
@@ -598,75 +756,6 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
598
756
  });
599
757
 
600
758
  describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
601
- /** 层级 schema 测试基座:wanda 供给两服务(单域 schema 文件)+ module-parking 单域双 demand */
602
- function layerHarness(): { app: TempApp; tk: AppToolkit; schemas: { query: object; parking: object } } {
603
- const app = createTempApp('tbox-v44-');
604
- invalidateContractsResolver();
605
- const store = joinStore(app.appDir, 'contracts');
606
- writeFakeContractsPackage(store, {
607
- version: '0.9.0',
608
- resolveBody: [
609
- 'const slot = integrations && integrations.services && integrations.services[service];',
610
- 'if (!slot) return { status: "service-not-configured" };',
611
- 'const prov = typeof slot.provider === "string" ? slot.provider : slot.provider && slot.provider.provider;',
612
- 'if (!prov) return { status: "service-not-configured" };',
613
- 'return { status: "ok", effective: { provider: prov, implementation: slot.implementation || "impl@1", config: slot.config || {} } };',
614
- ].join(' '),
615
- });
616
- linkContracts(app.appDir, store, { subdir: 'apps/server' });
617
- const schemas = {
618
- query: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
619
- parking: { type: 'object', properties: { mallId: { type: 'string' } } },
620
- };
621
- writeAppFile(
622
- app.appDir,
623
- 'packages/provider-w/tbox.module.json',
624
- JSON.stringify({
625
- schemaVersion: 1,
626
- name: 'provider-w',
627
- version: '0.1.0',
628
- kind: 'business',
629
- contributes: {
630
- providers: {
631
- slots: [
632
- { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
633
- { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
634
- ],
635
- },
636
- },
637
- dependencies: { modules: [] },
638
- env: [],
639
- }),
640
- );
641
- writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.query));
642
- writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.parking));
643
- writeAppFile(
644
- app.appDir,
645
- 'packages/module-parking/tbox.module.json',
646
- JSON.stringify({
647
- schemaVersion: 1,
648
- name: 'module-parking',
649
- version: '0.1.0',
650
- kind: 'business',
651
- contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'parking.payment', optional: true }] },
652
- dependencies: { modules: [] },
653
- env: [],
654
- }),
655
- );
656
- writeAppFile(
657
- app.appDir,
658
- 'config/integrations.json',
659
- JSON.stringify({
660
- provider: 'wanda',
661
- instances: [{ id: 'mall-bj-01', name: '北京店' }],
662
- defaultInstanceId: 'mall-bj-01',
663
- domains: { parking: { provider: 'wanda' } },
664
- services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
665
- }),
666
- );
667
- return { app, tk: createAppToolkit(app.appDir), schemas };
668
- }
669
-
670
759
  it('I1 include 正例:app/module 双键内嵌 + 未请求缺席(内嵌三律)+ issues 恒全量', async () => {
671
760
  const h2 = harnessWithContracts();
672
761
  const appInc = await h2.tk.loadApp({ include: ['serviceResolutions', 'integrationSchemas'] });
@@ -829,3 +918,938 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
829
918
  app.dispose();
830
919
  });
831
920
  });
921
+
922
+ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供给/实施载体非可配置业务模块)', () => {
923
+ /** 夹具基座 = harnessWithContracts(module-parking 业务模块)+ provider-custom
924
+ * (channel 2 零贡献空壳——模板出厂真实形态)+ vendor-mock(channel 1 sdk 登记且
925
+ * id 无家族前缀 + pkg 有——pkg basename 判定分支独测钉,防实现退化只查 id) */
926
+ function providerHarness(): Harness {
927
+ const h = harnessWithContracts();
928
+ writeAppFile(
929
+ h.app.appDir,
930
+ 'packages/provider-custom/tbox.module.json',
931
+ JSON.stringify({
932
+ schemaVersion: 1,
933
+ name: 'provider-custom',
934
+ version: '0.1.0',
935
+ kind: 'business',
936
+ contributes: { providers: { slots: [] }, services: [] },
937
+ dependencies: { modules: [] },
938
+ env: [],
939
+ }),
940
+ );
941
+ writeAppFile(
942
+ h.app.appDir,
943
+ join('.tbox', 'app.json'),
944
+ JSON.stringify({
945
+ templateVersion: 'test',
946
+ npmModules: [{ id: 'vendor-mock', package: '@tbox.cn/app-provider-mock', version: '0.2.0', mode: 'sdk' }],
947
+ }),
948
+ );
949
+ writeAppFile(
950
+ h.app.appDir,
951
+ join('node_modules', '@tbox.cn', 'app-provider-mock', 'tbox.module.json'),
952
+ JSON.stringify({
953
+ schemaVersion: 1,
954
+ name: 'provider-mock',
955
+ contributes: {
956
+ providers: { slots: [{ service: 'parking.query', provider: 'mock', implementation: 'mock-parking@1', credentialType: '-' }] },
957
+ },
958
+ }),
959
+ );
960
+ return h;
961
+ }
962
+
963
+ it('清单排除 provider 家族包(双通道 + id/pkg 漂移);walk 声明集不变量 + 详情 200 + 供给轴保持', async () => {
964
+ const h2 = providerHarness();
965
+ // 清单排除:仅业务模块(provider-custom 经 id 判定;vendor-mock 经 pkg basename 判定——id 漂移形态)
966
+ const view = await h2.tk.loadModules();
967
+ expect(view.modules.map((m) => m.id)).toEqual(['module-parking']);
968
+ // walk 不变量:声明集仍含双 provider(predicate / 平台服务占用 / CLI 装配消费面依赖 provider 在内)
969
+ const walk = walkManifests(h2.app.appDir);
970
+ expect(walk.modules.map((m) => m.id).sort()).toEqual(['module-parking', 'provider-custom', 'vendor-mock']);
971
+ // 详情 200 保持(装包仍合法——零域空态;写通道零域由 D34 守卫拦)
972
+ const custom = await h2.tk.loadModule('provider-custom');
973
+ expect(custom.domainKeys).toEqual([]);
974
+ expect(custom.configEditable).toBe(false);
975
+ expect(custom.services).toEqual([]);
976
+ const mock = await h2.tk.loadModule('vendor-mock');
977
+ expect(mock.domainKeys).toEqual([]);
978
+ // 供给轴不受影响:provider-mock 槽位照常进 catalog(清单排除不伤聚合)
979
+ const providers = await h2.tk.loadProviders();
980
+ expect(providers.providers.map((p) => p.provider)).toContain('mock');
981
+ h2.app.dispose();
982
+ });
983
+ });
984
+
985
+ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
986
+ /** E 系列基座:wanda 双槽(layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json
987
+ * (O15 形态)+ module-parking demand。schema 内容互异防断言串扰:
988
+ * wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
989
+ * 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。 */
990
+ function eHarness(): {
991
+ app: TempApp;
992
+ tk: AppToolkit;
993
+ schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
994
+ } {
995
+ const app = createTempApp('tbox-d38-');
996
+ invalidateContractsResolver();
997
+ const store = join(app.appDir, 'contracts');
998
+ writeFakeContractsPackage(store, { version: '0.9.0' });
999
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
1000
+ const schemas = {
1001
+ wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
1002
+ wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
1003
+ joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
1004
+ joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
1005
+ ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
1006
+ };
1007
+ writeAppFile(
1008
+ app.appDir,
1009
+ 'packages/provider-w/tbox.module.json',
1010
+ JSON.stringify({
1011
+ schemaVersion: 1,
1012
+ name: 'provider-w',
1013
+ version: '0.1.0',
1014
+ kind: 'business',
1015
+ contributes: {
1016
+ providers: {
1017
+ slots: [
1018
+ { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
1019
+ { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
1020
+ ],
1021
+ },
1022
+ },
1023
+ dependencies: { modules: [] },
1024
+ env: [],
1025
+ }),
1026
+ );
1027
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
1028
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
1029
+ writeAppFile(
1030
+ app.appDir,
1031
+ 'packages/provider-multi/tbox.module.json',
1032
+ JSON.stringify({
1033
+ schemaVersion: 1,
1034
+ name: 'provider-multi',
1035
+ version: '0.1.0',
1036
+ kind: 'business',
1037
+ contributes: {
1038
+ providers: {
1039
+ slots: [
1040
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
1041
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
1042
+ ],
1043
+ },
1044
+ },
1045
+ dependencies: { modules: [] },
1046
+ env: [],
1047
+ }),
1048
+ );
1049
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
1050
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
1051
+ writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
1052
+ writeAppFile(
1053
+ app.appDir,
1054
+ 'packages/module-parking/tbox.module.json',
1055
+ JSON.stringify({
1056
+ schemaVersion: 1,
1057
+ name: 'module-parking',
1058
+ version: '0.1.0',
1059
+ kind: 'business',
1060
+ contributes: { services: [{ service: 'parking.query', optional: false }] },
1061
+ dependencies: { modules: [] },
1062
+ env: [],
1063
+ }),
1064
+ );
1065
+ return { app, tk: createAppToolkit(app.appDir), schemas };
1066
+ }
1067
+
1068
+ /** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
1069
+ function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
1070
+ writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
1071
+ }
1072
+
1073
+ it('E1 显式 string + catalog schema(分支②)', async () => {
1074
+ const h = eHarness();
1075
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1076
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1077
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1078
+ h.app.dispose();
1079
+ });
1080
+
1081
+ it('E2 多 impl 钉版不影响 schema(§4.9 升级窗口——解析自默认 impl = 声明序首个)', async () => {
1082
+ const h = eHarness();
1083
+ writeIntegrations(h.app.appDir, {
1084
+ services: { 'parking.query': { provider: 'joycity', implementation: 'joycity-parking@2' } },
1085
+ });
1086
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1087
+ // 钉版 @2 不影响 schema——复合键取声明序首个(@1 的 query.json;与组合单体同源同判)
1088
+ expect(view.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
1089
+ h.app.dispose();
1090
+ });
1091
+
1092
+ it('E3 受控内联 + ref(分支③);catalog > 内联 ref 优先级', async () => {
1093
+ // ① catalog miss → 内联 ref(config/schemas/ok.json)
1094
+ const h = eHarness();
1095
+ writeIntegrations(h.app.appDir, {
1096
+ services: {
1097
+ 'parking.query': {
1098
+ provider: { provider: 'my-api', credentialType: '-', configSchema: 'config/schemas/ok.json' },
1099
+ implementation: 'my-api-parking@1',
1100
+ },
1101
+ },
1102
+ });
1103
+ const inline = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1104
+ expect(inline.integrationSchemas).toEqual({ provider: 'my-api', configSchema: h.schemas.ok });
1105
+ // ② 内联名命中 catalog → catalog 胜出(组合单体同判)
1106
+ const h2 = eHarness();
1107
+ writeIntegrations(h2.app.appDir, {
1108
+ services: {
1109
+ 'parking.query': { provider: { provider: 'wanda', credentialType: '-', configSchema: 'config/schemas/ok.json' } },
1110
+ },
1111
+ });
1112
+ const hit = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1113
+ expect(hit.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h2.schemas.wQuery });
1114
+ h.app.dispose();
1115
+ h2.app.dispose();
1116
+ });
1117
+
1118
+ it('E4 内联 ref 路径逃逸 → 400(H9 共享路径复测——lexical 形态)', async () => {
1119
+ const h = eHarness();
1120
+ writeIntegrations(h.app.appDir, {
1121
+ services: {
1122
+ 'parking.query': { provider: { provider: 'my-api', credentialType: '-', configSchema: 'config/schemas-evil/x.json' } },
1123
+ },
1124
+ });
1125
+ await expect(h.tk.loadService('parking.query', { include: ['integrationSchemas'] })).rejects.toMatchObject({
1126
+ code: 'VALIDATION_FAILED',
1127
+ httpStatus: 400,
1128
+ });
1129
+ h.app.dispose();
1130
+ });
1131
+
1132
+ it('E5 bare 未知厂商 → { provider, configSchema: null } 不 404(富化非门控)', async () => {
1133
+ const h = eHarness();
1134
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'nope-vendor' } } });
1135
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1136
+ expect(view.integrationSchemas).toEqual({ provider: 'nope-vendor', configSchema: null });
1137
+ h.app.dispose();
1138
+ });
1139
+
1140
+ it('E6 域级联(分支①)+ ghost 域厂商诚实态', async () => {
1141
+ const h = eHarness();
1142
+ writeIntegrations(h.app.appDir, { domains: { parking: { provider: 'wanda' } }, services: { 'parking.query': {} } });
1143
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1144
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1145
+ // ghost 域厂商(catalog 外)→ configSchema null(诊断归谓词 #11——富化非门控)
1146
+ const h2 = eHarness();
1147
+ writeIntegrations(h2.app.appDir, { domains: { parking: { provider: 'ghost-vendor' } }, services: { 'parking.query': {} } });
1148
+ const ghost = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1149
+ expect(ghost.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
1150
+ h.app.dispose();
1151
+ h2.app.dispose();
1152
+ });
1153
+
1154
+ it('E7 root 级联(分支①)', async () => {
1155
+ const h = eHarness();
1156
+ writeIntegrations(h.app.appDir, { provider: 'wanda', services: { 'parking.query': {} } });
1157
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1158
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1159
+ h.app.dispose();
1160
+ });
1161
+
1162
+ it('E8 全链未绑定 → 整字段 null', async () => {
1163
+ const h = eHarness();
1164
+ writeIntegrations(h.app.appDir, { services: { 'parking.payment': {} } });
1165
+ const view = await h.tk.loadService('parking.payment', { include: ['integrationSchemas'] });
1166
+ expect(view.integrationSchemas).toBeNull();
1167
+ h.app.dispose();
1168
+ });
1169
+
1170
+ it('E9 实例层无条件排除(链仍走 root/域)', async () => {
1171
+ const h = eHarness();
1172
+ writeIntegrations(h.app.appDir, {
1173
+ provider: 'wanda',
1174
+ services: { 'parking.query': { instances: { 'mall-1': { provider: 'joycity' } } } },
1175
+ });
1176
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1177
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1178
+ h.app.dispose();
1179
+ });
1180
+
1181
+ it('E10 未请求 = 缺席(缺省零富化键)', async () => {
1182
+ const h = eHarness();
1183
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1184
+ const view = await h.tk.loadService('parking.query');
1185
+ expect('integrationSchemas' in view).toBe(false);
1186
+ h.app.dispose();
1187
+ });
1188
+
1189
+ it('E11 未知 include 键 → 400 先于 404', async () => {
1190
+ const h = eHarness();
1191
+ await expect(h.tk.loadService('ghost', { include: ['bogus'] })).rejects.toMatchObject({
1192
+ code: 'VALIDATION_FAILED',
1193
+ httpStatus: 400,
1194
+ fields: ['include'],
1195
+ });
1196
+ h.app.dispose();
1197
+ });
1198
+
1199
+ it('E12 include + 未知服务 → 404 SERVICE_NOT_FOUND', async () => {
1200
+ const h = eHarness();
1201
+ await expect(h.tk.loadService('ghost', { include: ['integrationSchemas'] })).rejects.toMatchObject({
1202
+ code: 'SERVICE_NOT_FOUND',
1203
+ httpStatus: 404,
1204
+ });
1205
+ h.app.dispose();
1206
+ });
1207
+
1208
+ it('E13 受控内联无 ref → { provider, configSchema: null }', async () => {
1209
+ const h = eHarness();
1210
+ writeIntegrations(h.app.appDir, {
1211
+ services: { 'parking.query': { provider: { provider: 'my-api', credentialType: '-' }, implementation: 'x@1' } },
1212
+ });
1213
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1214
+ expect(view.integrationSchemas).toEqual({ provider: 'my-api', configSchema: null });
1215
+ h.app.dispose();
1216
+ });
1217
+
1218
+ it('E14 malformed → 整字段 null(不落域级联——分支回归锚)', async () => {
1219
+ // ① 坏对象(缺 credentialType)——配 domains 佐证未走域级联
1220
+ const h = eHarness();
1221
+ writeIntegrations(h.app.appDir, {
1222
+ domains: { parking: { provider: 'wanda' } },
1223
+ services: { 'parking.query': { provider: { provider: 'my-api' } } },
1224
+ });
1225
+ const bad = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1226
+ expect(bad.integrationSchemas).toBeNull();
1227
+ // ② provider: null(谓词 #5 同判 INLINE_PROVIDER_INVALID——坏形态非缺席)
1228
+ const h2 = eHarness();
1229
+ writeIntegrations(h2.app.appDir, {
1230
+ domains: { parking: { provider: 'wanda' } },
1231
+ services: { 'parking.query': { provider: null } },
1232
+ });
1233
+ const nul = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1234
+ expect(nul.integrationSchemas).toBeNull();
1235
+ h.app.dispose();
1236
+ h2.app.dispose();
1237
+ });
1238
+
1239
+ it('E15 provider 空串 → 走域级联(非 null)', async () => {
1240
+ const h = eHarness();
1241
+ writeIntegrations(h.app.appDir, {
1242
+ domains: { parking: { provider: 'wanda' } },
1243
+ services: { 'parking.query': { provider: '' } },
1244
+ });
1245
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1246
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1247
+ h.app.dispose();
1248
+ });
1249
+ });
1250
+
1251
+ /**
1252
+ * v4.6 D36/D37 测试族(S5 接续 S1-S4):S5a 纯代数(直调合成 layer 字面量,零 fs)/
1253
+ * S5b 接线序 + 等价锁 / S6 同构(假想 ≡ 写后实读)/ S7 边界(400·null 两态·I2·空集)/
1254
+ * S8 双投影互证(依赖契约锁——旁路重取 serviceSchemas 破 S8-2/S8-3 即刻红)。
1255
+ */
1256
+ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () => {
1257
+ /** sources 单元 */
1258
+ const src = (service: string, required: boolean): { service: string; required: boolean } => ({ service, required });
1259
+
1260
+ /** E1:module 轴 moduleProvider=joycity(scope = demand 序 [query, payment]) */
1261
+ const E1 = {
1262
+ provider: 'joycity',
1263
+ schemaless: [],
1264
+ unsupplied: [],
1265
+ additionalPropertiesAllowed: false,
1266
+ 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)] },
1276
+ ],
1277
+ };
1278
+
1279
+ /** E2:app 轴 appProvider=joycity(scope = 足迹字母序 [payment, query, vehicle]——序判别 + 首源漂移) */
1280
+ const E2 = {
1281
+ provider: 'joycity',
1282
+ schemaless: ['parking.vehicle'],
1283
+ unsupplied: [],
1284
+ additionalPropertiesAllowed: false,
1285
+ 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)] },
1295
+ ],
1296
+ };
1297
+
1298
+ /** E3:module 轴 moduleProvider=partial(scope=[payment],schemaless + 部分 unsupplied 四合一) */
1299
+ const E3 = { provider: 'partial', schemaless: ['parking.payment'], unsupplied: ['parking.query'], additionalPropertiesAllowed: true, fields: [] };
1300
+
1301
+ /** E4:module 轴 moduleProvider=ghost-x(未知厂商——非 null 空集 + 全 demand 回填,demand 序) */
1302
+ const E4 = { provider: 'ghost-x', schemaless: [], unsupplied: ['parking.query', 'parking.payment'], additionalPropertiesAllowed: true, fields: [] };
1303
+
1304
+ /** E5:实际态(无轴参,wanda;scope = [query, payment]) */
1305
+ const E5 = {
1306
+ provider: 'wanda',
1307
+ schemaless: [],
1308
+ unsupplied: [],
1309
+ additionalPropertiesAllowed: true,
1310
+ 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)] },
1313
+ ],
1314
+ };
1315
+
1316
+ // ── S5a 纯代数(直调——合成 layer 字面量,零 fs)──
1317
+ const reg = (services: Record<string, { configSchema: object | null }>, o?: { unsupplied?: string[] }) =>
1318
+ buildIntegrationFieldRegistry({ provider: 'x', services }, o);
1319
+
1320
+ it('S5a-1..4 R1 标量:双无 enum 首源保真 / enum 同集异序 / 异集 / 混布', () => {
1321
+ const both = reg({
1322
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string', format: 'uri', title: 'T' } } } },
1323
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
1324
+ });
1325
+ expect(both.fields[0].schema).toEqual({ type: 'string', format: 'uri', title: 'T' }); // 首源逐字保真
1326
+ const sameSet = reg({
1327
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['a', 'b'] } } } },
1328
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['b', 'a', 'a'] } } } },
1329
+ });
1330
+ expect(sameSet.fields[0].schema).toEqual({ type: 'string', enum: ['a', 'b'] }); // 序不敏感 + 去重
1331
+ const diffSet = reg({
1332
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['a'] } } } },
1333
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['b'] } } } },
1334
+ });
1335
+ expect(diffSet.fields[0].schema).toBeNull();
1336
+ const mixed = reg({
1337
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['a'] } } } },
1338
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
1339
+ });
1340
+ expect(mixed.fields[0].schema).toBeNull(); // 混布(哨兵 ≠ 集签名)
1341
+ });
1342
+
1343
+ it('S5a-5/6 R2 object:深相等(键序不敏感)→ 首源;异构 → null', () => {
1344
+ const deep = reg({
1345
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'object', properties: { x: { type: 'string' }, y: { type: 'number' } } } } } },
1346
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'object', properties: { y: { type: 'number' }, x: { type: 'string' } } } } } },
1347
+ });
1348
+ expect(deep.fields[0].schema).toEqual({ type: 'object', properties: { x: { type: 'string' }, y: { type: 'number' } } });
1349
+ const hetero = reg({
1350
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'object', properties: { x: { type: 'string' } } } } } },
1351
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'object', properties: { y: { type: 'string' } } } } } },
1352
+ });
1353
+ expect(hetero.fields[0].schema).toBeNull();
1354
+ });
1355
+
1356
+ it('S5a-7 R3 族:type 不等 / integer≠number / type 缺席 / 布尔子 schema 入列 / type 数组序异', () => {
1357
+ const r3 = reg({
1358
+ 'a.one': {
1359
+ configSchema: {
1360
+ type: 'object',
1361
+ properties: {
1362
+ mismatch: { type: 'string' },
1363
+ numeric: { type: 'number' },
1364
+ untypedK: { description: 'x' },
1365
+ boolK: true,
1366
+ typeArr: { type: ['string', 'null'] },
1367
+ objK: { type: 'object', properties: { p: { type: 'string' } } },
1368
+ },
1369
+ },
1370
+ },
1371
+ 'a.two': {
1372
+ configSchema: {
1373
+ type: 'object',
1374
+ properties: {
1375
+ mismatch: { type: 'integer' },
1376
+ numeric: { type: 'integer' },
1377
+ untypedK: { type: 'string' },
1378
+ boolK: { type: 'string' },
1379
+ typeArr: { type: ['null', 'string'] },
1380
+ objK: { type: 'object', properties: { q: { type: 'number' } } },
1381
+ },
1382
+ },
1383
+ },
1384
+ });
1385
+ const byKey = Object.fromEntries(r3.fields.map((f) => [f.key, f]));
1386
+ for (const k of ['mismatch', 'numeric', 'untypedK', 'boolK', 'typeArr', 'objK']) {
1387
+ expect(byKey[k]?.schema, k).toBeNull();
1388
+ }
1389
+ expect(byKey.boolK?.sources).toHaveLength(2); // F1:布尔子 schema 照样入列(key 在、sources 在、schema null)
1390
+ });
1391
+
1392
+ it('S5a-8/9 单源直通 + required 聚合(some;conflict 照常聚合)', () => {
1393
+ 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)] });
1395
+ const some = reg({
1396
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string' } }, required: ['k'] } },
1397
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
1398
+ });
1399
+ expect(some.fields[0].sources).toEqual([src('a.one', true), src('a.two', false)]);
1400
+ const conflictReq = reg({
1401
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string' } }, required: ['k'] } },
1402
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'integer' } } } },
1403
+ });
1404
+ expect(conflictReq.fields[0].schema).toBeNull();
1405
+ expect(conflictReq.fields[0].sources.some((s) => s.required)).toBe(true);
1406
+ });
1407
+
1408
+ it('S5a-10/11/12 聚合与装配:apAllowed 四态 / schemaless / unsupplied / 空 scope / 布尔整 schema 防御', () => {
1409
+ expect(reg({ 'a.one': { configSchema: { type: 'object', properties: {} } } }).additionalPropertiesAllowed).toBe(true);
1410
+ expect(reg({ 'a.one': { configSchema: { type: 'object', additionalProperties: false } } }).additionalPropertiesAllowed).toBe(false);
1411
+ expect(reg({}).additionalPropertiesAllowed).toBe(true); // 空真
1412
+ // N4:布尔整 schema 防御——不入 schemaless、零参与、fields 空
1413
+ expect(reg({ 'a.one': { configSchema: true as unknown as object } })).toEqual({
1414
+ provider: 'x', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
1415
+ });
1416
+ expect(reg({})).toEqual({ provider: 'x', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true, fields: [] });
1417
+ const asm = reg(
1418
+ { 'a.one': { configSchema: null }, 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } } },
1419
+ { unsupplied: ['b.x'] },
1420
+ );
1421
+ expect(asm.schemaless).toEqual(['a.one']);
1422
+ expect(asm.unsupplied).toEqual(['b.x']);
1423
+ });
1424
+
1425
+ it('S5a-13 fields 序 = services 迭代序(首声明序 + 首源归序)', () => {
1426
+ const order = reg({
1427
+ 'b.second': { configSchema: { type: 'object', properties: { beta: { type: 'string' }, shared: { type: 'string', format: 'uri' } } } },
1428
+ 'a.first': { configSchema: { type: 'object', properties: { alpha: { type: 'string' }, shared: { type: 'string' } } } },
1429
+ });
1430
+ expect(order.fields.map((f) => f.key)).toEqual(['beta', 'shared', 'alpha']);
1431
+ expect(order.fields[1].sources.map((s) => s.service)).toEqual(['b.second', 'a.first']);
1432
+ expect(order.fields[1].schema).toEqual({ type: 'string', format: 'uri' }); // 首源 = b.second
1433
+ });
1434
+
1435
+ // ── S5b 接线序 + 等价锁 ──
1436
+ it('S5b-1/2/3 接线:假想双轴 registry 逐字(E1/E2/E3)+ 实际态(E5)+ fields 序判别', async () => {
1437
+ const h2 = layerHarness();
1438
+ const modInc = await h2.tk.loadModule('module-parking', {
1439
+ include: ['integrationSchemas', 'integrationFieldRegistry'],
1440
+ moduleProvider: 'joycity',
1441
+ });
1442
+ expect(modInc.integrationFieldRegistry).toEqual(E1);
1443
+ expect(modInc.integrationSchemas).toEqual({
1444
+ provider: 'joycity',
1445
+ services: { 'parking.query': { configSchema: h2.joycity.query }, 'parking.payment': { configSchema: h2.joycity.payment } },
1446
+ });
1447
+ const appInc = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1448
+ expect(appInc.integrationFieldRegistry).toEqual(E2);
1449
+ expect(appInc.integrationFieldRegistry?.fields[0].key).toBe('apiKey'); // 足迹字母序(vs E1 首键 baseUrl)
1450
+ expect(appInc.integrationFieldRegistry?.fields[1].schema).toEqual({ type: 'string' }); // 首源漂移(payment 先)
1451
+ const partial = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: 'partial' });
1452
+ expect(partial.integrationFieldRegistry).toEqual(E3);
1453
+ const actual = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'] });
1454
+ expect(actual.integrationFieldRegistry).toEqual(E5);
1455
+ h2.app.dispose();
1456
+ });
1457
+
1458
+ it('S5b-4 旧 loader 等价锁:loadModuleIntegrationSchemas ≡ loadModuleFormViews.integrationSchemas', async () => {
1459
+ const h2 = layerHarness();
1460
+ const walk = walkManifests(h2.app.appDir);
1461
+ const snap: ViewSnapshot = {
1462
+ appDir: h2.app.appDir,
1463
+ walk,
1464
+ catalog: walk.catalog,
1465
+ integrations: await h2.tk.readIntegrationsConfig(),
1466
+ contracts: await h2.tk.resolveContracts(),
1467
+ };
1468
+ const viaOld = loadModuleIntegrationSchemas(snap, 'module-parking');
1469
+ expect(viaOld).toEqual(loadModuleFormViews(snap, 'module-parking').integrationSchemas);
1470
+ expect(viaOld).toEqual({
1471
+ provider: 'wanda',
1472
+ services: { 'parking.query': { configSchema: h2.schemas.query }, 'parking.payment': { configSchema: h2.schemas.parking } },
1473
+ });
1474
+ h2.app.dispose();
1475
+ });
1476
+
1477
+ // ── S6 同构(状态演进全经 toolkit 写通道——免外部写缓存关注)──
1478
+ it('S6 同构:假想预览 ≡ 真实写入后实读(含跟随应用级 + config 值无关)', async () => {
1479
+ const h2 = layerHarness();
1480
+ const both = ['integrationSchemas', 'integrationFieldRegistry'];
1481
+ // ① 假想 joycity ≡ 写 {provider:'joycity'} 后实读(双投影)
1482
+ const hypJoy = await h2.tk.loadModule('module-parking', { include: both, moduleProvider: 'joycity' });
1483
+ const w1 = await h2.tk.writeModuleIntegration('module-parking', { provider: 'joycity' });
1484
+ expect(w1.written).toBe(true);
1485
+ const actJoy = await h2.tk.loadModule('module-parking', { include: both });
1486
+ expect(actJoy.integrationFieldRegistry).toEqual(hypJoy.integrationFieldRegistry);
1487
+ expect(actJoy.integrationSchemas).toEqual(hypJoy.integrationSchemas);
1488
+ // ② 跟随应用级(root.provider=wanda)≡ 清域(N1 删 domains.parking → root 兜底)后实读——非平凡:清域前实际态 = E1
1489
+ const hypFollow = await h2.tk.loadModule('module-parking', { include: both, moduleProvider: 'wanda' });
1490
+ expect(hypFollow.integrationFieldRegistry).toEqual(E5);
1491
+ await h2.tk.writeModuleIntegration('module-parking', {});
1492
+ const actFollow = await h2.tk.loadModule('module-parking', { include: both });
1493
+ expect(actFollow.integrationFieldRegistry).toEqual(hypFollow.integrationFieldRegistry);
1494
+ // ③ PUT 任意 config 值不改 registry(schema 侧投影与值无关)
1495
+ await h2.tk.writeModuleIntegration('module-parking', { provider: 'joycity', config: { baseUrl: 'https://example' } });
1496
+ const actCfg = await h2.tk.loadModule('module-parking', { include: both });
1497
+ expect(actCfg.integrationFieldRegistry).toEqual(E1);
1498
+ h2.app.dispose();
1499
+ });
1500
+
1501
+ it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(domains/services 不受扰)', async () => {
1502
+ const h2 = layerHarness();
1503
+ const hyp = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1504
+ const cur = await h2.tk.loadApp();
1505
+ const w = await h2.tk.writeAppIntegration({ ...(cur.integration ?? {}), provider: 'joycity' });
1506
+ expect(w.written).toBe(true);
1507
+ const act = await h2.tk.loadApp({ include: ['integrationFieldRegistry'] });
1508
+ expect(act.integrationFieldRegistry).toEqual(hyp.integrationFieldRegistry);
1509
+ expect(act.integrationFieldRegistry).toEqual(E2);
1510
+ // I2:域节点/mirror 不受 root 写入波及
1511
+ expect((await h2.tk.loadModule('module-parking')).integration).toEqual({ provider: 'wanda' });
1512
+ h2.app.dispose();
1513
+ });
1514
+
1515
+ // ── S7 边界 ──
1516
+ it('S7-1/2/3/6 轴参严检:400 矩阵(fields 随参数名)+ 先于 404 + 边界正例 + 未请求缺席', async () => {
1517
+ const h2 = layerHarness();
1518
+ await expect(h2.tk.loadApp({ appProvider: 'wanda' })).rejects.toMatchObject({
1519
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['appProvider'],
1520
+ });
1521
+ await expect(h2.tk.loadApp({ include: ['serviceResolutions'], appProvider: 'wanda' })).rejects.toMatchObject({
1522
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['appProvider'],
1523
+ });
1524
+ await expect(h2.tk.loadApp({ include: [], appProvider: 'x' })).rejects.toMatchObject({ fields: ['appProvider'] });
1525
+ await expect(h2.tk.loadModule('module-parking', { moduleProvider: 'x' })).rejects.toMatchObject({ fields: ['moduleProvider'] });
1526
+ // 先于 404;表单键在场 → 放行至 404
1527
+ await expect(h2.tk.loadModule('ghost', { moduleProvider: 'x' })).rejects.toMatchObject({
1528
+ code: 'VALIDATION_FAILED', httpStatus: 400,
1529
+ });
1530
+ await expect(h2.tk.loadModule('ghost', { include: ['integrationSchemas'], moduleProvider: 'x' })).rejects.toMatchObject({
1531
+ code: 'MODULE_NOT_FOUND', httpStatus: 404,
1532
+ });
1533
+ // 边界正例:仅 integrationSchemas + 轴参 → 200 假想计算;registry 未请求 = 缺席(内嵌三律)
1534
+ const ok = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas'], moduleProvider: 'joycity' });
1535
+ expect(ok.integrationSchemas?.provider).toBe('joycity');
1536
+ expect('integrationFieldRegistry' in ok).toBe(false);
1537
+ h2.app.dispose();
1538
+ });
1539
+
1540
+ it('S7-4 空串 ≡ 缺席;错轴参数静默忽略', async () => {
1541
+ const h2 = layerHarness();
1542
+ const empty = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: '' });
1543
+ const absent = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'] });
1544
+ expect(empty.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
1545
+ expect(empty.integrationFieldRegistry).toEqual(E5);
1546
+ // 错轴参数(变量绕过多余属性检查——运行时按名读取,appProvider 被无视)
1547
+ const opts = { include: ['integrationFieldRegistry'], moduleProvider: 'joycity', appProvider: 'wanda' };
1548
+ const wrongAxis = await h2.tk.loadModule('module-parking', opts);
1549
+ expect(wrongAxis.integrationFieldRegistry).toEqual(E1);
1550
+ h2.app.dispose();
1551
+ });
1552
+
1553
+ it('S7-5 I2:假想态下文件镜像与求值恒实际态', async () => {
1554
+ const h2 = layerHarness();
1555
+ const v = await h2.tk.loadModule('module-parking', {
1556
+ include: ['serviceResolutions', 'integrationSchemas', 'integrationFieldRegistry'],
1557
+ moduleProvider: 'joycity',
1558
+ });
1559
+ expect(v.integration).toEqual({ provider: 'wanda' });
1560
+ expect(v.providerBindings).toEqual([{ domain: 'parking', effective: 'wanda', layer: 'module' }]);
1561
+ expect(v.configEditable).toBe(true);
1562
+ expect(v.integrationFieldRegistry?.provider).toBe('joycity'); // 表单层重定向
1563
+ expect(v.integrationSchemas?.provider).toBe('joycity');
1564
+ expect(v.serviceResolutions?.resolutions['parking.query']?.provider).toBe('wanda'); // 求值恒实际
1565
+ h2.app.dispose();
1566
+ });
1567
+
1568
+ it('S7-7 null 两态:app 未绑定 / 可写未绑定 / D34 三形态(带 override 不豁免)', async () => {
1569
+ // app 未绑定(root.provider 缺席——带空串轴参同此)
1570
+ const h3 = layerHarness();
1571
+ writeAppFile(
1572
+ h3.app.appDir,
1573
+ 'config/integrations.json',
1574
+ JSON.stringify({
1575
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
1576
+ domains: { parking: { provider: 'joycity' } },
1577
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
1578
+ }),
1579
+ );
1580
+ expect((await h3.tk.loadApp({ include: ['integrationFieldRegistry'] })).integrationFieldRegistry).toBeNull();
1581
+ expect(
1582
+ (await h3.tk.loadApp({ include: ['integrationSchemas', 'integrationFieldRegistry'], appProvider: '' })).integrationSchemas,
1583
+ ).toBeNull();
1584
+ h3.app.dispose();
1585
+
1586
+ // 可写未绑定(无 domains 无 root.provider——无轴参 → null)
1587
+ const h4 = layerHarness();
1588
+ writeAppFile(
1589
+ h4.app.appDir,
1590
+ 'config/integrations.json',
1591
+ JSON.stringify({
1592
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
1593
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
1594
+ }),
1595
+ );
1596
+ const unbound = await h4.tk.loadModule('module-parking', { include: ['integrationFieldRegistry', 'integrationSchemas'] });
1597
+ expect(unbound.integrationFieldRegistry).toBeNull();
1598
+ expect(unbound.integrationSchemas).toBeNull();
1599
+ expect(unbound.configEditable).toBe(true);
1600
+ h4.app.dispose();
1601
+
1602
+ // D34 三形态(多域/零域/共享键——带 moduleProvider 仍 null)
1603
+ const d34 = createTempApp('tbox-v46-d34-');
1604
+ invalidateContractsResolver();
1605
+ const store34 = joinStore(d34.appDir, 'contracts');
1606
+ writeFakeContractsPackage(store34, { version: '0.9.0' });
1607
+ linkContracts(d34.appDir, store34, { subdir: 'apps/server' });
1608
+ const need = (service: string): { service: string; optional: boolean } => ({ service, optional: false });
1609
+ const manifest34 = (name: string, services: Array<{ service: string; optional: boolean }>): string =>
1610
+ JSON.stringify({ schemaVersion: 1, name, version: '0.1.0', kind: 'business', contributes: { services }, dependencies: { modules: [] }, env: [] });
1611
+ writeAppFile(d34.appDir, 'packages/module-multi/tbox.module.json', manifest34('module-multi', [need('parking.query'), need('member.account')]));
1612
+ writeAppFile(
1613
+ d34.appDir,
1614
+ 'packages/module-zero/tbox.module.json',
1615
+ JSON.stringify({ schemaVersion: 1, name: 'module-zero', version: '0.1.0', kind: 'business', contributes: {}, dependencies: { modules: [] }, env: [] }),
1616
+ );
1617
+ writeAppFile(d34.appDir, 'packages/module-a/tbox.module.json', manifest34('module-a', [need('parking.alpha')]));
1618
+ writeAppFile(d34.appDir, 'packages/module-b/tbox.module.json', manifest34('module-b', [need('parking.beta')]));
1619
+ const tk34 = createAppToolkit(d34.appDir);
1620
+ for (const id of ['module-multi', 'module-zero', 'module-a']) {
1621
+ const v = await tk34.loadModule(id, { include: ['integrationFieldRegistry', 'integrationSchemas'], moduleProvider: 'joycity' });
1622
+ expect(v.integrationFieldRegistry, id).toBeNull();
1623
+ expect(v.integrationSchemas, id).toBeNull();
1624
+ }
1625
+ d34.dispose();
1626
+ });
1627
+
1628
+ it('S7-8 正例:未绑定 + override → 可计算(预览首次配置);未知厂商 → 非 null 空集 + unsupplied 回填', async () => {
1629
+ const h5 = layerHarness();
1630
+ writeAppFile(
1631
+ h5.app.appDir,
1632
+ 'config/integrations.json',
1633
+ JSON.stringify({
1634
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
1635
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
1636
+ }),
1637
+ );
1638
+ const preview = await h5.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: 'joycity' });
1639
+ expect(preview.integrationFieldRegistry).toEqual(E1); // 无绑定亦可预览(scope = demand ∩ joycity 供给)
1640
+ const appPreview = await h5.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1641
+ expect(appPreview.integrationFieldRegistry).toEqual(E2);
1642
+ const ghost = await h5.tk.loadModule('module-parking', {
1643
+ include: ['integrationFieldRegistry', 'integrationSchemas'],
1644
+ moduleProvider: 'ghost-x',
1645
+ });
1646
+ expect(ghost.integrationFieldRegistry).toEqual(E4);
1647
+ expect(ghost.integrationSchemas).toEqual({ provider: 'ghost-x', services: {} });
1648
+ const ghostApp = await h5.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'ghost-x' });
1649
+ expect(ghostApp.integrationFieldRegistry).toEqual({
1650
+ provider: 'ghost-x', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
1651
+ });
1652
+ h5.app.dispose();
1653
+ });
1654
+
1655
+ it('S7-9 最小正例:mock 零足迹 → 非 null 空集(factory→formViews→registry 全链)', async () => {
1656
+ const h2 = harnessWithContracts();
1657
+ const app = await h2.tk.loadApp({ include: ['integrationFieldRegistry'] });
1658
+ expect(app.integrationFieldRegistry).toEqual({
1659
+ provider: 'mock', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
1660
+ });
1661
+ h2.app.dispose();
1662
+ });
1663
+
1664
+ // ── S8 双投影互证(依赖契约锁——旁路重取 serviceSchemas 破等式即刻红)──
1665
+ it('S8 双投影互证:properties 键集 ≡ sources 反查 + schemaless 等价 + unsupplied 等式', async () => {
1666
+ const h2 = layerHarness();
1667
+ const v = await h2.tk.loadModule('module-parking', {
1668
+ include: ['integrationSchemas', 'integrationFieldRegistry'],
1669
+ moduleProvider: 'joycity',
1670
+ });
1671
+ const layer = v.integrationSchemas!;
1672
+ const registry = v.integrationFieldRegistry!;
1673
+ expect(registry.provider).toBe(layer.provider); // I1:同源同口径
1674
+ for (const [s, entry] of Object.entries(layer.services)) {
1675
+ const cfg = entry.configSchema;
1676
+ if (cfg === null) {
1677
+ expect(registry.schemaless, s).toContain(s);
1678
+ continue;
1679
+ }
1680
+ expect(registry.schemaless, s).not.toContain(s);
1681
+ const c = cfg as Record<string, unknown>;
1682
+ const props = c.properties;
1683
+ const declared = props && typeof props === 'object' && !Array.isArray(props) ? Object.keys(props) : []; // 同款守卫
1684
+ const rev = registry.fields.filter((f) => f.sources.some((x) => x.service === s)).map((f) => f.key);
1685
+ expect([...declared].sort(), s).toEqual([...rev].sort());
1686
+ }
1687
+ for (const s of registry.schemaless) expect(layer.services[s]?.configSchema).toBeNull();
1688
+ // unsupplied ≡ demand − scope 键集
1689
+ const demand = v.services.map((x) => x.service);
1690
+ const scopeKeys = Object.keys(layer.services);
1691
+ expect([...registry.unsupplied].sort()).toEqual(demand.filter((s) => !scopeKeys.includes(s)).sort());
1692
+ h2.app.dispose();
1693
+ });
1694
+
1695
+ it('S8 app 轴:unsupplied ≡ [](结构性:足迹 ⊆ 供给)', async () => {
1696
+ const h2 = layerHarness();
1697
+ const v = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1698
+ expect(v.integrationFieldRegistry?.unsupplied).toEqual([]);
1699
+ h2.app.dispose();
1700
+ });
1701
+ });
1702
+
1703
+ /**
1704
+ * B1 展示名链(模块 meta / 词汇 meta 单源 / resolveServiceDisplay 兜底链):
1705
+ * 兜底链 = 词汇 meta(slotMeta)> demand meta(catalog.services)> service id(逐字段回落)。
1706
+ * fixture:词汇 meta 停车查询 vs demand meta 车位查询——断言词汇胜出(防漂移锁)。
1707
+ */
1708
+ describe('B1 展示名链', () => {
1709
+ /** 展示名 fixture 基座:词汇物化(3 槽 2 meta)+ 停车模块(带 meta + demand meta)+ demo 厂商 */
1710
+ function displayHarness(): Harness {
1711
+ const app = createTempApp('tbox-display-');
1712
+ invalidateContractsResolver();
1713
+ const store = joinStore(app.appDir, 'contracts');
1714
+ writeFakeContractsPackage(store, {
1715
+ version: '0.9.0',
1716
+ slots: ['parking.query', 'auth.alipay-login', 'mall.info'],
1717
+ meta: {
1718
+ 'parking.query': { title: '停车查询', description: '费用报价、车场信息、可享权益' },
1719
+ 'auth.alipay-login': { title: '支付宝登录' },
1720
+ },
1721
+ });
1722
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
1723
+ mkdirDeep(join(app.appDir, 'packages', 'module-parking'));
1724
+ writeAppFile(
1725
+ app.appDir,
1726
+ 'packages/module-parking/tbox.module.json',
1727
+ JSON.stringify({
1728
+ schemaVersion: 1,
1729
+ name: 'module-parking',
1730
+ version: '0.1.0',
1731
+ kind: 'business',
1732
+ title: '停车缴费',
1733
+ description: '车辆查询与绑定、停车费用查询、缴费支付与开票',
1734
+ contributes: {
1735
+ // demand meta title=车位查询——应被词汇 meta 停车查询压过(词汇主源)
1736
+ services: [{ service: 'parking.query', optional: false, title: '车位查询' }],
1737
+ },
1738
+ dependencies: { modules: [] },
1739
+ env: [],
1740
+ }),
1741
+ );
1742
+ mkdirDeep(join(app.appDir, 'packages', 'provider-demo'));
1743
+ writeAppFile(
1744
+ app.appDir,
1745
+ 'packages/provider-demo/tbox.module.json',
1746
+ JSON.stringify({
1747
+ schemaVersion: 1,
1748
+ name: 'provider-demo',
1749
+ version: '0.1.0',
1750
+ kind: 'third-party',
1751
+ title: '万达',
1752
+ description: '万达厂商集成',
1753
+ contributes: {
1754
+ providers: {
1755
+ slots: [{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'apikey' }],
1756
+ },
1757
+ },
1758
+ dependencies: { modules: [] },
1759
+ env: [],
1760
+ }),
1761
+ );
1762
+ const tk = createAppToolkit(app.appDir);
1763
+ return { app, tk };
1764
+ }
1765
+
1766
+ it('T1 resolveServiceDisplay 三态:词汇 > demand > id;逐字段回落(词汇仅 description 时 demand title 可达)', () => {
1767
+ const walk = {
1768
+ slotMeta: {
1769
+ 'a.b': { title: '甲', description: '甲描述' },
1770
+ 'c.d': { description: '只有描述' },
1771
+ },
1772
+ catalog: {
1773
+ services: {
1774
+ 'a.b': { title: '乙', description: '乙描述' },
1775
+ 'c.d': { title: '丙' },
1776
+ },
1777
+ },
1778
+ } as unknown as WalkManifestsResult;
1779
+ // 词汇全量在场 → 词汇胜出
1780
+ expect(resolveServiceDisplay(walk, 'a.b')).toEqual({ title: '甲', description: '甲描述' });
1781
+ // 词汇仅 description → title 回落 demand、description 取词汇(逐字段回落)
1782
+ expect(resolveServiceDisplay(walk, 'c.d')).toEqual({ title: '丙', description: '只有描述' });
1783
+ // 双缺席 → demand
1784
+ expect(resolveServiceDisplay(walk, 'e.f')).toEqual({ title: 'e.f' });
1785
+ // 双双缺席 → id 兜底(title 恒有值)
1786
+ const bare = { slotMeta: {}, catalog: { services: {} } } as unknown as WalkManifestsResult;
1787
+ expect(resolveServiceDisplay(bare, 'x.y')).toEqual({ title: 'x.y' });
1788
+ });
1789
+
1790
+ it('T2 loadModulesView:模块 meta 填充;name 兜底', async () => {
1791
+ const h2 = displayHarness();
1792
+ const v = await h2.tk.loadModules();
1793
+ const parking = v.modules.find((m) => m.id === 'module-parking');
1794
+ expect(parking?.title).toBe('停车缴费');
1795
+ expect(parking?.description).toBe('车辆查询与绑定、停车费用查询、缴费支付与开票');
1796
+ // 无 meta 模块(provider 家族不在清单——用第二基座验证兜底)
1797
+ const h3 = harnessWithContracts();
1798
+ const bare = (await h3.tk.loadModules()).modules.find((m) => m.id === 'module-parking');
1799
+ expect(bare?.title).toBe('module-parking');
1800
+ expect(bare?.description).toBe('');
1801
+ h3.app.dispose();
1802
+ h2.app.dispose();
1803
+ });
1804
+
1805
+ it('T3 loadModuleDetailView:meta 恒发;services[] 词汇优先(demand 车位查询被词汇停车查询压过——防漂移锁)', async () => {
1806
+ const h2 = displayHarness();
1807
+ const v = await h2.tk.loadModule('module-parking');
1808
+ expect(v.title).toBe('停车缴费');
1809
+ expect(v.description).toContain('停车费用查询');
1810
+ const svc = v.services.find((s) => s.service === 'parking.query');
1811
+ expect(svc?.title).toBe('停车查询');
1812
+ expect(svc?.description).toBe('费用报价、车场信息、可享权益');
1813
+ expect(svc?.required).toBe(true);
1814
+ h2.app.dispose();
1815
+ });
1816
+
1817
+ it('T-O3 词汇在场键集:resolutions 键集 = 词汇(退化态 = 既有 O3——无 slots 物化基座)', async () => {
1818
+ const h2 = displayHarness();
1819
+ const batch = await h2.tk.loadServiceResolutions();
1820
+ expect(Object.keys(batch.resolutions).sort()).toEqual(['auth.alipay-login', 'mall.info', 'parking.query']);
1821
+ h2.app.dispose();
1822
+ });
1823
+
1824
+ it('T4 loadAppView 平台条目:title 走链恒发;词汇无 meta 槽 = id 兜底', async () => {
1825
+ const h2 = displayHarness();
1826
+ const v = await h2.tk.loadApp();
1827
+ const byKey = Object.fromEntries(v.services.map((s) => [s.service, s]));
1828
+ // 平台条目 = 词汇 − 占用(parking.query 被模块占用)
1829
+ expect(Object.keys(byKey).sort()).toEqual(['auth.alipay-login', 'mall.info']);
1830
+ expect(byKey['auth.alipay-login']?.title).toBe('支付宝登录');
1831
+ expect(byKey['mall.info']?.title).toBe('mall.info');
1832
+ expect(byKey['auth.alipay-login']?.required).toBe(false);
1833
+ h2.app.dispose();
1834
+ });
1835
+
1836
+ it('T5 loadServiceDetailView:词汇优先 + id 兜底恒有值', async () => {
1837
+ const h2 = displayHarness();
1838
+ expect((await h2.tk.loadService('parking.query')).title).toBe('停车查询');
1839
+ expect((await h2.tk.loadService('mall.info')).title).toBe('mall.info');
1840
+ h2.app.dispose();
1841
+ });
1842
+
1843
+ it('T6 厂商 title 传播(walk 层首声明)+ /providers/:p services[] 走链', async () => {
1844
+ const h2 = displayHarness();
1845
+ const list = await h2.tk.loadProviders();
1846
+ const wanda = list.providers.find((p) => p.provider === 'wanda');
1847
+ expect(wanda?.title).toBe('万达');
1848
+ expect(wanda?.description).toBe('万达厂商集成');
1849
+ const detail = await h2.tk.loadProvider('wanda');
1850
+ expect(detail.title).toBe('万达');
1851
+ expect(detail.services['parking.query']?.title).toBe('停车查询');
1852
+ expect(detail.services['parking.query']?.description).toBe('费用报价、车场信息、可享权益');
1853
+ h2.app.dispose();
1854
+ });
1855
+ });