@tbox.cn/app-toolkit 0.5.0 → 0.7.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';
@@ -24,7 +24,7 @@ interface Harness {
24
24
  }
25
25
 
26
26
  function harnessWithContracts(
27
- opts: { version?: string; withStrict?: boolean; missingEvaluationExports?: boolean } = {},
27
+ opts: { version?: string; withStrict?: boolean; missingEvaluationExports?: boolean; resolveBody?: string[] } = {},
28
28
  ): Harness {
29
29
  const app = createTempApp('tbox-views-');
30
30
  invalidateContractsResolver();
@@ -33,6 +33,7 @@ function harnessWithContracts(
33
33
  version: opts.version ?? '0.9.0',
34
34
  withStrict: opts.withStrict ?? true,
35
35
  missingEvaluationExports: opts.missingEvaluationExports,
36
+ ...(opts.resolveBody ? { resolveBody: opts.resolveBody.join(' ') } : {}),
36
37
  });
37
38
  linkContracts(app.appDir, store, { subdir: 'apps/server' });
38
39
  // 应用结构:parking 模块(packages/ 约定发现)+ 注册表两实例 + integrations.json
@@ -240,6 +241,96 @@ function layerHarness(): {
240
241
  return { app, tk: createAppToolkit(app.appDir), schemas, joycity };
241
242
  }
242
243
 
