@tbox.cn/app-toolkit 0.6.0 → 0.7.1
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.
- package/README.md +2 -2
- package/dist/index.d.ts +47 -16
- package/dist/index.js +9 -9
- package/package.json +2 -2
- package/src/assembly/resolve-key.ts +4 -2
- package/src/assembly/walk-manifests.ts +28 -12
- package/src/core/errors.ts +12 -2
- package/src/dto.ts +10 -5
- package/src/factory.ts +34 -20
- package/src/index.ts +2 -2
- package/src/integrations/predicate.ts +6 -3
- package/src/integrations/read.ts +11 -4
- package/src/integrations/write.ts +10 -5
- package/src/views/service-detail.ts +4 -3
- package/src/views/service-resolutions.ts +124 -28
- package/tests/dev-manifest-catalog.test.ts +39 -1
- package/tests/views.test.ts +266 -31
|
@@ -119,7 +119,7 @@ describe('walkManifests · dev-manifest catalog 通道(聚合宽/声明窄)'
|
|
|
119
119
|
* (local/codegen 全量副本——mall 生成应用主形态,不依赖 install)。
|
|
120
120
|
* 修复前探测只扫包根 → 三形态全 miss(vocabulary 恒空)。
|
|
121
121
|
*/
|
|
122
|
-
describe('walkManifests ·
|
|
122
|
+
describe('walkManifests · 词汇探测四跳 + slotMeta(B0;v7 += 跳⓪ config override 层)', () => {
|
|
123
123
|
it('P1: nm 包根布局(跳①)→ vocabulary 命中;slotMeta 透传', () => {
|
|
124
124
|
app = createTempApp('tbox-probe-');
|
|
125
125
|
const store = join(app.appDir, '_store-a');
|
|
@@ -185,6 +185,44 @@ describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
|
|
|
185
185
|
expect(walk.slotMeta).toEqual({ 'e.f': { name: '甲' } }); // 空串 description 不入表(与 title 守卫对称)
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
it('P6: 跳⓪ config override 层——meta per-service 覆盖真源 + 无键键穿透真源(首声明 = config 最前)', () => {
|
|
189
|
+
app = createTempApp('tbox-probe-');
|
|
190
|
+
const store = join(app.appDir, '_store-truth');
|
|
191
|
+
writeFakeContractsPackage(store, {
|
|
192
|
+
slots: ['a.b', 'c.d'],
|
|
193
|
+
meta: { 'a.b': { name: '真源甲' }, 'c.d': { name: '真源丙' } },
|
|
194
|
+
});
|
|
195
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
196
|
+
// 用户 override:只改名 a.b;c.d 不写(穿透真源);e.f 仅快照声明(幻影语义由 doctor 报告)
|
|
197
|
+
writeFileSync(
|
|
198
|
+
join(app.appDir, 'config', 'service-slots.json'),
|
|
199
|
+
JSON.stringify({ $comment: { seededAt: 'x' }, slots: ['a.b', 'e.f'], meta: { 'a.b': { name: '用户改名甲' } } }),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const walk = walkManifests(app.appDir);
|
|
203
|
+
// 词汇并集:config ∪ 真源(config 不遮蔽真源新增)
|
|
204
|
+
expect(walk.vocabulary).toEqual(['a.b', 'c.d', 'e.f']);
|
|
205
|
+
// meta 覆盖:config 先声明胜出;未覆盖键穿透真源
|
|
206
|
+
expect(walk.slotMeta['a.b']).toEqual({ name: '用户改名甲' });
|
|
207
|
+
expect(walk.slotMeta['c.d']).toEqual({ name: '真源丙' });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('P7: 跳⓪ 快照 meta 恒空(seed 形态)→ 真源 meta 恒穿透;$comment 键零解析影响', () => {
|
|
211
|
+
app = createTempApp('tbox-probe-');
|
|
212
|
+
const store = join(app.appDir, '_store-seed');
|
|
213
|
+
writeFakeContractsPackage(store, { slots: ['a.b'], meta: { 'a.b': { name: '真源甲' } } });
|
|
214
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
215
|
+
// seed-once 产物形态:slots 全量 + meta 空对象
|
|
216
|
+
writeFileSync(
|
|
217
|
+
join(app.appDir, 'config', 'service-slots.json'),
|
|
218
|
+
JSON.stringify({ $comment: { seededAt: '2026-09-21', slots: 1 }, slots: ['a.b'], meta: {} }),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const walk = walkManifests(app.appDir);
|
|
222
|
+
expect(walk.vocabulary).toEqual(['a.b']);
|
|
223
|
+
expect(walk.slotMeta['a.b']).toEqual({ name: '真源甲' }); // 空 meta 不夺覆盖权
|
|
224
|
+
});
|
|
225
|
+
|
|
188
226
|
it('P6: 无契约包(退化态)→ vocabulary 空 + slotMeta 空(P9 退化路径语义锁)', () => {
|
|
189
227
|
app = createTempApp('tbox-probe-');
|
|
190
228
|
const walk = walkManifests(app.appDir);
|
package/tests/views.test.ts
CHANGED
|
@@ -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
|
|
@@ -378,6 +379,41 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
378
379
|
expect(single.resolution.inheritance).toHaveLength(4);
|
|
379
380
|
});
|
|
380
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
|
+
|
|
381
417
|
it('O3 键集 = 服务词汇 ∪ 已配置;required echo(OR 聚合 + 平台 false)', async () => {
|
|
382
418
|
// vocabulary 空(无契约包 fixture 物化 slots——退化态回归锁;词汇在场态见 B1 族 T-O3)
|
|
383
419
|
const batch = await h.tk.loadServiceResolutions();
|
|
@@ -393,28 +429,29 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
393
429
|
expect(batch.issues.length).toBeGreaterThanOrEqual(single.issues.length);
|
|
394
430
|
});
|
|
395
431
|
|
|
396
|
-
it('O13 门控正反例:<0.9
|
|
432
|
+
it('O13 门控正反例:<0.9 读求值轴降级骨架(v7 503 退役)+ 静态 200 + 写端点 503 保留;多实例无显式默认 → 通配口径', async () => {
|
|
397
433
|
// 正例:≥0.9 求值可用(harness 默认)
|
|
398
434
|
const batch = await h.tk.loadServiceResolutions();
|
|
399
435
|
expect(batch.contracts.strictValidation).toBe(true);
|
|
400
436
|
h.app.dispose();
|
|
401
437
|
|
|
402
|
-
// 反例:缺 zod 面(0.8.2)→
|
|
438
|
+
// 反例:缺 zod 面(0.8.2)→ 读求值轴降级骨架 + N1 升级指引入 statusMessage;静态照常
|
|
403
439
|
const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
|
|
404
|
-
await
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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([]);
|
|
409
447
|
// 静态永不 503
|
|
410
448
|
const appView = await low.tk.loadApp();
|
|
411
449
|
expect(appView.services).toBeInstanceOf(Array);
|
|
412
|
-
//
|
|
450
|
+
// 写端点 503 门控保留(policy at edge 收窄至写/迁移面)
|
|
413
451
|
await expect(
|
|
414
452
|
low.tk.writeServiceIntegration('parking.query', { provider: 'mock' }),
|
|
415
453
|
).rejects.toMatchObject({ code: 'CONTRACTS_NOT_RESOLVED', httpStatus: 503 });
|
|
416
454
|
low.app.dispose();
|
|
417
|
-
|
|
418
455
|
// 默认实例委托:无 defaultInstanceId(多实例)→ '*' 通配口径(normalizeInstances null → '*')
|
|
419
456
|
const wild = harnessWithContracts();
|
|
420
457
|
writeAppFile(
|
|
@@ -698,41 +735,102 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
|
|
|
698
735
|
});
|
|
699
736
|
|
|
700
737
|
describe('A2 错误面(P-4 notes 优先 / P-5 畸形包 503)', () => {
|
|
701
|
-
it('M1 导入失败 message 含构建指引且不再误报未安装(P-4 notes
|
|
738
|
+
it('M1 导入失败 message 含构建指引且不再误报未安装(P-4 notes 优先——v7 降级骨架 statusMessage 承载)', async () => {
|
|
702
739
|
const app = createTempApp('tbox-m1-');
|
|
703
740
|
invalidateContractsResolver();
|
|
704
741
|
const store = joinStore(app.appDir, 'contracts');
|
|
705
742
|
writeFakeContractsPackage(store, { version: '0.9.0', brokenDist: true });
|
|
706
743
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
744
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
|
|
707
745
|
const tk = createAppToolkit(app.appDir);
|
|
708
|
-
await
|
|
709
|
-
|
|
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(/未安装/);
|
|
710
751
|
app.dispose();
|
|
711
752
|
});
|
|
712
753
|
|
|
713
|
-
it('
|
|
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
|
+
}));
|
|
768
|
+
const tk = createAppToolkit(app.appDir);
|
|
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 = walk.ownership(v6 归属单源,install id 轴——v6.1 起非 demand owners 包名轴);required = OR 聚合
|
|
774
|
+
expect(entry.module).toBe('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');
|
|
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();
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('M2 <0.9 message 对齐 api.md 信封(需 ≥0.9.0——resolver note 文案入降级 statusMessage)', async () => {
|
|
714
804
|
const app = createTempApp('tbox-m2-');
|
|
715
805
|
invalidateContractsResolver();
|
|
716
806
|
const store = joinStore(app.appDir, 'contracts');
|
|
717
807
|
writeFakeContractsPackage(store, { version: '0.8.2', withStrict: false });
|
|
718
808
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
809
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
|
|
719
810
|
const tk = createAppToolkit(app.appDir);
|
|
720
|
-
await
|
|
811
|
+
const entry = (await tk.loadServiceResolutions()).resolutions['parking.query'];
|
|
812
|
+
expect(entry.status).toBe('evaluation-unavailable');
|
|
813
|
+
expect(entry.statusMessage).toMatch(/需 ≥0\.9\.0/);
|
|
721
814
|
app.dispose();
|
|
722
815
|
});
|
|
723
816
|
|
|
724
|
-
it('M3
|
|
817
|
+
it('M3 畸形包(严格面在场求值面缺席)→ 降级骨架非 5xx(v7——evaluationContracts throw 面不再可达;形态异常 note 入 statusMessage)', async () => {
|
|
725
818
|
const app = createTempApp('tbox-m3-');
|
|
726
819
|
invalidateContractsResolver();
|
|
727
820
|
const store = joinStore(app.appDir, 'contracts');
|
|
728
821
|
writeFakeContractsPackage(store, { version: '0.9.0', missingEvaluationExports: true, slots: ['parking.query'] });
|
|
729
822
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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);
|
|
736
834
|
app.dispose();
|
|
737
835
|
});
|
|
738
836
|
});
|
|
@@ -849,7 +947,133 @@ describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单
|
|
|
849
947
|
app.dispose();
|
|
850
948
|
});
|
|
851
949
|
|
|
852
|
-
it('
|
|
950
|
+
it('D5 v6.1 归属三副本修复:owned-not-consumed(mall-core 形态)批量归因 + 内嵌键集 + 谓词豁免', async () => {
|
|
951
|
+
const app = createTempApp('tbox-d5-');
|
|
952
|
+
invalidateContractsResolver();
|
|
953
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
954
|
+
writeFakeContractsPackage(store, {
|
|
955
|
+
version: '0.9.0',
|
|
956
|
+
slots: ['auth.alipay-login', 'mall.info', 'mall.info-cache'],
|
|
957
|
+
});
|
|
958
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
959
|
+
// owns 3 / consumes 0(mall-core 形态——owns ⊄ consumes 的修复靶形态)
|
|
960
|
+
writeAppFile(
|
|
961
|
+
app.appDir,
|
|
962
|
+
'packages/module-mall/tbox.module.json',
|
|
963
|
+
JSON.stringify({
|
|
964
|
+
schemaVersion: 1,
|
|
965
|
+
id: 'module-mall',
|
|
966
|
+
version: '0.1.0',
|
|
967
|
+
kind: 'business',
|
|
968
|
+
declare: {
|
|
969
|
+
owns: [{ service: 'auth.alipay-login' }, { service: 'mall.info' }, { service: 'mall.info-cache' }],
|
|
970
|
+
consumes: [],
|
|
971
|
+
},
|
|
972
|
+
dependencies: { modules: [] },
|
|
973
|
+
env: [],
|
|
974
|
+
}),
|
|
975
|
+
);
|
|
976
|
+
writeAppFile(
|
|
977
|
+
app.appDir,
|
|
978
|
+
'config/integrations.json',
|
|
979
|
+
JSON.stringify({
|
|
980
|
+
provider: 'mock',
|
|
981
|
+
services: { 'mall.info': { provider: 'mock' }, 'orphan.svc': { provider: 'mock' } },
|
|
982
|
+
}),
|
|
983
|
+
);
|
|
984
|
+
const tk = createAppToolkit(app.appDir);
|
|
985
|
+
const batch = await tk.loadServiceResolutions();
|
|
986
|
+
// (a) 批量归因 = walk.ownership(install id 轴);required 缺席(demand echo——无人 consume)
|
|
987
|
+
expect(batch.resolutions['mall.info']).toMatchObject({ module: 'module-mall' });
|
|
988
|
+
expect(batch.resolutions['mall.info']?.required).toBeUndefined();
|
|
989
|
+
expect(batch.resolutions['auth.alipay-login']).toMatchObject({ module: 'module-mall' });
|
|
990
|
+
expect(batch.resolutions['mall.info-cache']).toMatchObject({ module: 'module-mall' });
|
|
991
|
+
// (d) 谓词 #3 豁免:owned+已配置不再误报「无消费方声明」;真孤儿仍报(语义收窄锁)
|
|
992
|
+
const slotNotDeclared = batch.issues.filter((i) => i.code === 'SLOT_NOT_DECLARED');
|
|
993
|
+
expect(slotNotDeclared.map((i) => i.service)).toEqual(['orphan.svc']);
|
|
994
|
+
// (a) 内嵌键集 = owned ∪ demand = owned(consumes 空——demand-only 过滤恒空缺陷回归锁)
|
|
995
|
+
const mod = await tk.loadModule('module-mall', { include: ['serviceResolutions'] });
|
|
996
|
+
expect(Object.keys(mod.serviceResolutions!.resolutions).sort()).toEqual([
|
|
997
|
+
'auth.alipay-login',
|
|
998
|
+
'mall.info',
|
|
999
|
+
'mall.info-cache',
|
|
1000
|
+
]);
|
|
1001
|
+
// owned 服务不落平台条目(moduleOccupiedServices 含 owns——AppView.services 排除不变)
|
|
1002
|
+
const appView = await tk.loadApp();
|
|
1003
|
+
expect(appView.services.map((s) => s.service)).not.toContain('mall.info');
|
|
1004
|
+
app.dispose();
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it('D6 v6.1 通道 1 键轴:npmModules 归因 = entry.id(install id,与模块清单可 join)≠ entry.package(demand owners 旧轴)', async () => {
|
|
1008
|
+
const app = createTempApp('tbox-d6-');
|
|
1009
|
+
invalidateContractsResolver();
|
|
1010
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
1011
|
+
writeFakeContractsPackage(store, { version: '0.9.0', slots: ['mall.info'] });
|
|
1012
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
1013
|
+
writeAppFile(
|
|
1014
|
+
app.appDir,
|
|
1015
|
+
'.tbox/app.json',
|
|
1016
|
+
JSON.stringify({
|
|
1017
|
+
templateVersion: 'test',
|
|
1018
|
+
npmModules: [{ id: 'module-mall', package: '@tbox.cn/app-module-mall', version: '0.1.0', mode: 'sdk' }],
|
|
1019
|
+
}),
|
|
1020
|
+
);
|
|
1021
|
+
writeAppFile(
|
|
1022
|
+
app.appDir,
|
|
1023
|
+
'packages/module-mall/tbox.module.json',
|
|
1024
|
+
JSON.stringify({
|
|
1025
|
+
schemaVersion: 1,
|
|
1026
|
+
id: 'module-mall',
|
|
1027
|
+
version: '0.1.0',
|
|
1028
|
+
kind: 'business',
|
|
1029
|
+
declare: { owns: [{ service: 'mall.info' }], consumes: [] },
|
|
1030
|
+
dependencies: { modules: [] },
|
|
1031
|
+
env: [],
|
|
1032
|
+
}),
|
|
1033
|
+
);
|
|
1034
|
+
const tk = createAppToolkit(app.appDir);
|
|
1035
|
+
const batch = await tk.loadServiceResolutions();
|
|
1036
|
+
expect(batch.resolutions['mall.info']?.module).toBe('module-mall');
|
|
1037
|
+
expect(batch.resolutions['mall.info']?.module).not.toBe('@tbox.cn/app-module-mall');
|
|
1038
|
+
app.dispose();
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
it('D7 v6.1 归属键集全轴联动:owned ∖ 词汇 fail-visible(求值可见 + 静态 200 + 双键直击 + PUT 放行)', async () => {
|
|
1042
|
+
const app = createTempApp('tbox-d7-');
|
|
1043
|
+
invalidateContractsResolver();
|
|
1044
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
1045
|
+
writeFakeContractsPackage(store, { version: '0.9.0' }); // 词汇空——ghost.owned 纯归属形态
|
|
1046
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
1047
|
+
writeAppFile(
|
|
1048
|
+
app.appDir,
|
|
1049
|
+
'packages/module-x/tbox.module.json',
|
|
1050
|
+
JSON.stringify({
|
|
1051
|
+
schemaVersion: 1,
|
|
1052
|
+
id: 'module-x',
|
|
1053
|
+
version: '0.1.0',
|
|
1054
|
+
kind: 'business',
|
|
1055
|
+
declare: { owns: [{ service: 'ghost.owned' }], consumes: [] },
|
|
1056
|
+
dependencies: { modules: [] },
|
|
1057
|
+
env: [],
|
|
1058
|
+
}),
|
|
1059
|
+
);
|
|
1060
|
+
const tk = createAppToolkit(app.appDir);
|
|
1061
|
+
// 求值轴可见(resolutionKeys += 归属):module 归因 + 未配置态
|
|
1062
|
+
const batch = await tk.loadServiceResolutions();
|
|
1063
|
+
expect(batch.resolutions['ghost.owned']).toMatchObject({ module: 'module-x', status: 'service-not-configured' });
|
|
1064
|
+
// 静态单体可达(isKnownService += 归属——修复前 404 死锁)
|
|
1065
|
+
const svc = await tk.loadService('ghost.owned');
|
|
1066
|
+
expect(svc.service).toBe('ghost.owned');
|
|
1067
|
+
// 双键寻址直击(resolveServiceKey += 归属)
|
|
1068
|
+
const detail = await tk.loadServiceResolution('ghost.owned');
|
|
1069
|
+
expect(detail.resolution.module).toBe('module-x');
|
|
1070
|
+
// 写闸放行(P9 判据 += 归属——诊断指引的编辑不再被拒)
|
|
1071
|
+
const put = await tk.writeServiceIntegration('ghost.owned', { provider: 'mock' });
|
|
1072
|
+
expect(put.written).toBe(true);
|
|
1073
|
+
app.dispose();
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it('D6 偏斜态分级(P1 标记门 v7):v5 contracts 假件 + v6 toolkit → 写端点 503 保留;读求值轴降级骨架(503 退役)', async () => {
|
|
853
1077
|
const app = createTempApp('tbox-d6-');
|
|
854
1078
|
invalidateContractsResolver();
|
|
855
1079
|
const store = joinStore(app.appDir, 'contracts');
|
|
@@ -859,15 +1083,21 @@ describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单
|
|
|
859
1083
|
schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
|
|
860
1084
|
declare: { owns: [{ service: 'parking.query' }] },
|
|
861
1085
|
}));
|
|
1086
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
|
|
862
1087
|
const tk = createAppToolkit(app.appDir);
|
|
863
|
-
//
|
|
1088
|
+
// 写路径门控保留(assertContractsUsable——policy at edge 收窄至写/迁移面)
|
|
864
1089
|
await expect(tk.writeModuleIntegration('module-parking', { provider: 'wanda' })).rejects.toMatchObject({
|
|
865
1090
|
code: 'CONTRACTS_NOT_RESOLVED',
|
|
866
1091
|
httpStatus: 503,
|
|
867
1092
|
message: expect.stringContaining('OWNERSHIP_BINDING_V6'),
|
|
868
1093
|
});
|
|
869
|
-
|
|
870
|
-
|
|
1094
|
+
// 读求值轴降级(503 退役):statusMessage 与写门控同源(N1 标记分支单源)
|
|
1095
|
+
const degraded = await tk.loadServiceResolutions();
|
|
1096
|
+
expect(degraded.contracts.strictValidation).toBe(true);
|
|
1097
|
+
const entry = degraded.resolutions['parking.query'];
|
|
1098
|
+
expect(entry.status).toBe('evaluation-unavailable');
|
|
1099
|
+
expect(entry.statusMessage).toMatch(/OWNERSHIP_BINDING_V6 标记缺席/);
|
|
1100
|
+
// 静态端点照常(永不 503——门控 policy at edge 只拦写/迁移)
|
|
871
1101
|
const mod = await tk.loadModule('module-parking');
|
|
872
1102
|
expect(mod.ownedServices).toEqual(['parking.query']);
|
|
873
1103
|
app.dispose();
|
|
@@ -921,15 +1151,20 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
921
1151
|
h2.app.dispose();
|
|
922
1152
|
});
|
|
923
1153
|
|
|
924
|
-
it('I3 degrade
|
|
1154
|
+
it('I3 degrade:内嵌轴同步降级(v7 P14 (b)——D31 null 分支退役):三条件不满足 → 骨架非 null', async () => {
|
|
925
1155
|
const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
|
|
926
|
-
|
|
1156
|
+
const lowInc = await low.tk.loadApp({ include: ['serviceResolutions'] });
|
|
1157
|
+
expect(lowInc.serviceResolutions).not.toBeNull();
|
|
1158
|
+
const lowEntries = Object.values(lowInc.serviceResolutions!.resolutions);
|
|
1159
|
+
expect(lowEntries.length).toBeGreaterThan(0);
|
|
1160
|
+
expect(lowEntries.every((e) => e.status === 'evaluation-unavailable')).toBe(true);
|
|
927
1161
|
expect((await low.tk.loadApp({ include: ['integrationSchemas'] })).integrationSchemas).toEqual({ provider: 'mock', services: {} }); // 静态轴富化零 contracts
|
|
928
1162
|
low.app.dispose();
|
|
1163
|
+
// 畸形包(严格面在场求值面缺席)→ 内嵌轴同样降级(503/双 null 全退役)
|
|
929
1164
|
const malformed = harnessWithContracts({ missingEvaluationExports: true });
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1165
|
+
const malformedInc = await malformed.tk.loadApp({ include: ['serviceResolutions'] });
|
|
1166
|
+
expect(malformedInc.serviceResolutions).not.toBeNull();
|
|
1167
|
+
expect(Object.values(malformedInc.serviceResolutions!.resolutions)[0]?.status).toBe('evaluation-unavailable');
|
|
933
1168
|
malformed.app.dispose();
|
|
934
1169
|
});
|
|
935
1170
|
|