@tbox.cn/app-toolkit 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/chunk-VV3ERZXS.js +1 -0
  4. package/dist/contracts-resolver-QM4FBQQV.js +1 -0
  5. package/dist/index.d.ts +1909 -0
  6. package/dist/index.js +8 -0
  7. package/package.json +47 -0
  8. package/src/assembly/app-manifest.ts +132 -0
  9. package/src/assembly/provider-catalog.ts +167 -0
  10. package/src/assembly/walk-manifests.ts +461 -0
  11. package/src/core/contracts-expected.ts +31 -0
  12. package/src/core/contracts-resolver.ts +274 -0
  13. package/src/core/credential-format.ts +98 -0
  14. package/src/core/credential-mask.ts +19 -0
  15. package/src/core/env-expansion.ts +51 -0
  16. package/src/core/errors.ts +46 -0
  17. package/src/core/file-cache.ts +65 -0
  18. package/src/core/fskit.ts +21 -0
  19. package/src/core/module-schema.ts +139 -0
  20. package/src/dto.ts +298 -0
  21. package/src/factory.ts +297 -0
  22. package/src/index.ts +137 -0
  23. package/src/integrations/credentials.ts +153 -0
  24. package/src/integrations/mock-bindings.ts +55 -0
  25. package/src/integrations/predicate-io.ts +199 -0
  26. package/src/integrations/predicate.ts +598 -0
  27. package/src/integrations/read.ts +53 -0
  28. package/src/integrations/write.ts +448 -0
  29. package/src/views/app.ts +28 -0
  30. package/src/views/context.ts +70 -0
  31. package/src/views/modules.ts +48 -0
  32. package/src/views/providers.ts +165 -0
  33. package/src/views/service-detail.ts +32 -0
  34. package/src/views/service-resolutions.ts +323 -0
  35. package/tests/contracts-resolver.test.ts +203 -0
  36. package/tests/demo-app.ts +146 -0
  37. package/tests/dev-manifest-catalog.test.ts +114 -0
  38. package/tests/error-name-safety.test.ts +26 -0
  39. package/tests/file-cache.test.ts +242 -0
  40. package/tests/import-layers.test.ts +111 -0
  41. package/tests/module-schema.test.ts +84 -0
  42. package/tests/naming-alignment.test.ts +50 -0
  43. package/tests/views.test.ts +427 -0
  44. package/tests/write-core.test.ts +188 -0
  45. package/tsconfig.json +11 -0
  46. package/tsup.config.ts +29 -0
