@tbox.cn/app-toolkit 0.1.0 → 0.3.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.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/dist/{chunk-VV3ERZXS.js → chunk-KPD3LUH4.js} +1 -1
  3. package/dist/contracts-resolver-E4SECBOG.js +1 -0
  4. package/dist/index.d.ts +234 -43
  5. package/dist/index.js +8 -8
  6. package/package.json +2 -2
  7. package/src/assembly/app-manifest.ts +6 -0
  8. package/src/assembly/provider-catalog.ts +6 -1
  9. package/src/assembly/walk-manifests.ts +2 -1
  10. package/src/core/contracts-resolver.ts +3 -2
  11. package/src/core/credential-format.ts +4 -2
  12. package/src/core/errors.ts +9 -3
  13. package/src/dto.ts +127 -15
  14. package/src/factory.ts +129 -16
  15. package/src/index.ts +15 -4
  16. package/src/integrations/credentials.ts +1 -1
  17. package/src/integrations/domain-binding.ts +81 -0
  18. package/src/integrations/predicate.ts +7 -6
  19. package/src/integrations/read.ts +23 -3
  20. package/src/integrations/write.ts +93 -34
  21. package/src/views/app.ts +7 -5
  22. package/src/views/context.ts +3 -6
  23. package/src/views/credential-types.ts +28 -0
  24. package/src/views/credentials.ts +57 -0
  25. package/src/views/integration-schemas.ts +226 -0
  26. package/src/views/modules.ts +33 -14
  27. package/src/views/providers.ts +37 -22
  28. package/src/views/service-detail.ts +3 -4
  29. package/src/views/service-resolutions.ts +31 -25
  30. package/tests/credentials-view.test.ts +152 -0
  31. package/tests/demo-app.ts +14 -1
  32. package/tests/file-cache.test.ts +3 -1
  33. package/tests/naming-alignment.test.ts +8 -5
  34. package/tests/vendor-schema-keywords.test.ts +101 -0
  35. package/tests/views.test.ts +1285 -13
  36. package/tests/write-core.test.ts +107 -8
  37. package/dist/contracts-resolver-QM4FBQQV.js +0 -1
@@ -4,6 +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';
9
+ import type { ViewSnapshot } from '../src/views/context.js';
7
10
  import { URL_METHOD_MAP } from './naming-alignment.test.js';
8
11
  import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
9
12
 
@@ -19,11 +22,17 @@ interface Harness {
19
22
  tk: AppToolkit;
20
23
  }
21
24
 
