@tbox.cn/app-toolkit 0.5.0 → 0.6.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.
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { createAppToolkit, type AppToolkit } from '../src/factory.js';
6
6
  import { invalidateContractsResolver } from '../src/core/contracts-resolver.js';
7
7
  import { walkManifests, type WalkManifestsResult } from '../src/assembly/walk-manifests.js';
8
- import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas, loadServiceFieldRegistry } from '../src/views/integration-schemas.js';
8
+ import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas, loadServiceFieldRegistry, loadServiceIntegrationSchemas } from '../src/views/integration-schemas.js';
9
9
  import { resolveServiceDisplay } from '../src/views/context.js';
10
10
  import type { ViewSnapshot } from '../src/views/context.js';
11
11
  import { URL_METHOD_MAP } from './naming-alignment.test.js';
@@ -240,6 +240,96 @@ function layerHarness(): {
240
240
  return { app, tk: createAppToolkit(app.appDir), schemas, joycity };
241
241
  }
242
242
 
243
+ /** E 系列基座(v4.7 D38 引入;v5.1 D39 提升至模块层共用——原 D38 describe 内定义):wanda 双槽
244
+ * (layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json(O15 形态)+
245
+ * module-parking demand。schema 内容互异防断言串扰:
246
+ * wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
247
+ * 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。
248
+ * 供给矩阵:wanda 供给 {parking.query, parking.payment};joycity 仅供给 {parking.query}
249
+ * (provider-multi 双 impl 皆 query)——D39 用例 (厂商 × 服务) 选型须对照本矩阵。 */
250
+ function eHarness(): {
251
+ app: TempApp;
252
+ tk: AppToolkit;
253
+ schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
254
+ } {
255
+ const app = createTempApp('tbox-d38-');
256
+ invalidateContractsResolver();
257
+ const store = join(app.appDir, 'contracts');
258
+ writeFakeContractsPackage(store, { version: '0.9.0' });
259
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
260
+ const schemas = {
261
+ wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
262
+ wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
263
+ joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
264
+ joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
265
+ ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
266
+ };
267
+ writeAppFile(
268
+ app.appDir,
269
+ 'packages/provider-w/tbox.module.json',
270
+ JSON.stringify({
271
+ schemaVersion: 1,
272
+ id: 'provider-w',
273
+ version: '0.1.0',
274
+ kind: 'business',
275
+ declare: {
276
+ providers: {
277
+ slots: [
278
+ { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
279
+ { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
280
+ ],
281
+ },
282
+ },
283
+ dependencies: { modules: [] },
284
+ env: [],
285
+ }),
286
+ );
287
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
288
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
289
+ writeAppFile(
290
+ app.appDir,
291
+ 'packages/provider-multi/tbox.module.json',
292
+ JSON.stringify({
293
+ schemaVersion: 1,
294
+ id: 'provider-multi',
295
+ version: '0.1.0',
296
+ kind: 'business',
297
+ declare: {
298
+ providers: {
299
+ slots: [
300
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
301
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
302
+ ],
303
+ },
304
+ },
305
+ dependencies: { modules: [] },
306
+ env: [],
307
+ }),
308
+ );
309
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
310
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
311
+ writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
312
+ writeAppFile(
313
+ app.appDir,
314
+ 'packages/module-parking/tbox.module.json',
315
+ JSON.stringify({
316
+ schemaVersion: 1,
317
+ id: 'module-parking',
318
+ version: '0.1.0',
319
+ kind: 'business',
320
+ declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
321
+ dependencies: { modules: [] },
322
+ env: [],
323
+ }),
324
+ );
325
+ return { app, tk: createAppToolkit(app.appDir), schemas };
326
+ }
327
+
328
+ /** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
329
+ function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
330
+ writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
331
+ }
332
+
243
333
  describe('views/工厂(O 矩阵核心族)', () => {
244
334
  let h: Harness;
245
335
 
@@ -267,7 +357,8 @@ describe('views/工厂(O 矩阵核心族)', () => {
267
357
  const mod = await h.tk.loadModule('module-parking');
268
358
  // v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
269
359
  // v7 词汇统一:模块展示名 name 前置(恒发——兜底 id)
270
- expect(Object.keys(mod)).toEqual(['name', 'description', 'ownedServices', 'integration', 'binding', 'configEditable', 'services']);
360
+ // resources 只读镜像(N2 读半边——基座 integrations modules 节无 resources → null 仍恒发)
361
+ expect(Object.keys(mod)).toEqual(['name', 'description', 'ownedServices', 'integration', 'resources', 'binding', 'configEditable', 'services']);
271
362
  const svc = await h.tk.loadService('parking.query');
272
363
  expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
273
364
  // 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
@@ -1022,93 +1113,6 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
1022
1113
  });
1023
1114
 
1024
1115
  describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
1025
- /** E 系列基座:wanda 双槽(layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json
1026
- * (O15 形态)+ module-parking demand。schema 内容互异防断言串扰:
1027
- * wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
1028
- * 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。 */
1029
- function eHarness(): {
1030
- app: TempApp;
1031
- tk: AppToolkit;
1032
- schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
1033
- } {
1034
- const app = createTempApp('tbox-d38-');
1035
- invalidateContractsResolver();
1036
- const store = join(app.appDir, 'contracts');
1037
- writeFakeContractsPackage(store, { version: '0.9.0' });
1038
- linkContracts(app.appDir, store, { subdir: 'apps/server' });
1039
- const schemas = {
1040
- wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
1041
- wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
1042
- joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
1043
- joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
1044
- ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
1045
- };
1046
- writeAppFile(
1047
- app.appDir,
1048
- 'packages/provider-w/tbox.module.json',
1049
- JSON.stringify({
1050
- schemaVersion: 1,
1051
- id: 'provider-w',
1052
- version: '0.1.0',
1053
- kind: 'business',
1054
- declare: {
1055
- providers: {
1056
- slots: [
1057
- { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
1058
- { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
1059
- ],
1060
- },
1061
- },
1062
- dependencies: { modules: [] },
1063
- env: [],
1064
- }),
1065
- );
1066
- writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
1067
- writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
1068
- writeAppFile(
1069
- app.appDir,
1070
- 'packages/provider-multi/tbox.module.json',
1071
- JSON.stringify({
1072
- schemaVersion: 1,
1073
- id: 'provider-multi',
1074
- version: '0.1.0',
1075
- kind: 'business',
1076
- declare: {
1077
- providers: {
1078
- slots: [
1079
- { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
1080
- { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
1081
- ],
1082
- },
1083
- },
1084
- dependencies: { modules: [] },
1085
- env: [],
1086
- }),
1087
- );
1088
- writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
1089
- writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
1090
- writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
1091
- writeAppFile(
1092
- app.appDir,
1093
- 'packages/module-parking/tbox.module.json',
1094
- JSON.stringify({
1095
- schemaVersion: 1,
1096
- id: 'module-parking',
1097
- version: '0.1.0',
1098
- kind: 'business',
1099
- declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
1100
- dependencies: { modules: [] },
1101
- env: [],
1102
- }),
1103
- );
1104
- return { app, tk: createAppToolkit(app.appDir), schemas };
1105
- }
1106
-
1107
- /** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
1108
- function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
1109
- writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
1110
- }
1111
-
1112
1116
  it('E1 显式 string + catalog schema(分支②)', async () => {
1113
1117
  const h = eHarness();
1114
1118
  writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
@@ -1287,6 +1291,170 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
1287
1291
  });
1288
1292
  });
1289
1293
 
1294
+ /**
1295
+ * v5.1 D39 测试族(服务轴向假想绑定——?serviceProvider= 表单双投影重定向)。
1296
+ * 供给矩阵(eHarness):wanda 供给 {parking.query, parking.payment};joycity 仅供给
1297
+ * {parking.query}(provider-multi 双 impl 皆 query)——用例 (厂商 × 服务) 选型须对照本矩阵:
1298
+ * catalog-miss 形态(D39-7)与豁免链正例(D39-6)不得混淆供给边界。每用例独立 eHarness
1299
+ * (零 file-cache stat 指纹串扰);D39-8/D39-9 用 layerHarness(x-tbox-global 标记 fixture)。
1300
+ */
1301
+ describe('v5.1 D39(服务轴向假想绑定 serviceProvider)', () => {
1302
+ it('D39-1 假想 happy + I1:双投影切 joycity 且 provider 一致 + 单键边界正例', async () => {
1303
+ const h = eHarness();
1304
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1305
+ const both = ['integrationSchemas', 'integrationFieldRegistry'];
1306
+ const v = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
1307
+ // I1:双投影同源同口径(fields = joycity schema 字段——catalog 复合键声明序首个 = @1 的 joycityUrl)
1308
+ expect(v.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
1309
+ expect(v.integrationFieldRegistry?.provider).toBe('joycity');
1310
+ expect(v.integrationFieldRegistry!.fields.map((f) => f.key)).toEqual(['joycityUrl']);
1311
+ // 单键边界正例:仅 integrationSchemas + 轴参 → 200 假想计算;registry 未请求 = 缺席(S7-1 尾部先例)
1312
+ const only = await h.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
1313
+ expect(only.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
1314
+ expect('integrationFieldRegistry' in only).toBe(false);
1315
+ h.app.dispose();
1316
+ });
1317
+
1318
+ it('D39-2 I2 锁:integration/suppliedBy/defaults 恒实际态', async () => {
1319
+ const h = eHarness();
1320
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda', config: { baseUrl: 'https://w.example' } } } });
1321
+ const base = await h.tk.loadService('parking.query');
1322
+ const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'joycity' });
1323
+ expect(v.integration).toEqual(base.integration); // 文件逐字原值不被重定向(预览不产生「已保存」误导)
1324
+ expect(v.suppliedBy).toEqual(base.suppliedBy); // catalog 静态切片恒实际
1325
+ expect(v.defaults).toEqual(base.defaults); // manifest 预填恒实际
1326
+ expect(v.integrationSchemas?.provider).toBe('joycity'); // 对照:表单层已重定向
1327
+ h.app.dispose();
1328
+ });
1329
+
1330
+ it('D39-3 I3 锁 + 错轴静默忽略:空串 ≡ 缺席 ≡ 实际;appProvider/moduleProvider 被无视', async () => {
1331
+ const h = eHarness();
1332
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1333
+ const empty = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: '' });
1334
+ const absent = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'] });
1335
+ expect(empty.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
1336
+ // 错轴参数(变量绕过多余属性检查——运行时按名读取,appProvider/moduleProvider 被无视;S7-4 先例镜像)
1337
+ const opts = { include: ['integrationFieldRegistry'], serviceProvider: '', appProvider: 'joycity', moduleProvider: 'joycity' };
1338
+ const wrongAxis = await h.tk.loadService('parking.query', opts);
1339
+ expect(wrongAxis.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
1340
+ h.app.dispose();
1341
+ });
1342
+
1343
+ it('D39-4 轴参严检矩阵:400 先于 404(镜像 S7-1 形态)', async () => {
1344
+ const h = eHarness();
1345
+ await expect(h.tk.loadService('parking.query', { serviceProvider: 'x' })).rejects.toMatchObject({
1346
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
1347
+ });
1348
+ await expect(h.tk.loadService('parking.query', { include: [], serviceProvider: 'x' })).rejects.toMatchObject({
1349
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
1350
+ });
1351
+ // 先于 404:ghost 服务 + 无表单键轴参 → 400(非 SERVICE_NOT_FOUND);表单键在场 → 放行至 404
1352
+ await expect(h.tk.loadService('ghost', { serviceProvider: 'x' })).rejects.toMatchObject({
1353
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
1354
+ });
1355
+ await expect(h.tk.loadService('ghost', { include: ['integrationSchemas'], serviceProvider: 'x' })).rejects.toMatchObject({
1356
+ code: 'SERVICE_NOT_FOUND', httpStatus: 404,
1357
+ });
1358
+ h.app.dispose();
1359
+ });
1360
+
1361
+ it('D39-5 同构承诺锁:假想 ≡ PUT 改写 provider 后实读(双投影;S6 写通道先例)', async () => {
1362
+ const h = eHarness();
1363
+ const both = ['integrationSchemas', 'integrationFieldRegistry'];
1364
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1365
+ const hyp = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
1366
+ // 状态演进全经 toolkit 写通道——写后本实例缓存即刻失效,免外部写缓存关注
1367
+ const cur = await h.tk.loadService('parking.query');
1368
+ const w = await h.tk.writeServiceIntegration('parking.query', { ...cur.integration!, provider: 'joycity' });
1369
+ expect(w.written).toBe(true);
1370
+ const act = await h.tk.loadService('parking.query', { include: both });
1371
+ expect(act.integrationSchemas).toEqual(hyp.integrationSchemas);
1372
+ expect(act.integrationFieldRegistry).toEqual(hyp.integrationFieldRegistry);
1373
+ h.app.dispose();
1374
+ });
1375
+
1376
+ it('D39-6 假想豁免链三分支:malformed 不污染 / 内联不读取无 H9 / 未绑定仍非 null', async () => {
1377
+ // ① malformed 节点(E14 形态)+ 假想 → 非 null(短路在节点读取之前——坏形态只影响实际态投影)
1378
+ const h = eHarness();
1379
+ writeIntegrations(h.app.appDir, {
1380
+ modules: { 'module-parking': { provider: 'wanda' } },
1381
+ services: { 'parking.query': { provider: { provider: 'my-api' } } },
1382
+ });
1383
+ const bad = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
1384
+ expect(bad.integrationFieldRegistry).not.toBeNull();
1385
+ expect(bad.integrationFieldRegistry!.provider).toBe('joycity');
1386
+ // 实际态对照:同坏节点无轴参 → 整字段 null(E14 双态互证——预览与坏文件解耦)
1387
+ const badActual = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1388
+ expect(badActual.integrationSchemas).toBeNull();
1389
+ h.app.dispose();
1390
+ // ② 内联 ref 逃逸节点(E4 形态)+ 假想 → 无 H9 400(短路不读节点内联 ref——换厂商即离开内联)
1391
+ const h2 = eHarness();
1392
+ writeIntegrations(h2.app.appDir, {
1393
+ services: { 'parking.query': { provider: { provider: 'my-api', credentialType: '-', configSchema: 'config/schemas-evil/x.json' } } },
1394
+ });
1395
+ const inline = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
1396
+ expect(inline.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h2.schemas.joycity1 });
1397
+ h2.app.dispose();
1398
+ // ③ 全链未绑定(E8 形态)+ 假想 → 仍非 null(服务轴恒可写——无 module 轴零归属豁免形态);
1399
+ // wanda 供给 payment(供给矩阵)→ mallId 字段在场
1400
+ const h3 = eHarness();
1401
+ writeIntegrations(h3.app.appDir, { services: { 'parking.payment': {} } });
1402
+ const unbound = await h3.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
1403
+ // fields[].schema = 字段子 schema(非整文件 schema——G 批锁 :1777 同型)
1404
+ expect(unbound.integrationFieldRegistry).toEqual({
1405
+ provider: 'wanda', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true,
1406
+ fields: [{ key: 'mallId', schema: { type: 'string' }, sources: [{ service: 'parking.payment', required: false }], global: false }],
1407
+ });
1408
+ h3.app.dispose();
1409
+ });
1410
+
1411
+ it('D39-7 catalog miss:假想未知厂商 → 非 null 空集(schemaless=[service])', async () => {
1412
+ const h = eHarness();
1413
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1414
+ const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'ghost-vendor' });
1415
+ expect(v.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
1416
+ expect(v.integrationFieldRegistry).toEqual({
1417
+ provider: 'ghost-vendor', schemaless: ['parking.query'], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
1418
+ });
1419
+ h.app.dispose();
1420
+ });
1421
+
1422
+ it('D39-8 global 旗标假想透传:标记位如实(layerHarness——true 正例 + 未标记 false 不过滤)', async () => {
1423
+ const h = layerHarness();
1424
+ // joycity 供给 parking.query → 假想后 fields = joycity query schema 字段(fixture 全标记 → global:true)
1425
+ const joy = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
1426
+ expect(joy.integrationFieldRegistry!.provider).toBe('joycity');
1427
+ expect(joy.integrationFieldRegistry!.fields.length).toBeGreaterThan(0);
1428
+ expect(joy.integrationFieldRegistry!.fields.every((f) => f.global)).toBe(true);
1429
+ // wanda 供给 parking.payment → mallId global:true / parkLotId global:false 在场(服务轴不过滤——G 批语义假想不变);
1430
+ // payment 先配置入词汇(P9:契约包 ∪ 已配置——layerHarness 初始 integrations.json 未配置 payment;
1431
+ // 覆盖写后 payment 节点 {} 实际态级联双缺 → null,假想 wanda 短路仍非 null——豁免链③同型,S7-7 外部写先例)
1432
+ writeAppFile(h.app.appDir, 'config/integrations.json', JSON.stringify({ services: { 'parking.payment': {} } }));
1433
+ const wan = await h.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
1434
+ expect(wan.integrationFieldRegistry!.fields.map((f) => f.key).sort()).toEqual(['mallId', 'parkLotId']);
1435
+ expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'mallId')!.global).toBe(true);
1436
+ expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'parkLotId')!.global).toBe(false);
1437
+ h.app.dispose();
1438
+ });
1439
+
1440
+ it('D39-9 单源锁:loadServiceFieldRegistry(snap,s,X) ≡ buildIntegrationFieldRegistry over 链函数', async () => {
1441
+ const h = layerHarness();
1442
+ const { loadViewSnapshotSync } = await import('../src/views/context.js');
1443
+ const snap = loadViewSnapshotSync({ appDir: h.app.appDir }, await h.tk.resolveContracts());
1444
+ const X = 'joycity';
1445
+ const reg = loadServiceFieldRegistry(snap, 'parking.query', X);
1446
+ const schemas = loadServiceIntegrationSchemas(snap, 'parking.query', X);
1447
+ expect(schemas).not.toBeNull();
1448
+ // 结构性断言(依赖契约锁——registry 旁路重取 schema 破 I1 即刻红;S8 同族意图)
1449
+ const manual = buildIntegrationFieldRegistry({
1450
+ provider: schemas!.provider,
1451
+ services: { 'parking.query': { configSchema: schemas!.configSchema } },
1452
+ });
1453
+ expect(reg).toEqual(manual);
1454
+ h.app.dispose();
1455
+ });
1456
+ });
1457
+
1290
1458
  /**
1291
1459
  * v4.6 D36/D37 测试族(S5 接续 S1-S4):S5a 纯代数(直调合成 layer 字面量,零 fs)/
1292
1460
  * S5b 接线序 + 等价锁 / S6 同构(假想 ≡ 写后实读)/ S7 边界(400·null 两态·I2·空集)/
@@ -1891,6 +2059,46 @@ describe('B1 展示名链', () => {
1891
2059
  h2.app.dispose();
1892
2060
  });
1893
2061
 
2062
+ it('T2b loadModulesView:hidden 模块默认过滤;includeHidden 可见 + 徽标;detail 恒 200 不滤', async () => {
2063
+ const app = createTempApp('tbox-hidden-');
2064
+ invalidateContractsResolver();
2065
+ const store = joinStore(app.appDir, 'contracts');
2066
+ writeFakeContractsPackage(store, { version: '0.9.0' });
2067
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
2068
+ const manifest = (id: string, name: string, extra: Record<string, unknown> = {}): string =>
2069
+ JSON.stringify({
2070
+ schemaVersion: 1,
2071
+ id,
2072
+ version: '0.1.0',
2073
+ kind: 'business',
2074
+ name,
2075
+ declare: { owns: [], consumes: [] },
2076
+ dependencies: { modules: [] },
2077
+ env: [],
2078
+ ...extra,
2079
+ });
2080
+ mkdirSync(join(app.appDir, 'packages', 'module-visible'), { recursive: true });
2081
+ writeAppFile(app.appDir, 'packages/module-visible/tbox.module.json', manifest('module-visible', '可见模块'));
2082
+ mkdirSync(join(app.appDir, 'packages', 'module-secret'), { recursive: true });
2083
+ writeAppFile(app.appDir, 'packages/module-secret/tbox.module.json', manifest('module-secret', '隐藏模块', { hidden: true }));
2084
+
2085
+ const tk = createAppToolkit(app.appDir);
2086
+ // 默认:hidden 模块不进清单(可见模块无 hidden 键——additive 纪律)
2087
+ const def = await tk.loadModules();
2088
+ expect(def.modules.map((m) => m.id)).toEqual(['module-visible']);
2089
+ expect(def.modules[0]).not.toHaveProperty('hidden');
2090
+ // includeHidden = true:可见并携带 hidden: true 徽标
2091
+ const inc = await tk.loadModules({ includeHidden: true });
2092
+ expect(inc.modules.map((m) => m.id).sort()).toEqual(['module-secret', 'module-visible']);
2093
+ expect(inc.modules.find((m) => m.id === 'module-secret')?.hidden).toBe(true);
2094
+ expect(inc.modules.find((m) => m.id === 'module-visible')).not.toHaveProperty('hidden');
2095
+ // detail 端点恒 200 不滤——视图携带 hidden 徽标
2096
+ const detail = await tk.loadModule('module-secret');
2097
+ expect(detail.name).toBe('隐藏模块');
2098
+ expect(detail.hidden).toBe(true);
2099
+ app.dispose();
2100
+ });
2101
+
1894
2102
  it('T3 loadModuleDetailView:meta 恒发;services[] 词汇优先(demand 车位查询被词汇停车查询压过——防漂移锁)', async () => {
1895
2103
  const h2 = displayHarness();
1896
2104
  const v = await h2.tk.loadModule('module-parking');
@@ -368,3 +368,37 @@ describe('写核心管线(FX-1c)', () => {
368
368
  expect(err.fields).toEqual(['a.b']);
369
369
  });
370
370
  });
371
+
372
+ describe('孤儿凭据写后对账(PUT 路径追平——finalize 单点)', () => {
373
+ let h: Harness;
374
+ beforeEach(() => {
375
+ h = harness();
376
+ });
377
+ afterEach(() => {
378
+ h.app.dispose();
379
+ });
380
+
381
+ it('P1 PUT 替换弃 credentialRef → ORPHANED_CREDENTIAL warning(message 含被弃 stem)', async () => {
382
+ writeAppFile(h.app.appDir, 'config/credentials/wd-parking.json', JSON.stringify({ type: 'wanda-c', k: 'v' }));
383
+ await h.tk.writeServiceIntegration('parking.query', {
384
+ provider: 'wanda', implementation: 'w@1', credentialRef: 'secret://wd-parking',
385
+ } as never);
386
+ const r = await h.tk.writeServiceIntegration('parking.query', {} as never);
387
+ expect(r.written).toBe(true);
388
+ const orphan = r.issues.find((i) => i.code === 'ORPHANED_CREDENTIAL');
389
+ expect(orphan).toBeTruthy();
390
+ expect(orphan?.message).toContain('wd-parking');
391
+ });
392
+
393
+ it('P2 真写报全量孤儿 / dryRun 不报 / no-diff 不报', async () => {
394
+ writeAppFile(h.app.appDir, 'config/credentials/stray.json', JSON.stringify({ type: 'x', k: 'v' }));
395
+ const node = { provider: 'wanda', implementation: 'w@1' } as never;
396
+ const dry = await h.tk.writeServiceIntegration('parking.query', node, { dryRun: true });
397
+ expect(dry.issues.some((i) => i.code === 'ORPHANED_CREDENTIAL')).toBe(false);
398
+ const real = await h.tk.writeServiceIntegration('parking.query', node);
399
+ expect(real.issues.some((i) => i.code === 'ORPHANED_CREDENTIAL' && i.message.includes('stray'))).toBe(true);
400
+ const nodiff = await h.tk.writeServiceIntegration('parking.query', node);
401
+ expect(nodiff.written).toBe(false);
402
+ expect(nodiff.issues.some((i) => i.code === 'ORPHANED_CREDENTIAL')).toBe(false);
403
+ });
404
+ });