244
+ /** E 系列基座(v4.7 D38 引入;v5.1 D39 提升至模块层共用——原 D38 describe 内定义):wanda 双槽
245
+ * (layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json(O15 形态)+
246
+ * module-parking demand。schema 内容互异防断言串扰:
247
+ * wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
248
+ * 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。
249
+ * 供给矩阵:wanda 供给 {parking.query, parking.payment};joycity 仅供给 {parking.query}
250
+ * (provider-multi 双 impl 皆 query)——D39 用例 (厂商 × 服务) 选型须对照本矩阵。 */
251
+ function eHarness(): {
252
+ app: TempApp;
253
+ tk: AppToolkit;
254
+ schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
255
+ } {
256
+ const app = createTempApp('tbox-d38-');
257
+ invalidateContractsResolver();
258
+ const store = join(app.appDir, 'contracts');
259
+ writeFakeContractsPackage(store, { version: '0.9.0' });
260
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
261
+ const schemas = {
262
+ wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
263
+ wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
264
+ joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
265
+ joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
266
+ ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
267
+ };
268
+ writeAppFile(
269
+ app.appDir,
270
+ 'packages/provider-w/tbox.module.json',
271
+ JSON.stringify({
272
+ schemaVersion: 1,
273
+ id: 'provider-w',
274
+ version: '0.1.0',
275
+ kind: 'business',
276
+ declare: {
277
+ providers: {
278
+ slots: [
279
+ { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
280
+ { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
281
+ ],
282
+ },
283
+ },
284
+ dependencies: { modules: [] },
285
+ env: [],
286
+ }),
287
+ );
288
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
289
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
290
+ writeAppFile(
291
+ app.appDir,
292
+ 'packages/provider-multi/tbox.module.json',
293
+ JSON.stringify({
294
+ schemaVersion: 1,
295
+ id: 'provider-multi',
296
+ version: '0.1.0',
297
+ kind: 'business',
298
+ declare: {
299
+ providers: {
300
+ slots: [
301
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
302
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
303
+ ],
304
+ },
305
+ },
306
+ dependencies: { modules: [] },
307
+ env: [],
308
+ }),
309
+ );
310
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
311
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
312
+ writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
313
+ writeAppFile(
314
+ app.appDir,
315
+ 'packages/module-parking/tbox.module.json',
316
+ JSON.stringify({
317
+ schemaVersion: 1,
318
+ id: 'module-parking',
319
+ version: '0.1.0',
320
+ kind: 'business',
321
+ declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
322
+ dependencies: { modules: [] },
323
+ env: [],
324
+ }),
325
+ );
326
+ return { app, tk: createAppToolkit(app.appDir), schemas };
327
+ }
328
+
329
+ /** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
330
+ function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
331
+ writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
332
+ }
333
+
243
334
  describe('views/工厂(O 矩阵核心族)', () => {
244
335
  let h: Harness;
245
336
 
@@ -267,7 +358,8 @@ describe('views/工厂(O 矩阵核心族)', () => {
267
358
  const mod = await h.tk.loadModule('module-parking');
268
359
  // v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
269
360
  // v7 词汇统一:模块展示名 name 前置(恒发——兜底 id)
270
- expect(Object.keys(mod)).toEqual(['name', 'description', 'ownedServices', 'integration', 'binding', 'configEditable', 'services']);
361
+ // resources 只读镜像(N2 读半边——基座 integrations modules 节无 resources → null 仍恒发)
362
+ expect(Object.keys(mod)).toEqual(['name', 'description', 'ownedServices', 'integration', 'resources', 'binding', 'configEditable', 'services']);
271
363
  const svc = await h.tk.loadService('parking.query');
272
364
  expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
273
365
  // 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
@@ -287,6 +379,41 @@ describe('views/工厂(O 矩阵核心族)', () => {
287
379
  expect(single.resolution.inheritance).toHaveLength(4);
288
380
  });
289
381
 
382
+ it('O17 v6 ownerOf 视图轴透传(回归锁——fef9a71a 漏改点):求值单源带 walk.ownership 第 5 参;modules 级 config 不触发 fail-visible', async () => {
383
+ // 假 contracts 复刻 v6 fail-visible 语义 + 回显 ownerOf:缺席 + modules 绑定面 → binding-unresolved;
384
+ // 在场 → effective.provider 前缀回显 owner 模块 id(证明透传值 = walk.ownership 内容本身)
385
+ const ownerEcho = harnessWithContracts({
386
+ resolveBody: [
387
+ 'const modBinding = integrations && integrations.modules',
388
+ ' && Object.values(integrations.modules).some((m) => m.provider != null || m.config != null);',
389
+ 'if ((ownerOf === undefined || ownerOf === null) && modBinding) return { status: "binding-unresolved", message: "OWNEROF_MISSING" };',
390
+ 'const owner = ownerOf ? ownerOf[service] : undefined;',
391
+ 'const slot = integrations && integrations.services && integrations.services[service];',
392
+ 'const prov = (slot && slot.provider) || "root-mock";',
393
+ 'return { status: "ok", effective: { provider: owner ? owner + ":" + prov : prov, implementation: "impl@1", config: {} } };',
394
+ ],
395
+ });
396
+ // 触发形态(bug 实景):modules 仅 config(无 provider)+ 空 slot——依赖 ownerOf 注入才能过 fail-visible
397
+ writeAppFile(
398
+ ownerEcho.app.appDir,
399
+ 'config/integrations.json',
400
+ JSON.stringify({
401
+ provider: 'root-mock',
402
+ modules: { 'module-parking': { config: { parkName: '示例车场' } } },
403
+ services: { 'parking.query': {}, 'auth.alipay-login': {} },
404
+ }),
405
+ );
406
+ const single = await ownerEcho.tk.loadServiceResolution('parking.query');
407
+ // ① fail-visible 未触发(ownerOf 已注入)
408
+ expect(single.resolution.status).toBe('ok');
409
+ // ② 透传值 = walk.ownership 内容(parking.query 归属 module-parking——harness manifest declare.owns)
410
+ expect(single.resolution.provider).toBe('module-parking:root-mock');
411
+ // ③ 无主服务回退无前缀(排除「任意 truthy 值即绿」的假阳性)
412
+ const orphan = await ownerEcho.tk.loadServiceResolution('auth.alipay-login');
413
+ expect(orphan.resolution.provider).toBe('root-mock');
414
+ ownerEcho.app.dispose();
415
+ });
416
+
290
417
  it('O3 键集 = 服务词汇 ∪ 已配置;required echo(OR 聚合 + 平台 false)', async () => {
291
418
  // vocabulary 空(无契约包 fixture 物化 slots——退化态回归锁;词汇在场态见 B1 族 T-O3)
292
419
  const batch = await h.tk.loadServiceResolutions();
@@ -302,28 +429,29 @@ describe('views/工厂(O 矩阵核心族)', () => {
302
429
  expect(batch.issues.length).toBeGreaterThanOrEqual(single.issues.length);
303
430
  });
304
431
 
305
- it('O13 门控正反例:<0.9 求值 503 三分支 + 静态 200;多实例无显式默认 → 通配口径', async () => {
432
+ it('O13 门控正反例:<0.9 读求值轴降级骨架(v7 503 退役)+ 静态 200 + 写端点 503 保留;多实例无显式默认 → 通配口径', async () => {
306
433
  // 正例:≥0.9 求值可用(harness 默认)
307
434
  const batch = await h.tk.loadServiceResolutions();
308
435
  expect(batch.contracts.strictValidation).toBe(true);
309
436
  h.app.dispose();
310
437
 
311
- // 反例:缺 zod 面(0.8.2)→ 求值 throw 503 + message 升级指引;静态照常
438
+ // 反例:缺 zod 面(0.8.2)→ 读求值轴降级骨架 + N1 升级指引入 statusMessage;静态照常
312
439
  const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
313
- await expect(low.tk.loadServiceResolutions()).rejects.toMatchObject({
314
- code: 'CONTRACTS_NOT_RESOLVED',
315
- httpStatus: 503,
316
- });
317
- await expect(low.tk.loadServiceResolutions()).rejects.toThrow(/升级应用 @tbox.cn\/app-contracts/);
440
+ const lowBatch = await low.tk.loadServiceResolutions();
441
+ expect(lowBatch.contracts.strictValidation).toBe(false);
442
+ const lowEntries = Object.values(lowBatch.resolutions);
443
+ expect(lowEntries.length).toBeGreaterThan(0);
444
+ expect(lowEntries.every((e) => e.status === 'evaluation-unavailable')).toBe(true);
445
+ expect(lowEntries[0]?.statusMessage).toMatch(/升级应用 @tbox\.cn\/app-contracts/);
446
+ expect(lowBatch.issues).toEqual([]);
318
447
  // 静态永不 503
319
448
  const appView = await low.tk.loadApp();
320
449
  expect(appView.services).toBeInstanceOf(Array);
321
- // 写端点同样门控
450
+ // 写端点 503 门控保留(policy at edge 收窄至写/迁移面)
322
451
  await expect(
323
452
  low.tk.writeServiceIntegration('parking.query', { provider: 'mock' }),
324
453
  ).rejects.toMatchObject({ code: 'CONTRACTS_NOT_RESOLVED', httpStatus: 503 });
325
454
  low.app.dispose();
326
-
327
455
  // 默认实例委托:无 defaultInstanceId(多实例)→ '*' 通配口径(normalizeInstances null → '*')
328
456
  const wild = harnessWithContracts();
329
457
  writeAppFile(
@@ -607,41 +735,102 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
607
735
  });
608
736
 
609
737
  describe('A2 错误面(P-4 notes 优先 / P-5 畸形包 503)', () => {
610
- it('M1 导入失败 message 含构建指引且不再误报未安装(P-4 notes 优先)', async () => {
738
+ it('M1 导入失败 message 含构建指引且不再误报未安装(P-4 notes 优先——v7 降级骨架 statusMessage 承载)', async () => {
611
739
  const app = createTempApp('tbox-m1-');
612
740
  invalidateContractsResolver();
613
741
  const store = joinStore(app.appDir, 'contracts');
614
742
  writeFakeContractsPackage(store, { version: '0.9.0', brokenDist: true });
615
743
  linkContracts(app.appDir, store, { subdir: 'apps/server' });
744
+ writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
745
+ const tk = createAppToolkit(app.appDir);
746
+ const batch = await tk.loadServiceResolutions();
747
+ const entry = batch.resolutions['parking.query'];
748
+ expect(entry.status).toBe('evaluation-unavailable');
749
+ expect(entry.statusMessage).toMatch(/构建产物导入失败/);
750
+ expect(entry.statusMessage).not.toMatch(/未安装/);
751
+ app.dispose();
752
+ });
753
+
754
+ it('M4 无契约包(未安装)→ 降级骨架 + 声明实例枚举 + T2 空键集诚实态(问题②主形态)', async () => {
755
+ // T1 形态:contracts 未安装,walk 静态面在场(manifest demand/owns + integrations 配置)
756
+ const app = createTempApp('tbox-m4-');
757
+ invalidateContractsResolver();
758
+ writeAppFile(app.appDir, 'packages/module-parking/tbox.module.json', JSON.stringify({
759
+ schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
760
+ declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
761
+ dependencies: { modules: [] },
762
+ env: [],
763
+ }));
764
+ writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({
765
+ provider: 'mock', instances: [{ id: 'mall-1' }], defaultInstanceId: 'mall-1',
766
+ services: { 'parking.query': { instances: { 'mall-1': { provider: 'wanda', implementation: 'wanda-parking@1' } } } },
767
+ }));
616
768
  const tk = createAppToolkit(app.appDir);
617
- await expect(tk.loadServiceResolutions()).rejects.toThrow(/构建产物导入失败/);
618
- await expect(tk.loadServiceResolutions()).rejects.not.toThrow(/未安装/);
769
+ const batch = await tk.loadServiceResolutions();
770
+ const entry = batch.resolutions['parking.query'];
771
+ expect(entry.status).toBe('evaluation-unavailable');
772
+ expect(entry.statusMessage).toMatch(/未安装/);
773
+ // 静态可算字段:module = demand owners 首(与求值轴同源);required = OR 聚合
774
+ expect(entry.module).toBe('@app/module-parking');
775
+ expect(entry.required).toBe(true);
776
+ expect(batch.issues).toEqual([]);
777
+ // 单体:声明实例枚举(原始数据投影——root instances[].id)逐实例同标记;
778
+ // inheritance 四层静态投影(app=root.provider;instance=显式 defaultInstanceId 位声明——降级默认实例口径实证)
779
+ const detail = await tk.loadServiceResolution('parking.query');
780
+ expect(detail.resolution.instances.map((i) => i.instanceId)).toEqual(['mall-1']);
781
+ expect(detail.resolution.instances.every((i) => i.status === 'evaluation-unavailable')).toBe(true);
782
+ expect(detail.resolution.inheritance).toHaveLength(4);
783
+ expect(detail.resolution.inheritance.find((l) => l.layer === 'app')?.provider).toBe('mock');
784
+ const instLayer = detail.resolution.inheritance.find((l) => l.layer === 'instance');
785
+ expect(instLayer?.provider).toBe('wanda');
786
+ expect(instLayer?.implementation).toBe('wanda-parking@1');
787
+ // include 轴内嵌骨架(四面统一——P14 (b))
788
+ const appInc = await tk.loadApp({ include: ['serviceResolutions'] });
789
+ expect(appInc.serviceResolutions?.resolutions['parking.query']?.status).toBe('evaluation-unavailable');
619
790
  app.dispose();
791
+
792
+ // T2 形态:无 manifest 无 integrations → 空键集诚实态(空列表非 503;envelope 携带原因)
793
+ const bare = createTempApp('tbox-m4b-');
794
+ invalidateContractsResolver();
795
+ const bareTk = createAppToolkit(bare.appDir);
796
+ const bareBatch = await bareTk.loadServiceResolutions();
797
+ expect(bareBatch.resolutions).toEqual({});
798
+ expect(bareBatch.contracts.version).toBeNull();
799
+ expect(bareBatch.contracts.notes.join()).toMatch(/未安装/);
800
+ bare.dispose();
620
801
  });
621
802
 
622
- it('M2 <0.9 message 对齐 api.md 信封(需 ≥0.9.0——resolver note 文案)', async () => {
803
+ it('M2 <0.9 message 对齐 api.md 信封(需 ≥0.9.0——resolver note 文案入降级 statusMessage)', async () => {
623
804
  const app = createTempApp('tbox-m2-');
624
805
  invalidateContractsResolver();
625
806
  const store = joinStore(app.appDir, 'contracts');
626
807
  writeFakeContractsPackage(store, { version: '0.8.2', withStrict: false });
627
808
  linkContracts(app.appDir, store, { subdir: 'apps/server' });
809
+ writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
628
810
  const tk = createAppToolkit(app.appDir);
629
- await expect(tk.loadServiceResolutions()).rejects.toThrow(/需 ≥0\.9\.0/);
811
+ const entry = (await tk.loadServiceResolutions()).resolutions['parking.query'];
812
+ expect(entry.status).toBe('evaluation-unavailable');
813
+ expect(entry.statusMessage).toMatch(/需 ≥0\.9\.0/);
630
814
  app.dispose();
631
815
  });
632
816
 
633
- it('M3 畸形包(求值面缺席)+ config 503 500(P-5——predicateIssues 早退绕过 assertFn 的唯一后续拦截点)', async () => {
817
+ it('M3 畸形包(严格面在场求值面缺席)→ 降级骨架非 5xx(v7——evaluationContracts throw 面不再可达;形态异常 note statusMessage)', async () => {
634
818
  const app = createTempApp('tbox-m3-');
635
819
  invalidateContractsResolver();
636
820
  const store = joinStore(app.appDir, 'contracts');
637
821
  writeFakeContractsPackage(store, { version: '0.9.0', missingEvaluationExports: true, slots: ['parking.query'] });
638
822
  linkContracts(app.appDir, store, { subdir: 'apps/server' });
639
- const tk = createAppToolkit(app.appDir); // 不写 integrations.json——config === null 早退路径
640
- await expect(tk.loadServiceResolutions()).rejects.toMatchObject({
641
- code: 'CONTRACTS_NOT_RESOLVED',
642
- httpStatus: 503,
643
- });
644
- await expect(tk.loadServiceResolutions()).rejects.not.toThrow(/工厂门控应先行拦截/);
823
+ writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
824
+ const tk = createAppToolkit(app.appDir);
825
+ const batch = await tk.loadServiceResolutions();
826
+ const entry = batch.resolutions['parking.query'];
827
+ expect(entry.status).toBe('evaluation-unavailable');
828
+ expect(entry.statusMessage).toMatch(/求值面具名导出缺席/);
829
+ expect(entry.statusMessage).toMatch(/契约包形态异常/);
830
+ // 声明实例投影在场(降级 detail——原始数据非 normalizeInstances 派生)
831
+ const detail = await tk.loadServiceResolution('parking.query');
832
+ expect(detail.resolution.status).toBe('evaluation-unavailable');
833
+ expect(detail.resolution.inheritance).toHaveLength(4);
645
834
  app.dispose();
646
835
  });
647
836
  });
@@ -758,7 +947,7 @@ describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单
758
947
  app.dispose();
759
948
  });
760
949
 
761
- it('D6 偏斜态 503 第四分支(P1 标记门):v5 contracts 假件(无 OWNERSHIP_BINDING_V6)+ v6 toolkit → 503', async () => {
950
+ it('D6 偏斜态分级(P1 标记门 v7):v5 contracts 假件 + v6 toolkit → 写端点 503 保留;读求值轴降级骨架(503 退役)', async () => {
762
951
  const app = createTempApp('tbox-d6-');
763
952
  invalidateContractsResolver();
764
953
  const store = joinStore(app.appDir, 'contracts');
@@ -768,15 +957,21 @@ describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单
768
957
  schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
769
958
  declare: { owns: [{ service: 'parking.query' }] },
770
959
  }));
960
+ writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
771
961
  const tk = createAppToolkit(app.appDir);
772
- // 写路径门控(求值轴同此门——assertContractsUsable 单点)
962
+ // 写路径门控保留(assertContractsUsable——policy at edge 收窄至写/迁移面)
773
963
  await expect(tk.writeModuleIntegration('module-parking', { provider: 'wanda' })).rejects.toMatchObject({
774
964
  code: 'CONTRACTS_NOT_RESOLVED',
775
965
  httpStatus: 503,
776
966
  message: expect.stringContaining('OWNERSHIP_BINDING_V6'),
777
967
  });
778
- await expect(tk.loadServiceResolutions()).rejects.toMatchObject({ code: 'CONTRACTS_NOT_RESOLVED', httpStatus: 503 });
779
- // 静态端点照常(永不 503——门控 policy at edge 只拦求值/写/migrate)
968
+ // 读求值轴降级(503 退役):statusMessage 与写门控同源(N1 标记分支单源)
969
+ const degraded = await tk.loadServiceResolutions();
970
+ expect(degraded.contracts.strictValidation).toBe(true);
971
+ const entry = degraded.resolutions['parking.query'];
972
+ expect(entry.status).toBe('evaluation-unavailable');
973
+ expect(entry.statusMessage).toMatch(/OWNERSHIP_BINDING_V6 标记缺席/);
974
+ // 静态端点照常(永不 503——门控 policy at edge 只拦写/迁移)
780
975
  const mod = await tk.loadModule('module-parking');
781
976
  expect(mod.ownedServices).toEqual(['parking.query']);
782
977
  app.dispose();
@@ -830,15 +1025,20 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
830
1025
  h2.app.dispose();
831
1026
  });
832
1027
 
833
- it('I3 degrade:<0.9 null 永不 503;形态异常包(严格面在场求值面缺席)→ null(双条件)', async () => {
1028
+ it('I3 degrade:内嵌轴同步降级(v7 P14 (b)——D31 null 分支退役):三条件不满足 骨架非 null', async () => {
834
1029
  const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
835
- expect((await low.tk.loadApp({ include: ['serviceResolutions'] })).serviceResolutions).toBeNull();
1030
+ const lowInc = await low.tk.loadApp({ include: ['serviceResolutions'] });
1031
+ expect(lowInc.serviceResolutions).not.toBeNull();
1032
+ const lowEntries = Object.values(lowInc.serviceResolutions!.resolutions);
1033
+ expect(lowEntries.length).toBeGreaterThan(0);
1034
+ expect(lowEntries.every((e) => e.status === 'evaluation-unavailable')).toBe(true);
836
1035
  expect((await low.tk.loadApp({ include: ['integrationSchemas'] })).integrationSchemas).toEqual({ provider: 'mock', services: {} }); // 静态轴富化零 contracts
837
1036
  low.app.dispose();
1037
+ // 畸形包(严格面在场求值面缺席)→ 内嵌轴同样降级(503/双 null 全退役)
838
1038
  const malformed = harnessWithContracts({ missingEvaluationExports: true });
839
- expect((await malformed.tk.loadApp({ include: ['serviceResolutions'] })).serviceResolutions).toBeNull();
840
- // 对照:求值轴端点同 fixture → 503(M3 已钉——门控与内嵌两机制不共用断言)
841
- await expect(malformed.tk.loadServiceResolutions()).rejects.toMatchObject({ httpStatus: 503 });
1039
+ const malformedInc = await malformed.tk.loadApp({ include: ['serviceResolutions'] });
1040
+ expect(malformedInc.serviceResolutions).not.toBeNull();
1041
+ expect(Object.values(malformedInc.serviceResolutions!.resolutions)[0]?.status).toBe('evaluation-unavailable');
842
1042
  malformed.app.dispose();
843
1043
  });
844
1044
 
@@ -1022,93 +1222,6 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
1022
1222
  });
1023
1223
 
1024
1224
  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
1225
  it('E1 显式 string + catalog schema(分支②)', async () => {
1113
1226
  const h = eHarness();
1114
1227
  writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
@@ -1287,6 +1400,170 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
1287
1400
  });
1288
1401
  });
1289
1402
 
1403
+ /**
1404
+ * v5.1 D39 测试族(服务轴向假想绑定——?serviceProvider= 表单双投影重定向)。
1405
+ * 供给矩阵(eHarness):wanda 供给 {parking.query, parking.payment};joycity 仅供给
1406
+ * {parking.query}(provider-multi 双 impl 皆 query)——用例 (厂商 × 服务) 选型须对照本矩阵:
1407
+ * catalog-miss 形态(D39-7)与豁免链正例(D39-6)不得混淆供给边界。每用例独立 eHarness
1408
+ * (零 file-cache stat 指纹串扰);D39-8/D39-9 用 layerHarness(x-tbox-global 标记 fixture)。
1409
+ */
1410
+ describe('v5.1 D39(服务轴向假想绑定 serviceProvider)', () => {
1411
+ it('D39-1 假想 happy + I1:双投影切 joycity 且 provider 一致 + 单键边界正例', async () => {
1412
+ const h = eHarness();
1413
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1414
+ const both = ['integrationSchemas', 'integrationFieldRegistry'];
1415
+ const v = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
1416
+ // I1:双投影同源同口径(fields = joycity schema 字段——catalog 复合键声明序首个 = @1 的 joycityUrl)
1417
+ expect(v.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
1418
+ expect(v.integrationFieldRegistry?.provider).toBe('joycity');
1419
+ expect(v.integrationFieldRegistry!.fields.map((f) => f.key)).toEqual(['joycityUrl']);
1420
+ // 单键边界正例:仅 integrationSchemas + 轴参 → 200 假想计算;registry 未请求 = 缺席(S7-1 尾部先例)
1421
+ const only = await h.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
1422
+ expect(only.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
1423
+ expect('integrationFieldRegistry' in only).toBe(false);
1424
+ h.app.dispose();
1425
+ });
1426
+
1427
+ it('D39-2 I2 锁:integration/suppliedBy/defaults 恒实际态', async () => {
1428
+ const h = eHarness();
1429
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda', config: { baseUrl: 'https://w.example' } } } });
1430
+ const base = await h.tk.loadService('parking.query');
1431
+ const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'joycity' });
1432
+ expect(v.integration).toEqual(base.integration); // 文件逐字原值不被重定向(预览不产生「已保存」误导)
1433
+ expect(v.suppliedBy).toEqual(base.suppliedBy); // catalog 静态切片恒实际
1434
+ expect(v.defaults).toEqual(base.defaults); // manifest 预填恒实际
1435
+ expect(v.integrationSchemas?.provider).toBe('joycity'); // 对照:表单层已重定向
1436
+ h.app.dispose();
1437
+ });
1438
+
1439
+ it('D39-3 I3 锁 + 错轴静默忽略:空串 ≡ 缺席 ≡ 实际;appProvider/moduleProvider 被无视', async () => {
1440
+ const h = eHarness();
1441
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1442
+ const empty = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: '' });
1443
+ const absent = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'] });
1444
+ expect(empty.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
1445
+ // 错轴参数(变量绕过多余属性检查——运行时按名读取,appProvider/moduleProvider 被无视;S7-4 先例镜像)
1446
+ const opts = { include: ['integrationFieldRegistry'], serviceProvider: '', appProvider: 'joycity', moduleProvider: 'joycity' };
1447
+ const wrongAxis = await h.tk.loadService('parking.query', opts);
1448
+ expect(wrongAxis.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
1449
+ h.app.dispose();
1450
+ });
1451
+
1452
+ it('D39-4 轴参严检矩阵:400 先于 404(镜像 S7-1 形态)', async () => {
1453
+ const h = eHarness();
1454
+ await expect(h.tk.loadService('parking.query', { serviceProvider: 'x' })).rejects.toMatchObject({
1455
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
1456
+ });
1457
+ await expect(h.tk.loadService('parking.query', { include: [], serviceProvider: 'x' })).rejects.toMatchObject({
1458
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
1459
+ });
1460
+ // 先于 404:ghost 服务 + 无表单键轴参 → 400(非 SERVICE_NOT_FOUND);表单键在场 → 放行至 404
1461
+ await expect(h.tk.loadService('ghost', { serviceProvider: 'x' })).rejects.toMatchObject({
1462
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
1463
+ });
1464
+ await expect(h.tk.loadService('ghost', { include: ['integrationSchemas'], serviceProvider: 'x' })).rejects.toMatchObject({
1465
+ code: 'SERVICE_NOT_FOUND', httpStatus: 404,
1466
+ });
1467
+ h.app.dispose();
1468
+ });
1469
+
1470
+ it('D39-5 同构承诺锁:假想 ≡ PUT 改写 provider 后实读(双投影;S6 写通道先例)', async () => {
1471
+ const h = eHarness();
1472
+ const both = ['integrationSchemas', 'integrationFieldRegistry'];
1473
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1474
+ const hyp = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
1475
+ // 状态演进全经 toolkit 写通道——写后本实例缓存即刻失效,免外部写缓存关注
1476
+ const cur = await h.tk.loadService('parking.query');
1477
+ const w = await h.tk.writeServiceIntegration('parking.query', { ...cur.integration!, provider: 'joycity' });
1478
+ expect(w.written).toBe(true);
1479
+ const act = await h.tk.loadService('parking.query', { include: both });
1480
+ expect(act.integrationSchemas).toEqual(hyp.integrationSchemas);
1481
+ expect(act.integrationFieldRegistry).toEqual(hyp.integrationFieldRegistry);
1482
+ h.app.dispose();
1483
+ });
1484
+
1485
+ it('D39-6 假想豁免链三分支:malformed 不污染 / 内联不读取无 H9 / 未绑定仍非 null', async () => {
1486
+ // ① malformed 节点(E14 形态)+ 假想 → 非 null(短路在节点读取之前——坏形态只影响实际态投影)
1487
+ const h = eHarness();
1488
+ writeIntegrations(h.app.appDir, {
1489
+ modules: { 'module-parking': { provider: 'wanda' } },
1490
+ services: { 'parking.query': { provider: { provider: 'my-api' } } },
1491
+ });
1492
+ const bad = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
1493
+ expect(bad.integrationFieldRegistry).not.toBeNull();
1494
+ expect(bad.integrationFieldRegistry!.provider).toBe('joycity');
1495
+ // 实际态对照:同坏节点无轴参 → 整字段 null(E14 双态互证——预览与坏文件解耦)
1496
+ const badActual = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1497
+ expect(badActual.integrationSchemas).toBeNull();
1498
+ h.app.dispose();
1499
+ // ② 内联 ref 逃逸节点(E4 形态)+ 假想 → 无 H9 400(短路不读节点内联 ref——换厂商即离开内联)
1500
+ const h2 = eHarness();
1501
+ writeIntegrations(h2.app.appDir, {
1502
+ services: { 'parking.query': { provider: { provider: 'my-api', credentialType: '-', configSchema: 'config/schemas-evil/x.json' } } },
1503
+ });
1504
+ const inline = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
1505
+ expect(inline.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h2.schemas.joycity1 });
1506
+ h2.app.dispose();
1507
+ // ③ 全链未绑定(E8 形态)+ 假想 → 仍非 null(服务轴恒可写——无 module 轴零归属豁免形态);
1508
+ // wanda 供给 payment(供给矩阵)→ mallId 字段在场
1509
+ const h3 = eHarness();
1510
+ writeIntegrations(h3.app.appDir, { services: { 'parking.payment': {} } });
1511
+ const unbound = await h3.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
1512
+ // fields[].schema = 字段子 schema(非整文件 schema——G 批锁 :1777 同型)
1513
+ expect(unbound.integrationFieldRegistry).toEqual({
1514
+ provider: 'wanda', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true,
1515
+ fields: [{ key: 'mallId', schema: { type: 'string' }, sources: [{ service: 'parking.payment', required: false }], global: false }],
1516
+ });
1517
+ h3.app.dispose();
1518
+ });
1519
+
1520
+ it('D39-7 catalog miss:假想未知厂商 → 非 null 空集(schemaless=[service])', async () => {
1521
+ const h = eHarness();
1522
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1523
+ const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'ghost-vendor' });
1524
+ expect(v.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
1525
+ expect(v.integrationFieldRegistry).toEqual({
1526
+ provider: 'ghost-vendor', schemaless: ['parking.query'], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
1527
+ });
1528
+ h.app.dispose();
1529
+ });
1530
+
1531
+ it('D39-8 global 旗标假想透传:标记位如实(layerHarness——true 正例 + 未标记 false 不过滤)', async () => {
1532
+ const h = layerHarness();
1533
+ // joycity 供给 parking.query → 假想后 fields = joycity query schema 字段(fixture 全标记 → global:true)
1534
+ const joy = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
1535
+ expect(joy.integrationFieldRegistry!.provider).toBe('joycity');
1536
+ expect(joy.integrationFieldRegistry!.fields.length).toBeGreaterThan(0);
1537
+ expect(joy.integrationFieldRegistry!.fields.every((f) => f.global)).toBe(true);
1538
+ // wanda 供给 parking.payment → mallId global:true / parkLotId global:false 在场(服务轴不过滤——G 批语义假想不变);
1539
+ // payment 先配置入词汇(P9:契约包 ∪ 已配置——layerHarness 初始 integrations.json 未配置 payment;
1540
+ // 覆盖写后 payment 节点 {} 实际态级联双缺 → null,假想 wanda 短路仍非 null——豁免链③同型,S7-7 外部写先例)
1541
+ writeAppFile(h.app.appDir, 'config/integrations.json', JSON.stringify({ services: { 'parking.payment': {} } }));
1542
+ const wan = await h.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
1543
+ expect(wan.integrationFieldRegistry!.fields.map((f) => f.key).sort()).toEqual(['mallId', 'parkLotId']);
1544
+ expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'mallId')!.global).toBe(true);
1545
+ expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'parkLotId')!.global).toBe(false);
1546
+ h.app.dispose();
1547
+ });
1548
+
1549
+ it('D39-9 单源锁:loadServiceFieldRegistry(snap,s,X) ≡ buildIntegrationFieldRegistry over 链函数', async () => {
1550
+ const h = layerHarness();
1551
+ const { loadViewSnapshotSync } = await import('../src/views/context.js');
1552
+ const snap = loadViewSnapshotSync({ appDir: h.app.appDir }, await h.tk.resolveContracts());
1553
+ const X = 'joycity';
1554
+ const reg = loadServiceFieldRegistry(snap, 'parking.query', X);
1555
+ const schemas = loadServiceIntegrationSchemas(snap, 'parking.query', X);
1556
+ expect(schemas).not.toBeNull();
1557
+ // 结构性断言(依赖契约锁——registry 旁路重取 schema 破 I1 即刻红;S8 同族意图)
1558
+ const manual = buildIntegrationFieldRegistry({
1559
+ provider: schemas!.provider,
1560
+ services: { 'parking.query': { configSchema: schemas!.configSchema } },
1561
+ });
1562
+ expect(reg).toEqual(manual);
1563
+ h.app.dispose();
1564
+ });
1565
+ });
1566
+
1290
1567
  /**
1291
1568
  * v4.6 D36/D37 测试族(S5 接续 S1-S4):S5a 纯代数(直调合成 layer 字面量,零 fs)/
1292
1569
  * S5b 接线序 + 等价锁 / S6 同构(假想 ≡ 写后实读)/ S7 边界(400·null 两态·I2·空集)/
@@ -1891,6 +2168,46 @@ describe('B1 展示名链', () => {
1891
2168
  h2.app.dispose();
1892
2169
  });
1893
2170
 
2171
+ it('T2b loadModulesView:hidden 模块默认过滤;includeHidden 可见 + 徽标;detail 恒 200 不滤', async () => {
2172
+ const app = createTempApp('tbox-hidden-');
2173
+ invalidateContractsResolver();
2174
+ const store = joinStore(app.appDir, 'contracts');
2175
+ writeFakeContractsPackage(store, { version: '0.9.0' });
2176
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
2177
+ const manifest = (id: string, name: string, extra: Record<string, unknown> = {}): string =>
2178
+ JSON.stringify({
2179
+ schemaVersion: 1,
2180
+ id,
2181
+ version: '0.1.0',
2182
+ kind: 'business',
2183
+ name,
2184
+ declare: { owns: [], consumes: [] },
2185
+ dependencies: { modules: [] },
2186
+ env: [],
2187
+ ...extra,
2188
+ });
2189
+ mkdirSync(join(app.appDir, 'packages', 'module-visible'), { recursive: true });
2190
+ writeAppFile(app.appDir, 'packages/module-visible/tbox.module.json', manifest('module-visible', '可见模块'));
2191
+ mkdirSync(join(app.appDir, 'packages', 'module-secret'), { recursive: true });
2192
+ writeAppFile(app.appDir, 'packages/module-secret/tbox.module.json', manifest('module-secret', '隐藏模块', { hidden: true }));
2193
+
2194
+ const tk = createAppToolkit(app.appDir);
2195
+ // 默认:hidden 模块不进清单(可见模块无 hidden 键——additive 纪律)
2196
+ const def = await tk.loadModules();
2197
+ expect(def.modules.map((m) => m.id)).toEqual(['module-visible']);
2198
+ expect(def.modules[0]).not.toHaveProperty('hidden');
2199
+ // includeHidden = true:可见并携带 hidden: true 徽标
2200
+ const inc = await tk.loadModules({ includeHidden: true });
2201
+ expect(inc.modules.map((m) => m.id).sort()).toEqual(['module-secret', 'module-visible']);
2202
+ expect(inc.modules.find((m) => m.id === 'module-secret')?.hidden).toBe(true);
2203
+ expect(inc.modules.find((m) => m.id === 'module-visible')).not.toHaveProperty('hidden');
2204
+ // detail 端点恒 200 不滤——视图携带 hidden 徽标
2205
+ const detail = await tk.loadModule('module-secret');
2206
+ expect(detail.name).toBe('隐藏模块');
2207
+ expect(detail.hidden).toBe(true);
2208
+ app.dispose();
2209
+ });
2210
+
1894
2211
  it('T3 loadModuleDetailView:meta 恒发;services[] 词汇优先(demand 车位查询被词汇停车查询压过——防漂移锁)', async () => {
1895
2212
  const h2 = displayHarness();
1896
2213
  const v = await h2.tk.loadModule('module-parking');