22
- function harnessWithContracts(opts: { version?: string; withStrict?: boolean } = {}): Harness {
25
+ function harnessWithContracts(
26
+ opts: { version?: string; withStrict?: boolean; missingEvaluationExports?: boolean } = {},
27
+ ): Harness {
23
28
  const app = createTempApp('tbox-views-');
24
29
  invalidateContractsResolver();
25
30
  const store = joinStore(app.appDir, 'contracts');
26
- writeFakeContractsPackage(store, { version: opts.version ?? '0.9.0', withStrict: opts.withStrict ?? true });
31
+ writeFakeContractsPackage(store, {
32
+ version: opts.version ?? '0.9.0',
33
+ withStrict: opts.withStrict ?? true,
34
+ missingEvaluationExports: opts.missingEvaluationExports,
35
+ });
27
36
  linkContracts(app.appDir, store, { subdir: 'apps/server' });
28
37
  // 应用结构:parking 模块(packages/ 约定发现)+ 注册表两实例 + integrations.json
29
38
  mkdirDeep(join(app.appDir, 'packages', 'module-parking'));
@@ -70,6 +79,159 @@ function mkdirDeep(dir: string): void {
70
79
  mkdirSync(dir, { recursive: true });
71
80
  }
72
81
 
82
+ /** v4.4/v4.6 层级 schema 测试基座(模块作用域共享——S 系列 + D36/D37 族):wanda 供给两服务
83
+ * (单域 schema 文件)+ module-parking 单域双 demand。v4.6 additive:joycity 三 slot
84
+ * (query/payment 双 schema 同 type 部分重叠 + 冲突 key 全分支样本 + vehicle 无 schema)+
85
+ * partial 单 slot 无 schema——slot 不进 catalog.services(owners 仅 demand 触发)→ 既有断言键零触碰。 */
86
+ function layerHarness(): {
87
+ app: TempApp;
88
+ tk: AppToolkit;
89
+ schemas: { query: object; parking: object };
90
+ joycity: { query: object; payment: object };
91
+ } {
92
+ const app = createTempApp('tbox-v44-');
93
+ invalidateContractsResolver();
94
+ const store = joinStore(app.appDir, 'contracts');
95
+ writeFakeContractsPackage(store, {
96
+ version: '0.9.0',
97
+ resolveBody: [
98
+ 'const slot = integrations && integrations.services && integrations.services[service];',
99
+ 'if (!slot) return { status: "service-not-configured" };',
100
+ 'const prov = typeof slot.provider === "string" ? slot.provider : slot.provider && slot.provider.provider;',
101
+ 'if (!prov) return { status: "service-not-configured" };',
102
+ 'return { status: "ok", effective: { provider: prov, implementation: slot.implementation || "impl@1", config: slot.config || {} } };',
103
+ ].join(' '),
104
+ });
105
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
106
+ const schemas = {
107
+ query: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
108
+ parking: { type: 'object', properties: { mallId: { type: 'string' } } },
109
+ };
110
+ // v4.6 joycity 代数 fixture:R1(baseUrl 双无 enum / env 同集异序 / envMixed 混布)/
111
+ // R2(options 深相等 / meta 异构)/ R3(lotId string≠integer / timeout number≠integer / untyped 无 type)/
112
+ // required 聚合(query [baseUrl,lotId] / payment [apiKey,options])+ payment 属性序 apiKey 先在(双轴序判别)
113
+ const joycity = {
114
+ query: {
115
+ type: 'object',
116
+ 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' },
125
+ },
126
+ required: ['baseUrl', 'lotId'],
127
+ },
128
+ payment: {
129
+ type: 'object',
130
+ additionalProperties: false,
131
+ 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' },
141
+ },
142
+ required: ['apiKey', 'options'],
143
+ },
144
+ };
145
+ writeAppFile(
146
+ app.appDir,
147
+ 'packages/provider-w/tbox.module.json',
148
+ JSON.stringify({
149
+ schemaVersion: 1,
150
+ name: 'provider-w',
151
+ version: '0.1.0',
152
+ kind: 'business',
153
+ contributes: {
154
+ providers: {
155
+ slots: [
156
+ { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
157
+ { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
158
+ ],
159
+ },
160
+ },
161
+ dependencies: { modules: [] },
162
+ env: [],
163
+ }),
164
+ );
165
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.query));
166
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.parking));
167
+ // v4.6:provider-j(joycity——vehicle 无 configSchema:app 轴 schemaless / module 轴作用域差)+ provider-p(partial)
168
+ writeAppFile(
169
+ app.appDir,
170
+ 'packages/provider-j/tbox.module.json',
171
+ JSON.stringify({
172
+ schemaVersion: 1,
173
+ name: 'provider-j',
174
+ version: '0.1.0',
175
+ kind: 'business',
176
+ contributes: {
177
+ providers: {
178
+ slots: [
179
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
180
+ { service: 'parking.payment', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/payment.json' },
181
+ { service: 'parking.vehicle', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c' },
182
+ ],
183
+ },
184
+ },
185
+ dependencies: { modules: [] },
186
+ env: [],
187
+ }),
188
+ );
189
+ writeAppFile(app.appDir, 'packages/provider-j/schemas/query.json', JSON.stringify(joycity.query));
190
+ writeAppFile(app.appDir, 'packages/provider-j/schemas/payment.json', JSON.stringify(joycity.payment));
191
+ writeAppFile(
192
+ app.appDir,
193
+ 'packages/provider-p/tbox.module.json',
194
+ JSON.stringify({
195
+ schemaVersion: 1,
196
+ name: 'provider-p',
197
+ version: '0.1.0',
198
+ kind: 'business',
199
+ contributes: {
200
+ providers: {
201
+ slots: [{ service: 'parking.payment', provider: 'partial', implementation: 'partial-payment@1', credentialType: 'partial-c' }],
202
+ },
203
+ },
204
+ dependencies: { modules: [] },
205
+ env: [],
206
+ }),
207
+ );
208
+ writeAppFile(
209
+ app.appDir,
210
+ 'packages/module-parking/tbox.module.json',
211
+ JSON.stringify({
212
+ schemaVersion: 1,
213
+ name: 'module-parking',
214
+ version: '0.1.0',
215
+ kind: 'business',
216
+ contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'parking.payment', optional: true }] },
217
+ dependencies: { modules: [] },
218
+ env: [],
219
+ }),
220
+ );
221
+ writeAppFile(
222
+ app.appDir,
223
+ 'config/integrations.json',
224
+ JSON.stringify({
225
+ provider: 'wanda',
226
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
227
+ defaultInstanceId: 'mall-bj-01',
228
+ domains: { parking: { provider: 'wanda' } },
229
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
230
+ }),
231
+ );
232
+ return { app, tk: createAppToolkit(app.appDir), schemas, joycity };
233
+ }
234
+
73
235
  describe('views/工厂(O 矩阵核心族)', () => {
74
236
  let h: Harness;
75
237
 
@@ -95,13 +257,14 @@ describe('views/工厂(O 矩阵核心族)', () => {
95
257
  const appView = await h.tk.loadApp();
96
258
  expect(Object.keys(appView)).toEqual(['services', 'integration']);
97
259
  const mod = await h.tk.loadModule('module-parking');
98
- expect(Object.keys(mod)).toEqual(['integration', 'services']);
260
+ // v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
261
+ expect(Object.keys(mod)).toEqual(['domainKeys', 'integration', 'providerBindings', 'configEditable', 'services']);
99
262
  const svc = await h.tk.loadService('parking.query');
100
263
  expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
101
264
  // 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
102
265
  });
103
266
 
104
- it('O8 P14 三处等价:map 值 ≡ 单体 resolution 摘要基(单源函数结构保证)', async () => {
267
+ it('O8 P14 等价:map 值 ≡ 单体 resolution 摘要基(单源函数结构保证;v4.4 内嵌两处见 I4)', async () => {
105
268
  const batch = await h.tk.loadServiceResolutions();
106
269
  const single = await h.tk.loadServiceResolution('parking.query');
107
270
  const entry = batch.resolutions['parking.query'];
@@ -172,7 +335,7 @@ describe('views/工厂(O 矩阵核心族)', () => {
172
335
  wild.app.dispose();
173
336
  });
174
337
 
175
- it('N1 命名对齐:13 行映射表 × 工厂方法 typeof(L1 改名漂移即红)', async () => {
338
+ it('N1 命名对齐:15 行映射表 × 工厂方法 typeof(L1 改名漂移即红——v4.4 15 端点)', async () => {
176
339
  for (const row of URL_METHOD_MAP) {
177
340
  const fn = (h.tk as unknown as Record<string, unknown>)[row.method];
178
341
  expect(typeof fn, row.method).toBe('function');
@@ -193,23 +356,23 @@ describe('views/工厂(O 矩阵核心族)', () => {
193
356
  expect(reread.integration).toEqual(node);
194
357
  });
195
358
 
196
- it('no-diff 深度等价 → written:false + restartScheduled:false;dryRun files []', async () => {
359
+ it('no-diff 深度等价 → written:false + restartScheduled:false;dryRun+diff files = 将触碰预览(D35)', async () => {
197
360
  const svc = await h.tk.loadService('parking.query');
198
361
  const result = await h.tk.writeServiceIntegration('parking.query', svc.integration ?? {});
199
362
  expect(result.written).toBe(false);
200
363
  expect(result.restartScheduled).toBe(false);
201
- // dryRun:复读管线不落盘
364
+ // dryRun:复读管线不落盘;body({provider:'mock'})与现值(inline custom API)实为 diff → files 预览
202
365
  const dry = await h.tk.writeServiceIntegration('parking.query', { provider: 'mock' }, { dryRun: true });
203
366
  expect(dry.written).toBe(false);
204
- expect(dry.files).toEqual([]);
367
+ expect(dry.files).toEqual(['config/integrations.json']);
205
368
  });
206
369
 
207
- it('写时自动初始化(R13 语义翻转提示)+ N1 清域(模块 {} 删 domains 键)', async () => {
370
+ it('写时自动初始化(R13 语义翻转提示)+ N1 清域(模块 {} 删域键)', async () => {
208
371
  h.app.dispose();
209
372
  const fresh = createTempApp('tbox-views-fresh-');
210
373
  invalidateContractsResolver();
211
374
  const store = joinStore(fresh.appDir, 'contracts');
212
- writeFakeContractsPackage(store, { version: '0.9.0' });
375
+ writeFakeContractsPackage(store, { version: '0.9.0', slots: ['auth.alipay-login'] });
213
376
  linkContracts(fresh.appDir, store, { subdir: 'apps/server' });
214
377
  const tk = createAppToolkit(fresh.appDir);
215
378
  // 缺席文件首写 → 骨架物化 + R13 warning
@@ -315,14 +478,18 @@ describe('views/工厂(O 矩阵核心族)', () => {
315
478
  credentialRef: 'secret://wanda-parking.query',
316
479
  layer: 'service',
317
480
  });
318
- expect(inst?.credential).toEqual({
481
+ // v4.4 D33 删净:instances[].credential 移除(凭据掩码归 GET /credentials——求值路径零凭据 I/O)
482
+ expect(inst && 'credential' in inst).toBe(false);
483
+ // 本位 ref(服务级 credentialRef)掩码经 GET /credentials 回显
484
+ const credView = await tk.loadCredentials();
485
+ expect(credView.services['parking.query']?.credential).toEqual({
319
486
  type: 'wanda-c-signed-v1',
320
487
  ref: 'secret://wanda-parking.query',
321
488
  file: 'config/credentials/wanda-parking.query.json',
322
489
  masked: { appKey: 'SE****', appSecret: 'SE****' },
323
490
  });
324
491
  // O4 零真值:序列化后 grep 断言
325
- const serialized = JSON.stringify(detail);
492
+ const serialized = JSON.stringify({ detail, credView });
326
493
  expect(serialized).not.toContain('SECRET-APP-KEY');
327
494
  expect(serialized).not.toContain('SECRET-APP-SECRET');
328
495
  // inheritance 实例层 = 默认实例口径(FX-2c):无实例级声明 → 继承位(provider 缺席)
@@ -367,7 +534,8 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
367
534
  const combo = await tk.loadProviderService('joycity', 'parking.query');
368
535
  expect(combo.implementations).toHaveLength(2);
369
536
  expect(combo.integrationSchemas.configSchema).toEqual({ type: 'object', properties: { baseUrl: { type: 'string' } } });
370
- expect(combo.integrationSchemas.credentialSchema).toBeNull(); // 供给在场未声明 credentialSchema null(非 404
537
+ // v4.4 D33:credentialSchema 删净(结构归 GET /credential-types——组合单体纯 config
538
+ expect('credentialSchema' in combo.integrationSchemas).toBe(false);
371
539
  app.dispose();
372
540
  });
373
541
 
@@ -425,3 +593,1107 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
425
593
  }
426
594
  });
427
595
  });
596
+
597
+ describe('A2 错误面(P-4 notes 优先 / P-5 畸形包 503)', () => {
598
+ it('M1 导入失败 message 含构建指引且不再误报未安装(P-4 notes 优先)', async () => {
599
+ const app = createTempApp('tbox-m1-');
600
+ invalidateContractsResolver();
601
+ const store = joinStore(app.appDir, 'contracts');
602
+ writeFakeContractsPackage(store, { version: '0.9.0', brokenDist: true });
603
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
604
+ const tk = createAppToolkit(app.appDir);
605
+ await expect(tk.loadServiceResolutions()).rejects.toThrow(/构建产物导入失败/);
606
+ await expect(tk.loadServiceResolutions()).rejects.not.toThrow(/未安装/);
607
+ app.dispose();
608
+ });
609
+
610
+ it('M2 <0.9 message 对齐 api.md 信封(需 ≥0.9.0——resolver note 文案)', async () => {
611
+ const app = createTempApp('tbox-m2-');
612
+ invalidateContractsResolver();
613
+ const store = joinStore(app.appDir, 'contracts');
614
+ writeFakeContractsPackage(store, { version: '0.8.2', withStrict: false });
615
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
616
+ const tk = createAppToolkit(app.appDir);
617
+ await expect(tk.loadServiceResolutions()).rejects.toThrow(/需 ≥0\.9\.0/);
618
+ app.dispose();
619
+ });
620
+
621
+ it('M3 畸形包(求值面缺席)+ 无 config → 503 非 500(P-5——predicateIssues 早退绕过 assertFn 的唯一后续拦截点)', async () => {
622
+ const app = createTempApp('tbox-m3-');
623
+ invalidateContractsResolver();
624
+ const store = joinStore(app.appDir, 'contracts');
625
+ writeFakeContractsPackage(store, { version: '0.9.0', missingEvaluationExports: true, slots: ['parking.query'] });
626
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
627
+ const tk = createAppToolkit(app.appDir); // 不写 integrations.json——config === null 早退路径
628
+ await expect(tk.loadServiceResolutions()).rejects.toMatchObject({
629
+ code: 'CONTRACTS_NOT_RESOLVED',
630
+ httpStatus: 503,
631
+ });
632
+ await expect(tk.loadServiceResolutions()).rejects.not.toThrow(/工厂门控应先行拦截/);
633
+ app.dispose();
634
+ });
635
+ });
636
+
637
+ describe('D34 多域模块钉死(v4.4 F3)', () => {
638
+ it('D1 单域回归:domainKeys/providerBindings/configEditable 装配 + integration 原值', async () => {
639
+ const h2 = harnessWithContracts();
640
+ const mod = await h2.tk.loadModule('module-parking');
641
+ expect(mod.domainKeys).toEqual(['parking']);
642
+ expect(mod.configEditable).toBe(true);
643
+ expect(mod.providerBindings).toEqual([{ domain: 'parking', effective: 'wanda', layer: 'module' }]);
644
+ expect(mod.integration).toEqual({ provider: 'wanda' });
645
+ h2.app.dispose();
646
+ });
647
+
648
+ it('D2 多域模块:GET integration null + providerBindings 双条 + PUT 400 分支①(含 {} 清域)', async () => {
649
+ const app = createTempApp('tbox-d2-');
650
+ invalidateContractsResolver();
651
+ const store = joinStore(app.appDir, 'contracts');
652
+ writeFakeContractsPackage(store, { version: '0.9.0' });
653
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
654
+ writeAppFile(
655
+ app.appDir,
656
+ 'packages/module-multi/tbox.module.json',
657
+ JSON.stringify({
658
+ schemaVersion: 1,
659
+ name: 'module-multi',
660
+ version: '0.1.0',
661
+ kind: 'business',
662
+ contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
663
+ dependencies: { modules: [] },
664
+ env: [],
665
+ }),
666
+ );
667
+ const tk = createAppToolkit(app.appDir);
668
+ const mod = await tk.loadModule('module-multi');
669
+ expect(mod.domainKeys).toEqual(['member', 'parking']);
670
+ expect(mod.integration).toBeNull();
671
+ expect(mod.configEditable).toBe(false);
672
+ expect(mod.providerBindings).toEqual([
673
+ { domain: 'member', effective: null, layer: null },
674
+ { domain: 'parking', effective: null, layer: null },
675
+ ]);
676
+ await expect(tk.writeModuleIntegration('module-multi', { provider: 'wanda' })).rejects.toMatchObject({
677
+ code: 'VALIDATION_FAILED',
678
+ httpStatus: 400,
679
+ message: expect.stringContaining('跨 2 个域(member、parking)'),
680
+ });
681
+ await expect(tk.writeModuleIntegration('module-multi', {})).rejects.toMatchObject({ code: 'VALIDATION_FAILED' });
682
+ app.dispose();
683
+ });
684
+
685
+ it('D3 零域模块:domainKeys [] + PUT 400 分支②', async () => {
686
+ const app = createTempApp('tbox-d3-');
687
+ invalidateContractsResolver();
688
+ const store = joinStore(app.appDir, 'contracts');
689
+ writeFakeContractsPackage(store, { version: '0.9.0' });
690
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
691
+ writeAppFile(
692
+ app.appDir,
693
+ 'packages/module-zero/tbox.module.json',
694
+ JSON.stringify({
695
+ schemaVersion: 1,
696
+ name: 'module-zero',
697
+ version: '0.1.0',
698
+ kind: 'business',
699
+ contributes: {},
700
+ dependencies: { modules: [] },
701
+ env: [],
702
+ }),
703
+ );
704
+ const tk = createAppToolkit(app.appDir);
705
+ const mod = await tk.loadModule('module-zero');
706
+ expect(mod.domainKeys).toEqual([]);
707
+ expect(mod.providerBindings).toEqual([]);
708
+ expect(mod.integration).toBeNull();
709
+ await expect(tk.writeModuleIntegration('module-zero', { provider: 'x' })).rejects.toMatchObject({
710
+ code: 'VALIDATION_FAILED',
711
+ httpStatus: 400,
712
+ message: expect.stringContaining('无服务需求声明'),
713
+ });
714
+ app.dispose();
715
+ });
716
+
717
+ it('D4 域键共享(P13):双方 configEditable false + PUT 400 分支③(他方同域异服务——并集计数)', async () => {
718
+ const app = createTempApp('tbox-d4-');
719
+ invalidateContractsResolver();
720
+ const store = joinStore(app.appDir, 'contracts');
721
+ writeFakeContractsPackage(store, { version: '0.9.0' });
722
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
723
+ const manifest = (name: string, service: string): string =>
724
+ JSON.stringify({
725
+ schemaVersion: 1,
726
+ name,
727
+ version: '0.1.0',
728
+ kind: 'business',
729
+ contributes: { services: [{ service, optional: false }] },
730
+ dependencies: { modules: [] },
731
+ env: [],
732
+ });
733
+ 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.beta'));
735
+ const tk = createAppToolkit(app.appDir);
736
+ const a = await tk.loadModule('module-a');
737
+ const b = await tk.loadModule('module-b');
738
+ expect(a.configEditable).toBe(false);
739
+ expect(b.configEditable).toBe(false);
740
+ await expect(tk.writeModuleIntegration('module-a', { provider: 'x' })).rejects.toMatchObject({
741
+ code: 'VALIDATION_FAILED',
742
+ httpStatus: 400,
743
+ message: expect.stringContaining('由 module-a、module-b 共用'),
744
+ });
745
+ app.dispose();
746
+ });
747
+
748
+ it('D5 死键扫尾:moduleDomainKey/moduleDomainKeyOf 全链删除(源文件内容断言)', () => {
749
+ const read = (rel: string): string => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8');
750
+ expect(read('../src/views/modules.ts')).not.toContain('moduleDomainKeyOf');
751
+ expect(read('../src/integrations/write.ts')).not.toContain('moduleDomainKey');
752
+ expect(read('../src/index.ts')).not.toContain('moduleDomainKeyOf');
753
+ });
754
+ });
755
+
756
+ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
757
+ it('I1 include 正例:app/module 双键内嵌 + 未请求缺席(内嵌三律)+ issues 恒全量', async () => {
758
+ const h2 = harnessWithContracts();
759
+ const appInc = await h2.tk.loadApp({ include: ['serviceResolutions', 'integrationSchemas'] });
760
+ expect(Object.keys(appInc.serviceResolutions!.resolutions).sort()).toEqual(['auth.alipay-login', 'parking.query']);
761
+ // harness 无供给 manifest → mock 足迹空(供给在场未声明形态由 S 系列覆盖)
762
+ expect(appInc.integrationSchemas).toEqual({ provider: 'mock', services: {} });
763
+ const modInc = await h2.tk.loadModule('module-parking', { include: ['serviceResolutions'] });
764
+ expect(Object.keys(modInc.serviceResolutions!.resolutions)).toEqual(['parking.query']); // demand 过滤
765
+ const batch = await h2.tk.loadServiceResolutions();
766
+ expect(modInc.serviceResolutions!.issues).toEqual(batch.issues); // 恒全量(不随过滤收窄)
767
+ // 未请求 → 字段缺席(D30 三律:未请求 = 缺席)
768
+ const plain = await h2.tk.loadApp();
769
+ expect('serviceResolutions' in plain).toBe(false);
770
+ expect('integrationSchemas' in plain).toBe(false);
771
+ h2.app.dispose();
772
+ });
773
+
774
+ it('I2 未知/越权键 400 + 校验先于 404 + 重复键去重(P15)', async () => {
775
+ const h2 = harnessWithContracts();
776
+ await expect(h2.tk.loadApp({ include: ['bogus'] })).rejects.toMatchObject({
777
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['include'],
778
+ });
779
+ // 越权:provider 白名单仅 integrationSchemas——且 400 先于 PROVIDER_NOT_FOUND
780
+ await expect(h2.tk.loadProvider('wanda', { include: ['serviceResolutions'] })).rejects.toMatchObject({
781
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['include'],
782
+ });
783
+ // 校验先于 MODULE_NOT_FOUND
784
+ await expect(h2.tk.loadModule('ghost', { include: ['bogus'] })).rejects.toMatchObject({
785
+ code: 'VALIDATION_FAILED', httpStatus: 400,
786
+ });
787
+ const ok = await h2.tk.loadApp({ include: ['serviceResolutions', 'serviceResolutions'] });
788
+ expect(ok.serviceResolutions).not.toBeNull();
789
+ h2.app.dispose();
790
+ });
791
+
792
+ it('I3 degrade:<0.9 → null 永不 503;形态异常包(严格面在场求值面缺席)→ null(双条件)', async () => {
793
+ const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
794
+ expect((await low.tk.loadApp({ include: ['serviceResolutions'] })).serviceResolutions).toBeNull();
795
+ expect((await low.tk.loadApp({ include: ['integrationSchemas'] })).integrationSchemas).toEqual({ provider: 'mock', services: {} }); // 静态轴富化零 contracts
796
+ low.app.dispose();
797
+ const malformed = harnessWithContracts({ missingEvaluationExports: true });
798
+ expect((await malformed.tk.loadApp({ include: ['serviceResolutions'] })).serviceResolutions).toBeNull();
799
+ // 对照:求值轴端点同 fixture → 503(M3 已钉——门控与内嵌两机制不共用断言)
800
+ await expect(malformed.tk.loadServiceResolutions()).rejects.toMatchObject({ httpStatus: 503 });
801
+ malformed.app.dispose();
802
+ });
803
+
804
+ it('I4 P14 四处等价:批量 / 单体摘要基 / app 内嵌 / module 内嵌(deep-equal 含 layer)', async () => {
805
+ const h2 = layerHarness();
806
+ const batch = await h2.tk.loadServiceResolutions();
807
+ const appInc = await h2.tk.loadApp({ include: ['serviceResolutions'] });
808
+ const modInc = await h2.tk.loadModule('module-parking', { include: ['serviceResolutions'] });
809
+ expect(Object.keys(modInc.serviceResolutions!.resolutions).sort()).toEqual(['parking.payment', 'parking.query']); // demand = 双服务
810
+ for (const s of ['parking.query', 'parking.payment']) {
811
+ const single = await h2.tk.loadServiceResolution(s);
812
+ const { instances, inheritance, ...base } = single.resolution;
813
+ void instances;
814
+ void inheritance;
815
+ expect(batch.resolutions[s], s).toEqual(base);
816
+ expect(appInc.serviceResolutions!.resolutions[s], `app:${s}`).toEqual(base);
817
+ expect(modInc.serviceResolutions!.resolutions[s], `module:${s}`).toEqual(base);
818
+ }
819
+ h2.app.dispose();
820
+ });
821
+
822
+ it('I5 layer 一致性:Entry.layer ≡ inheritance 逆序首个有 provider 层;未配置双缺席', async () => {
823
+ const h2 = layerHarness();
824
+ const batch = await h2.tk.loadServiceResolutions();
825
+ for (const [s, entry] of Object.entries(batch.resolutions)) {
826
+ const single = await h2.tk.loadServiceResolution(s);
827
+ const expected = [...single.resolution.inheritance].reverse().find((i) => i.provider !== undefined)?.layer ?? null;
828
+ if (entry.provider === undefined) {
829
+ expect(entry.layer, `${s} 双缺席`).toBeUndefined();
830
+ } else {
831
+ expect(entry.layer, s).toEqual(expected);
832
+ }
833
+ }
834
+ // parking.payment 未配置(slot 缺席)→ provider/layer 双缺席(无孤儿态)
835
+ expect(batch.resolutions['parking.payment']?.provider).toBeUndefined();
836
+ expect(batch.resolutions['parking.payment']?.layer).toBeUndefined();
837
+ // parking.query 槽级 wanda → layer 'service'
838
+ expect(batch.resolutions['parking.query']).toMatchObject({ status: 'ok', provider: 'wanda', layer: 'service' });
839
+ h2.app.dispose();
840
+ });
841
+
842
+ it('S1 模块级层级 schema:demand ∩ 供给 + schema 文件原文(D32)+ 缺省缺席', async () => {
843
+ const h2 = layerHarness();
844
+ const mod = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas'] });
845
+ expect(mod.integrationSchemas).toEqual({
846
+ provider: 'wanda', // resolveDomainBinding 单源(domains.parking)
847
+ services: {
848
+ 'parking.query': { configSchema: h2.schemas.query },
849
+ 'parking.payment': { configSchema: h2.schemas.parking },
850
+ },
851
+ });
852
+ const plain = await h2.tk.loadModule('module-parking');
853
+ expect('integrationSchemas' in plain).toBe(false);
854
+ h2.app.dispose();
855
+ });
856
+
857
+ it('S2 应用级:供给足迹全量(≠ AppView.services)+ root.provider 缺席 → null', async () => {
858
+ const h2 = layerHarness();
859
+ const appInc = await h2.tk.loadApp({ include: ['integrationSchemas'] });
860
+ expect(appInc.integrationSchemas).toEqual({
861
+ provider: 'wanda',
862
+ services: {
863
+ 'parking.query': { configSchema: h2.schemas.query },
864
+ 'parking.payment': { configSchema: h2.schemas.parking },
865
+ },
866
+ });
867
+ h2.app.dispose();
868
+ // root.provider 缺席 → null(未绑定——表单降级 ExtraFields)
869
+ const h3 = layerHarness();
870
+ writeAppFile(
871
+ h3.app.appDir,
872
+ 'config/integrations.json',
873
+ JSON.stringify({
874
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
875
+ domains: { parking: { provider: 'wanda' } },
876
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
877
+ }),
878
+ );
879
+ expect((await h3.tk.loadApp({ include: ['integrationSchemas'] })).integrationSchemas).toBeNull();
880
+ h3.app.dispose();
881
+ });
882
+
883
+ it('S3 切换态批量:loadProvider include=integrationSchemas(供给轴逐服务)+ 未请求缺席', async () => {
884
+ const h2 = layerHarness();
885
+ const detail = await h2.tk.loadProvider('wanda', { include: ['integrationSchemas'] });
886
+ expect(detail.services['parking.query']?.integrationSchemas).toEqual({ configSchema: h2.schemas.query });
887
+ expect(detail.services['parking.payment']?.integrationSchemas).toEqual({ configSchema: h2.schemas.parking });
888
+ const plain = await h2.tk.loadProvider('wanda');
889
+ expect(plain.services['parking.query'] && 'integrationSchemas' in plain.services['parking.query']).toBe(false);
890
+ h2.app.dispose();
891
+ });
892
+
893
+ it('S4 多域模块 integrationSchemas → 整字段 null(D32——表单降级 ExtraFields)', async () => {
894
+ const app = createTempApp('tbox-s4-');
895
+ invalidateContractsResolver();
896
+ const store = joinStore(app.appDir, 'contracts');
897
+ writeFakeContractsPackage(store, { version: '0.9.0' });
898
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
899
+ writeAppFile(
900
+ app.appDir,
901
+ 'packages/module-multi/tbox.module.json',
902
+ JSON.stringify({
903
+ schemaVersion: 1,
904
+ name: 'module-multi',
905
+ version: '0.1.0',
906
+ kind: 'business',
907
+ contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
908
+ dependencies: { modules: [] },
909
+ env: [],
910
+ }),
911
+ );
912
+ const tk = createAppToolkit(app.appDir);
913
+ const mod = await tk.loadModule('module-multi', { include: ['integrationSchemas'] });
914
+ expect(mod.integrationSchemas).toBeNull();
915
+ expect(mod.serviceResolutions).toBeUndefined(); // 未请求
916
+ app.dispose();
917
+ });
918
+ });
919
+
920
+ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供给/实施载体非可配置业务模块)', () => {
921
+ /** 夹具基座 = harnessWithContracts(module-parking 业务模块)+ provider-custom
922
+ * (channel 2 零贡献空壳——模板出厂真实形态)+ vendor-mock(channel 1 sdk 登记且
923
+ * id 无家族前缀 + pkg 有——pkg basename 判定分支独测钉,防实现退化只查 id) */
924
+ function providerHarness(): Harness {
925
+ const h = harnessWithContracts();
926
+ writeAppFile(
927
+ h.app.appDir,
928
+ 'packages/provider-custom/tbox.module.json',
929
+ JSON.stringify({
930
+ schemaVersion: 1,
931
+ name: 'provider-custom',
932
+ version: '0.1.0',
933
+ kind: 'business',
934
+ contributes: { providers: { slots: [] }, services: [] },
935
+ dependencies: { modules: [] },
936
+ env: [],
937
+ }),
938
+ );
939
+ writeAppFile(
940
+ h.app.appDir,
941
+ join('.tbox', 'app.json'),
942
+ JSON.stringify({
943
+ templateVersion: 'test',
944
+ npmModules: [{ id: 'vendor-mock', package: '@tbox.cn/app-provider-mock', version: '0.2.0', mode: 'sdk' }],
945
+ }),
946
+ );
947
+ writeAppFile(
948
+ h.app.appDir,
949
+ join('node_modules', '@tbox.cn', 'app-provider-mock', 'tbox.module.json'),
950
+ JSON.stringify({
951
+ schemaVersion: 1,
952
+ name: 'provider-mock',
953
+ contributes: {
954
+ providers: { slots: [{ service: 'parking.query', provider: 'mock', implementation: 'mock-parking@1', credentialType: '-' }] },
955
+ },
956
+ }),
957
+ );
958
+ return h;
959
+ }
960
+
961
+ it('清单排除 provider 家族包(双通道 + id/pkg 漂移);walk 声明集不变量 + 详情 200 + 供给轴保持', async () => {
962
+ const h2 = providerHarness();
963
+ // 清单排除:仅业务模块(provider-custom 经 id 判定;vendor-mock 经 pkg basename 判定——id 漂移形态)
964
+ const view = await h2.tk.loadModules();
965
+ expect(view.modules.map((m) => m.id)).toEqual(['module-parking']);
966
+ // walk 不变量:声明集仍含双 provider(predicate / 平台服务占用 / CLI 装配消费面依赖 provider 在内)
967
+ const walk = walkManifests(h2.app.appDir);
968
+ expect(walk.modules.map((m) => m.id).sort()).toEqual(['module-parking', 'provider-custom', 'vendor-mock']);
969
+ // 详情 200 保持(装包仍合法——零域空态;写通道零域由 D34 守卫拦)
970
+ const custom = await h2.tk.loadModule('provider-custom');
971
+ expect(custom.domainKeys).toEqual([]);
972
+ expect(custom.configEditable).toBe(false);
973
+ expect(custom.services).toEqual([]);
974
+ const mock = await h2.tk.loadModule('vendor-mock');
975
+ expect(mock.domainKeys).toEqual([]);
976
+ // 供给轴不受影响:provider-mock 槽位照常进 catalog(清单排除不伤聚合)
977
+ const providers = await h2.tk.loadProviders();
978
+ expect(providers.providers.map((p) => p.provider)).toContain('mock');
979
+ h2.app.dispose();
980
+ });
981
+ });
982
+
983
+ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
984
+ /** E 系列基座:wanda 双槽(layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json
985
+ * (O15 形态)+ module-parking demand。schema 内容互异防断言串扰:
986
+ * wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
987
+ * 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。 */
988
+ function eHarness(): {
989
+ app: TempApp;
990
+ tk: AppToolkit;
991
+ schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
992
+ } {
993
+ const app = createTempApp('tbox-d38-');
994
+ invalidateContractsResolver();
995
+ const store = join(app.appDir, 'contracts');
996
+ writeFakeContractsPackage(store, { version: '0.9.0' });
997
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
998
+ const schemas = {
999
+ wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
1000
+ wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
1001
+ joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
1002
+ joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
1003
+ ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
1004
+ };
1005
+ writeAppFile(
1006
+ app.appDir,
1007
+ 'packages/provider-w/tbox.module.json',
1008
+ JSON.stringify({
1009
+ schemaVersion: 1,
1010
+ name: 'provider-w',
1011
+ version: '0.1.0',
1012
+ kind: 'business',
1013
+ contributes: {
1014
+ providers: {
1015
+ slots: [
1016
+ { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
1017
+ { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
1018
+ ],
1019
+ },
1020
+ },
1021
+ dependencies: { modules: [] },
1022
+ env: [],
1023
+ }),
1024
+ );
1025
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
1026
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
1027
+ writeAppFile(
1028
+ app.appDir,
1029
+ 'packages/provider-multi/tbox.module.json',
1030
+ JSON.stringify({
1031
+ schemaVersion: 1,
1032
+ name: 'provider-multi',
1033
+ version: '0.1.0',
1034
+ kind: 'business',
1035
+ contributes: {
1036
+ providers: {
1037
+ slots: [
1038
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
1039
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
1040
+ ],
1041
+ },
1042
+ },
1043
+ dependencies: { modules: [] },
1044
+ env: [],
1045
+ }),
1046
+ );
1047
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
1048
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
1049
+ writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
1050
+ writeAppFile(
1051
+ app.appDir,
1052
+ 'packages/module-parking/tbox.module.json',
1053
+ JSON.stringify({
1054
+ schemaVersion: 1,
1055
+ name: 'module-parking',
1056
+ version: '0.1.0',
1057
+ kind: 'business',
1058
+ contributes: { services: [{ service: 'parking.query', optional: false }] },
1059
+ dependencies: { modules: [] },
1060
+ env: [],
1061
+ }),
1062
+ );
1063
+ return { app, tk: createAppToolkit(app.appDir), schemas };
1064
+ }
1065
+
1066
+ /** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
1067
+ function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
1068
+ writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
1069
+ }
1070
+
1071
+ it('E1 显式 string + catalog schema(分支②)', async () => {
1072
+ const h = eHarness();
1073
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1074
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1075
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1076
+ h.app.dispose();
1077
+ });
1078
+
1079
+ it('E2 多 impl 钉版不影响 schema(§4.9 升级窗口——解析自默认 impl = 声明序首个)', async () => {
1080
+ const h = eHarness();
1081
+ writeIntegrations(h.app.appDir, {
1082
+ services: { 'parking.query': { provider: 'joycity', implementation: 'joycity-parking@2' } },
1083
+ });
1084
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1085
+ // 钉版 @2 不影响 schema——复合键取声明序首个(@1 的 query.json;与组合单体同源同判)
1086
+ expect(view.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
1087
+ h.app.dispose();
1088
+ });
1089
+
1090
+ it('E3 受控内联 + ref(分支③);catalog > 内联 ref 优先级', async () => {
1091
+ // ① catalog miss → 内联 ref(config/schemas/ok.json)
1092
+ const h = eHarness();
1093
+ writeIntegrations(h.app.appDir, {
1094
+ services: {
1095
+ 'parking.query': {
1096
+ provider: { provider: 'my-api', credentialType: '-', configSchema: 'config/schemas/ok.json' },
1097
+ implementation: 'my-api-parking@1',
1098
+ },
1099
+ },
1100
+ });
1101
+ const inline = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1102
+ expect(inline.integrationSchemas).toEqual({ provider: 'my-api', configSchema: h.schemas.ok });
1103
+ // ② 内联名命中 catalog → catalog 胜出(组合单体同判)
1104
+ const h2 = eHarness();
1105
+ writeIntegrations(h2.app.appDir, {
1106
+ services: {
1107
+ 'parking.query': { provider: { provider: 'wanda', credentialType: '-', configSchema: 'config/schemas/ok.json' } },
1108
+ },
1109
+ });
1110
+ const hit = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1111
+ expect(hit.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h2.schemas.wQuery });
1112
+ h.app.dispose();
1113
+ h2.app.dispose();
1114
+ });
1115
+
1116
+ it('E4 内联 ref 路径逃逸 → 400(H9 共享路径复测——lexical 形态)', async () => {
1117
+ const h = eHarness();
1118
+ writeIntegrations(h.app.appDir, {
1119
+ services: {
1120
+ 'parking.query': { provider: { provider: 'my-api', credentialType: '-', configSchema: 'config/schemas-evil/x.json' } },
1121
+ },
1122
+ });
1123
+ await expect(h.tk.loadService('parking.query', { include: ['integrationSchemas'] })).rejects.toMatchObject({
1124
+ code: 'VALIDATION_FAILED',
1125
+ httpStatus: 400,
1126
+ });
1127
+ h.app.dispose();
1128
+ });
1129
+
1130
+ it('E5 bare 未知厂商 → { provider, configSchema: null } 不 404(富化非门控)', async () => {
1131
+ const h = eHarness();
1132
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'nope-vendor' } } });
1133
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1134
+ expect(view.integrationSchemas).toEqual({ provider: 'nope-vendor', configSchema: null });
1135
+ h.app.dispose();
1136
+ });
1137
+
1138
+ it('E6 域级联(分支①)+ ghost 域厂商诚实态', async () => {
1139
+ const h = eHarness();
1140
+ writeIntegrations(h.app.appDir, { domains: { parking: { provider: 'wanda' } }, services: { 'parking.query': {} } });
1141
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1142
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1143
+ // ghost 域厂商(catalog 外)→ configSchema null(诊断归谓词 #11——富化非门控)
1144
+ const h2 = eHarness();
1145
+ writeIntegrations(h2.app.appDir, { domains: { parking: { provider: 'ghost-vendor' } }, services: { 'parking.query': {} } });
1146
+ const ghost = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1147
+ expect(ghost.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
1148
+ h.app.dispose();
1149
+ h2.app.dispose();
1150
+ });
1151
+
1152
+ it('E7 root 级联(分支①)', async () => {
1153
+ const h = eHarness();
1154
+ writeIntegrations(h.app.appDir, { provider: 'wanda', services: { 'parking.query': {} } });
1155
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1156
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1157
+ h.app.dispose();
1158
+ });
1159
+
1160
+ it('E8 全链未绑定 → 整字段 null', async () => {
1161
+ const h = eHarness();
1162
+ writeIntegrations(h.app.appDir, { services: { 'parking.payment': {} } });
1163
+ const view = await h.tk.loadService('parking.payment', { include: ['integrationSchemas'] });
1164
+ expect(view.integrationSchemas).toBeNull();
1165
+ h.app.dispose();
1166
+ });
1167
+
1168
+ it('E9 实例层无条件排除(链仍走 root/域)', async () => {
1169
+ const h = eHarness();
1170
+ writeIntegrations(h.app.appDir, {
1171
+ provider: 'wanda',
1172
+ services: { 'parking.query': { instances: { 'mall-1': { provider: 'joycity' } } } },
1173
+ });
1174
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1175
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1176
+ h.app.dispose();
1177
+ });
1178
+
1179
+ it('E10 未请求 = 缺席(缺省零富化键)', async () => {
1180
+ const h = eHarness();
1181
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1182
+ const view = await h.tk.loadService('parking.query');
1183
+ expect('integrationSchemas' in view).toBe(false);
1184
+ h.app.dispose();
1185
+ });
1186
+
1187
+ it('E11 未知 include 键 → 400 先于 404', async () => {
1188
+ const h = eHarness();
1189
+ await expect(h.tk.loadService('ghost', { include: ['bogus'] })).rejects.toMatchObject({
1190
+ code: 'VALIDATION_FAILED',
1191
+ httpStatus: 400,
1192
+ fields: ['include'],
1193
+ });
1194
+ h.app.dispose();
1195
+ });
1196
+
1197
+ it('E12 include + 未知服务 → 404 SERVICE_NOT_FOUND', async () => {
1198
+ const h = eHarness();
1199
+ await expect(h.tk.loadService('ghost', { include: ['integrationSchemas'] })).rejects.toMatchObject({
1200
+ code: 'SERVICE_NOT_FOUND',
1201
+ httpStatus: 404,
1202
+ });
1203
+ h.app.dispose();
1204
+ });
1205
+
1206
+ it('E13 受控内联无 ref → { provider, configSchema: null }', async () => {
1207
+ const h = eHarness();
1208
+ writeIntegrations(h.app.appDir, {
1209
+ services: { 'parking.query': { provider: { provider: 'my-api', credentialType: '-' }, implementation: 'x@1' } },
1210
+ });
1211
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1212
+ expect(view.integrationSchemas).toEqual({ provider: 'my-api', configSchema: null });
1213
+ h.app.dispose();
1214
+ });
1215
+
1216
+ it('E14 malformed → 整字段 null(不落域级联——分支回归锚)', async () => {
1217
+ // ① 坏对象(缺 credentialType)——配 domains 佐证未走域级联
1218
+ const h = eHarness();
1219
+ writeIntegrations(h.app.appDir, {
1220
+ domains: { parking: { provider: 'wanda' } },
1221
+ services: { 'parking.query': { provider: { provider: 'my-api' } } },
1222
+ });
1223
+ const bad = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1224
+ expect(bad.integrationSchemas).toBeNull();
1225
+ // ② provider: null(谓词 #5 同判 INLINE_PROVIDER_INVALID——坏形态非缺席)
1226
+ const h2 = eHarness();
1227
+ writeIntegrations(h2.app.appDir, {
1228
+ domains: { parking: { provider: 'wanda' } },
1229
+ services: { 'parking.query': { provider: null } },
1230
+ });
1231
+ const nul = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1232
+ expect(nul.integrationSchemas).toBeNull();
1233
+ h.app.dispose();
1234
+ h2.app.dispose();
1235
+ });
1236
+
1237
+ it('E15 provider 空串 → 走域级联(非 null)', async () => {
1238
+ const h = eHarness();
1239
+ writeIntegrations(h.app.appDir, {
1240
+ domains: { parking: { provider: 'wanda' } },
1241
+ services: { 'parking.query': { provider: '' } },
1242
+ });
1243
+ const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1244
+ expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1245
+ h.app.dispose();
1246
+ });
1247
+ });
1248
+
1249
+ /**
1250
+ * v4.6 D36/D37 测试族(S5 接续 S1-S4):S5a 纯代数(直调合成 layer 字面量,零 fs)/
1251
+ * S5b 接线序 + 等价锁 / S6 同构(假想 ≡ 写后实读)/ S7 边界(400·null 两态·I2·空集)/
1252
+ * S8 双投影互证(依赖契约锁——旁路重取 serviceSchemas 破 S8-2/S8-3 即刻红)。
1253
+ */
1254
+ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () => {
1255
+ /** sources 单元 */
1256
+ const src = (service: string, required: boolean): { service: string; required: boolean } => ({ service, required });
1257
+
1258
+ /** E1:module 轴 moduleProvider=joycity(scope = demand 序 [query, payment]) */
1259
+ const E1 = {
1260
+ provider: 'joycity',
1261
+ schemaless: [],
1262
+ unsupplied: [],
1263
+ additionalPropertiesAllowed: false,
1264
+ 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)] },
1274
+ ],
1275
+ };
1276
+
1277
+ /** E2:app 轴 appProvider=joycity(scope = 足迹字母序 [payment, query, vehicle]——序判别 + 首源漂移) */
1278
+ const E2 = {
1279
+ provider: 'joycity',
1280
+ schemaless: ['parking.vehicle'],
1281
+ unsupplied: [],
1282
+ additionalPropertiesAllowed: false,
1283
+ 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)] },
1293
+ ],
1294
+ };
1295
+
1296
+ /** E3:module 轴 moduleProvider=partial(scope=[payment],schemaless + 部分 unsupplied 四合一) */
1297
+ const E3 = { provider: 'partial', schemaless: ['parking.payment'], unsupplied: ['parking.query'], additionalPropertiesAllowed: true, fields: [] };
1298
+
1299
+ /** E4:module 轴 moduleProvider=ghost-x(未知厂商——非 null 空集 + 全 demand 回填,demand 序) */
1300
+ const E4 = { provider: 'ghost-x', schemaless: [], unsupplied: ['parking.query', 'parking.payment'], additionalPropertiesAllowed: true, fields: [] };
1301
+
1302
+ /** E5:实际态(无轴参,wanda;scope = [query, payment]) */
1303
+ const E5 = {
1304
+ provider: 'wanda',
1305
+ schemaless: [],
1306
+ unsupplied: [],
1307
+ additionalPropertiesAllowed: true,
1308
+ 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)] },
1311
+ ],
1312
+ };
1313
+
1314
+ // ── S5a 纯代数(直调——合成 layer 字面量,零 fs)──
1315
+ const reg = (services: Record<string, { configSchema: object | null }>, o?: { unsupplied?: string[] }) =>
1316
+ buildIntegrationFieldRegistry({ provider: 'x', services }, o);
1317
+
1318
+ it('S5a-1..4 R1 标量:双无 enum 首源保真 / enum 同集异序 / 异集 / 混布', () => {
1319
+ const both = reg({
1320
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string', format: 'uri', title: 'T' } } } },
1321
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
1322
+ });
1323
+ expect(both.fields[0].schema).toEqual({ type: 'string', format: 'uri', title: 'T' }); // 首源逐字保真
1324
+ const sameSet = reg({
1325
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['a', 'b'] } } } },
1326
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['b', 'a', 'a'] } } } },
1327
+ });
1328
+ expect(sameSet.fields[0].schema).toEqual({ type: 'string', enum: ['a', 'b'] }); // 序不敏感 + 去重
1329
+ const diffSet = reg({
1330
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['a'] } } } },
1331
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['b'] } } } },
1332
+ });
1333
+ expect(diffSet.fields[0].schema).toBeNull();
1334
+ const mixed = reg({
1335
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string', enum: ['a'] } } } },
1336
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
1337
+ });
1338
+ expect(mixed.fields[0].schema).toBeNull(); // 混布(哨兵 ≠ 集签名)
1339
+ });
1340
+
1341
+ it('S5a-5/6 R2 object:深相等(键序不敏感)→ 首源;异构 → null', () => {
1342
+ const deep = reg({
1343
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'object', properties: { x: { type: 'string' }, y: { type: 'number' } } } } } },
1344
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'object', properties: { y: { type: 'number' }, x: { type: 'string' } } } } } },
1345
+ });
1346
+ expect(deep.fields[0].schema).toEqual({ type: 'object', properties: { x: { type: 'string' }, y: { type: 'number' } } });
1347
+ const hetero = reg({
1348
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'object', properties: { x: { type: 'string' } } } } } },
1349
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'object', properties: { y: { type: 'string' } } } } } },
1350
+ });
1351
+ expect(hetero.fields[0].schema).toBeNull();
1352
+ });
1353
+
1354
+ it('S5a-7 R3 族:type 不等 / integer≠number / type 缺席 / 布尔子 schema 入列 / type 数组序异', () => {
1355
+ const r3 = reg({
1356
+ 'a.one': {
1357
+ configSchema: {
1358
+ type: 'object',
1359
+ properties: {
1360
+ mismatch: { type: 'string' },
1361
+ numeric: { type: 'number' },
1362
+ untypedK: { description: 'x' },
1363
+ boolK: true,
1364
+ typeArr: { type: ['string', 'null'] },
1365
+ objK: { type: 'object', properties: { p: { type: 'string' } } },
1366
+ },
1367
+ },
1368
+ },
1369
+ 'a.two': {
1370
+ configSchema: {
1371
+ type: 'object',
1372
+ properties: {
1373
+ mismatch: { type: 'integer' },
1374
+ numeric: { type: 'integer' },
1375
+ untypedK: { type: 'string' },
1376
+ boolK: { type: 'string' },
1377
+ typeArr: { type: ['null', 'string'] },
1378
+ objK: { type: 'object', properties: { q: { type: 'number' } } },
1379
+ },
1380
+ },
1381
+ },
1382
+ });
1383
+ const byKey = Object.fromEntries(r3.fields.map((f) => [f.key, f]));
1384
+ for (const k of ['mismatch', 'numeric', 'untypedK', 'boolK', 'typeArr', 'objK']) {
1385
+ expect(byKey[k]?.schema, k).toBeNull();
1386
+ }
1387
+ expect(byKey.boolK?.sources).toHaveLength(2); // F1:布尔子 schema 照样入列(key 在、sources 在、schema null)
1388
+ });
1389
+
1390
+ it('S5a-8/9 单源直通 + required 聚合(some;conflict 照常聚合)', () => {
1391
+ 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)] });
1393
+ const some = reg({
1394
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string' } }, required: ['k'] } },
1395
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
1396
+ });
1397
+ expect(some.fields[0].sources).toEqual([src('a.one', true), src('a.two', false)]);
1398
+ const conflictReq = reg({
1399
+ 'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string' } }, required: ['k'] } },
1400
+ 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'integer' } } } },
1401
+ });
1402
+ expect(conflictReq.fields[0].schema).toBeNull();
1403
+ expect(conflictReq.fields[0].sources.some((s) => s.required)).toBe(true);
1404
+ });
1405
+
1406
+ it('S5a-10/11/12 聚合与装配:apAllowed 四态 / schemaless / unsupplied / 空 scope / 布尔整 schema 防御', () => {
1407
+ expect(reg({ 'a.one': { configSchema: { type: 'object', properties: {} } } }).additionalPropertiesAllowed).toBe(true);
1408
+ expect(reg({ 'a.one': { configSchema: { type: 'object', additionalProperties: false } } }).additionalPropertiesAllowed).toBe(false);
1409
+ expect(reg({}).additionalPropertiesAllowed).toBe(true); // 空真
1410
+ // N4:布尔整 schema 防御——不入 schemaless、零参与、fields 空
1411
+ expect(reg({ 'a.one': { configSchema: true as unknown as object } })).toEqual({
1412
+ provider: 'x', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
1413
+ });
1414
+ expect(reg({})).toEqual({ provider: 'x', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true, fields: [] });
1415
+ const asm = reg(
1416
+ { 'a.one': { configSchema: null }, 'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } } },
1417
+ { unsupplied: ['b.x'] },
1418
+ );
1419
+ expect(asm.schemaless).toEqual(['a.one']);
1420
+ expect(asm.unsupplied).toEqual(['b.x']);
1421
+ });
1422
+
1423
+ it('S5a-13 fields 序 = services 迭代序(首声明序 + 首源归序)', () => {
1424
+ const order = reg({
1425
+ 'b.second': { configSchema: { type: 'object', properties: { beta: { type: 'string' }, shared: { type: 'string', format: 'uri' } } } },
1426
+ 'a.first': { configSchema: { type: 'object', properties: { alpha: { type: 'string' }, shared: { type: 'string' } } } },
1427
+ });
1428
+ expect(order.fields.map((f) => f.key)).toEqual(['beta', 'shared', 'alpha']);
1429
+ expect(order.fields[1].sources.map((s) => s.service)).toEqual(['b.second', 'a.first']);
1430
+ expect(order.fields[1].schema).toEqual({ type: 'string', format: 'uri' }); // 首源 = b.second
1431
+ });
1432
+
1433
+ // ── S5b 接线序 + 等价锁 ──
1434
+ it('S5b-1/2/3 接线:假想双轴 registry 逐字(E1/E2/E3)+ 实际态(E5)+ fields 序判别', async () => {
1435
+ const h2 = layerHarness();
1436
+ const modInc = await h2.tk.loadModule('module-parking', {
1437
+ include: ['integrationSchemas', 'integrationFieldRegistry'],
1438
+ moduleProvider: 'joycity',
1439
+ });
1440
+ expect(modInc.integrationFieldRegistry).toEqual(E1);
1441
+ expect(modInc.integrationSchemas).toEqual({
1442
+ provider: 'joycity',
1443
+ services: { 'parking.query': { configSchema: h2.joycity.query }, 'parking.payment': { configSchema: h2.joycity.payment } },
1444
+ });
1445
+ const appInc = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1446
+ expect(appInc.integrationFieldRegistry).toEqual(E2);
1447
+ expect(appInc.integrationFieldRegistry?.fields[0].key).toBe('apiKey'); // 足迹字母序(vs E1 首键 baseUrl)
1448
+ expect(appInc.integrationFieldRegistry?.fields[1].schema).toEqual({ type: 'string' }); // 首源漂移(payment 先)
1449
+ const partial = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: 'partial' });
1450
+ expect(partial.integrationFieldRegistry).toEqual(E3);
1451
+ const actual = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'] });
1452
+ expect(actual.integrationFieldRegistry).toEqual(E5);
1453
+ h2.app.dispose();
1454
+ });
1455
+
1456
+ it('S5b-4 旧 loader 等价锁:loadModuleIntegrationSchemas ≡ loadModuleFormViews.integrationSchemas', async () => {
1457
+ const h2 = layerHarness();
1458
+ const walk = walkManifests(h2.app.appDir);
1459
+ const snap: ViewSnapshot = {
1460
+ appDir: h2.app.appDir,
1461
+ walk,
1462
+ catalog: walk.catalog,
1463
+ integrations: await h2.tk.readIntegrationsConfig(),
1464
+ contracts: await h2.tk.resolveContracts(),
1465
+ };
1466
+ const viaOld = loadModuleIntegrationSchemas(snap, 'module-parking');
1467
+ expect(viaOld).toEqual(loadModuleFormViews(snap, 'module-parking').integrationSchemas);
1468
+ expect(viaOld).toEqual({
1469
+ provider: 'wanda',
1470
+ services: { 'parking.query': { configSchema: h2.schemas.query }, 'parking.payment': { configSchema: h2.schemas.parking } },
1471
+ });
1472
+ h2.app.dispose();
1473
+ });
1474
+
1475
+ // ── S6 同构(状态演进全经 toolkit 写通道——免外部写缓存关注)──
1476
+ it('S6 同构:假想预览 ≡ 真实写入后实读(含跟随应用级 + config 值无关)', async () => {
1477
+ const h2 = layerHarness();
1478
+ const both = ['integrationSchemas', 'integrationFieldRegistry'];
1479
+ // ① 假想 joycity ≡ 写 {provider:'joycity'} 后实读(双投影)
1480
+ const hypJoy = await h2.tk.loadModule('module-parking', { include: both, moduleProvider: 'joycity' });
1481
+ const w1 = await h2.tk.writeModuleIntegration('module-parking', { provider: 'joycity' });
1482
+ expect(w1.written).toBe(true);
1483
+ const actJoy = await h2.tk.loadModule('module-parking', { include: both });
1484
+ expect(actJoy.integrationFieldRegistry).toEqual(hypJoy.integrationFieldRegistry);
1485
+ expect(actJoy.integrationSchemas).toEqual(hypJoy.integrationSchemas);
1486
+ // ② 跟随应用级(root.provider=wanda)≡ 清域(N1 删 domains.parking → root 兜底)后实读——非平凡:清域前实际态 = E1
1487
+ const hypFollow = await h2.tk.loadModule('module-parking', { include: both, moduleProvider: 'wanda' });
1488
+ expect(hypFollow.integrationFieldRegistry).toEqual(E5);
1489
+ await h2.tk.writeModuleIntegration('module-parking', {});
1490
+ const actFollow = await h2.tk.loadModule('module-parking', { include: both });
1491
+ expect(actFollow.integrationFieldRegistry).toEqual(hypFollow.integrationFieldRegistry);
1492
+ // ③ PUT 任意 config 值不改 registry(schema 侧投影与值无关)
1493
+ await h2.tk.writeModuleIntegration('module-parking', { provider: 'joycity', config: { baseUrl: 'https://example' } });
1494
+ const actCfg = await h2.tk.loadModule('module-parking', { include: both });
1495
+ expect(actCfg.integrationFieldRegistry).toEqual(E1);
1496
+ h2.app.dispose();
1497
+ });
1498
+
1499
+ it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(domains/services 不受扰)', async () => {
1500
+ const h2 = layerHarness();
1501
+ const hyp = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1502
+ const cur = await h2.tk.loadApp();
1503
+ const w = await h2.tk.writeAppIntegration({ ...(cur.integration ?? {}), provider: 'joycity' });
1504
+ expect(w.written).toBe(true);
1505
+ const act = await h2.tk.loadApp({ include: ['integrationFieldRegistry'] });
1506
+ expect(act.integrationFieldRegistry).toEqual(hyp.integrationFieldRegistry);
1507
+ expect(act.integrationFieldRegistry).toEqual(E2);
1508
+ // I2:域节点/mirror 不受 root 写入波及
1509
+ expect((await h2.tk.loadModule('module-parking')).integration).toEqual({ provider: 'wanda' });
1510
+ h2.app.dispose();
1511
+ });
1512
+
1513
+ // ── S7 边界 ──
1514
+ it('S7-1/2/3/6 轴参严检:400 矩阵(fields 随参数名)+ 先于 404 + 边界正例 + 未请求缺席', async () => {
1515
+ const h2 = layerHarness();
1516
+ await expect(h2.tk.loadApp({ appProvider: 'wanda' })).rejects.toMatchObject({
1517
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['appProvider'],
1518
+ });
1519
+ await expect(h2.tk.loadApp({ include: ['serviceResolutions'], appProvider: 'wanda' })).rejects.toMatchObject({
1520
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['appProvider'],
1521
+ });
1522
+ await expect(h2.tk.loadApp({ include: [], appProvider: 'x' })).rejects.toMatchObject({ fields: ['appProvider'] });
1523
+ await expect(h2.tk.loadModule('module-parking', { moduleProvider: 'x' })).rejects.toMatchObject({ fields: ['moduleProvider'] });
1524
+ // 先于 404;表单键在场 → 放行至 404
1525
+ await expect(h2.tk.loadModule('ghost', { moduleProvider: 'x' })).rejects.toMatchObject({
1526
+ code: 'VALIDATION_FAILED', httpStatus: 400,
1527
+ });
1528
+ await expect(h2.tk.loadModule('ghost', { include: ['integrationSchemas'], moduleProvider: 'x' })).rejects.toMatchObject({
1529
+ code: 'MODULE_NOT_FOUND', httpStatus: 404,
1530
+ });
1531
+ // 边界正例:仅 integrationSchemas + 轴参 → 200 假想计算;registry 未请求 = 缺席(内嵌三律)
1532
+ const ok = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas'], moduleProvider: 'joycity' });
1533
+ expect(ok.integrationSchemas?.provider).toBe('joycity');
1534
+ expect('integrationFieldRegistry' in ok).toBe(false);
1535
+ h2.app.dispose();
1536
+ });
1537
+
1538
+ it('S7-4 空串 ≡ 缺席;错轴参数静默忽略', async () => {
1539
+ const h2 = layerHarness();
1540
+ const empty = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: '' });
1541
+ const absent = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'] });
1542
+ expect(empty.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
1543
+ expect(empty.integrationFieldRegistry).toEqual(E5);
1544
+ // 错轴参数(变量绕过多余属性检查——运行时按名读取,appProvider 被无视)
1545
+ const opts = { include: ['integrationFieldRegistry'], moduleProvider: 'joycity', appProvider: 'wanda' };
1546
+ const wrongAxis = await h2.tk.loadModule('module-parking', opts);
1547
+ expect(wrongAxis.integrationFieldRegistry).toEqual(E1);
1548
+ h2.app.dispose();
1549
+ });
1550
+
1551
+ it('S7-5 I2:假想态下文件镜像与求值恒实际态', async () => {
1552
+ const h2 = layerHarness();
1553
+ const v = await h2.tk.loadModule('module-parking', {
1554
+ include: ['serviceResolutions', 'integrationSchemas', 'integrationFieldRegistry'],
1555
+ moduleProvider: 'joycity',
1556
+ });
1557
+ expect(v.integration).toEqual({ provider: 'wanda' });
1558
+ expect(v.providerBindings).toEqual([{ domain: 'parking', effective: 'wanda', layer: 'module' }]);
1559
+ expect(v.configEditable).toBe(true);
1560
+ expect(v.integrationFieldRegistry?.provider).toBe('joycity'); // 表单层重定向
1561
+ expect(v.integrationSchemas?.provider).toBe('joycity');
1562
+ expect(v.serviceResolutions?.resolutions['parking.query']?.provider).toBe('wanda'); // 求值恒实际
1563
+ h2.app.dispose();
1564
+ });
1565
+
1566
+ it('S7-7 null 两态:app 未绑定 / 可写未绑定 / D34 三形态(带 override 不豁免)', async () => {
1567
+ // app 未绑定(root.provider 缺席——带空串轴参同此)
1568
+ const h3 = layerHarness();
1569
+ writeAppFile(
1570
+ h3.app.appDir,
1571
+ 'config/integrations.json',
1572
+ JSON.stringify({
1573
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
1574
+ domains: { parking: { provider: 'joycity' } },
1575
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
1576
+ }),
1577
+ );
1578
+ expect((await h3.tk.loadApp({ include: ['integrationFieldRegistry'] })).integrationFieldRegistry).toBeNull();
1579
+ expect(
1580
+ (await h3.tk.loadApp({ include: ['integrationSchemas', 'integrationFieldRegistry'], appProvider: '' })).integrationSchemas,
1581
+ ).toBeNull();
1582
+ h3.app.dispose();
1583
+
1584
+ // 可写未绑定(无 domains 无 root.provider——无轴参 → null)
1585
+ const h4 = layerHarness();
1586
+ writeAppFile(
1587
+ h4.app.appDir,
1588
+ 'config/integrations.json',
1589
+ JSON.stringify({
1590
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
1591
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
1592
+ }),
1593
+ );
1594
+ const unbound = await h4.tk.loadModule('module-parking', { include: ['integrationFieldRegistry', 'integrationSchemas'] });
1595
+ expect(unbound.integrationFieldRegistry).toBeNull();
1596
+ expect(unbound.integrationSchemas).toBeNull();
1597
+ expect(unbound.configEditable).toBe(true);
1598
+ h4.app.dispose();
1599
+
1600
+ // D34 三形态(多域/零域/共享键——带 moduleProvider 仍 null)
1601
+ const d34 = createTempApp('tbox-v46-d34-');
1602
+ invalidateContractsResolver();
1603
+ const store34 = joinStore(d34.appDir, 'contracts');
1604
+ writeFakeContractsPackage(store34, { version: '0.9.0' });
1605
+ linkContracts(d34.appDir, store34, { subdir: 'apps/server' });
1606
+ const need = (service: string): { service: string; optional: boolean } => ({ service, optional: false });
1607
+ const manifest34 = (name: string, services: Array<{ service: string; optional: boolean }>): string =>
1608
+ JSON.stringify({ schemaVersion: 1, name, version: '0.1.0', kind: 'business', contributes: { services }, dependencies: { modules: [] }, env: [] });
1609
+ writeAppFile(d34.appDir, 'packages/module-multi/tbox.module.json', manifest34('module-multi', [need('parking.query'), need('member.account')]));
1610
+ writeAppFile(
1611
+ d34.appDir,
1612
+ 'packages/module-zero/tbox.module.json',
1613
+ JSON.stringify({ schemaVersion: 1, name: 'module-zero', version: '0.1.0', kind: 'business', contributes: {}, dependencies: { modules: [] }, env: [] }),
1614
+ );
1615
+ writeAppFile(d34.appDir, 'packages/module-a/tbox.module.json', manifest34('module-a', [need('parking.alpha')]));
1616
+ writeAppFile(d34.appDir, 'packages/module-b/tbox.module.json', manifest34('module-b', [need('parking.beta')]));
1617
+ const tk34 = createAppToolkit(d34.appDir);
1618
+ for (const id of ['module-multi', 'module-zero', 'module-a']) {
1619
+ const v = await tk34.loadModule(id, { include: ['integrationFieldRegistry', 'integrationSchemas'], moduleProvider: 'joycity' });
1620
+ expect(v.integrationFieldRegistry, id).toBeNull();
1621
+ expect(v.integrationSchemas, id).toBeNull();
1622
+ }
1623
+ d34.dispose();
1624
+ });
1625
+
1626
+ it('S7-8 正例:未绑定 + override → 可计算(预览首次配置);未知厂商 → 非 null 空集 + unsupplied 回填', async () => {
1627
+ const h5 = layerHarness();
1628
+ writeAppFile(
1629
+ h5.app.appDir,
1630
+ 'config/integrations.json',
1631
+ JSON.stringify({
1632
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
1633
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
1634
+ }),
1635
+ );
1636
+ const preview = await h5.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: 'joycity' });
1637
+ expect(preview.integrationFieldRegistry).toEqual(E1); // 无绑定亦可预览(scope = demand ∩ joycity 供给)
1638
+ const appPreview = await h5.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1639
+ expect(appPreview.integrationFieldRegistry).toEqual(E2);
1640
+ const ghost = await h5.tk.loadModule('module-parking', {
1641
+ include: ['integrationFieldRegistry', 'integrationSchemas'],
1642
+ moduleProvider: 'ghost-x',
1643
+ });
1644
+ expect(ghost.integrationFieldRegistry).toEqual(E4);
1645
+ expect(ghost.integrationSchemas).toEqual({ provider: 'ghost-x', services: {} });
1646
+ const ghostApp = await h5.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'ghost-x' });
1647
+ expect(ghostApp.integrationFieldRegistry).toEqual({
1648
+ provider: 'ghost-x', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
1649
+ });
1650
+ h5.app.dispose();
1651
+ });
1652
+
1653
+ it('S7-9 最小正例:mock 零足迹 → 非 null 空集(factory→formViews→registry 全链)', async () => {
1654
+ const h2 = harnessWithContracts();
1655
+ const app = await h2.tk.loadApp({ include: ['integrationFieldRegistry'] });
1656
+ expect(app.integrationFieldRegistry).toEqual({
1657
+ provider: 'mock', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
1658
+ });
1659
+ h2.app.dispose();
1660
+ });
1661
+
1662
+ // ── S8 双投影互证(依赖契约锁——旁路重取 serviceSchemas 破等式即刻红)──
1663
+ it('S8 双投影互证:properties 键集 ≡ sources 反查 + schemaless 等价 + unsupplied 等式', async () => {
1664
+ const h2 = layerHarness();
1665
+ const v = await h2.tk.loadModule('module-parking', {
1666
+ include: ['integrationSchemas', 'integrationFieldRegistry'],
1667
+ moduleProvider: 'joycity',
1668
+ });
1669
+ const layer = v.integrationSchemas!;
1670
+ const registry = v.integrationFieldRegistry!;
1671
+ expect(registry.provider).toBe(layer.provider); // I1:同源同口径
1672
+ for (const [s, entry] of Object.entries(layer.services)) {
1673
+ const cfg = entry.configSchema;
1674
+ if (cfg === null) {
1675
+ expect(registry.schemaless, s).toContain(s);
1676
+ continue;
1677
+ }
1678
+ expect(registry.schemaless, s).not.toContain(s);
1679
+ const c = cfg as Record<string, unknown>;
1680
+ const props = c.properties;
1681
+ const declared = props && typeof props === 'object' && !Array.isArray(props) ? Object.keys(props) : []; // 同款守卫
1682
+ const rev = registry.fields.filter((f) => f.sources.some((x) => x.service === s)).map((f) => f.key);
1683
+ expect([...declared].sort(), s).toEqual([...rev].sort());
1684
+ }
1685
+ for (const s of registry.schemaless) expect(layer.services[s]?.configSchema).toBeNull();
1686
+ // unsupplied ≡ demand − scope 键集
1687
+ const demand = v.services.map((x) => x.service);
1688
+ const scopeKeys = Object.keys(layer.services);
1689
+ expect([...registry.unsupplied].sort()).toEqual(demand.filter((s) => !scopeKeys.includes(s)).sort());
1690
+ h2.app.dispose();
1691
+ });
1692
+
1693
+ it('S8 app 轴:unsupplied ≡ [](结构性:足迹 ⊆ 供给)', async () => {
1694
+ const h2 = layerHarness();
1695
+ const v = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1696
+ expect(v.integrationFieldRegistry?.unsupplied).toEqual([]);
1697
+ h2.app.dispose();
1698
+ });
1699
+ });