@tbox.cn/app-toolkit 0.2.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.
@@ -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
 
@@ -76,6 +79,159 @@ function mkdirDeep(dir: string): void {
76
79
  mkdirSync(dir, { recursive: true });
77
80
  }
78
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
+
79
235
  describe('views/工厂(O 矩阵核心族)', () => {
80
236
  let h: Harness;
81
237
 
@@ -200,15 +356,15 @@ describe('views/工厂(O 矩阵核心族)', () => {
200
356
  expect(reread.integration).toEqual(node);
201
357
  });
202
358
 
203
- it('no-diff 深度等价 → written:false + restartScheduled:false;dryRun files []', async () => {
359
+ it('no-diff 深度等价 → written:false + restartScheduled:false;dryRun+diff files = 将触碰预览(D35)', async () => {
204
360
  const svc = await h.tk.loadService('parking.query');
205
361
  const result = await h.tk.writeServiceIntegration('parking.query', svc.integration ?? {});
206
362
  expect(result.written).toBe(false);
207
363
  expect(result.restartScheduled).toBe(false);
208
- // dryRun:复读管线不落盘
364
+ // dryRun:复读管线不落盘;body({provider:'mock'})与现值(inline custom API)实为 diff → files 预览
209
365
  const dry = await h.tk.writeServiceIntegration('parking.query', { provider: 'mock' }, { dryRun: true });
210
366
  expect(dry.written).toBe(false);
211
- expect(dry.files).toEqual([]);
367
+ expect(dry.files).toEqual(['config/integrations.json']);
212
368
  });
213
369
 
214
370
  it('写时自动初始化(R13 语义翻转提示)+ N1 清域(模块 {} 删域键)', async () => {
@@ -598,75 +754,6 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
598
754
  });
599
755
 
600
756
  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
757
  it('I1 include 正例:app/module 双键内嵌 + 未请求缺席(内嵌三律)+ issues 恒全量', async () => {
671
758
  const h2 = harnessWithContracts();
672
759
  const appInc = await h2.tk.loadApp({ include: ['serviceResolutions', 'integrationSchemas'] });
@@ -829,3 +916,784 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
829
916
  app.dispose();
830
917
  });
831
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
+ });