@tbox.cn/app-toolkit 0.5.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.
@@ -119,7 +119,7 @@ describe('walkManifests · dev-manifest catalog 通道(聚合宽/声明窄)'
119
119
  * (local/codegen 全量副本——mall 生成应用主形态,不依赖 install)。
120
120
  * 修复前探测只扫包根 → 三形态全 miss(vocabulary 恒空)。
121
121
  */
122
- describe('walkManifests · 词汇探测三跳 + slotMeta(B0', () => {
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);
@@ -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
+ });
@@ -128,6 +128,20 @@ describe('module-schema(descriptor zod 单源)', () => {
128
128
  }
129
129
  });
130
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
+
131
145
  // ── v7 词汇统一:legacy 四象限矩阵(旧态/半迁移定向拒;v7 两形态放行)──
132
146
 
133
147
  it('v7 legacy ①:v6 旧态(name 标识 + title 展示、无 id)→ 定向拒 + legacy 标志 + 双键指引', () => {