@tbox.cn/app-toolkit 0.4.0 → 0.6.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/chunk-WUULQDOV.js +1 -0
- package/dist/contracts-resolver-PS2UH2G6.js +1 -0
- package/dist/index.d.ts +374 -150
- package/dist/index.js +9 -9
- package/package.json +2 -2
- package/src/assembly/provider-catalog.ts +1 -1
- package/src/assembly/resolve-key.ts +75 -0
- package/src/assembly/walk-manifests.ts +138 -85
- package/src/core/contracts-expected.ts +2 -2
- package/src/core/contracts-resolver.ts +12 -1
- package/src/core/module-schema.ts +90 -14
- package/src/dto.ts +80 -33
- package/src/factory.ts +78 -26
- package/src/index.ts +16 -7
- package/src/integrations/mock-bindings.ts +1 -1
- package/src/integrations/module-binding.ts +60 -0
- package/src/integrations/predicate-io.ts +26 -5
- package/src/integrations/predicate.ts +97 -57
- package/src/integrations/prune-bindings.ts +33 -14
- package/src/integrations/write.ts +47 -27
- package/src/views/app.ts +4 -4
- package/src/views/context.ts +15 -14
- package/src/views/integration-schemas.ts +76 -18
- package/src/views/modules.ts +93 -25
- package/src/views/providers.ts +5 -5
- package/src/views/service-detail.ts +1 -1
- package/src/views/service-resolutions.ts +14 -13
- package/tests/credentials-view.test.ts +2 -2
- package/tests/demo-app.ts +11 -6
- package/tests/dev-manifest-catalog.test.ts +14 -14
- package/tests/file-cache.test.ts +5 -5
- package/tests/module-resources.test.ts +148 -0
- package/tests/module-schema.test.ts +120 -20
- package/tests/resolve-key.test.ts +154 -0
- package/tests/vendor-schema-keywords.test.ts +74 -0
- package/tests/views.test.ts +585 -247
- package/tests/write-core.test.ts +117 -0
- package/dist/chunk-KPD3LUH4.js +0 -1
- package/dist/contracts-resolver-E4SECBOG.js +0 -1
- package/src/integrations/domain-binding.ts +0 -81
|
@@ -11,10 +11,10 @@ import type {
|
|
|
11
11
|
ServiceStatus,
|
|
12
12
|
} from '../dto.js';
|
|
13
13
|
import { AppToolkitError } from '../core/errors.js';
|
|
14
|
-
import {
|
|
14
|
+
import { resolveModuleBinding } from '../integrations/module-binding.js';
|
|
15
15
|
import type { ResolvedContracts } from '../core/contracts-resolver.js';
|
|
16
16
|
import type { WalkCatalog } from '../assembly/walk-manifests.js';
|
|
17
|
-
import {
|
|
17
|
+
import { isSupplied, type ViewSnapshot } from './context.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* 求值轴投影(views/service-resolutions;C4;FX-2c——B6 effective 填充 + F2 layer 推导;
|
|
@@ -50,22 +50,22 @@ interface InstanceEvaluation {
|
|
|
50
50
|
effective?: EffectiveBinding;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/** 最特异声明层推导(F2):inst 位 provider 在场 → 'instance';槽级 → 'service'
|
|
53
|
+
/** 最特异声明层推导(F2):inst 位 provider 在场 → 'instance';槽级 → 'service';模块级 → 'module';
|
|
54
54
|
* root → 'app'。实例位查找复刻 TIER0 语义(精确键优先、'*' 通配兜底——与 resolveEffectiveService
|
|
55
|
-
* 的 instances 查找层同轴)。
|
|
56
|
-
* (四层全无 provider 声明 → null——Entry.layer「未配置双缺席」口径,D31)。 */
|
|
55
|
+
* 的 instances 查找层同轴)。v6:模块级段消费 resolveModuleBinding 单源(ownerOf=walk.ownership);
|
|
56
|
+
* 返回扩 null(四层全无 provider 声明 → null——Entry.layer「未配置双缺席」口径,D31)。 */
|
|
57
57
|
function deriveBindingLayer(snapshot: ViewSnapshot, service: string, instanceId: string): BindingLayer | null {
|
|
58
58
|
const integrations = snapshot.integrations as {
|
|
59
59
|
provider?: unknown;
|
|
60
|
-
domains?: Record<string, { provider?: unknown }>;
|
|
61
60
|
services?: Record<string, { provider?: unknown; instances?: Record<string, { provider?: unknown }> }>;
|
|
62
61
|
} | null;
|
|
63
|
-
const domain = domainOfService(service);
|
|
64
62
|
const slot = integrations?.services?.[service];
|
|
65
63
|
const inst = slot?.instances?.[instanceId] ?? slot?.instances?.['*'];
|
|
66
64
|
if (inst && inst.provider !== undefined) return 'instance';
|
|
67
65
|
if (slot?.provider !== undefined) return 'service';
|
|
68
|
-
|
|
66
|
+
const owner = snapshot.walk.ownership[service];
|
|
67
|
+
if (owner !== undefined
|
|
68
|
+
&& resolveModuleBinding(snapshot.integrations, owner).layer === 'module') return 'module';
|
|
69
69
|
if (typeof integrations?.provider === 'string' && integrations.provider.length > 0) return 'app';
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
@@ -292,10 +292,8 @@ function buildInheritance(
|
|
|
292
292
|
): ServiceResolution['inheritance'] {
|
|
293
293
|
const integrations = snapshot.integrations as {
|
|
294
294
|
provider?: string;
|
|
295
|
-
domains?: Record<string, { provider?: string }>;
|
|
296
295
|
services?: Record<string, { provider?: unknown; implementation?: string; instances?: Record<string, { provider?: string; implementation?: string }> }>;
|
|
297
296
|
} | null;
|
|
298
|
-
const domain = domainOfService(service);
|
|
299
297
|
const slot = integrations?.services?.[service];
|
|
300
298
|
const cfg = snapshot.integrations as { instances?: unknown; defaultInstanceId?: unknown } | null;
|
|
301
299
|
const normalized = evaluationContracts(snapshot).normalizeInstances({
|
|
@@ -304,13 +302,16 @@ function buildInheritance(
|
|
|
304
302
|
});
|
|
305
303
|
const defaultInstanceId = normalized.defaultInstanceId ?? '*';
|
|
306
304
|
const defaultInstance = slot?.instances?.[defaultInstanceId] ?? slot?.instances?.['*'];
|
|
307
|
-
// module 段消费
|
|
308
|
-
const
|
|
305
|
+
// module 段消费 resolveModuleBinding 单源(v6——与 binding / 谓词 #7 同源;ownerOf=walk.ownership)
|
|
306
|
+
const owner = snapshot.walk.ownership[service];
|
|
307
|
+
const moduleBinding = owner !== undefined
|
|
308
|
+
? resolveModuleBinding(integrations as Record<string, unknown> | null, owner)
|
|
309
|
+
: { provider: null as string | null, layer: null as 'module' | 'app' | null };
|
|
309
310
|
return [
|
|
310
311
|
{ layer: 'app' as const, ...(integrations?.provider ? { provider: integrations.provider } : {}) },
|
|
311
312
|
{
|
|
312
313
|
layer: 'module' as const,
|
|
313
|
-
...(
|
|
314
|
+
...(moduleBinding.layer === 'module' && moduleBinding.provider ? { provider: moduleBinding.provider } : {}),
|
|
314
315
|
},
|
|
315
316
|
{
|
|
316
317
|
layer: 'service' as const,
|
|
@@ -31,10 +31,10 @@ function credHarness(): { app: TempApp; tk: AppToolkit } {
|
|
|
31
31
|
'packages/provider-w/tbox.module.json',
|
|
32
32
|
JSON.stringify({
|
|
33
33
|
schemaVersion: 1,
|
|
34
|
-
|
|
34
|
+
id: 'provider-w',
|
|
35
35
|
version: '0.1.0',
|
|
36
36
|
kind: 'business',
|
|
37
|
-
|
|
37
|
+
declare: {
|
|
38
38
|
providers: {
|
|
39
39
|
slots: [
|
|
40
40
|
{ service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c-signed-v1', credentialSchema: 'schemas/cred.json' },
|
package/tests/demo-app.ts
CHANGED
|
@@ -46,12 +46,15 @@ export interface FakeContractsOptions {
|
|
|
46
46
|
/** 严格校验失败 fixture(FX-1c T5):parseIntegrationsConfig 对含该子串文档 throw */
|
|
47
47
|
rejectMarker?: string;
|
|
48
48
|
/** resolveEffectiveService 自定义函数体(FX-2c O4 fixture:默认恒 service-not-configured;
|
|
49
|
-
* 入参 (integrations, service, instanceId, supplies),需 return EffectiveServiceResolution 形状) */
|
|
49
|
+
* 入参 (integrations, service, instanceId, supplies, ownerOf),需 return EffectiveServiceResolution 形状) */
|
|
50
50
|
resolveBody?: string;
|
|
51
|
+
/** v6 形状标记(OWNERSHIP_BINDING_V6)缺席 fixture——默认在场(v6 假件签名随动 N11);
|
|
52
|
+
* false = v5 contracts 形态(ownershipBindingV6: false → assertContractsUsable 503 第四分支) */
|
|
53
|
+
withMarker?: boolean;
|
|
51
54
|
/** service-slots.json 词汇(P9 身份判据/求值键集;缺省不物化 = 词汇空) */
|
|
52
55
|
slots?: string[];
|
|
53
|
-
/** 词汇 meta 节(per-service {
|
|
54
|
-
meta?: Record<string, {
|
|
56
|
+
/** 词汇 meta 节(per-service {name,description}——B1 展示名链 fixture) */
|
|
57
|
+
meta?: Record<string, { name?: string; description?: string }>;
|
|
55
58
|
/** service-slots.json 落位:'root' = 包根(缺省——夹具/旧布局形态);
|
|
56
59
|
* 'src' = 包内 src/(发布包 src 附带 / shim 链接形态——B0 探测跳 ②) */
|
|
57
60
|
slotsLayout?: 'root' | 'src';
|
|
@@ -63,7 +66,7 @@ export interface FakeContractsOptions {
|
|
|
63
66
|
/** 假 contracts 包:package.json exports 形态逐字段复刻发布面(types+import 双条件、无 require/default)
|
|
64
67
|
* ——resolver 不经 exports 解析(包根直探),该形态为 E4 类错误的机械防线。 */
|
|
65
68
|
export function writeFakeContractsPackage(packageRoot: string, opts: FakeContractsOptions = {}): void {
|
|
66
|
-
const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, slots, meta, slotsLayout = 'root', zodRejectPaths } = opts;
|
|
69
|
+
const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, withMarker = true, slots, meta, slotsLayout = 'root', zodRejectPaths } = opts;
|
|
67
70
|
const strictBody = withStrict
|
|
68
71
|
? `export function parseIntegrationsConfig(raw) { ${
|
|
69
72
|
rejectMarker ? `if (JSON.stringify(raw).includes(${JSON.stringify(rejectMarker)})) throw new Error("fake strict reject"); ` : ''
|
|
@@ -73,9 +76,10 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
|
|
|
73
76
|
: ''
|
|
74
77
|
}return raw; }`
|
|
75
78
|
: '';
|
|
76
|
-
const resolveLine = `export function resolveEffectiveService(integrations, service, instanceId, supplies) { ${
|
|
79
|
+
const resolveLine = `export function resolveEffectiveService(integrations, service, instanceId, supplies, ownerOf) { ${
|
|
77
80
|
resolveBody ?? 'return { status: "service-not-configured" };'
|
|
78
81
|
} }`;
|
|
82
|
+
const markerLine = withMarker ? 'export const OWNERSHIP_BINDING_V6 = true;' : '';
|
|
79
83
|
const exportsBody = missingEvaluationExports
|
|
80
84
|
? `export const marker = 1;\n${strictBody}`
|
|
81
85
|
: [
|
|
@@ -84,6 +88,7 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
|
|
|
84
88
|
'export function moduleDomainOf(service) { return String(service).split(".")[0] ?? ""; }',
|
|
85
89
|
'export function normalizeInstances(config) { return { instances: (config && config.instances) || [], defaultInstanceId: (config && config.defaultInstanceId) ?? null }; }',
|
|
86
90
|
'export const RESOURCE_TYPE_IDS = ["knowledge"];',
|
|
91
|
+
markerLine,
|
|
87
92
|
strictBody,
|
|
88
93
|
'',
|
|
89
94
|
]
|
|
@@ -135,7 +140,7 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
|
|
|
135
140
|
export function writeSelfSrcContractPackage(
|
|
136
141
|
appDir: string,
|
|
137
142
|
name: string,
|
|
138
|
-
content: { slots: string[]; meta?: Record<string, {
|
|
143
|
+
content: { slots: string[]; meta?: Record<string, { name?: string; description?: string }> },
|
|
139
144
|
): string {
|
|
140
145
|
const dir = join(appDir, 'packages', name, 'src');
|
|
141
146
|
mkdirSync(dir, { recursive: true });
|
|
@@ -11,20 +11,20 @@ import { createTempApp, linkContracts, writeFakeContractsPackage, writeSelfSrcCo
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/** strict descriptor(形状对齐 provider-custom 出厂 manifest——parseModuleDescriptor 严格面) */
|
|
14
|
-
function descriptor(
|
|
14
|
+
function descriptor(id: string, provider: string, service: string): Record<string, unknown> {
|
|
15
15
|
return {
|
|
16
16
|
schemaVersion: 1,
|
|
17
|
-
|
|
17
|
+
id,
|
|
18
18
|
version: '1.0.0',
|
|
19
19
|
kind: 'business',
|
|
20
20
|
risk: { level: 'low', writeBoundary: 'source' },
|
|
21
21
|
distribution: { defaultMode: 'codegen' },
|
|
22
|
-
|
|
23
|
-
handlers: [], tools: [], cards: [], routes: [], pages: [], tabs: [],
|
|
22
|
+
declare: {
|
|
23
|
+
handlers: [], tools: [], cards: [], routes: [], pages: [], tabs: [], owns: [], consumes: [],
|
|
24
24
|
providers: {
|
|
25
25
|
slots: [{ service, provider, implementation: `${provider}-impl@1`, credentialType: 'apikey' }],
|
|
26
26
|
},
|
|
27
|
-
|
|
27
|
+
resources: [],
|
|
28
28
|
},
|
|
29
29
|
dependencies: { modules: [] },
|
|
30
30
|
env: [],
|
|
@@ -125,13 +125,13 @@ describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
|
|
|
125
125
|
const store = join(app.appDir, '_store-a');
|
|
126
126
|
writeFakeContractsPackage(store, {
|
|
127
127
|
slots: ['auth.alipay-login', 'mall.info'],
|
|
128
|
-
meta: { 'auth.alipay-login': {
|
|
128
|
+
meta: { 'auth.alipay-login': { name: '支付宝登录', description: '登录换码(code2Session)' } },
|
|
129
129
|
});
|
|
130
130
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
131
131
|
|
|
132
132
|
const walk = walkManifests(app.appDir);
|
|
133
133
|
expect(walk.vocabulary).toEqual(['auth.alipay-login', 'mall.info']);
|
|
134
|
-
expect(walk.slotMeta['auth.alipay-login']).toEqual({
|
|
134
|
+
expect(walk.slotMeta['auth.alipay-login']).toEqual({ name: '支付宝登录', description: '登录换码(code2Session)' });
|
|
135
135
|
expect(walk.slotMeta['mall.info']).toBeUndefined(); // meta 缺席槽不入表
|
|
136
136
|
});
|
|
137
137
|
|
|
@@ -149,25 +149,25 @@ describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
|
|
|
149
149
|
app = createTempApp('tbox-probe-');
|
|
150
150
|
writeSelfSrcContractPackage(app.appDir, 'contracts-mall', {
|
|
151
151
|
slots: ['member.account', 'parking.payment'],
|
|
152
|
-
meta: { 'member.account': {
|
|
152
|
+
meta: { 'member.account': { name: '会员账户' } },
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
const walk = walkManifests(app.appDir);
|
|
156
156
|
expect(walk.vocabulary).toEqual(['member.account', 'parking.payment']);
|
|
157
|
-
expect(walk.slotMeta['member.account']).toEqual({
|
|
157
|
+
expect(walk.slotMeta['member.account']).toEqual({ name: '会员账户' });
|
|
158
158
|
});
|
|
159
159
|
|
|
160
160
|
it('P4: 多包同槽 meta → 发现序首声明胜出(apps/* 先于 packages/* 候选序)', () => {
|
|
161
161
|
app = createTempApp('tbox-probe-');
|
|
162
162
|
const storeA = join(app.appDir, '_store-first');
|
|
163
|
-
writeFakeContractsPackage(storeA, { slots: ['x.y'], meta: { 'x.y': {
|
|
163
|
+
writeFakeContractsPackage(storeA, { slots: ['x.y'], meta: { 'x.y': { name: '甲' } } });
|
|
164
164
|
linkContracts(app.appDir, storeA, { subdir: 'apps/server' });
|
|
165
165
|
const storeB = join(app.appDir, '_store-second');
|
|
166
|
-
writeFakeContractsPackage(storeB, { slots: ['x.y'], meta: { 'x.y': {
|
|
166
|
+
writeFakeContractsPackage(storeB, { slots: ['x.y'], meta: { 'x.y': { name: '乙' } } });
|
|
167
167
|
linkContracts(app.appDir, storeB, { subdir: 'packages/mock' });
|
|
168
168
|
|
|
169
169
|
const walk = walkManifests(app.appDir);
|
|
170
|
-
expect(walk.slotMeta['x.y']).toEqual({
|
|
170
|
+
expect(walk.slotMeta['x.y']).toEqual({ name: '甲' });
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
it('P5: 坏 meta 条目(非对象/非 string/空串)→ 宽松跳过,slots 正常', () => {
|
|
@@ -176,13 +176,13 @@ describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
|
|
|
176
176
|
writeFakeContractsPackage(store, { slots: ['a.b'] });
|
|
177
177
|
writeFileSync(
|
|
178
178
|
join(store, 'service-slots.json'),
|
|
179
|
-
JSON.stringify({ slots: ['a.b', 'c.d', 'e.f'], meta: { 'a.b': '字符串', 'c.d': {
|
|
179
|
+
JSON.stringify({ slots: ['a.b', 'c.d', 'e.f'], meta: { 'a.b': '字符串', 'c.d': { name: 42 }, 'e.f': { name: '甲', description: '' } } }),
|
|
180
180
|
);
|
|
181
181
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
182
182
|
|
|
183
183
|
const walk = walkManifests(app.appDir);
|
|
184
184
|
expect(walk.vocabulary).toEqual(['a.b', 'c.d', 'e.f']);
|
|
185
|
-
expect(walk.slotMeta).toEqual({ 'e.f': {
|
|
185
|
+
expect(walk.slotMeta).toEqual({ 'e.f': { name: '甲' } }); // 空串 description 不入表(与 title 守卫对称)
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
it('P6: 无契约包(退化态)→ vocabulary 空 + slotMeta 空(P9 退化路径语义锁)', () => {
|
package/tests/file-cache.test.ts
CHANGED
|
@@ -132,7 +132,7 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
|
|
|
132
132
|
mkdirSync(join(app.appDir, 'packages', 'module-new'), { recursive: true });
|
|
133
133
|
writeFileSync(
|
|
134
134
|
join(app.appDir, 'packages', 'module-new', 'tbox.module.json'),
|
|
135
|
-
JSON.stringify({ schemaVersion: 1,
|
|
135
|
+
JSON.stringify({ schemaVersion: 1, id: 'module-new', declare: { owns: [{ service: 'new.slot' }], consumes: [{ service: 'new.slot' }] } }),
|
|
136
136
|
);
|
|
137
137
|
const walk = walkManifests(app.appDir);
|
|
138
138
|
expect(walk.modules.map((m) => m.id)).toContain('module-new');
|
|
@@ -172,10 +172,10 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
|
|
|
172
172
|
join(app.appDir, 'packages', 'provider-x', 'tbox.module.json'),
|
|
173
173
|
JSON.stringify({
|
|
174
174
|
schemaVersion: 1,
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
id: 'provider-x',
|
|
176
|
+
declare: {
|
|
177
177
|
providers: { slots: [{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c' }] },
|
|
178
|
-
|
|
178
|
+
consumes: [{ service: 'member.account' }],
|
|
179
179
|
},
|
|
180
180
|
}),
|
|
181
181
|
);
|
|
@@ -196,7 +196,7 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
|
|
|
196
196
|
mkdirSync(sdkDir, { recursive: true });
|
|
197
197
|
writeFileSync(
|
|
198
198
|
join(sdkDir, 'tbox.module.json'),
|
|
199
|
-
JSON.stringify({ schemaVersion: 1,
|
|
199
|
+
JSON.stringify({ schemaVersion: 1, id: 'module-sdk-y', declare: { owns: [{ service: 'sdk.slot-y' }], consumes: [{ service: 'sdk.slot-y', name: 'Sdk 槽' }] } }),
|
|
200
200
|
);
|
|
201
201
|
const modules2 = await tk.loadModules();
|
|
202
202
|
expect(modules2.modules.map((m) => m.id)).toContain('module-sdk-y');
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { createAppToolkit, type AppToolkit } from '../src/factory.js';
|
|
5
|
+
import { invalidateContractsResolver } from '../src/core/contracts-resolver.js';
|
|
6
|
+
import { MODULE_INTEGRATION_NODE_KEYS } from '../src/dto.js';
|
|
7
|
+
import type { ModuleDetailView } from '../src/dto.js';
|
|
8
|
+
import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 模块资源只读投影矩阵(N2 读半边):GET /modules/:m `resources` 四态(直填 datasetId /
|
|
12
|
+
* ref 命中台账 / ref 未命中(台账缺失·坏文件·键 miss 同归 null)/ 无 modules 节 = null)
|
|
13
|
+
* + 写侧保全不变量(MODULE_INTEGRATION_NODE_KEYS 白名单不变——resources 不进写位)。
|
|
14
|
+
* 展开语义镜像 SDK resolveKnowledge:datasetId 直填优先、ref 台账命中/miss-warn 降级。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface Harness {
|
|
18
|
+
app: TempApp;
|
|
19
|
+
tk: AppToolkit;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 自足基座:module-cs(declare.resources 声明对齐真实客服 manifest)+ integrations + 可选台账 */
|
|
23
|
+
function resourcesHarness(integrations: object, knowledge: object | null): Harness {
|
|
24
|
+
const app = createTempApp('tbox-res-');
|
|
25
|
+
invalidateContractsResolver();
|
|
26
|
+
const store = join(app.appDir, 'contracts');
|
|
27
|
+
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
28
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
29
|
+
mkdirSync(join(app.appDir, 'packages', 'module-cs'), { recursive: true });
|
|
30
|
+
writeAppFile(
|
|
31
|
+
app.appDir,
|
|
32
|
+
'packages/module-cs/tbox.module.json',
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
id: 'module-cs',
|
|
36
|
+
version: '0.1.0',
|
|
37
|
+
kind: 'business',
|
|
38
|
+
declare: {
|
|
39
|
+
owns: [],
|
|
40
|
+
consumes: [],
|
|
41
|
+
resources: [
|
|
42
|
+
{ id: 'customer-service-faq-kb', type: 'knowledge' },
|
|
43
|
+
{ id: 'knowledge-assistance-kb', type: 'knowledge' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
dependencies: { modules: [] },
|
|
47
|
+
env: [],
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify(integrations));
|
|
51
|
+
if (knowledge !== null) {
|
|
52
|
+
writeAppFile(app.appDir, 'config/knowledge.json', JSON.stringify(knowledge));
|
|
53
|
+
}
|
|
54
|
+
const tk = createAppToolkit(app.appDir);
|
|
55
|
+
return { app, tk };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const FAQ_DIRECT = {
|
|
59
|
+
provider: 'mock',
|
|
60
|
+
modules: {
|
|
61
|
+
'module-cs': {
|
|
62
|
+
resources: { 'customer-service-faq-kb': { type: 'knowledge', datasetId: '20260101direct00000000000001', topK: 5, scoreThreshold: 0.5 } },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const FAQ_REF = {
|
|
68
|
+
provider: 'mock',
|
|
69
|
+
modules: {
|
|
70
|
+
'module-cs': {
|
|
71
|
+
resources: { 'customer-service-faq-kb': { type: 'knowledge', ref: 'customer-service-faq-generic' } },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const KB_REGISTRY = {
|
|
77
|
+
version: '1.0',
|
|
78
|
+
knowledge_bases: {
|
|
79
|
+
'customer-service-faq-generic': { datasetId: '20260921999def645305kQj02050918', retrieve_config: { topK: 5, scoreThreshold: 0.5 } },
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
async function detailOf(h: Harness): Promise<ModuleDetailView> {
|
|
84
|
+
return h.tk.loadModule('module-cs');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('module resources 只读投影(N2 读半边)', () => {
|
|
88
|
+
it('T1 直填 datasetId:resolvedDatasetId = 原值(无台账读取依赖)', async () => {
|
|
89
|
+
const h = resourcesHarness(FAQ_DIRECT, null); // 台账缺失——直填态不受影响
|
|
90
|
+
const v = await detailOf(h);
|
|
91
|
+
expect(v.resources).not.toBeNull();
|
|
92
|
+
expect(v.resources!['customer-service-faq-kb']).toEqual({
|
|
93
|
+
type: 'knowledge',
|
|
94
|
+
datasetId: '20260101direct00000000000001',
|
|
95
|
+
resolvedDatasetId: '20260101direct00000000000001',
|
|
96
|
+
topK: 5,
|
|
97
|
+
scoreThreshold: 0.5,
|
|
98
|
+
});
|
|
99
|
+
h.app.dispose();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('T2 ref 命中台账:resolvedDatasetId = 台账 datasetId;文件原值 ref 保留', async () => {
|
|
103
|
+
const h = resourcesHarness(FAQ_REF, KB_REGISTRY);
|
|
104
|
+
const v = await detailOf(h);
|
|
105
|
+
expect(v.resources!['customer-service-faq-kb']).toEqual({
|
|
106
|
+
type: 'knowledge',
|
|
107
|
+
ref: 'customer-service-faq-generic',
|
|
108
|
+
resolvedDatasetId: '20260921999def645305kQj02050918',
|
|
109
|
+
});
|
|
110
|
+
h.app.dispose();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('T3 ref 未命中键:resolvedDatasetId = null(原值 ref 保留)', async () => {
|
|
114
|
+
const h = resourcesHarness(FAQ_REF, { knowledge_bases: { 'other-kb': { datasetId: 'x' } } });
|
|
115
|
+
const v = await detailOf(h);
|
|
116
|
+
expect(v.resources!['customer-service-faq-kb']).toEqual({
|
|
117
|
+
type: 'knowledge',
|
|
118
|
+
ref: 'customer-service-faq-generic',
|
|
119
|
+
resolvedDatasetId: null,
|
|
120
|
+
});
|
|
121
|
+
h.app.dispose();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('T3b 台账文件缺失 / 坏 JSON:同归 resolvedDatasetId = null(fail-soft 镜像 SDK)', async () => {
|
|
125
|
+
const missing = resourcesHarness(FAQ_REF, null);
|
|
126
|
+
expect((await detailOf(missing)).resources!['customer-service-faq-kb']?.resolvedDatasetId).toBeNull();
|
|
127
|
+
missing.app.dispose();
|
|
128
|
+
|
|
129
|
+
const broken = resourcesHarness(FAQ_REF, KB_REGISTRY);
|
|
130
|
+
writeAppFile(broken.app.appDir, 'config/knowledge.json', '{not-json');
|
|
131
|
+
expect((await detailOf(broken)).resources!['customer-service-faq-kb']?.resolvedDatasetId).toBeNull();
|
|
132
|
+
broken.app.dispose();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('T4 无 modules 节 / 模块键无 resources:resources = null(null 语义:无资源 ≠ 空表)', async () => {
|
|
136
|
+
const noModules = resourcesHarness({ provider: 'mock' }, null);
|
|
137
|
+
expect((await detailOf(noModules)).resources).toBeNull();
|
|
138
|
+
noModules.app.dispose();
|
|
139
|
+
|
|
140
|
+
const noResourcesKey = resourcesHarness({ provider: 'mock', modules: { 'module-cs': { config: { k: 1 } } } }, KB_REGISTRY);
|
|
141
|
+
expect((await detailOf(noResourcesKey)).resources).toBeNull();
|
|
142
|
+
noResourcesKey.app.dispose();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('T5 写侧保全不变量:MODULE_INTEGRATION_NODE_KEYS 白名单恒不含 resources(N2 写半边不变)', () => {
|
|
146
|
+
expect(MODULE_INTEGRATION_NODE_KEYS).toEqual(['provider', 'config']);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -7,37 +7,67 @@ import {
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* module-schema(descriptor zod 单源)迁移改造验证(C2-5 + C1 P6 归位):
|
|
10
|
-
* - demand meta additive(
|
|
10
|
+
* - demand meta additive(serviceConsumeContribSchema 增 name?/description?——P6)
|
|
11
11
|
* - resource type 放宽(z.enum(RESOURCE_TYPE_IDS) → z.string()——词汇校验移谓词)
|
|
12
12
|
* - 安全钉子 H9b(provider 字符集 pattern)
|
|
13
13
|
* - 既有无 meta 素材全过(additive 等价)
|
|
14
|
+
* - v7 词汇统一(标识/展示名分轴):id/name 翻转 + 四象限 legacy 矩阵
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
const BASE_MANIFEST = {
|
|
17
18
|
schemaVersion: 1,
|
|
18
|
-
|
|
19
|
+
id: 'module-sample',
|
|
19
20
|
version: '0.1.0',
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
describe('module-schema(descriptor zod 单源)', () => {
|
|
23
|
-
it('
|
|
24
|
+
it('v6 N1 旧键定向拒:顶层 contributes → ok:false + 三路径升级指引(非泛化 strict 报错)', () => {
|
|
25
|
+
const r = parseModuleDescriptor({ ...BASE_MANIFEST, contributes: { services: [] } });
|
|
26
|
+
expect(r.ok).toBe(false);
|
|
27
|
+
if (r.ok) return;
|
|
28
|
+
expect(r.errors[0]).toContain('`contributes` 已更名 `declare`');
|
|
29
|
+
expect(r.errors[0]).toContain('module update --source');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('v6 N1 旧键定向拒:declare.services → ok:false + consumes 指引', () => {
|
|
33
|
+
const r = parseModuleDescriptor({ ...BASE_MANIFEST, declare: { services: [{ service: 'x.y' }] } });
|
|
34
|
+
expect(r.ok).toBe(false);
|
|
35
|
+
if (r.ok) return;
|
|
36
|
+
expect(r.errors[0]).toContain('`declare.services` 已更名 `declare.consumes`');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('v6 owns 薄形状:strict 单键(元数据键拒——S1 防双数组漂移);owns/consumes 双键并存合法', () => {
|
|
40
|
+
const thin = parseModuleDescriptor({ ...BASE_MANIFEST, declare: { owns: [{ service: 'a.b', optional: true } as never] } });
|
|
41
|
+
expect(thin.ok).toBe(false); // owns 元素 strict——元数据归 consumes
|
|
42
|
+
const both = parseModuleDescriptor({
|
|
43
|
+
...BASE_MANIFEST,
|
|
44
|
+
declare: { owns: [{ service: 'a.b' }], consumes: [{ service: 'a.b', optional: false, name: 'T' }] },
|
|
45
|
+
});
|
|
46
|
+
expect(both.ok).toBe(true);
|
|
47
|
+
if (both.ok) {
|
|
48
|
+
expect(both.value.declare.owns).toEqual([{ service: 'a.b' }]);
|
|
49
|
+
expect(both.value.declare.consumes[0].name).toBe('T');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('P6 demand meta additive:services 元素带 name/description 通过;缺席仍合法', () => {
|
|
24
54
|
const withMeta = parseModuleDescriptor({
|
|
25
55
|
...BASE_MANIFEST,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ service: 'parking.query', optional: false,
|
|
56
|
+
declare: {
|
|
57
|
+
consumes: [
|
|
58
|
+
{ service: 'parking.query', optional: false, name: '车位查询', description: '查询车位状态' },
|
|
29
59
|
],
|
|
30
60
|
},
|
|
31
61
|
});
|
|
32
62
|
expect(withMeta.ok).toBe(true);
|
|
33
63
|
if (withMeta.ok) {
|
|
34
|
-
expect(withMeta.value.
|
|
35
|
-
expect(withMeta.value.
|
|
64
|
+
expect(withMeta.value.declare.consumes[0].name).toBe('车位查询');
|
|
65
|
+
expect(withMeta.value.declare.consumes[0].description).toBe('查询车位状态');
|
|
36
66
|
}
|
|
37
67
|
// 既有无 meta 素材全过(additive 等价)
|
|
38
68
|
const withoutMeta = parseModuleDescriptor({
|
|
39
69
|
...BASE_MANIFEST,
|
|
40
|
-
|
|
70
|
+
declare: { consumes: [{ service: 'parking.query' }] },
|
|
41
71
|
});
|
|
42
72
|
expect(withoutMeta.ok).toBe(true);
|
|
43
73
|
});
|
|
@@ -45,7 +75,7 @@ describe('module-schema(descriptor zod 单源)', () => {
|
|
|
45
75
|
it('resource type 放宽:任意 string 通过(词汇校验移谓词经 resolver 取词表)', () => {
|
|
46
76
|
const result = parseModuleDescriptor({
|
|
47
77
|
...BASE_MANIFEST,
|
|
48
|
-
|
|
78
|
+
declare: { resources: [{ id: 'kb-1', type: 'knowledge' }, { id: 'kb-2', type: 'future-type' }] },
|
|
49
79
|
});
|
|
50
80
|
expect(result.ok).toBe(true);
|
|
51
81
|
});
|
|
@@ -53,14 +83,14 @@ describe('module-schema(descriptor zod 单源)', () => {
|
|
|
53
83
|
it('H9b 安全钉子:provider 名含路径敌对字符 → 拒绝;合法字符集通过', () => {
|
|
54
84
|
const evil = parseModuleDescriptor({
|
|
55
85
|
...BASE_MANIFEST,
|
|
56
|
-
|
|
86
|
+
declare: {
|
|
57
87
|
providers: { slots: [{ service: 'a.b', provider: '../etc', implementation: 'x@1', credentialType: '-' }] },
|
|
58
88
|
},
|
|
59
89
|
});
|
|
60
90
|
expect(evil.ok).toBe(false);
|
|
61
91
|
const good = parseModuleDescriptor({
|
|
62
92
|
...BASE_MANIFEST,
|
|
63
|
-
|
|
93
|
+
declare: {
|
|
64
94
|
providers: { slots: [{ service: 'a.b', provider: 'wanda', implementation: 'wanda-a@1', credentialType: '-' }] },
|
|
65
95
|
},
|
|
66
96
|
});
|
|
@@ -72,28 +102,98 @@ describe('module-schema(descriptor zod 单源)', () => {
|
|
|
72
102
|
expect(SAFE_NAME_PATTERN.test('-lead')).toBe(false);
|
|
73
103
|
});
|
|
74
104
|
|
|
75
|
-
it('零贡献合法(provider-custom 空包形态):
|
|
105
|
+
it('零贡献合法(provider-custom 空包形态):declare 全 default', () => {
|
|
76
106
|
const parsed = moduleDescriptorSchema.safeParse(BASE_MANIFEST);
|
|
77
107
|
expect(parsed.success).toBe(true);
|
|
78
108
|
if (parsed.success) {
|
|
79
|
-
expect(parsed.data.
|
|
80
|
-
expect(parsed.data.
|
|
81
|
-
expect(parsed.data.
|
|
109
|
+
expect(parsed.data.declare.providers.slots).toEqual([]);
|
|
110
|
+
expect(parsed.data.declare.consumes).toEqual([]);
|
|
111
|
+
expect(parsed.data.declare.resources).toEqual([]);
|
|
82
112
|
}
|
|
83
113
|
});
|
|
84
114
|
|
|
85
|
-
it('B1 模块 meta
|
|
86
|
-
const withMeta = parseModuleDescriptor({ ...BASE_MANIFEST,
|
|
115
|
+
it('v7 B1 模块 meta:name = 展示名(id = 标识键);缺席仍合法(回退归视图层)', () => {
|
|
116
|
+
const withMeta = parseModuleDescriptor({ ...BASE_MANIFEST, name: '停车缴费', description: '车辆查询与绑定、停车费用查询、缴费支付与开票' });
|
|
87
117
|
expect(withMeta.ok).toBe(true);
|
|
88
118
|
if (withMeta.ok) {
|
|
89
|
-
expect(withMeta.value.
|
|
119
|
+
expect(withMeta.value.id).toBe('module-sample');
|
|
120
|
+
expect(withMeta.value.name).toBe('停车缴费');
|
|
90
121
|
expect(withMeta.value.description).toContain('停车费用查询');
|
|
91
122
|
}
|
|
92
123
|
const withoutMeta = moduleDescriptorSchema.safeParse(BASE_MANIFEST);
|
|
93
124
|
expect(withoutMeta.success).toBe(true);
|
|
94
125
|
if (withoutMeta.success) {
|
|
95
|
-
expect(withoutMeta.data.
|
|
126
|
+
expect(withoutMeta.data.name).toBeUndefined();
|
|
96
127
|
expect(withoutMeta.data.description).toBeUndefined();
|
|
97
128
|
}
|
|
98
129
|
});
|
|
130
|
+
|
|
131
|
+
it('hidden 展示元数据:boolean 可选(true/false 通过、缺席合法、非 boolean 拒——清单过滤语义归视图层)', () => {
|
|
132
|
+
const hidden = parseModuleDescriptor({ ...BASE_MANIFEST, hidden: true });
|
|
133
|
+
expect(hidden.ok).toBe(true);
|
|
134
|
+
if (hidden.ok) expect(hidden.value.hidden).toBe(true);
|
|
135
|
+
const unhidden = parseModuleDescriptor({ ...BASE_MANIFEST, hidden: false });
|
|
136
|
+
expect(unhidden.ok).toBe(true);
|
|
137
|
+
if (unhidden.ok) expect(unhidden.value.hidden).toBe(false);
|
|
138
|
+
const absent = moduleDescriptorSchema.safeParse(BASE_MANIFEST);
|
|
139
|
+
expect(absent.success).toBe(true);
|
|
140
|
+
if (absent.success) expect(absent.data.hidden).toBeUndefined();
|
|
141
|
+
const bad = parseModuleDescriptor({ ...BASE_MANIFEST, hidden: 'yes' });
|
|
142
|
+
expect(bad.ok).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── v7 词汇统一:legacy 四象限矩阵(旧态/半迁移定向拒;v7 两形态放行)──
|
|
146
|
+
|
|
147
|
+
it('v7 legacy ①:v6 旧态(name 标识 + title 展示、无 id)→ 定向拒 + legacy 标志 + 双键指引', () => {
|
|
148
|
+
const r = parseModuleDescriptor({
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
name: 'module-old',
|
|
151
|
+
title: '旧模块',
|
|
152
|
+
version: '0.1.0',
|
|
153
|
+
declare: { consumes: [{ service: 'a.b' }] },
|
|
154
|
+
});
|
|
155
|
+
expect(r.ok).toBe(false);
|
|
156
|
+
if (r.ok) return;
|
|
157
|
+
expect(r.legacy).toBe(true);
|
|
158
|
+
const joined = r.errors.join('\n');
|
|
159
|
+
expect(joined).toContain('标识键 `name` 已更名 `id`');
|
|
160
|
+
expect(joined).toContain('展示名键 `title` 已更名 `name`');
|
|
161
|
+
expect(joined).toContain('module update --source');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('v7 legacy ②:v6 最小旧态(仅 name 无 title)→ 定向拒 + legacy 标志', () => {
|
|
165
|
+
const r = parseModuleDescriptor({ schemaVersion: 1, name: 'module-old', version: '0.1.0' });
|
|
166
|
+
expect(r.ok).toBe(false);
|
|
167
|
+
if (r.ok) return;
|
|
168
|
+
expect(r.legacy).toBe(true);
|
|
169
|
+
expect(r.errors[0]).toContain('标识键 `name` 已更名 `id`');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('v7 legacy ③:半迁移(有 id 残留 title)→ 定向拒(title→name)', () => {
|
|
173
|
+
const r = parseModuleDescriptor({ ...BASE_MANIFEST, title: '残留展示名' });
|
|
174
|
+
expect(r.ok).toBe(false);
|
|
175
|
+
if (r.ok) return;
|
|
176
|
+
expect(r.legacy).toBe(true);
|
|
177
|
+
expect(r.errors[0]).toContain('展示名键 `title` 已更名 `name`');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('v7 legacy ④:declare.consumes[].title 残留(根级已翻转)→ 定向拒', () => {
|
|
181
|
+
const r = parseModuleDescriptor({
|
|
182
|
+
...BASE_MANIFEST,
|
|
183
|
+
declare: { consumes: [{ service: 'a.b', title: '旧槽名' }] },
|
|
184
|
+
});
|
|
185
|
+
expect(r.ok).toBe(false);
|
|
186
|
+
if (r.ok) return;
|
|
187
|
+
expect(r.legacy).toBe(true);
|
|
188
|
+
expect(r.errors[0]).toContain('展示名键 `title` 已更名 `name`');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('v7 完整形态:id + name(展示名)+ description 并存合法(同名键歧义以 id 在场性消解)', () => {
|
|
192
|
+
const r = parseModuleDescriptor({ ...BASE_MANIFEST, name: '示例模块' });
|
|
193
|
+
expect(r.ok).toBe(true);
|
|
194
|
+
if (r.ok) {
|
|
195
|
+
expect(r.value.id).toBe('module-sample');
|
|
196
|
+
expect(r.value.name).toBe('示例模块');
|
|
197
|
+
}
|
|
198
|
+
});
|
|
99
199
|
});
|