@@ -0,0 +1,203 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { mkdirSync, writeFileSync, readFileSync, readlinkSync, symlinkSync, unlinkSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { resolveContractsForApp, invalidateContractsResolver } from '../src/core/contracts-resolver.js';
5
+ import {
6
+ bumpMtime,
7
+ createTempApp,
8
+ writeFakeContractsPackage,
9
+ linkContracts,
10
+ type TempApp,
11
+ } from './demo-app.js';
12
+
13
+ /**
14
+ * contracts 动态解析器 fixtures 五态(F1-F5)+ symlink 换目标变体 + import 拒绝。
15
+ * store 假包置于 appDir 内部(非候选位——候选仅 apps 与 packages 各成员下 node_modules),
16
+ * dispose 随 appDir 一并清理;appDir 名经 mkdtemp 唯一,无并行碰撞。
17
+ */
18
+
19
+ describe('contracts-resolver(D3 方案心脏)', () => {
20
+ let app: TempApp;
21
+
22
+ beforeEach(() => {
23
+ app = createTempApp('tbox-resolver-');
24
+ invalidateContractsResolver();
25
+ });
26
+
27
+ it('F1 pnpm 成员级链接:apps/server 候选命中(realpath 解 symlink + version + 双面可用)', async () => {
28
+ const store = join(app.appDir, 'store-f1');
29
+ writeFakeContractsPackage(store, { version: '0.9.0' });
30
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
31
+ const resolved = await resolveContractsForApp(app.appDir);
32
+ expect(resolved.version).toBe('0.9.0');
33
+ expect(resolved.evaluation).toBe(true);
34
+ expect(resolved.strictValidation).toBe(true);
35
+ expect(resolved.module.resolveEffectiveService).toBeTypeOf('function');
36
+ expect(resolved.module.parseIntegrationsConfig).toBeTypeOf('function');
37
+ expect(resolved.notes).toEqual([]);
38
+ });
39
+
40
+ it('F2 npm 平铺:appDir 根首候选天然覆盖', async () => {
41
+ const store = join(app.appDir, 'store-f2');
42
+ writeFakeContractsPackage(store, { version: '0.9.1' });
43
+ linkContracts(app.appDir, store, { subdir: '' });
44
+ const resolved = await resolveContractsForApp(app.appDir);
45
+ expect(resolved.version).toBe('0.9.1');
46
+ expect(resolved.strictValidation).toBe(true);
47
+ });
48
+
49
+ it('F3 store 多版本:apps/server(0.9)优先于 apps/client(0.8)——候选序即优先级', async () => {
50
+ const storeV9 = join(app.appDir, 'store-f3-v9');
51
+ const storeV8 = join(app.appDir, 'store-f3-v8');
52
+ writeFakeContractsPackage(storeV9, { version: '0.9.0' });
53
+ writeFakeContractsPackage(storeV8, { version: '0.8.0' });
54
+ linkContracts(app.appDir, storeV9, { subdir: 'apps/server' });
55
+ linkContracts(app.appDir, storeV8, { subdir: 'apps/client' });
56
+ const resolved = await resolveContractsForApp(app.appDir);
57
+ expect(resolved.version).toBe('0.9.0');
58
+ });
59
+
60
+ it('F4 缺席:空目录 → 未解析产物(version null,绝不 throw)', async () => {
61
+ const resolved = await resolveContractsForApp(app.appDir);
62
+ expect(resolved.version).toBeNull();
63
+ expect(resolved.evaluation).toBe(false);
64
+ expect(resolved.strictValidation).toBe(false);
65
+ expect(resolved.module).toEqual({});
66
+ expect(resolved.notes.length).toBeGreaterThan(0);
67
+ });
68
+
69
+ it('F5 缺 zod 面:仅求值面在场 → evaluation true / strictValidation false(门控分支输入)', async () => {
70
+ const store = join(app.appDir, 'store-f5');
71
+ writeFakeContractsPackage(store, { version: '0.8.2', withStrict: false });
72
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
73
+ const resolved = await resolveContractsForApp(app.appDir);
74
+ expect(resolved.evaluation).toBe(true);
75
+ expect(resolved.strictValidation).toBe(false);
76
+ expect(resolved.notes.some((n) => n.includes('0.8.2'))).toBe(true);
77
+ });
78
+
79
+ it('W4 symlink 换目标:version 轻探测读候选路径——pnpm 升级换 store 目标可觉察', async () => {
80
+ // 真 bug 候选防线:memoize 比对读**候选路径** version(非 realpath 缓存路径)
81
+ const storeV1 = join(app.appDir, 'store-w4-v1');
82
+ const storeV2 = join(app.appDir, 'store-w4-v2');
83
+ writeFakeContractsPackage(storeV1, { version: '0.8.0', withStrict: false });
84
+ writeFakeContractsPackage(storeV2, { version: '0.9.0', withStrict: true });
85
+ // storeV2 晚装语义——粗粒度 fs(CI overlayfs)同 tick 双写同 size package.json 指纹不变(边界②)
86
+ bumpMtime(join(storeV2, 'package.json'));
87
+ const linkDir = join(app.appDir, 'apps', 'server', 'node_modules', '@tbox.cn');
88
+ mkdirSync(linkDir, { recursive: true });
89
+ const link = join(linkDir, 'app-contracts');
90
+ symlinkSync(storeV1, link, 'dir');
91
+
92
+ const first = await resolveContractsForApp(app.appDir);
93
+ expect(first.version).toBe('0.8.0');
94
+ expect(first.strictValidation).toBe(false);
95
+
96
+ // symlink 换目标(pnpm 升级换 store 路径形态)——unlink 只摘链接本体(不动 store 内容)
97
+ unlinkSync(link);
98
+ symlinkSync(storeV2, link, 'dir');
99
+ expect(readlinkSync(link)).toBe(storeV2);
100
+
101
+ const second = await resolveContractsForApp(app.appDir);
102
+ expect(second.version).toBe('0.9.0');
103
+ expect(second.strictValidation).toBe(true);
104
+ });
105
+
106
+ it('import 拒绝:dist 存在但产物损坏 → 未解析返回(绝不 throw)——N1 构建指引分支输入', async () => {
107
+ const store = join(app.appDir, 'store-broken');
108
+ writeFakeContractsPackage(store, { version: '0.9.0', brokenDist: true });
109
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
110
+ const resolved = await resolveContractsForApp(app.appDir);
111
+ expect(resolved.evaluation).toBe(false);
112
+ expect(resolved.strictValidation).toBe(false);
113
+ expect(resolved.notes.some((n) => n.includes('导入失败') || n.includes('未构建'))).toBe(true);
114
+ });
115
+
116
+ it('workspace 源码态:仅 src/runtime.ts(dist 缺失)→ 命中 src 态或未解析 + 构建指引(绝不 throw)', async () => {
117
+ const store = join(app.appDir, 'store-src-only');
118
+ writeFakeContractsPackage(store, { version: '0.9.0', withDist: false });
119
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
120
+ const resolved = await resolveContractsForApp(app.appDir);
121
+ // vitest 管线可能转换 .ts 动态导入(TS-host 形态命中)——两种结果均合法:
122
+ // 命中 src 态(双面可用)或导入拒绝/产物缺失(未解析 + 构建指引)
123
+ if (resolved.evaluation) {
124
+ expect(resolved.version).toBe('0.9.0');
125
+ } else {
126
+ expect(resolved.notes.length).toBeGreaterThan(0);
127
+ }
128
+ });
129
+
130
+ it('memoize:同 appDir 并发调用共享单次解析 promise', async () => {
131
+ const store = join(app.appDir, 'store-memo');
132
+ writeFakeContractsPackage(store, { version: '0.9.0' });
133
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
134
+ const [a, b] = await Promise.all([
135
+ resolveContractsForApp(app.appDir),
136
+ resolveContractsForApp(app.appDir),
137
+ ]);
138
+ expect(a).toBe(b); // 同一 promise 实例——per appDir memoize
139
+ });
140
+
141
+ it('假包 exports 形态防线:复刻发布面双条件(types+import,无 require/default)', async () => {
142
+ const store = join(app.appDir, 'store-exports');
143
+ writeFakeContractsPackage(store, { version: '0.9.0' });
144
+ const pkg = JSON.parse(readFileSync(join(store, 'package.json'), 'utf8')) as {
145
+ exports: Record<string, Record<string, string>>;
146
+ };
147
+ expect(pkg.exports['.']).toEqual({ types: './dist/index.d.ts', import: './dist/runtime.js' });
148
+ expect(pkg.exports['.']?.require).toBeUndefined();
149
+ });
150
+
151
+ it('W4 补充:候选 package.json version 原地改写 → 轻探测察觉重解析', async () => {
152
+ const store = join(app.appDir, 'store-w4b');
153
+ writeFakeContractsPackage(store, { version: '0.9.0' });
154
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
155
+ expect((await resolveContractsForApp(app.appDir)).version).toBe('0.9.0');
156
+ // 原地 bump version(pnpm update 语义形态);mtime 前移防粗粒度 fs 同 tick 指纹不变(边界②)
157
+ const pkgPath = join(store, 'package.json');
158
+ writeFileSync(pkgPath, readFileSync(pkgPath, 'utf8').replace('0.9.0', '0.9.2'));
159
+ bumpMtime(pkgPath);
160
+ expect((await resolveContractsForApp(app.appDir)).version).toBe('0.9.2');
161
+ });
162
+
163
+ it('F9 dist 物化可觉察:unresolved(产物缺失,version 不变)→ 物化 dist → 下一笔 resolve 命中', async () => {
164
+ const store = join(app.appDir, 'store-f9-build');
165
+ // 源码态:仅 src/runtime.ts(vitest 下可能命中 src 态——先收敛为「双面可用或不可用」基线)
166
+ writeFakeContractsPackage(store, { version: '0.9.0', withDist: false });
167
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
168
+ const before = await resolveContractsForApp(app.appDir);
169
+ // 物化构建产物(等同 pnpm --filter build;version 不变——旧 memoize 键(仅 version)不觉察)
170
+ const dist = join(store, 'dist');
171
+ mkdirSync(dist, { recursive: true });
172
+ writeFileSync(
173
+ join(dist, 'runtime.js'),
174
+ [
175
+ 'export function resolveEffectiveService() { return { status: "service-not-configured" }; }',
176
+ 'export function buildSupplyLookup() { return {}; }',
177
+ 'export function moduleDomainOf(service) { return String(service).split(".")[0] ?? ""; }',
178
+ 'export function normalizeInstances(config) { return config ?? { instances: [], defaultInstanceId: null }; }',
179
+ 'export const RESOURCE_TYPE_IDS = ["knowledge"];',
180
+ 'export function parseIntegrationsConfig(raw) { return raw; }',
181
+ ].join('\n'),
182
+ );
183
+ const after = await resolveContractsForApp(app.appDir);
184
+ // F9 指纹(version + 产物态)——dist 物化后恒命中双面(不再返回旧解析产物)
185
+ expect(after.evaluation).toBe(true);
186
+ expect(after.strictValidation).toBe(true);
187
+ expect(before).not.toBe(after); // 触发重解析(非 memo 缓存 promise)
188
+ });
189
+
190
+ it('F9 候选序变化可觉察:更近候选出现(首候选指纹变)→ 重扫取近者', async () => {
191
+ const storeFar = join(app.appDir, 'store-far');
192
+ writeFakeContractsPackage(storeFar, { version: '0.8.2', withStrict: false });
193
+ linkContracts(app.appDir, storeFar, { subdir: 'apps/server' });
194
+ expect((await resolveContractsForApp(app.appDir)).version).toBe('0.8.2');
195
+ // 更近候选(appDir 根)出现——候选序优先于 apps/server
196
+ const storeNear = join(app.appDir, 'store-near');
197
+ writeFakeContractsPackage(storeNear, { version: '0.9.0' });
198
+ linkContracts(app.appDir, storeNear, { subdir: '' });
199
+ const after = await resolveContractsForApp(app.appDir);
200
+ expect(after.version).toBe('0.9.0');
201
+ expect(after.strictValidation).toBe(true);
202
+ });
203
+ });
@@ -0,0 +1,146 @@
1
+ import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, existsSync, rmSync, statSync, utimesSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join, dirname } from 'node:path';
4
+
5
+ /**
6
+ * 共享 demo-app builder(F6 钉子):W7 动线矩阵 / O 视图矩阵 / CLI golden 三族共用,
7
+ * 防 fixture 三份漂移。沿 cli/tests mkdtempSync 惯例(临时应用建在系统 tmp)。
8
+ *
9
+ * C2 版:app 树布局 + contracts 假包 fixture(resolver 五态)。C3/C4 随 walkManifests/views
10
+ * 扩展 demand/supply/schemas 产物与集成配置。
11
+ */
12
+
13
+ export interface TempApp {
14
+ /** 应用根(tmp 下) */
15
+ appDir: string;
16
+ /** 清理(rm -rf tmp 根) */
17
+ dispose(): void;
18
+ }
19
+
20
+ /** 临时应用树:.tbox/app.json + apps/server + packages/ + config/(最小可解析布局) */
21
+ export function createTempApp(prefix = 'tbox-toolkit-'): TempApp {
22
+ const appDir = mkdtempSync(join(tmpdir(), prefix));
23
+ mkdirSync(join(appDir, '.tbox'), { recursive: true });
24
+ writeFileSync(
25
+ join(appDir, '.tbox', 'app.json'),
26
+ `${JSON.stringify({ templateVersion: 'test', npmModules: [] }, null, 2)}\n`,
27
+ );
28
+ mkdirSync(join(appDir, 'apps', 'server'), { recursive: true });
29
+ mkdirSync(join(appDir, 'apps', 'client'), { recursive: true });
30
+ mkdirSync(join(appDir, 'packages'), { recursive: true });
31
+ mkdirSync(join(appDir, 'config'), { recursive: true });
32
+ return { appDir, dispose: () => rmSync(appDir, { recursive: true, force: true }) };
33
+ }
34
+
35
+ export interface FakeContractsOptions {
36
+ /** package.json version(默认 0.9.0) */
37
+ version?: string;
38
+ /** 是否携带严格校验面 parseIntegrationsConfig(false = F5 缺 zod 面) */
39
+ withStrict?: boolean;
40
+ /** 是否物化构建产物 dist/runtime.js(false = 仅 src/runtime.ts——workspace 源码态) */
41
+ withDist?: boolean;
42
+ /** dist/runtime.js 注入语法错误(import 拒绝 fixture) */
43
+ brokenDist?: boolean;
44
+ /** dist/runtime.js 缺求值面具名导出(形态异常 fixture) */
45
+ missingEvaluationExports?: boolean;
46
+ /** 严格校验失败 fixture(FX-1c T5):parseIntegrationsConfig 对含该子串文档 throw */
47
+ rejectMarker?: string;
48
+ /** resolveEffectiveService 自定义函数体(FX-2c O4 fixture:默认恒 service-not-configured;
49
+ * 入参 (integrations, service, instanceId, supplies),需 return EffectiveServiceResolution 形状) */
50
+ resolveBody?: string;
51
+ }
52
+
53
+ /** 假 contracts 包:package.json exports 形态逐字段复刻发布面(types+import 双条件、无 require/default)
54
+ * ——resolver 不经 exports 解析(包根直探),该形态为 E4 类错误的机械防线。 */
55
+ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContractsOptions = {}): void {
56
+ const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody } = opts;
57
+ const strictBody = withStrict
58
+ ? `export function parseIntegrationsConfig(raw) { ${
59
+ rejectMarker ? `if (JSON.stringify(raw).includes(${JSON.stringify(rejectMarker)})) throw new Error("fake strict reject"); ` : ''
60
+ }return raw; }`
61
+ : '';
62
+ const resolveLine = `export function resolveEffectiveService(integrations, service, instanceId, supplies) { ${
63
+ resolveBody ?? 'return { status: "service-not-configured" };'
64
+ } }`;
65
+ const exportsBody = missingEvaluationExports
66
+ ? `export const marker = 1;\n${strictBody}`
67
+ : [
68
+ resolveLine,
69
+ 'export function buildSupplyLookup() { return {}; }',
70
+ 'export function moduleDomainOf(service) { return String(service).split(".")[0] ?? ""; }',
71
+ 'export function normalizeInstances(config) { return { instances: (config && config.instances) || [], defaultInstanceId: (config && config.defaultInstanceId) ?? null }; }',
72
+ 'export const RESOURCE_TYPE_IDS = ["knowledge"];',
73
+ strictBody,
74
+ '',
75
+ ]
76
+ .filter(Boolean)
77
+ .join('\n');
78
+ mkdirSync(packageRoot, { recursive: true });
79
+ writeFileSync(
80
+ join(packageRoot, 'package.json'),
81
+ `${JSON.stringify(
82
+ {
83
+ name: '@tbox.cn/app-contracts',
84
+ version,
85
+ type: 'module',
86
+ exports: {
87
+ '.': {
88
+ types: './dist/index.d.ts',
89
+ import: './dist/runtime.js',
90
+ },
91
+ },
92
+ },
93
+ null,
94
+ 2,
95
+ )}\n`,
96
+ );
97
+ if (withDist) {
98
+ mkdirSync(join(packageRoot, 'dist'), { recursive: true });
99
+ writeFileSync(
100
+ join(packageRoot, 'dist', 'runtime.js'),
101
+ brokenDist ? 'export { syntax error here ===' : exportsBody,
102
+ );
103
+ } else {
104
+ mkdirSync(join(packageRoot, 'src'), { recursive: true });
105
+ writeFileSync(join(packageRoot, 'src', 'runtime.ts'), exportsBody);
106
+ }
107
+ }
108
+
109
+ export interface LinkOptions {
110
+ /** 候选子目录:''(appDir 根)/ 'apps/server' / 'apps/client' / 'packages/<id>' */
111
+ subdir: string;
112
+ }
113
+
114
+ /** 把假 contracts 包 symlink 进应用候选位(node_modules/@tbox.cn/app-contracts) */
115
+ export function linkContracts(appDir: string, packageRoot: string, opts: LinkOptions): void {
116
+ const linkDir = join(appDir, opts.subdir, 'node_modules', '@tbox.cn');
117
+ mkdirSync(linkDir, { recursive: true });
118
+ symlinkSync(packageRoot, join(linkDir, 'app-contracts'), 'dir');
119
+ }
120
+
121
+ /** 应用内写文件(外部写模拟——files API / CLI / agent 动线) */
122
+ export function writeAppFile(appDir: string, relPath: string, content: string): void {
123
+ const abs = join(appDir, relPath);
124
+ mkdirSync(dirname(abs), { recursive: true });
125
+ writeFileSync(abs, content);
126
+ }
127
+
128
+ /**
129
+ * 外部写 mtime 前移(粗粒度文件系统确定性防御):模拟「更晚发生的外部写」。
130
+ *
131
+ * Linux coarse-clock fs(CI 容器 overlayfs/tmpfs)时间戳粒度 = 内核 jiffy(1-10ms)——
132
+ * 同 tick 内同 size 重写产生相同 stat 指纹(mtimeMs+size),file-cache 依 Freshness Contract
133
+ * 边界②不觉察;macOS/APFS 亚毫秒粒度恒可觉察。显式前移 mtime 后 bump 值 = 写入时刻+forwardMs,
134
+ * 恒大于此前任何缓存指纹(时间单调性),跨文件系统确定(CI 失败复盘见 Agent Note
135
+ * implemented/testing/2026-09-14-file-cache-test-mtime-determinism.md)。
136
+ */
137
+ export function bumpMtime(path: string, forwardMs = 60_000): void {
138
+ const st = statSync(path);
139
+ const later = new Date(st.mtimeMs + forwardMs);
140
+ utimesSync(path, later, later);
141
+ }
142
+
143
+ /** 应用内文件是否存在 */
144
+ export function appFileExists(appDir: string, relPath: string): boolean {
145
+ return existsSync(join(appDir, relPath));
146
+ }
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { walkManifests } from '../src/assembly/walk-manifests.js';
5
+ import { createTempApp, type TempApp } from './demo-app.js';
6
+
7
+ /**
8
+ * dev-manifest catalog 通道(通道 1.5)单测:聚合宽/声明窄不变量锁定。
9
+ * 回归背景(2026-09-15):模板出厂 npmModules 写入 provider-custom(a41feb7c)打破
10
+ * 「dev 原地恒空」假设 → 旧替换式兜底永不触发 → dev 会话 catalog 为空。
11
+ */
12
+
13
+ /** strict descriptor(形状对齐 provider-custom 出厂 manifest——parseModuleDescriptor 严格面) */
14
+ function descriptor(name: string, provider: string, service: string): Record<string, unknown> {
15
+ return {
16
+ schemaVersion: 1,
17
+ name,
18
+ version: '1.0.0',
19
+ kind: 'business',
20
+ risk: { level: 'low', writeBoundary: 'source' },
21
+ distribution: { defaultMode: 'codegen' },
22
+ contributes: {
23
+ handlers: [], tools: [], cards: [], routes: [], pages: [], tabs: [],
24
+ providers: {
25
+ slots: [{ service, provider, implementation: `${provider}-impl@1`, credentialType: 'apikey' }],
26
+ },
27
+ services: [],
28
+ },
29
+ dependencies: { modules: [] },
30
+ env: [],
31
+ };
32
+ }
33
+
34
+ function writeManifest(dir: string, manifest: Record<string, unknown>): void {
35
+ mkdirSync(dir, { recursive: true });
36
+ writeFileSync(join(dir, 'tbox.module.json'), JSON.stringify(manifest));
37
+ }
38
+
39
+ function writeDevManifest(appDir: string, redirects: Record<string, string>): void {
40
+ const stage = join(appDir, '.tbox-dev.local');
41
+ mkdirSync(stage, { recursive: true });
42
+ writeFileSync(join(stage, 'dev-manifest.json'), JSON.stringify({ redirects }));
43
+ }
44
+
45
+ function writeNpmModules(appDir: string, entries: unknown[]): void {
46
+ writeFileSync(
47
+ join(appDir, '.tbox', 'app.json'),
48
+ JSON.stringify({ templateVersion: 'test', npmModules: entries }),
49
+ );
50
+ }
51
+
52
+ const REG = { id: 'provider-reg', package: '@tbox.cn/app-provider-reg', version: '1.0.0' };
53
+
54
+ let app: TempApp | undefined;
55
+ afterEach(() => {
56
+ app?.dispose();
57
+ app = undefined;
58
+ });
59
+
60
+ describe('walkManifests · dev-manifest catalog 通道(聚合宽/声明窄)', () => {
61
+ it('D1: 登记 ∪ dev 真身并集进 catalog;双键单聚合;dev 真身不进 modules(回归锁 + 不变量锁)', () => {
62
+ app = createTempApp('tbox-devcat-');
63
+ writeNpmModules(app.appDir, [REG]);
64
+ writeManifest(join(app.appDir, 'packages', 'provider-reg'), descriptor('provider-reg', 'p-reg', 'reg.svc'));
65
+ writeManifest(join(app.appDir, 'modules', 'provider-dev'), descriptor('provider-dev', 'p-dev', 'dev.svc'));
66
+ writeDevManifest(app.appDir, {
67
+ '@tbox.cn/app-provider-dev': '../modules/provider-dev',
68
+ '@app/provider-dev': '../modules/provider-dev', // dev-manifest 双键同指 → 按名去重单次聚合
69
+ });
70
+
71
+ const walk = walkManifests(app.appDir);
72
+
73
+ expect(Object.keys(walk.catalog.providers)).toContain('p-reg');
74
+ expect(Object.keys(walk.catalog.providers)).toContain('p-dev');
75
+ expect(walk.catalog.providers['p-dev']?.['p-dev-impl@1']?.services).toEqual(['dev.svc']);
76
+ expect(walk.modules.map((m) => m.id)).toEqual(['provider-reg']); // 不变量:dev 真身不进声明集
77
+ });
78
+
79
+ it('D2: dev 真身与登记同名 → 登记真源胜(seen 短路,无二次聚合)', () => {
80
+ app = createTempApp('tbox-devcat-');
81
+ writeNpmModules(app.appDir, [REG]);
82
+ writeManifest(join(app.appDir, 'packages', 'provider-reg'), descriptor('provider-reg', 'p-reg', 'reg.svc'));
83
+ writeManifest(join(app.appDir, 'modules', 'provider-x'), descriptor('provider-reg', 'p-shadow', 'shadow.svc'));
84
+ writeDevManifest(app.appDir, { '@tbox.cn/app-provider-x': '../modules/provider-x' });
85
+
86
+ const walk = walkManifests(app.appDir);
87
+ expect(Object.keys(walk.catalog.providers)).toEqual(['p-reg']);
88
+ expect(walk.modules).toHaveLength(1);
89
+ });
90
+
91
+ it('D3: 坏 dev-manifest → 不 throw,catalog 退回纯登记(宽松哲学)', () => {
92
+ app = createTempApp('tbox-devcat-');
93
+ writeNpmModules(app.appDir, [REG]);
94
+ writeManifest(join(app.appDir, 'packages', 'provider-reg'), descriptor('provider-reg', 'p-reg', 'reg.svc'));
95
+ const stage = join(app.appDir, '.tbox-dev.local');
96
+ mkdirSync(stage, { recursive: true });
97
+ writeFileSync(join(stage, 'dev-manifest.json'), '{broken json');
98
+
99
+ const appDir = app.appDir; // 闭包内 let 控制流收窄丢失(TS18048)——闭包外捕获
100
+ expect(() => walkManifests(appDir)).not.toThrow();
101
+ expect(Object.keys(walkManifests(app.appDir).catalog.providers)).toEqual(['p-reg']);
102
+ });
103
+
104
+ it('D4: 空登记 + dev-manifest → catalog 含 dev 真身;modules 恒空(旧 AC-2b 语义经新通道等价)', () => {
105
+ app = createTempApp('tbox-devcat-');
106
+ writeNpmModules(app.appDir, []);
107
+ writeManifest(join(app.appDir, 'modules', 'provider-dev'), descriptor('provider-dev', 'p-dev', 'dev.svc'));
108
+ writeDevManifest(app.appDir, { '@tbox.cn/app-provider-dev': '../modules/provider-dev' });
109
+
110
+ const walk = walkManifests(app.appDir);
111
+ expect(Object.keys(walk.catalog.providers)).toContain('p-dev');
112
+ expect(walk.modules).toHaveLength(0);
113
+ });
114
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as toolkit from '../src/index';
3
+
4
+ /**
5
+ * error-name-safety 守卫(src 态——C4;dist 验证归 C6 minified 冒烟层②)。
6
+ * 沿 app-sdk 模式:barrel 枚举 Error 子类 → 断言 new X('m').name === 导出名
7
+ * (禁 this.name = X.name binding 形态——mangle 后静默漂移;真源 process note
8
+ * 2026-09-11-publish-dist-minify)。toolkit 消费面属性分发(.httpStatus/.code/.fields)
9
+ * 比 err.name 分发更宽裕——守卫仍钉字面量约定。
10
+ */
11
+
12
+ const ERROR_EXPORTS = Object.entries(toolkit).filter(
13
+ ([, v]) => typeof v === 'function' && (v.prototype instanceof Error || v.prototype?.constructor === Error),
14
+ );
15
+
16
+ describe('error-name-safety(src 态守卫)', () => {
17
+ it('空集合自检阈值 ≥1(Error 子类枚举非空——barrel 变更不静默跳过)', () => {
18
+ expect(ERROR_EXPORTS.length).toBeGreaterThanOrEqual(1);
19
+ });
20
+
21
+ it.each(ERROR_EXPORTS.map(([name]) => [name]))('%s.name === 导出名(字面量约定)', (name) => {
22
+ const Ctor = (toolkit as unknown as Record<string, new (msg: string) => Error>)[name];
23
+ const err = new Ctor('m');
24
+ expect(err.name).toBe(name);
25
+ });
26
+ });