@tbox.cn/app-toolkit 0.3.0 → 0.4.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/dist/index.d.ts +91 -9
- package/dist/index.js +9 -8
- package/package.json +2 -2
- package/src/assembly/walk-manifests.ts +95 -28
- package/src/core/env-expansion.ts +14 -12
- package/src/core/module-schema.ts +3 -0
- package/src/dto.ts +13 -0
- package/src/index.ts +4 -0
- package/src/integrations/evaluate-app.ts +41 -0
- package/src/integrations/prune-bindings.ts +186 -0
- package/src/views/app.ts +6 -2
- package/src/views/context.ts +20 -0
- package/src/views/modules.ts +25 -13
- package/src/views/providers.ts +13 -1
- package/src/views/service-detail.ts +5 -3
- package/tests/demo-app.ts +27 -3
- package/tests/dev-manifest-catalog.test.ts +81 -1
- package/tests/module-schema.test.ts +15 -0
- package/tests/vendor-schema-keywords.test.ts +12 -0
- package/tests/views.test.ts +159 -3
package/src/views/modules.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ModuleDeclaration, ModuleDetailView, ProviderBinding, ServiceDemand } from '../dto.js';
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
3
|
import { moduleDomainWritable, resolveDomainBinding } from '../integrations/domain-binding.js';
|
|
4
|
-
import type
|
|
4
|
+
import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 需求轴/模块静态投影(views/modules;C4)。静态端点结构性零求值(铁律 #3——
|
|
@@ -19,7 +19,7 @@ function familyName(s: string): string {
|
|
|
19
19
|
return s.replace(/^@tbox\.cn\/app-/, '').replace(/^@[^/]+\//, '');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** provider 家族包判定(模块清单排除供给/实施载体;生态三先例同源:
|
|
22
|
+
/** provider 家族包判定(模块清单排除供给/实施载体;生态三先例同源:stage 排除判定 /
|
|
23
23
|
* guard-vendor-neutrality「provider-* 包」/ cli deriveModuleId 家族推导)。install id 与
|
|
24
24
|
* 包名 basename 双查 = 防 .tbox/app.json 登记漂移纵深;命名而非结构(provider-custom 零贡献
|
|
25
25
|
* 空壳与「零贡献合法」形态不可结构区分);未来收敛 = manifest 显式角色标记(届时整体退场)。 */
|
|
@@ -34,10 +34,9 @@ export function loadModulesView(snapshot: ViewSnapshot): { modules: ModuleDeclar
|
|
|
34
34
|
.map((mod) => ({
|
|
35
35
|
id: mod.id,
|
|
36
36
|
name: mod.descriptor.name,
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
description: '',
|
|
37
|
+
// 模块 meta(B1 additive):manifest title/description——兜底 name / ''(视图层灌值)
|
|
38
|
+
title: mod.descriptor.title ?? mod.descriptor.name,
|
|
39
|
+
description: mod.descriptor.description ?? '',
|
|
41
40
|
}))
|
|
42
41
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
43
42
|
return { modules };
|
|
@@ -49,12 +48,16 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
|
|
|
49
48
|
if (!mod) {
|
|
50
49
|
throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
|
|
51
50
|
}
|
|
52
|
-
const services: ServiceDemand[] = (mod.descriptor.contributes.services ?? []).map((need) =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
const services: ServiceDemand[] = (mod.descriptor.contributes.services ?? []).map((need) => {
|
|
52
|
+
// 展示名单源走链(B1):词汇 meta > demand meta > id——词汇主源防 manifest demand 漂移
|
|
53
|
+
const display = resolveServiceDisplay(snapshot.walk, need.service);
|
|
54
|
+
return {
|
|
55
|
+
service: need.service,
|
|
56
|
+
title: display.title,
|
|
57
|
+
...(display.description !== undefined ? { description: display.description } : {}),
|
|
58
|
+
required: need.optional === false,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
58
61
|
const w = moduleDomainWritable(snapshot.walk, mod);
|
|
59
62
|
const domains = (snapshot.integrations as { domains?: Record<string, unknown> } | null)?.domains ?? {};
|
|
60
63
|
// integration = domains[换算键] 节点原值(D21 编辑预填源);未配置 = null;多域/零域/共享键 = null(D34)
|
|
@@ -63,5 +66,14 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
|
|
|
63
66
|
const binding = resolveDomainBinding(snapshot.integrations, d);
|
|
64
67
|
return { domain: d, effective: binding.provider, layer: binding.layer };
|
|
65
68
|
});
|
|
66
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
// 模块 meta(B1 additive)——兜底与 /modules 同语义
|
|
71
|
+
title: mod.descriptor.title ?? mod.descriptor.name,
|
|
72
|
+
description: mod.descriptor.description ?? '',
|
|
73
|
+
domainKeys: [...w.domainKeys],
|
|
74
|
+
integration,
|
|
75
|
+
providerBindings,
|
|
76
|
+
configEditable: w.writable,
|
|
77
|
+
services,
|
|
78
|
+
};
|
|
67
79
|
}
|
package/src/views/providers.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ProviderDetail, ProviderEntry, ProviderServiceDetail, ProvidersVie
|
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
3
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
-
import type
|
|
5
|
+
import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 供给轴投影(views/providers;C4)。供给关系三切片(P5):
|
|
@@ -21,6 +21,9 @@ export function loadProvidersView(snapshot: ViewSnapshot): ProvidersView {
|
|
|
21
21
|
provider,
|
|
22
22
|
local: entries.every((e) => e.local === true),
|
|
23
23
|
owner: entries[0].owner,
|
|
24
|
+
// 厂商展示元数据(B1 walk 层传播——首条目 = 首声明,与 owner 同语义)
|
|
25
|
+
...(entries[0].title ? { title: entries[0].title } : {}),
|
|
26
|
+
...(entries[0].description ? { description: entries[0].description } : {}),
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
return { providers: providers.sort((a, b) => a.provider.localeCompare(b.provider)) };
|
|
@@ -40,6 +43,12 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
|
|
|
40
43
|
bucket.implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
|
|
41
44
|
}
|
|
42
45
|
}
|
|
46
|
+
// 服务展示名走链(B1):词汇 meta > demand meta > id(title 恒发)
|
|
47
|
+
for (const [service, bucket] of Object.entries(services)) {
|
|
48
|
+
const display = resolveServiceDisplay(snapshot.walk, service);
|
|
49
|
+
bucket.title = display.title;
|
|
50
|
+
if (display.description !== undefined) bucket.description = display.description;
|
|
51
|
+
}
|
|
43
52
|
if (include?.has('integrationSchemas')) {
|
|
44
53
|
for (const [service, bucket] of Object.entries(services)) {
|
|
45
54
|
bucket.integrationSchemas = { configSchema: resolveServiceConfigSchema(snapshot, provider, service) };
|
|
@@ -50,6 +59,9 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
|
|
|
50
59
|
provider,
|
|
51
60
|
local: entries.every((e) => e.local === true),
|
|
52
61
|
owner: entries[0].owner,
|
|
62
|
+
// 厂商展示元数据(B1 walk 层传播——首条目透传)
|
|
63
|
+
...(entries[0].title ? { title: entries[0].title } : {}),
|
|
64
|
+
...(entries[0].description ? { description: entries[0].description } : {}),
|
|
53
65
|
services,
|
|
54
66
|
};
|
|
55
67
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ServiceDetail } from '../dto.js';
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
3
|
import { isKnownService } from '../integrations/read.js';
|
|
4
|
-
import { suppliedByVendors, type ViewSnapshot } from './context.js';
|
|
4
|
+
import { resolveServiceDisplay, suppliedByVendors, type ViewSnapshot } from './context.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 服务静态单体投影(views/service-detail;C4)。求值数据不在此(→ /service-resolutions/:service)。
|
|
@@ -17,10 +17,12 @@ export function loadServiceDetailView(snapshot: ViewSnapshot, service: string):
|
|
|
17
17
|
}
|
|
18
18
|
// integration = 文件 services[s] 逐字原值(编辑预填源 D21);无声明 = null
|
|
19
19
|
const integration = (configured[service] as ServiceDetail['integration'] | undefined) ?? null;
|
|
20
|
+
// 展示名单源走链(B1):词汇 meta > demand meta > id——替换 demand 直拼(恒有值)
|
|
21
|
+
const display = resolveServiceDisplay(snapshot.walk, service);
|
|
20
22
|
return {
|
|
21
23
|
service,
|
|
22
|
-
|
|
23
|
-
...(
|
|
24
|
+
title: display.title,
|
|
25
|
+
...(display.description !== undefined ? { description: display.description } : {}),
|
|
24
26
|
required: demand ? !demand.optional : false,
|
|
25
27
|
// defaults 唯一落位(manifest 预填)
|
|
26
28
|
defaults: demand ? demand.defaults.map((d) => ({ ...d })) : [],
|
package/tests/demo-app.ts
CHANGED
|
@@ -50,6 +50,11 @@ export interface FakeContractsOptions {
|
|
|
50
50
|
resolveBody?: string;
|
|
51
51
|
/** service-slots.json 词汇(P9 身份判据/求值键集;缺省不物化 = 词汇空) */
|
|
52
52
|
slots?: string[];
|
|
53
|
+
/** 词汇 meta 节(per-service {title,description}——B1 展示名链 fixture) */
|
|
54
|
+
meta?: Record<string, { title?: string; description?: string }>;
|
|
55
|
+
/** service-slots.json 落位:'root' = 包根(缺省——夹具/旧布局形态);
|
|
56
|
+
* 'src' = 包内 src/(发布包 src 附带 / shim 链接形态——B0 探测跳 ②) */
|
|
57
|
+
slotsLayout?: 'root' | 'src';
|
|
53
58
|
/** zod 形拒绝 fixture(A1 T9/T10):parseIntegrationsConfig 恒抛带 issues 的结构化错误
|
|
54
59
|
* (结构性 ZodError 形——issues[].path 供 fields 断言;仅在专属 fixture 上使用) */
|
|
55
60
|
zodRejectPaths?: string[][];
|
|
@@ -58,7 +63,7 @@ export interface FakeContractsOptions {
|
|
|
58
63
|
/** 假 contracts 包:package.json exports 形态逐字段复刻发布面(types+import 双条件、无 require/default)
|
|
59
64
|
* ——resolver 不经 exports 解析(包根直探),该形态为 E4 类错误的机械防线。 */
|
|
60
65
|
export function writeFakeContractsPackage(packageRoot: string, opts: FakeContractsOptions = {}): void {
|
|
61
|
-
const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, slots, zodRejectPaths } = opts;
|
|
66
|
+
const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, slots, meta, slotsLayout = 'root', zodRejectPaths } = opts;
|
|
62
67
|
const strictBody = withStrict
|
|
63
68
|
? `export function parseIntegrationsConfig(raw) { ${
|
|
64
69
|
rejectMarker ? `if (JSON.stringify(raw).includes(${JSON.stringify(rejectMarker)})) throw new Error("fake strict reject"); ` : ''
|
|
@@ -113,12 +118,31 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
|
|
|
113
118
|
mkdirSync(join(packageRoot, 'src'), { recursive: true });
|
|
114
119
|
writeFileSync(join(packageRoot, 'src', 'runtime.ts'), exportsBody);
|
|
115
120
|
}
|
|
116
|
-
// service-slots.json(P9
|
|
121
|
+
// service-slots.json(P9 词汇源——探测三跳发现位;缺省不物化 = 词汇空;
|
|
122
|
+
// slotsLayout 控制包根/src 落位——B0 探测跳①/②)
|
|
117
123
|
if (slots && slots.length > 0) {
|
|
118
|
-
|
|
124
|
+
const slotsFile = slotsLayout === 'src' ? join(packageRoot, 'src', 'service-slots.json') : join(packageRoot, 'service-slots.json');
|
|
125
|
+
mkdirSync(dirname(slotsFile), { recursive: true });
|
|
126
|
+
writeFileSync(slotsFile, `${JSON.stringify(meta ? { slots, meta } : { slots }, null, 2)}\n`);
|
|
119
127
|
}
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
/**
|
|
131
|
+
* 候选自身 src 词汇包(B0 探测跳 ③——local/codegen 全量副本形态):
|
|
132
|
+
* 落 <appDir>/packages/<name>/src/service-slots.json(无 package.json/node_modules——
|
|
133
|
+
* mall 生成应用主形态:stage rewriteExportsToSrc 展开后永驻 packages/*,不依赖 install)。
|
|
134
|
+
*/
|
|
135
|
+
export function writeSelfSrcContractPackage(
|
|
136
|
+
appDir: string,
|
|
137
|
+
name: string,
|
|
138
|
+
content: { slots: string[]; meta?: Record<string, { title?: string; description?: string }> },
|
|
139
|
+
): string {
|
|
140
|
+
const dir = join(appDir, 'packages', name, 'src');
|
|
141
|
+
mkdirSync(dir, { recursive: true });
|
|
142
|
+
writeFileSync(join(dir, 'service-slots.json'), `${JSON.stringify(content, null, 2)}\n`);
|
|
143
|
+
return join(appDir, 'packages', name);
|
|
144
|
+
}
|
|
145
|
+
|
|
122
146
|
export interface LinkOptions {
|
|
123
147
|
/** 候选子目录:''(appDir 根)/ 'apps/server' / 'apps/client' / 'packages/<id>' */
|
|
124
148
|
subdir: string;
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, afterEach } from 'vitest';
|
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { walkManifests } from '../src/assembly/walk-manifests.js';
|
|
5
|
-
import { createTempApp, type TempApp } from './demo-app.js';
|
|
5
|
+
import { createTempApp, linkContracts, writeFakeContractsPackage, writeSelfSrcContractPackage, type TempApp } from './demo-app.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* dev-manifest catalog 通道(通道 1.5)单测:聚合宽/声明窄不变量锁定。
|
|
@@ -112,3 +112,83 @@ describe('walkManifests · dev-manifest catalog 通道(聚合宽/声明窄)'
|
|
|
112
112
|
expect(walk.modules).toHaveLength(0);
|
|
113
113
|
});
|
|
114
114
|
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 词汇探测三跳(B0 回归锁):词汇源三真实布局全覆盖——
|
|
118
|
+
* ① nm 包根(夹具/旧布局)② nm src/(发布包 src 附带 + 模板 shim 链接)③ 候选自身 src/
|
|
119
|
+
* (local/codegen 全量副本——mall 生成应用主形态,不依赖 install)。
|
|
120
|
+
* 修复前探测只扫包根 → 三形态全 miss(vocabulary 恒空)。
|
|
121
|
+
*/
|
|
122
|
+
describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
|
|
123
|
+
it('P1: nm 包根布局(跳①)→ vocabulary 命中;slotMeta 透传', () => {
|
|
124
|
+
app = createTempApp('tbox-probe-');
|
|
125
|
+
const store = join(app.appDir, '_store-a');
|
|
126
|
+
writeFakeContractsPackage(store, {
|
|
127
|
+
slots: ['auth.alipay-login', 'mall.info'],
|
|
128
|
+
meta: { 'auth.alipay-login': { title: '支付宝登录', description: '登录换码(code2Session)' } },
|
|
129
|
+
});
|
|
130
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
131
|
+
|
|
132
|
+
const walk = walkManifests(app.appDir);
|
|
133
|
+
expect(walk.vocabulary).toEqual(['auth.alipay-login', 'mall.info']);
|
|
134
|
+
expect(walk.slotMeta['auth.alipay-login']).toEqual({ title: '支付宝登录', description: '登录换码(code2Session)' });
|
|
135
|
+
expect(walk.slotMeta['mall.info']).toBeUndefined(); // meta 缺席槽不入表
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('P2: nm src 布局(跳②——发布包 src 附带 / shim 链接形态)→ 命中', () => {
|
|
139
|
+
app = createTempApp('tbox-probe-');
|
|
140
|
+
const store = join(app.appDir, '_store-b');
|
|
141
|
+
writeFakeContractsPackage(store, { slots: ['parking.query'], slotsLayout: 'src' });
|
|
142
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
143
|
+
|
|
144
|
+
const walk = walkManifests(app.appDir);
|
|
145
|
+
expect(walk.vocabulary).toEqual(['parking.query']);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('P3: 候选自身 src 布局(跳③——local/codegen 全量副本,无 node_modules 链接)→ 命中', () => {
|
|
149
|
+
app = createTempApp('tbox-probe-');
|
|
150
|
+
writeSelfSrcContractPackage(app.appDir, 'contracts-mall', {
|
|
151
|
+
slots: ['member.account', 'parking.payment'],
|
|
152
|
+
meta: { 'member.account': { title: '会员账户' } },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const walk = walkManifests(app.appDir);
|
|
156
|
+
expect(walk.vocabulary).toEqual(['member.account', 'parking.payment']);
|
|
157
|
+
expect(walk.slotMeta['member.account']).toEqual({ title: '会员账户' });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('P4: 多包同槽 meta → 发现序首声明胜出(apps/* 先于 packages/* 候选序)', () => {
|
|
161
|
+
app = createTempApp('tbox-probe-');
|
|
162
|
+
const storeA = join(app.appDir, '_store-first');
|
|
163
|
+
writeFakeContractsPackage(storeA, { slots: ['x.y'], meta: { 'x.y': { title: '甲' } } });
|
|
164
|
+
linkContracts(app.appDir, storeA, { subdir: 'apps/server' });
|
|
165
|
+
const storeB = join(app.appDir, '_store-second');
|
|
166
|
+
writeFakeContractsPackage(storeB, { slots: ['x.y'], meta: { 'x.y': { title: '乙' } } });
|
|
167
|
+
linkContracts(app.appDir, storeB, { subdir: 'packages/mock' });
|
|
168
|
+
|
|
169
|
+
const walk = walkManifests(app.appDir);
|
|
170
|
+
expect(walk.slotMeta['x.y']).toEqual({ title: '甲' });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('P5: 坏 meta 条目(非对象/非 string/空串)→ 宽松跳过,slots 正常', () => {
|
|
174
|
+
app = createTempApp('tbox-probe-');
|
|
175
|
+
const store = join(app.appDir, '_store-bad');
|
|
176
|
+
writeFakeContractsPackage(store, { slots: ['a.b'] });
|
|
177
|
+
writeFileSync(
|
|
178
|
+
join(store, 'service-slots.json'),
|
|
179
|
+
JSON.stringify({ slots: ['a.b', 'c.d', 'e.f'], meta: { 'a.b': '字符串', 'c.d': { title: 42 }, 'e.f': { title: '甲', description: '' } } }),
|
|
180
|
+
);
|
|
181
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
182
|
+
|
|
183
|
+
const walk = walkManifests(app.appDir);
|
|
184
|
+
expect(walk.vocabulary).toEqual(['a.b', 'c.d', 'e.f']);
|
|
185
|
+
expect(walk.slotMeta).toEqual({ 'e.f': { title: '甲' } }); // 空串 description 不入表(与 title 守卫对称)
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('P6: 无契约包(退化态)→ vocabulary 空 + slotMeta 空(P9 退化路径语义锁)', () => {
|
|
189
|
+
app = createTempApp('tbox-probe-');
|
|
190
|
+
const walk = walkManifests(app.appDir);
|
|
191
|
+
expect(walk.vocabulary).toEqual([]);
|
|
192
|
+
expect(walk.slotMeta).toEqual({});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -81,4 +81,19 @@ describe('module-schema(descriptor zod 单源)', () => {
|
|
|
81
81
|
expect(parsed.data.contributes.resources).toEqual([]);
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
it('B1 模块 meta additive:根级 title/description 保真;缺席仍合法(回退归视图层)', () => {
|
|
86
|
+
const withMeta = parseModuleDescriptor({ ...BASE_MANIFEST, title: '停车缴费', description: '车辆查询与绑定、停车费用查询、缴费支付与开票' });
|
|
87
|
+
expect(withMeta.ok).toBe(true);
|
|
88
|
+
if (withMeta.ok) {
|
|
89
|
+
expect(withMeta.value.title).toBe('停车缴费');
|
|
90
|
+
expect(withMeta.value.description).toContain('停车费用查询');
|
|
91
|
+
}
|
|
92
|
+
const withoutMeta = moduleDescriptorSchema.safeParse(BASE_MANIFEST);
|
|
93
|
+
expect(withoutMeta.success).toBe(true);
|
|
94
|
+
if (withoutMeta.success) {
|
|
95
|
+
expect(withoutMeta.data.title).toBeUndefined();
|
|
96
|
+
expect(withoutMeta.data.description).toBeUndefined();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
84
99
|
});
|
|
@@ -98,4 +98,16 @@ describe('vendor schema 关键词矩阵 ⊆ 渲染器支持集(D32 表单引
|
|
|
98
98
|
expect(used.has(k), `基线关键词 ${k} 未被提取到——提取器或目录布局变化,人工核对`).toBe(true);
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
|
+
|
|
102
|
+
it('B5 展示名 tripwire:全文件 properties.*.title 必在(新字段裸键名结构性不可上线)', () => {
|
|
103
|
+
const files = schemaFiles();
|
|
104
|
+
expect(files.length).toBeGreaterThan(5);
|
|
105
|
+
for (const { pkg, rel } of files) {
|
|
106
|
+
const doc = JSON.parse(readFileSync(rel, 'utf8')) as { properties?: Record<string, unknown> };
|
|
107
|
+
for (const [key, sub] of Object.entries(doc.properties ?? {})) {
|
|
108
|
+
const hasTitle = !!sub && typeof sub === 'object' && typeof (sub as { title?: unknown }).title === 'string';
|
|
109
|
+
expect(hasTitle, `${pkg} 字段 "${key}" 缺 title——集成配置表单展示名必填(B5 防线;纯名词,长尾注记移 description)`).toBe(true);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
101
113
|
});
|
package/tests/views.test.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { mkdirSync, readFileSync, symlinkSync } from 'node:fs';
|
|
|
4
4
|
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
|
-
import { walkManifests } from '../src/assembly/walk-manifests.js';
|
|
7
|
+
import { walkManifests, type WalkManifestsResult } from '../src/assembly/walk-manifests.js';
|
|
8
8
|
import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas } from '../src/views/integration-schemas.js';
|
|
9
|
+
import { resolveServiceDisplay } from '../src/views/context.js';
|
|
9
10
|
import type { ViewSnapshot } from '../src/views/context.js';
|
|
10
11
|
import { URL_METHOD_MAP } from './naming-alignment.test.js';
|
|
11
12
|
import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
|
|
@@ -258,7 +259,8 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
258
259
|
expect(Object.keys(appView)).toEqual(['services', 'integration']);
|
|
259
260
|
const mod = await h.tk.loadModule('module-parking');
|
|
260
261
|
// v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
|
|
261
|
-
|
|
262
|
+
// B1 additive:模块 meta title/description 前置(恒发——兜底 name/'')
|
|
263
|
+
expect(Object.keys(mod)).toEqual(['title', 'description', 'domainKeys', 'integration', 'providerBindings', 'configEditable', 'services']);
|
|
262
264
|
const svc = await h.tk.loadService('parking.query');
|
|
263
265
|
expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
|
|
264
266
|
// 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
|
|
@@ -279,7 +281,7 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
279
281
|
});
|
|
280
282
|
|
|
281
283
|
it('O3 键集 = 服务词汇 ∪ 已配置;required echo(OR 聚合 + 平台 false)', async () => {
|
|
282
|
-
// vocabulary 空(无契约包 fixture
|
|
284
|
+
// vocabulary 空(无契约包 fixture 物化 slots——退化态回归锁;词汇在场态见 B1 族 T-O3)
|
|
283
285
|
const batch = await h.tk.loadServiceResolutions();
|
|
284
286
|
expect(Object.keys(batch.resolutions).sort()).toEqual(['auth.alipay-login', 'parking.query']);
|
|
285
287
|
// parking.query 由 module-parking demand required:0 → required true
|
|
@@ -1697,3 +1699,157 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1697
1699
|
h2.app.dispose();
|
|
1698
1700
|
});
|
|
1699
1701
|
});
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* B1 展示名链(模块 meta / 词汇 meta 单源 / resolveServiceDisplay 兜底链):
|
|
1705
|
+
* 兜底链 = 词汇 meta(slotMeta)> demand meta(catalog.services)> service id(逐字段回落)。
|
|
1706
|
+
* fixture:词汇 meta 停车查询 vs demand meta 车位查询——断言词汇胜出(防漂移锁)。
|
|
1707
|
+
*/
|
|
1708
|
+
describe('B1 展示名链', () => {
|
|
1709
|
+
/** 展示名 fixture 基座:词汇物化(3 槽 2 meta)+ 停车模块(带 meta + demand meta)+ demo 厂商 */
|
|
1710
|
+
function displayHarness(): Harness {
|
|
1711
|
+
const app = createTempApp('tbox-display-');
|
|
1712
|
+
invalidateContractsResolver();
|
|
1713
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
1714
|
+
writeFakeContractsPackage(store, {
|
|
1715
|
+
version: '0.9.0',
|
|
1716
|
+
slots: ['parking.query', 'auth.alipay-login', 'mall.info'],
|
|
1717
|
+
meta: {
|
|
1718
|
+
'parking.query': { title: '停车查询', description: '费用报价、车场信息、可享权益' },
|
|
1719
|
+
'auth.alipay-login': { title: '支付宝登录' },
|
|
1720
|
+
},
|
|
1721
|
+
});
|
|
1722
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
1723
|
+
mkdirDeep(join(app.appDir, 'packages', 'module-parking'));
|
|
1724
|
+
writeAppFile(
|
|
1725
|
+
app.appDir,
|
|
1726
|
+
'packages/module-parking/tbox.module.json',
|
|
1727
|
+
JSON.stringify({
|
|
1728
|
+
schemaVersion: 1,
|
|
1729
|
+
name: 'module-parking',
|
|
1730
|
+
version: '0.1.0',
|
|
1731
|
+
kind: 'business',
|
|
1732
|
+
title: '停车缴费',
|
|
1733
|
+
description: '车辆查询与绑定、停车费用查询、缴费支付与开票',
|
|
1734
|
+
contributes: {
|
|
1735
|
+
// demand meta title=车位查询——应被词汇 meta 停车查询压过(词汇主源)
|
|
1736
|
+
services: [{ service: 'parking.query', optional: false, title: '车位查询' }],
|
|
1737
|
+
},
|
|
1738
|
+
dependencies: { modules: [] },
|
|
1739
|
+
env: [],
|
|
1740
|
+
}),
|
|
1741
|
+
);
|
|
1742
|
+
mkdirDeep(join(app.appDir, 'packages', 'provider-demo'));
|
|
1743
|
+
writeAppFile(
|
|
1744
|
+
app.appDir,
|
|
1745
|
+
'packages/provider-demo/tbox.module.json',
|
|
1746
|
+
JSON.stringify({
|
|
1747
|
+
schemaVersion: 1,
|
|
1748
|
+
name: 'provider-demo',
|
|
1749
|
+
version: '0.1.0',
|
|
1750
|
+
kind: 'third-party',
|
|
1751
|
+
title: '万达',
|
|
1752
|
+
description: '万达厂商集成',
|
|
1753
|
+
contributes: {
|
|
1754
|
+
providers: {
|
|
1755
|
+
slots: [{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'apikey' }],
|
|
1756
|
+
},
|
|
1757
|
+
},
|
|
1758
|
+
dependencies: { modules: [] },
|
|
1759
|
+
env: [],
|
|
1760
|
+
}),
|
|
1761
|
+
);
|
|
1762
|
+
const tk = createAppToolkit(app.appDir);
|
|
1763
|
+
return { app, tk };
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
it('T1 resolveServiceDisplay 三态:词汇 > demand > id;逐字段回落(词汇仅 description 时 demand title 可达)', () => {
|
|
1767
|
+
const walk = {
|
|
1768
|
+
slotMeta: {
|
|
1769
|
+
'a.b': { title: '甲', description: '甲描述' },
|
|
1770
|
+
'c.d': { description: '只有描述' },
|
|
1771
|
+
},
|
|
1772
|
+
catalog: {
|
|
1773
|
+
services: {
|
|
1774
|
+
'a.b': { title: '乙', description: '乙描述' },
|
|
1775
|
+
'c.d': { title: '丙' },
|
|
1776
|
+
},
|
|
1777
|
+
},
|
|
1778
|
+
} as unknown as WalkManifestsResult;
|
|
1779
|
+
// 词汇全量在场 → 词汇胜出
|
|
1780
|
+
expect(resolveServiceDisplay(walk, 'a.b')).toEqual({ title: '甲', description: '甲描述' });
|
|
1781
|
+
// 词汇仅 description → title 回落 demand、description 取词汇(逐字段回落)
|
|
1782
|
+
expect(resolveServiceDisplay(walk, 'c.d')).toEqual({ title: '丙', description: '只有描述' });
|
|
1783
|
+
// 双缺席 → demand
|
|
1784
|
+
expect(resolveServiceDisplay(walk, 'e.f')).toEqual({ title: 'e.f' });
|
|
1785
|
+
// 双双缺席 → id 兜底(title 恒有值)
|
|
1786
|
+
const bare = { slotMeta: {}, catalog: { services: {} } } as unknown as WalkManifestsResult;
|
|
1787
|
+
expect(resolveServiceDisplay(bare, 'x.y')).toEqual({ title: 'x.y' });
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
it('T2 loadModulesView:模块 meta 填充;name 兜底', async () => {
|
|
1791
|
+
const h2 = displayHarness();
|
|
1792
|
+
const v = await h2.tk.loadModules();
|
|
1793
|
+
const parking = v.modules.find((m) => m.id === 'module-parking');
|
|
1794
|
+
expect(parking?.title).toBe('停车缴费');
|
|
1795
|
+
expect(parking?.description).toBe('车辆查询与绑定、停车费用查询、缴费支付与开票');
|
|
1796
|
+
// 无 meta 模块(provider 家族不在清单——用第二基座验证兜底)
|
|
1797
|
+
const h3 = harnessWithContracts();
|
|
1798
|
+
const bare = (await h3.tk.loadModules()).modules.find((m) => m.id === 'module-parking');
|
|
1799
|
+
expect(bare?.title).toBe('module-parking');
|
|
1800
|
+
expect(bare?.description).toBe('');
|
|
1801
|
+
h3.app.dispose();
|
|
1802
|
+
h2.app.dispose();
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
it('T3 loadModuleDetailView:meta 恒发;services[] 词汇优先(demand 车位查询被词汇停车查询压过——防漂移锁)', async () => {
|
|
1806
|
+
const h2 = displayHarness();
|
|
1807
|
+
const v = await h2.tk.loadModule('module-parking');
|
|
1808
|
+
expect(v.title).toBe('停车缴费');
|
|
1809
|
+
expect(v.description).toContain('停车费用查询');
|
|
1810
|
+
const svc = v.services.find((s) => s.service === 'parking.query');
|
|
1811
|
+
expect(svc?.title).toBe('停车查询');
|
|
1812
|
+
expect(svc?.description).toBe('费用报价、车场信息、可享权益');
|
|
1813
|
+
expect(svc?.required).toBe(true);
|
|
1814
|
+
h2.app.dispose();
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
it('T-O3 词汇在场键集:resolutions 键集 = 词汇(退化态 = 既有 O3——无 slots 物化基座)', async () => {
|
|
1818
|
+
const h2 = displayHarness();
|
|
1819
|
+
const batch = await h2.tk.loadServiceResolutions();
|
|
1820
|
+
expect(Object.keys(batch.resolutions).sort()).toEqual(['auth.alipay-login', 'mall.info', 'parking.query']);
|
|
1821
|
+
h2.app.dispose();
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
it('T4 loadAppView 平台条目:title 走链恒发;词汇无 meta 槽 = id 兜底', async () => {
|
|
1825
|
+
const h2 = displayHarness();
|
|
1826
|
+
const v = await h2.tk.loadApp();
|
|
1827
|
+
const byKey = Object.fromEntries(v.services.map((s) => [s.service, s]));
|
|
1828
|
+
// 平台条目 = 词汇 − 占用(parking.query 被模块占用)
|
|
1829
|
+
expect(Object.keys(byKey).sort()).toEqual(['auth.alipay-login', 'mall.info']);
|
|
1830
|
+
expect(byKey['auth.alipay-login']?.title).toBe('支付宝登录');
|
|
1831
|
+
expect(byKey['mall.info']?.title).toBe('mall.info');
|
|
1832
|
+
expect(byKey['auth.alipay-login']?.required).toBe(false);
|
|
1833
|
+
h2.app.dispose();
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
it('T5 loadServiceDetailView:词汇优先 + id 兜底恒有值', async () => {
|
|
1837
|
+
const h2 = displayHarness();
|
|
1838
|
+
expect((await h2.tk.loadService('parking.query')).title).toBe('停车查询');
|
|
1839
|
+
expect((await h2.tk.loadService('mall.info')).title).toBe('mall.info');
|
|
1840
|
+
h2.app.dispose();
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
it('T6 厂商 title 传播(walk 层首声明)+ /providers/:p services[] 走链', async () => {
|
|
1844
|
+
const h2 = displayHarness();
|
|
1845
|
+
const list = await h2.tk.loadProviders();
|
|
1846
|
+
const wanda = list.providers.find((p) => p.provider === 'wanda');
|
|
1847
|
+
expect(wanda?.title).toBe('万达');
|
|
1848
|
+
expect(wanda?.description).toBe('万达厂商集成');
|
|
1849
|
+
const detail = await h2.tk.loadProvider('wanda');
|
|
1850
|
+
expect(detail.title).toBe('万达');
|
|
1851
|
+
expect(detail.services['parking.query']?.title).toBe('停车查询');
|
|
1852
|
+
expect(detail.services['parking.query']?.description).toBe('费用报价、车场信息、可享权益');
|
|
1853
|
+
h2.app.dispose();
|
|
1854
|
+
});
|
|
1855
|
+
});
|