@tbox.cn/app-toolkit 0.1.0 → 0.2.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.
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ /**
7
+ * 同仓关键词矩阵静态断言(v4.4 D32 配套——tbox-app 侧义务):
8
+ * 真实 vendor schema 文件(modules/<pkg>/schemas/**,config + credentials 双面)关键词
9
+ * ⊆ agtcodingbox SchemaForm v2 渲染器支持集(清单钉 module-tab-config-refinement Note
10
+ * 前端配套节:format/minLength/maxLength/additionalProperties/$schema 提示级不降级;
11
+ * oneOf/anyOf/$ref/items/patternProperties 只读降级 + 值保全)。
12
+ *
13
+ * 断言红 = 新 vendor schema 引入渲染器不支持的键 → 人工确认渲染器扩容或改 schema 后
14
+ * 同批更新 SUPPORTED(禁止静默扩清单——该断言是「表单引导能力」的tripwire)。
15
+ */
16
+
17
+ const SUPPORTED: ReadonlySet<string> = new Set([
18
+ // 结构基础(SchemaForm v2 全渲染)
19
+ 'type', 'properties', 'required', 'enum', 'title', 'description', 'default',
20
+ 'minimum', 'maximum', 'writeOnly',
21
+ // 提示级(note 扩容清单——不降级)
22
+ 'format', 'minLength', 'maxLength', 'additionalProperties', '$schema',
23
+ // 结构性(只读降级 + 值保全——支持集含之,出现不红)
24
+ 'oneOf', 'anyOf', '$ref', 'items', 'patternProperties',
25
+ ]);
26
+
27
+ /** 递归走已知 subschema 位,收集 schema 节点自身键(properties 值下的键 = 字段名,不属关键词) */
28
+ function collectKeywords(node: unknown, out: Set<string>): void {
29
+ if (Array.isArray(node)) {
30
+ for (const v of node) collectKeywords(v, out);
31
+ return;
32
+ }
33
+ if (!node || typeof node !== 'object') return;
34
+ for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
35
+ out.add(k);
36
+ if (k === 'properties' || k === 'patternProperties') {
37
+ for (const sv of Object.values(v as Record<string, unknown>)) collectKeywords(sv, out);
38
+ } else if (k === 'items' && v !== null && typeof v === 'object') {
39
+ collectKeywords(v, out);
40
+ } else if ((k === 'oneOf' || k === 'anyOf' || k === 'allOf') && Array.isArray(v)) {
41
+ for (const sv of v) collectKeywords(sv, out);
42
+ } else if (k === 'additionalProperties' && v !== null && typeof v === 'object') {
43
+ collectKeywords(v, out);
44
+ } else if (k === '$defs' || k === 'definitions') {
45
+ for (const sv of Object.values(v as Record<string, unknown>)) collectKeywords(sv, out);
46
+ }
47
+ }
48
+ }
49
+
50
+ function schemaFiles(): Array<{ pkg: string; rel: string }> {
51
+ const modulesRoot = join(fileURLToPath(new URL('../../..', import.meta.url)), 'modules');
52
+ const out: Array<{ pkg: string; rel: string }> = [];
53
+ for (const pkg of readdirSync(modulesRoot)) {
54
+ const dir = join(modulesRoot, pkg, 'schemas');
55
+ if (!statSyncSafe(dir)) continue;
56
+ (function scan(d: string): void {
57
+ for (const e of readdirSync(d)) {
58
+ const p = join(d, e);
59
+ if (statSync(p).isDirectory()) scan(p);
60
+ else if (e.endsWith('.json')) out.push({ pkg, rel: p });
61
+ }
62
+ })(dir);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ function statSyncSafe(p: string): boolean {
68
+ try {
69
+ return statSync(p).isDirectory();
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ describe('vendor schema 关键词矩阵 ⊆ 渲染器支持集(D32 表单引导 tripwire)', () => {
76
+ it('modules/**/schemas 全文件关键词 ⊆ SUPPORTED(红 = 渲染器扩容人工确认)', () => {
77
+ const files = schemaFiles();
78
+ expect(files.length, 'vendor schema 文件扫描面非空(防目录改名静默失效)').toBeGreaterThan(5);
79
+ for (const { pkg, rel } of files) {
80
+ const keywords = new Set<string>();
81
+ try {
82
+ collectKeywords(JSON.parse(readFileSync(rel, 'utf8')), keywords);
83
+ } catch (err) {
84
+ throw new Error(`vendor schema 解析失败(${pkg}):${(err as Error).message}`);
85
+ }
86
+ for (const k of keywords) {
87
+ expect(SUPPORTED.has(k), `${pkg} schema 关键词 "${k}" ∉ 渲染器支持集——确认 SchemaForm v2 扩容后同批更新本清单`).toBe(true);
88
+ }
89
+ }
90
+ });
91
+
92
+ it('实际用到的关键词 ⊆ SUPPORTED 且扫描面覆盖 config/credentials 双面(自检)', () => {
93
+ const files = schemaFiles();
94
+ const used = new Set<string>();
95
+ for (const { rel } of files) collectKeywords(JSON.parse(readFileSync(rel, 'utf8')), used);
96
+ // 实测基线(2026-09 扫描):14 键——防「提取器失效全空集」假绿
97
+ for (const k of ['$schema', 'type', 'properties', 'required', 'format', 'writeOnly', 'additionalProperties']) {
98
+ expect(used.has(k), `基线关键词 ${k} 未被提取到——提取器或目录布局变化,人工核对`).toBe(true);
99
+ }
100
+ });
101
+ });
@@ -19,11 +19,17 @@ interface Harness {
19
19
  tk: AppToolkit;
20
20
  }
21
21
 
22
- function harnessWithContracts(opts: { version?: string; withStrict?: boolean } = {}): Harness {
22
+ function harnessWithContracts(
23
+ opts: { version?: string; withStrict?: boolean; missingEvaluationExports?: boolean } = {},
24
+ ): Harness {
23
25
  const app = createTempApp('tbox-views-');
24
26
  invalidateContractsResolver();
25
27
  const store = joinStore(app.appDir, 'contracts');
26
- writeFakeContractsPackage(store, { version: opts.version ?? '0.9.0', withStrict: opts.withStrict ?? true });
28
+ writeFakeContractsPackage(store, {
29
+ version: opts.version ?? '0.9.0',
30
+ withStrict: opts.withStrict ?? true,
31
+ missingEvaluationExports: opts.missingEvaluationExports,
32
+ });
27
33
  linkContracts(app.appDir, store, { subdir: 'apps/server' });
28
34
  // 应用结构:parking 模块(packages/ 约定发现)+ 注册表两实例 + integrations.json
29
35
  mkdirDeep(join(app.appDir, 'packages', 'module-parking'));
@@ -95,13 +101,14 @@ describe('views/工厂(O 矩阵核心族)', () => {
95
101
  const appView = await h.tk.loadApp();
96
102
  expect(Object.keys(appView)).toEqual(['services', 'integration']);
97
103
  const mod = await h.tk.loadModule('module-parking');
98
- expect(Object.keys(mod)).toEqual(['integration', 'services']);
104
+ // v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
105
+ expect(Object.keys(mod)).toEqual(['domainKeys', 'integration', 'providerBindings', 'configEditable', 'services']);
99
106
  const svc = await h.tk.loadService('parking.query');
100
107
  expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
101
108
  // 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
102
109
  });
103
110
 
104
- it('O8 P14 三处等价:map 值 ≡ 单体 resolution 摘要基(单源函数结构保证)', async () => {
111
+ it('O8 P14 等价:map 值 ≡ 单体 resolution 摘要基(单源函数结构保证;v4.4 内嵌两处见 I4)', async () => {
105
112
  const batch = await h.tk.loadServiceResolutions();
106
113
  const single = await h.tk.loadServiceResolution('parking.query');
107
114
  const entry = batch.resolutions['parking.query'];
@@ -172,7 +179,7 @@ describe('views/工厂(O 矩阵核心族)', () => {
172
179
  wild.app.dispose();
173
180
  });
174
181
 
175
- it('N1 命名对齐:13 行映射表 × 工厂方法 typeof(L1 改名漂移即红)', async () => {
182
+ it('N1 命名对齐:15 行映射表 × 工厂方法 typeof(L1 改名漂移即红——v4.4 15 端点)', async () => {
176
183
  for (const row of URL_METHOD_MAP) {
177
184
  const fn = (h.tk as unknown as Record<string, unknown>)[row.method];
178
185
  expect(typeof fn, row.method).toBe('function');
@@ -204,12 +211,12 @@ describe('views/工厂(O 矩阵核心族)', () => {
204
211
  expect(dry.files).toEqual([]);
205
212
  });
206
213
 
207
- it('写时自动初始化(R13 语义翻转提示)+ N1 清域(模块 {} 删 domains 键)', async () => {
214
+ it('写时自动初始化(R13 语义翻转提示)+ N1 清域(模块 {} 删域键)', async () => {
208
215
  h.app.dispose();
209
216
  const fresh = createTempApp('tbox-views-fresh-');
210
217
  invalidateContractsResolver();
211
218
  const store = joinStore(fresh.appDir, 'contracts');
212
- writeFakeContractsPackage(store, { version: '0.9.0' });
219
+ writeFakeContractsPackage(store, { version: '0.9.0', slots: ['auth.alipay-login'] });
213
220
  linkContracts(fresh.appDir, store, { subdir: 'apps/server' });
214
221
  const tk = createAppToolkit(fresh.appDir);
215
222
  // 缺席文件首写 → 骨架物化 + R13 warning
@@ -315,14 +322,18 @@ describe('views/工厂(O 矩阵核心族)', () => {
315
322
  credentialRef: 'secret://wanda-parking.query',
316
323
  layer: 'service',
317
324
  });
318
- expect(inst?.credential).toEqual({
325
+ // v4.4 D33 删净:instances[].credential 移除(凭据掩码归 GET /credentials——求值路径零凭据 I/O)
326
+ expect(inst && 'credential' in inst).toBe(false);
327
+ // 本位 ref(服务级 credentialRef)掩码经 GET /credentials 回显
328
+ const credView = await tk.loadCredentials();
329
+ expect(credView.services['parking.query']?.credential).toEqual({
319
330
  type: 'wanda-c-signed-v1',
320
331
  ref: 'secret://wanda-parking.query',
321
332
  file: 'config/credentials/wanda-parking.query.json',
322
333
  masked: { appKey: 'SE****', appSecret: 'SE****' },
323
334
  });
324
335
  // O4 零真值:序列化后 grep 断言
325
- const serialized = JSON.stringify(detail);
336
+ const serialized = JSON.stringify({ detail, credView });
326
337
  expect(serialized).not.toContain('SECRET-APP-KEY');
327
338
  expect(serialized).not.toContain('SECRET-APP-SECRET');
328
339
  // inheritance 实例层 = 默认实例口径(FX-2c):无实例级声明 → 继承位(provider 缺席)
@@ -367,7 +378,8 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
367
378
  const combo = await tk.loadProviderService('joycity', 'parking.query');
368
379
  expect(combo.implementations).toHaveLength(2);
369
380
  expect(combo.integrationSchemas.configSchema).toEqual({ type: 'object', properties: { baseUrl: { type: 'string' } } });
370
- expect(combo.integrationSchemas.credentialSchema).toBeNull(); // 供给在场未声明 credentialSchema → null(非 404)
381
+ // v4.4 D33:credentialSchema 删净(结构归 GET /credential-types——组合单体纯 config)
382
+ expect('credentialSchema' in combo.integrationSchemas).toBe(false);
371
383
  app.dispose();
372
384
  });
373
385
 
@@ -425,3 +437,395 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
425
437
  }
426
438
  });
427
439
  });
440
+
441
+ describe('A2 错误面(P-4 notes 优先 / P-5 畸形包 503)', () => {
442
+ it('M1 导入失败 message 含构建指引且不再误报未安装(P-4 notes 优先)', async () => {
443
+ const app = createTempApp('tbox-m1-');
444
+ invalidateContractsResolver();
445
+ const store = joinStore(app.appDir, 'contracts');
446
+ writeFakeContractsPackage(store, { version: '0.9.0', brokenDist: true });
447
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
448
+ const tk = createAppToolkit(app.appDir);
449
+ await expect(tk.loadServiceResolutions()).rejects.toThrow(/构建产物导入失败/);
450
+ await expect(tk.loadServiceResolutions()).rejects.not.toThrow(/未安装/);
451
+ app.dispose();
452
+ });
453
+
454
+ it('M2 <0.9 message 对齐 api.md 信封(需 ≥0.9.0——resolver note 文案)', async () => {
455
+ const app = createTempApp('tbox-m2-');
456
+ invalidateContractsResolver();
457
+ const store = joinStore(app.appDir, 'contracts');
458
+ writeFakeContractsPackage(store, { version: '0.8.2', withStrict: false });
459
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
460
+ const tk = createAppToolkit(app.appDir);
461
+ await expect(tk.loadServiceResolutions()).rejects.toThrow(/需 ≥0\.9\.0/);
462
+ app.dispose();
463
+ });
464
+
465
+ it('M3 畸形包(求值面缺席)+ 无 config → 503 非 500(P-5——predicateIssues 早退绕过 assertFn 的唯一后续拦截点)', async () => {
466
+ const app = createTempApp('tbox-m3-');
467
+ invalidateContractsResolver();
468
+ const store = joinStore(app.appDir, 'contracts');
469
+ writeFakeContractsPackage(store, { version: '0.9.0', missingEvaluationExports: true, slots: ['parking.query'] });
470
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
471
+ const tk = createAppToolkit(app.appDir); // 不写 integrations.json——config === null 早退路径
472
+ await expect(tk.loadServiceResolutions()).rejects.toMatchObject({
473
+ code: 'CONTRACTS_NOT_RESOLVED',
474
+ httpStatus: 503,
475
+ });
476
+ await expect(tk.loadServiceResolutions()).rejects.not.toThrow(/工厂门控应先行拦截/);
477
+ app.dispose();
478
+ });
479
+ });
480
+
481
+ describe('D34 多域模块钉死(v4.4 F3)', () => {
482
+ it('D1 单域回归:domainKeys/providerBindings/configEditable 装配 + integration 原值', async () => {
483
+ const h2 = harnessWithContracts();
484
+ const mod = await h2.tk.loadModule('module-parking');
485
+ expect(mod.domainKeys).toEqual(['parking']);
486
+ expect(mod.configEditable).toBe(true);
487
+ expect(mod.providerBindings).toEqual([{ domain: 'parking', effective: 'wanda', layer: 'module' }]);
488
+ expect(mod.integration).toEqual({ provider: 'wanda' });
489
+ h2.app.dispose();
490
+ });
491
+
492
+ it('D2 多域模块:GET integration null + providerBindings 双条 + PUT 400 分支①(含 {} 清域)', async () => {
493
+ const app = createTempApp('tbox-d2-');
494
+ invalidateContractsResolver();
495
+ const store = joinStore(app.appDir, 'contracts');
496
+ writeFakeContractsPackage(store, { version: '0.9.0' });
497
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
498
+ writeAppFile(
499
+ app.appDir,
500
+ 'packages/module-multi/tbox.module.json',
501
+ JSON.stringify({
502
+ schemaVersion: 1,
503
+ name: 'module-multi',
504
+ version: '0.1.0',
505
+ kind: 'business',
506
+ contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
507
+ dependencies: { modules: [] },
508
+ env: [],
509
+ }),
510
+ );
511
+ const tk = createAppToolkit(app.appDir);
512
+ const mod = await tk.loadModule('module-multi');
513
+ expect(mod.domainKeys).toEqual(['member', 'parking']);
514
+ expect(mod.integration).toBeNull();
515
+ expect(mod.configEditable).toBe(false);
516
+ expect(mod.providerBindings).toEqual([
517
+ { domain: 'member', effective: null, layer: null },
518
+ { domain: 'parking', effective: null, layer: null },
519
+ ]);
520
+ await expect(tk.writeModuleIntegration('module-multi', { provider: 'wanda' })).rejects.toMatchObject({
521
+ code: 'VALIDATION_FAILED',
522
+ httpStatus: 400,
523
+ message: expect.stringContaining('跨 2 个域(member、parking)'),
524
+ });
525
+ await expect(tk.writeModuleIntegration('module-multi', {})).rejects.toMatchObject({ code: 'VALIDATION_FAILED' });
526
+ app.dispose();
527
+ });
528
+
529
+ it('D3 零域模块:domainKeys [] + PUT 400 分支②', async () => {
530
+ const app = createTempApp('tbox-d3-');
531
+ invalidateContractsResolver();
532
+ const store = joinStore(app.appDir, 'contracts');
533
+ writeFakeContractsPackage(store, { version: '0.9.0' });
534
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
535
+ writeAppFile(
536
+ app.appDir,
537
+ 'packages/module-zero/tbox.module.json',
538
+ JSON.stringify({
539
+ schemaVersion: 1,
540
+ name: 'module-zero',
541
+ version: '0.1.0',
542
+ kind: 'business',
543
+ contributes: {},
544
+ dependencies: { modules: [] },
545
+ env: [],
546
+ }),
547
+ );
548
+ const tk = createAppToolkit(app.appDir);
549
+ const mod = await tk.loadModule('module-zero');
550
+ expect(mod.domainKeys).toEqual([]);
551
+ expect(mod.providerBindings).toEqual([]);
552
+ expect(mod.integration).toBeNull();
553
+ await expect(tk.writeModuleIntegration('module-zero', { provider: 'x' })).rejects.toMatchObject({
554
+ code: 'VALIDATION_FAILED',
555
+ httpStatus: 400,
556
+ message: expect.stringContaining('无服务需求声明'),
557
+ });
558
+ app.dispose();
559
+ });
560
+
561
+ it('D4 域键共享(P13):双方 configEditable false + PUT 400 分支③(他方同域异服务——并集计数)', async () => {
562
+ const app = createTempApp('tbox-d4-');
563
+ invalidateContractsResolver();
564
+ const store = joinStore(app.appDir, 'contracts');
565
+ writeFakeContractsPackage(store, { version: '0.9.0' });
566
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
567
+ const manifest = (name: string, service: string): string =>
568
+ JSON.stringify({
569
+ schemaVersion: 1,
570
+ name,
571
+ version: '0.1.0',
572
+ kind: 'business',
573
+ contributes: { services: [{ service, optional: false }] },
574
+ dependencies: { modules: [] },
575
+ env: [],
576
+ });
577
+ writeAppFile(app.appDir, 'packages/module-a/tbox.module.json', manifest('module-a', 'parking.alpha'));
578
+ writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b', 'parking.beta'));
579
+ const tk = createAppToolkit(app.appDir);
580
+ const a = await tk.loadModule('module-a');
581
+ const b = await tk.loadModule('module-b');
582
+ expect(a.configEditable).toBe(false);
583
+ expect(b.configEditable).toBe(false);
584
+ await expect(tk.writeModuleIntegration('module-a', { provider: 'x' })).rejects.toMatchObject({
585
+ code: 'VALIDATION_FAILED',
586
+ httpStatus: 400,
587
+ message: expect.stringContaining('由 module-a、module-b 共用'),
588
+ });
589
+ app.dispose();
590
+ });
591
+
592
+ it('D5 死键扫尾:moduleDomainKey/moduleDomainKeyOf 全链删除(源文件内容断言)', () => {
593
+ const read = (rel: string): string => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8');
594
+ expect(read('../src/views/modules.ts')).not.toContain('moduleDomainKeyOf');
595
+ expect(read('../src/integrations/write.ts')).not.toContain('moduleDomainKey');
596
+ expect(read('../src/index.ts')).not.toContain('moduleDomainKeyOf');
597
+ });
598
+ });
599
+
600
+ 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
+ it('I1 include 正例:app/module 双键内嵌 + 未请求缺席(内嵌三律)+ issues 恒全量', async () => {
671
+ const h2 = harnessWithContracts();
672
+ const appInc = await h2.tk.loadApp({ include: ['serviceResolutions', 'integrationSchemas'] });
673
+ expect(Object.keys(appInc.serviceResolutions!.resolutions).sort()).toEqual(['auth.alipay-login', 'parking.query']);
674
+ // harness 无供给 manifest → mock 足迹空(供给在场未声明形态由 S 系列覆盖)
675
+ expect(appInc.integrationSchemas).toEqual({ provider: 'mock', services: {} });
676
+ const modInc = await h2.tk.loadModule('module-parking', { include: ['serviceResolutions'] });
677
+ expect(Object.keys(modInc.serviceResolutions!.resolutions)).toEqual(['parking.query']); // demand 过滤
678
+ const batch = await h2.tk.loadServiceResolutions();
679
+ expect(modInc.serviceResolutions!.issues).toEqual(batch.issues); // 恒全量(不随过滤收窄)
680
+ // 未请求 → 字段缺席(D30 三律:未请求 = 缺席)
681
+ const plain = await h2.tk.loadApp();
682
+ expect('serviceResolutions' in plain).toBe(false);
683
+ expect('integrationSchemas' in plain).toBe(false);
684
+ h2.app.dispose();
685
+ });
686
+
687
+ it('I2 未知/越权键 400 + 校验先于 404 + 重复键去重(P15)', async () => {
688
+ const h2 = harnessWithContracts();
689
+ await expect(h2.tk.loadApp({ include: ['bogus'] })).rejects.toMatchObject({
690
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['include'],
691
+ });
692
+ // 越权:provider 白名单仅 integrationSchemas——且 400 先于 PROVIDER_NOT_FOUND
693
+ await expect(h2.tk.loadProvider('wanda', { include: ['serviceResolutions'] })).rejects.toMatchObject({
694
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['include'],
695
+ });
696
+ // 校验先于 MODULE_NOT_FOUND
697
+ await expect(h2.tk.loadModule('ghost', { include: ['bogus'] })).rejects.toMatchObject({
698
+ code: 'VALIDATION_FAILED', httpStatus: 400,
699
+ });
700
+ const ok = await h2.tk.loadApp({ include: ['serviceResolutions', 'serviceResolutions'] });
701
+ expect(ok.serviceResolutions).not.toBeNull();
702
+ h2.app.dispose();
703
+ });
704
+
705
+ it('I3 degrade:<0.9 → null 永不 503;形态异常包(严格面在场求值面缺席)→ null(双条件)', async () => {
706
+ const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
707
+ expect((await low.tk.loadApp({ include: ['serviceResolutions'] })).serviceResolutions).toBeNull();
708
+ expect((await low.tk.loadApp({ include: ['integrationSchemas'] })).integrationSchemas).toEqual({ provider: 'mock', services: {} }); // 静态轴富化零 contracts
709
+ low.app.dispose();
710
+ const malformed = harnessWithContracts({ missingEvaluationExports: true });
711
+ expect((await malformed.tk.loadApp({ include: ['serviceResolutions'] })).serviceResolutions).toBeNull();
712
+ // 对照:求值轴端点同 fixture → 503(M3 已钉——门控与内嵌两机制不共用断言)
713
+ await expect(malformed.tk.loadServiceResolutions()).rejects.toMatchObject({ httpStatus: 503 });
714
+ malformed.app.dispose();
715
+ });
716
+
717
+ it('I4 P14 四处等价:批量 / 单体摘要基 / app 内嵌 / module 内嵌(deep-equal 含 layer)', async () => {
718
+ const h2 = layerHarness();
719
+ const batch = await h2.tk.loadServiceResolutions();
720
+ const appInc = await h2.tk.loadApp({ include: ['serviceResolutions'] });
721
+ const modInc = await h2.tk.loadModule('module-parking', { include: ['serviceResolutions'] });
722
+ expect(Object.keys(modInc.serviceResolutions!.resolutions).sort()).toEqual(['parking.payment', 'parking.query']); // demand = 双服务
723
+ for (const s of ['parking.query', 'parking.payment']) {
724
+ const single = await h2.tk.loadServiceResolution(s);
725
+ const { instances, inheritance, ...base } = single.resolution;
726
+ void instances;
727
+ void inheritance;
728
+ expect(batch.resolutions[s], s).toEqual(base);
729
+ expect(appInc.serviceResolutions!.resolutions[s], `app:${s}`).toEqual(base);
730
+ expect(modInc.serviceResolutions!.resolutions[s], `module:${s}`).toEqual(base);
731
+ }
732
+ h2.app.dispose();
733
+ });
734
+
735
+ it('I5 layer 一致性:Entry.layer ≡ inheritance 逆序首个有 provider 层;未配置双缺席', async () => {
736
+ const h2 = layerHarness();
737
+ const batch = await h2.tk.loadServiceResolutions();
738
+ for (const [s, entry] of Object.entries(batch.resolutions)) {
739
+ const single = await h2.tk.loadServiceResolution(s);
740
+ const expected = [...single.resolution.inheritance].reverse().find((i) => i.provider !== undefined)?.layer ?? null;
741
+ if (entry.provider === undefined) {
742
+ expect(entry.layer, `${s} 双缺席`).toBeUndefined();
743
+ } else {
744
+ expect(entry.layer, s).toEqual(expected);
745
+ }
746
+ }
747
+ // parking.payment 未配置(slot 缺席)→ provider/layer 双缺席(无孤儿态)
748
+ expect(batch.resolutions['parking.payment']?.provider).toBeUndefined();
749
+ expect(batch.resolutions['parking.payment']?.layer).toBeUndefined();
750
+ // parking.query 槽级 wanda → layer 'service'
751
+ expect(batch.resolutions['parking.query']).toMatchObject({ status: 'ok', provider: 'wanda', layer: 'service' });
752
+ h2.app.dispose();
753
+ });
754
+
755
+ it('S1 模块级层级 schema:demand ∩ 供给 + schema 文件原文(D32)+ 缺省缺席', async () => {
756
+ const h2 = layerHarness();
757
+ const mod = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas'] });
758
+ expect(mod.integrationSchemas).toEqual({
759
+ provider: 'wanda', // resolveDomainBinding 单源(domains.parking)
760
+ services: {
761
+ 'parking.query': { configSchema: h2.schemas.query },
762
+ 'parking.payment': { configSchema: h2.schemas.parking },
763
+ },
764
+ });
765
+ const plain = await h2.tk.loadModule('module-parking');
766
+ expect('integrationSchemas' in plain).toBe(false);
767
+ h2.app.dispose();
768
+ });
769
+
770
+ it('S2 应用级:供给足迹全量(≠ AppView.services)+ root.provider 缺席 → null', async () => {
771
+ const h2 = layerHarness();
772
+ const appInc = await h2.tk.loadApp({ include: ['integrationSchemas'] });
773
+ expect(appInc.integrationSchemas).toEqual({
774
+ provider: 'wanda',
775
+ services: {
776
+ 'parking.query': { configSchema: h2.schemas.query },
777
+ 'parking.payment': { configSchema: h2.schemas.parking },
778
+ },
779
+ });
780
+ h2.app.dispose();
781
+ // root.provider 缺席 → null(未绑定——表单降级 ExtraFields)
782
+ const h3 = layerHarness();
783
+ writeAppFile(
784
+ h3.app.appDir,
785
+ 'config/integrations.json',
786
+ JSON.stringify({
787
+ instances: [{ id: 'mall-bj-01', name: '北京店' }],
788
+ domains: { parking: { provider: 'wanda' } },
789
+ services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
790
+ }),
791
+ );
792
+ expect((await h3.tk.loadApp({ include: ['integrationSchemas'] })).integrationSchemas).toBeNull();
793
+ h3.app.dispose();
794
+ });
795
+
796
+ it('S3 切换态批量:loadProvider include=integrationSchemas(供给轴逐服务)+ 未请求缺席', async () => {
797
+ const h2 = layerHarness();
798
+ const detail = await h2.tk.loadProvider('wanda', { include: ['integrationSchemas'] });
799
+ expect(detail.services['parking.query']?.integrationSchemas).toEqual({ configSchema: h2.schemas.query });
800
+ expect(detail.services['parking.payment']?.integrationSchemas).toEqual({ configSchema: h2.schemas.parking });
801
+ const plain = await h2.tk.loadProvider('wanda');
802
+ expect(plain.services['parking.query'] && 'integrationSchemas' in plain.services['parking.query']).toBe(false);
803
+ h2.app.dispose();
804
+ });
805
+
806
+ it('S4 多域模块 integrationSchemas → 整字段 null(D32——表单降级 ExtraFields)', async () => {
807
+ const app = createTempApp('tbox-s4-');
808
+ invalidateContractsResolver();
809
+ const store = joinStore(app.appDir, 'contracts');
810
+ writeFakeContractsPackage(store, { version: '0.9.0' });
811
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
812
+ writeAppFile(
813
+ app.appDir,
814
+ 'packages/module-multi/tbox.module.json',
815
+ JSON.stringify({
816
+ schemaVersion: 1,
817
+ name: 'module-multi',
818
+ version: '0.1.0',
819
+ kind: 'business',
820
+ contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
821
+ dependencies: { modules: [] },
822
+ env: [],
823
+ }),
824
+ );
825
+ const tk = createAppToolkit(app.appDir);
826
+ const mod = await tk.loadModule('module-multi', { include: ['integrationSchemas'] });
827
+ expect(mod.integrationSchemas).toBeNull();
828
+ expect(mod.serviceResolutions).toBeUndefined(); // 未请求
829
+ app.dispose();
830
+ });
831
+ });