@tbox.cn/app-toolkit 0.6.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.
- package/README.md +2 -2
- package/dist/index.d.ts +37 -9
- package/dist/index.js +9 -9
- package/package.json +1 -1
- package/src/assembly/walk-manifests.ts +28 -12
- package/src/core/errors.ts +12 -2
- package/src/dto.ts +6 -2
- package/src/factory.ts +30 -18
- package/src/index.ts +2 -2
- package/src/views/service-resolutions.ts +113 -25
- package/tests/dev-manifest-catalog.test.ts +39 -1
- package/tests/views.test.ts +140 -31
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': {} } }));
|
|
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
|
+
}));
|
|
707
768
|
const tk = createAppToolkit(app.appDir);
|
|
708
|
-
await
|
|
709
|
-
|
|
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');
|
|
710
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();
|
|
711
801
|
});
|
|
712
802
|
|
|
713
|
-
it('M2 <0.9 message 对齐 api.md 信封(需 ≥0.9.0——resolver note
|
|
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,7 @@ describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单
|
|
|
849
947
|
app.dispose();
|
|
850
948
|
});
|
|
851
949
|
|
|
852
|
-
it('D6
|
|
950
|
+
it('D6 偏斜态分级(P1 标记门 v7):v5 contracts 假件 + v6 toolkit → 写端点 503 保留;读求值轴降级骨架(503 退役)', async () => {
|
|
853
951
|
const app = createTempApp('tbox-d6-');
|
|
854
952
|
invalidateContractsResolver();
|
|
855
953
|
const store = joinStore(app.appDir, 'contracts');
|
|
@@ -859,15 +957,21 @@ describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单
|
|
|
859
957
|
schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
|
|
860
958
|
declare: { owns: [{ service: 'parking.query' }] },
|
|
861
959
|
}));
|
|
960
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
|
|
862
961
|
const tk = createAppToolkit(app.appDir);
|
|
863
|
-
//
|
|
962
|
+
// 写路径门控保留(assertContractsUsable——policy at edge 收窄至写/迁移面)
|
|
864
963
|
await expect(tk.writeModuleIntegration('module-parking', { provider: 'wanda' })).rejects.toMatchObject({
|
|
865
964
|
code: 'CONTRACTS_NOT_RESOLVED',
|
|
866
965
|
httpStatus: 503,
|
|
867
966
|
message: expect.stringContaining('OWNERSHIP_BINDING_V6'),
|
|
868
967
|
});
|
|
869
|
-
|
|
870
|
-
|
|
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 只拦写/迁移)
|
|
871
975
|
const mod = await tk.loadModule('module-parking');
|
|
872
976
|
expect(mod.ownedServices).toEqual(['parking.query']);
|
|
873
977
|
app.dispose();
|
|
@@ -921,15 +1025,20 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
921
1025
|
h2.app.dispose();
|
|
922
1026
|
});
|
|
923
1027
|
|
|
924
|
-
it('I3 degrade
|
|
1028
|
+
it('I3 degrade:内嵌轴同步降级(v7 P14 (b)——D31 null 分支退役):三条件不满足 → 骨架非 null', async () => {
|
|
925
1029
|
const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
|
|
926
|
-
|
|
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);
|
|
927
1035
|
expect((await low.tk.loadApp({ include: ['integrationSchemas'] })).integrationSchemas).toEqual({ provider: 'mock', services: {} }); // 静态轴富化零 contracts
|
|
928
1036
|
low.app.dispose();
|
|
1037
|
+
// 畸形包(严格面在场求值面缺席)→ 内嵌轴同样降级(503/双 null 全退役)
|
|
929
1038
|
const malformed = harnessWithContracts({ missingEvaluationExports: true });
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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');
|
|
933
1042
|
malformed.app.dispose();
|
|
934
1043
|
});
|
|
935
1044
|
|