@longzai-intelligence-issues/ledger 0.0.1

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 (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +26 -0
  3. package/dist/index.d.ts +2123 -0
  4. package/dist/index.js +78 -0
  5. package/dist/rolldown-runtime-BqT_7tdF.js +1 -0
  6. package/lzi-builder.config.ts +15 -0
  7. package/lzi-bun.config.ts +8 -0
  8. package/oxlint.config.ts +3 -0
  9. package/package.json +39 -0
  10. package/src/__tests__/close/verify-close.commands.test.ts +560 -0
  11. package/src/__tests__/docs-refs/docs-refs.commands.test.ts +213 -0
  12. package/src/__tests__/fs/mini-glob.utils.test.ts +71 -0
  13. package/src/__tests__/fs/walk-surface.utils.test.ts +170 -0
  14. package/src/__tests__/health/health.commands.test.ts +131 -0
  15. package/src/__tests__/lint/lint.core.test.ts +332 -0
  16. package/src/__tests__/numbering/numbering.commands.test.ts +194 -0
  17. package/src/__tests__/numbering/numbering.core.test.ts +318 -0
  18. package/src/__tests__/parser/guide-fixture.utils.test.ts +45 -0
  19. package/src/__tests__/parser/registry.parser.test.ts +316 -0
  20. package/src/__tests__/prompts/prompts.commands.test.ts +47 -0
  21. package/src/__tests__/template/issue-template.renderer.test.ts +133 -0
  22. package/src/close/evidence-reader.utils.ts +201 -0
  23. package/src/close/green-flip.commands.ts +199 -0
  24. package/src/close/verify-close.commands.ts +676 -0
  25. package/src/docs-refs/docs-refs.commands.ts +693 -0
  26. package/src/freeze/freeze.commands.ts +261 -0
  27. package/src/fs/mini-glob.utils.ts +216 -0
  28. package/src/fs/walk-surface.utils.ts +298 -0
  29. package/src/health/health.commands.ts +327 -0
  30. package/src/index.ts +46 -0
  31. package/src/lint/lint-baseline.commands.ts +147 -0
  32. package/src/lint/lint.core.ts +584 -0
  33. package/src/normalize/normalize-header.commands.ts +361 -0
  34. package/src/numbering/numbering.commands.ts +375 -0
  35. package/src/numbering/numbering.core.ts +685 -0
  36. package/src/parser/format.utils.ts +279 -0
  37. package/src/parser/guide-fixture.utils.ts +117 -0
  38. package/src/parser/registry.parser.ts +679 -0
  39. package/src/prompts/prompts.commands.ts +211 -0
  40. package/src/template/issue-template.renderer.ts +351 -0
  41. package/src/triage/triage.classify.ts +93 -0
  42. package/src/vault/vault.commands.ts +434 -0
  43. package/tsconfig/.cache/build.tsbuildinfo +1 -0
  44. package/tsconfig/.cache/node.tsbuildinfo +1 -0
  45. package/tsconfig/.cache/test.tsbuildinfo +1 -0
  46. package/tsconfig/app.json +13 -0
  47. package/tsconfig/build.json +15 -0
  48. package/tsconfig/node.json +12 -0
  49. package/tsconfig/test.json +15 -0
  50. package/tsconfig.json +23 -0
@@ -0,0 +1,213 @@
1
+ import { parseLziIssuesConfig } from '@longzai-intelligence-issues/config';
2
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
3
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ import { runDocsCheck, runDocsFix, runWriteDocsBaseline } from '@/docs-refs/docs-refs.commands';
7
+
8
+ /**
9
+ * 测试夹具根(包内临时目录)
10
+ */
11
+ const FIXTURE_ROOT = join(import.meta.dir, '__tmp-docsrefs__');
12
+
13
+ /**
14
+ * 解析后的测试配置
15
+ */
16
+ const CONFIG = parseLziIssuesConfig({
17
+ schemaVersion: 1,
18
+ scopes: [
19
+ { key: 'root', title: '全局', paths: ['.'], registryDir: 'docs/issues' },
20
+ { key: 'domains', title: '领域层', paths: ['domains/**'], registryDir: 'docs/domain-issues' },
21
+ ],
22
+ docs: { scanRoots: ['docs-guides'] },
23
+ });
24
+
25
+ /**
26
+ * 写入夹具:两 scope 注册表 + 引用文档
27
+ */
28
+ function writeFixture(): void {
29
+ rmSync(FIXTURE_ROOT, { recursive: true, force: true });
30
+
31
+ for (const dir of ['docs/issues', 'docs/domain-issues', 'docs-guides']) {
32
+ mkdirSync(join(FIXTURE_ROOT, dir), { recursive: true });
33
+ }
34
+
35
+ writeFileSync(join(FIXTURE_ROOT, 'docs/issues/0048-runs-list-grace.md'), '**状态**: 🔴 待处理\n');
36
+ writeFileSync(join(FIXTURE_ROOT, 'docs/issues/0050-alpha-one.md'), '**状态**: 🔴 待处理\n');
37
+ writeFileSync(join(FIXTURE_ROOT, 'docs/issues/0050-alpha-two.md'), '**状态**: 🔴 待处理\n');
38
+ writeFileSync(
39
+ join(FIXTURE_ROOT, 'docs/domain-issues/0048-domain-variant.md'),
40
+ '**状态**: 🔴 待处理\n',
41
+ );
42
+
43
+ /**
44
+ * 引用文档:完整 / 纯编号多义 / 唯一前缀(两种粒度)/ 前缀撞车 / 未知编号
45
+ */
46
+ writeFileSync(
47
+ join(FIXTURE_ROOT, 'docs-guides/guide.md'),
48
+ [
49
+ '# 指南',
50
+ '',
51
+ '完整引用:[0048-runs-list-grace](./../docs/issues/0048-runs-list-grace.md) 通过。',
52
+ '纯编号跨 scope 多义:[0048] 应报 ambiguous。',
53
+ '唯一前缀(短):[0048-runs] 应通过并可机械展开。',
54
+ '唯一前缀(长):[0048-runs-list] 应通过并可机械展开。',
55
+ '前缀撞车:[0050-alpha] 命中两条,应 refused 交人工。',
56
+ '未知编号:[0099-ghost]。',
57
+ ].join('\n'),
58
+ );
59
+ }
60
+
61
+ /**
62
+ * 清理夹具
63
+ */
64
+ function cleanFixture(): void {
65
+ rmSync(FIXTURE_ROOT, { recursive: true, force: true });
66
+ }
67
+
68
+ beforeEach(writeFixture);
69
+
70
+ afterEach(cleanFixture);
71
+
72
+ describe('runDocsCheck', () => {
73
+ test('完整引用与唯一前缀通过;多义列候选;未知报 unknown', () => {
74
+ const result = runDocsCheck({ root: FIXTURE_ROOT, config: CONFIG });
75
+
76
+ /**
77
+ * 规则 → 违例数
78
+ */
79
+ const byRule = new Map<string, number>();
80
+
81
+ for (const violation of result.violations) {
82
+ byRule.set(violation.rule, (byRule.get(violation.rule) ?? 0) + 1);
83
+ }
84
+
85
+ expect(byRule.get('doc-ref-ambiguous')).toBe(2);
86
+ expect(byRule.get('doc-ref-unknown')).toBe(1);
87
+ expect(result.scannedFiles).toBe(1);
88
+
89
+ /**
90
+ * 纯编号跨 scope 多义报文含 scope 与完整 slug 候选
91
+ */
92
+ const pureNumber = result.violations.find(
93
+ (v) => v.rule === 'doc-ref-ambiguous' && v.message.includes('引用 0048 '),
94
+ );
95
+
96
+ expect(pureNumber?.message).toContain('root/0048-runs-list-grace');
97
+ expect(pureNumber?.message).toContain('domains/0048-domain-variant');
98
+ });
99
+
100
+ test('未登记 docs 节显式拒绝', () => {
101
+ expect(() =>
102
+ runDocsCheck({
103
+ root: FIXTURE_ROOT,
104
+ config: parseLziIssuesConfig({
105
+ schemaVersion: 1,
106
+ scopes: CONFIG.scopes,
107
+ }),
108
+ }),
109
+ ).toThrow('docs 节');
110
+ });
111
+
112
+ test('棘轮基线三态(豁免/新增红/陈旧红)', () => {
113
+ /**
114
+ * 带基线配置:豁免 unknown + 登记一条陈旧
115
+ */
116
+ const configWithBaseline = parseLziIssuesConfig({
117
+ schemaVersion: 1,
118
+ scopes: CONFIG.scopes,
119
+ docs: {
120
+ scanRoots: ['docs-guides'],
121
+ baseline: [
122
+ { file: 'docs-guides/guide.md', rule: 'doc-ref-unknown', issue: '0050' },
123
+ { file: 'docs-guides/other.md', rule: 'doc-ref-unknown', issue: '0051' },
124
+ ],
125
+ },
126
+ });
127
+
128
+ const result = runDocsCheck({ root: FIXTURE_ROOT, config: configWithBaseline });
129
+
130
+ expect(result.suppressed).toBe(1);
131
+ expect(result.violations.some((v) => v.rule === 'baseline-stale')).toBe(true);
132
+ expect(result.violations.some((v) => v.rule === 'doc-ref-unknown')).toBe(false);
133
+ expect(result.violations.some((v) => v.rule === 'doc-ref-ambiguous')).toBe(true);
134
+ });
135
+ });
136
+
137
+ describe('runDocsFix', () => {
138
+ test('唯一前缀机械展开为完整 slug,撞车 refused 交人工', () => {
139
+ const result = runDocsFix({ root: FIXTURE_ROOT, config: CONFIG });
140
+
141
+ expect(result.fixedFiles).toEqual(['guide.md']);
142
+ expect(result.refused).toHaveLength(1);
143
+ expect(result.refused[0]?.message).toContain('0050-alpha');
144
+
145
+ /**
146
+ * 修复产物:两种粒度前缀均已展开,其余文本不动
147
+ */
148
+ const fixed = readFileSync(join(FIXTURE_ROOT, 'docs-guides/guide.md'), 'utf8');
149
+
150
+ expect(fixed).toContain('[0048-runs-list-grace](./../docs/issues/0048-runs-list-grace.md)');
151
+ expect(fixed).toContain('[0048-runs-list-grace] 应通过并可机械展开。');
152
+ expect(fixed).toContain('[0050-alpha] 命中两条,应 refused 交人工。');
153
+ });
154
+
155
+ test('dry-run 只呈现不落盘', () => {
156
+ runDocsFix({ root: FIXTURE_ROOT, config: CONFIG, dryRun: true });
157
+
158
+ const content = readFileSync(join(FIXTURE_ROOT, 'docs-guides/guide.md'), 'utf8');
159
+
160
+ expect(content).toContain('[0048-runs-list]');
161
+ });
162
+
163
+ test('复跑幂等(无前缀可展开时零修复)', () => {
164
+ runDocsFix({ root: FIXTURE_ROOT, config: CONFIG });
165
+
166
+ const second = runDocsFix({ root: FIXTURE_ROOT, config: CONFIG });
167
+
168
+ expect(second.fixedFiles).toEqual([]);
169
+ });
170
+ });
171
+
172
+ describe('runWriteDocsBaseline', () => {
173
+ test('存在基线外新增违规拒绝再生', () => {
174
+ const result = runWriteDocsBaseline({
175
+ root: FIXTURE_ROOT,
176
+ config: CONFIG,
177
+ issuePointer: '0060',
178
+ });
179
+
180
+ expect(result.ok).toBe(false);
181
+ expect(result.ok === false && result.error).toContain('拒绝再生');
182
+ });
183
+
184
+ test('基线覆盖全集时再生成功', () => {
185
+ /**
186
+ * 全集违例
187
+ */
188
+ const all = runDocsCheck({
189
+ root: FIXTURE_ROOT,
190
+ config: CONFIG,
191
+ ignoreBaseline: true,
192
+ }).violations;
193
+
194
+ const configWithBaseline = parseLziIssuesConfig({
195
+ schemaVersion: 1,
196
+ scopes: CONFIG.scopes,
197
+ docs: {
198
+ scanRoots: ['docs-guides'],
199
+ baseline: all.map((v) => ({ file: v.file, rule: v.rule, issue: '0050' })),
200
+ },
201
+ });
202
+
203
+ const result = runWriteDocsBaseline({
204
+ root: FIXTURE_ROOT,
205
+ config: configWithBaseline,
206
+ issuePointer: '0060',
207
+ });
208
+
209
+ expect(result.ok).toBe(true);
210
+ expect(result.ok === true && result.content).toContain('棘轮只许缩短');
211
+ expect(result.ok === true && result.entryCount).toBe(all.length);
212
+ });
213
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { compileMiniGlob, matchMiniGlob, MiniGlobError } from '@/fs/mini-glob.utils';
4
+
5
+ describe('matchMiniGlob 语义', () => {
6
+ test('`*` 匹配段内任意字符不跨分隔', () => {
7
+ expect(matchMiniGlob('docs/*.md', 'docs/a.md')).toBe(true);
8
+ expect(matchMiniGlob('docs/*.md', 'docs/x/y.md')).toBe(false);
9
+ expect(matchMiniGlob('docs/*.md', 'apps/a.md')).toBe(false);
10
+ });
11
+
12
+ test('`**/` 匹配零或多层目录', () => {
13
+ expect(matchMiniGlob('docs/**/*.md', 'docs/a.md')).toBe(true);
14
+ expect(matchMiniGlob('docs/**/*.md', 'docs/x/y/z.md')).toBe(true);
15
+ expect(matchMiniGlob('docs/**/*.md', 'a.md')).toBe(false);
16
+ expect(matchMiniGlob('docs/**/*.md', 'docs/x/y.txt')).toBe(false);
17
+ });
18
+
19
+ test('前缀 `**/` 同样成立', () => {
20
+ expect(matchMiniGlob('**/issues/*.md', 'issues/a.md')).toBe(true);
21
+ expect(matchMiniGlob('**/issues/*.md', 'docs/issues/a.md')).toBe(true);
22
+ expect(matchMiniGlob('**/issues/*.md', 'docs/other/a.md')).toBe(false);
23
+ });
24
+
25
+ test('尾缀 `**` 匹配子树全部', () => {
26
+ expect(matchMiniGlob('docs/**', 'docs/a/b.md')).toBe(true);
27
+ expect(matchMiniGlob('docs/**', 'docs')).toBe(false);
28
+ });
29
+
30
+ test('`?` 匹配段内单字符', () => {
31
+ expect(matchMiniGlob('docs/?eadme.md', 'docs/readme.md')).toBe(true);
32
+ expect(matchMiniGlob('docs/?eadme.md', 'docs/reaadme.md')).toBe(false);
33
+ });
34
+
35
+ test('`{a,b}` 段内多选一', () => {
36
+ expect(matchMiniGlob('{docs,apps}/x.md', 'docs/x.md')).toBe(true);
37
+ expect(matchMiniGlob('{docs,apps}/x.md', 'apps/x.md')).toBe(true);
38
+ expect(matchMiniGlob('{docs,apps}/x.md', 'pkg/x.md')).toBe(false);
39
+ });
40
+
41
+ test('`{a,b}` 候选项可含通配', () => {
42
+ expect(matchMiniGlob('docs/{*a,b}/x.md', 'docs/alpha/x.md')).toBe(true);
43
+ expect(matchMiniGlob('docs/{*a,b}/x.md', 'docs/b/x.md')).toBe(true);
44
+ expect(matchMiniGlob('docs/{*a,b}/x.md', 'docs/zzz/x.md')).toBe(false);
45
+ });
46
+
47
+ test('字面模式仅精确匹配', () => {
48
+ expect(matchMiniGlob('docs/issues/0001-a.md', 'docs/issues/0001-a.md')).toBe(true);
49
+ expect(matchMiniGlob('docs/issues/0001-a.md', 'docs/issues/0001-b.md')).toBe(false);
50
+ });
51
+
52
+ test('中文与点号字面安全', () => {
53
+ expect(matchMiniGlob('docs/报告/*.md', 'docs/报告/一.md')).toBe(true);
54
+ expect(matchMiniGlob('docs/a.b.md', 'docs/a.b.md')).toBe(true);
55
+ expect(matchMiniGlob('docs/a.b.md', 'docs/aXb.md')).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe('不支持语法显式抛错', () => {
60
+ test('字符类 `[` 抛错', () => {
61
+ expect(() => compileMiniGlob('docs/[abc].md')).toThrow(MiniGlobError);
62
+ });
63
+
64
+ test('大括号未闭合抛错', () => {
65
+ expect(() => compileMiniGlob('docs/{a/*.md')).toThrow(MiniGlobError);
66
+ });
67
+
68
+ test('嵌套大括号抛错', () => {
69
+ expect(() => compileMiniGlob('docs/{{a,b}}.md')).toThrow(MiniGlobError);
70
+ });
71
+ });
@@ -0,0 +1,170 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ import { matchMiniGlob } from '@/fs/mini-glob.utils';
6
+ import {
7
+ deriveWalkPrefixes,
8
+ walkFilesUnderPrefixes,
9
+ walkFilesUnderRoots,
10
+ } from '@/fs/walk-surface.utils';
11
+
12
+ /**
13
+ * 测试夹具根目录(包内临时目录)
14
+ */
15
+ const FIXTURE_ROOT = join(import.meta.dir, '__tmp-walk__');
16
+
17
+ /**
18
+ * 写入测试夹具目录树
19
+ */
20
+ function writeFixture(): void {
21
+ rmSync(FIXTURE_ROOT, { recursive: true, force: true });
22
+
23
+ /**
24
+ * 目录清单
25
+ */
26
+ const dirs = ['docs/issues', 'docs/generated', 'docs/other/deep', 'apps/cli', '.git/objects'];
27
+
28
+ for (const dir of dirs) {
29
+ mkdirSync(join(FIXTURE_ROOT, dir), { recursive: true });
30
+ }
31
+
32
+ /**
33
+ * 文件清单(相对路径 → 内容)
34
+ */
35
+ const files: Record<string, string> = {
36
+ 'a.md': '',
37
+ 'root.txt': '',
38
+ 'docs/b.md': '',
39
+ 'docs/issues/0001-x.md': '',
40
+ 'docs/issues/README.md': '',
41
+ 'docs/generated/g.md': '',
42
+ 'docs/other/deep/d.md': '',
43
+ 'apps/c.md': '',
44
+ 'apps/cli/main.ts': '',
45
+ '.git/hidden.md': '',
46
+ };
47
+
48
+ for (const [rel, content] of Object.entries(files)) {
49
+ writeFileSync(join(FIXTURE_ROOT, rel), content);
50
+ }
51
+
52
+ /**
53
+ * 符号链接(指向目录,验证跳过语义)
54
+ */
55
+ symlinkSync(join(FIXTURE_ROOT, 'docs'), join(FIXTURE_ROOT, 'docs-link'));
56
+ }
57
+
58
+ /**
59
+ * 清理测试夹具
60
+ */
61
+ function cleanFixture(): void {
62
+ rmSync(FIXTURE_ROOT, { recursive: true, force: true });
63
+ }
64
+
65
+ beforeEach(writeFixture);
66
+
67
+ afterEach(cleanFixture);
68
+
69
+ describe('deriveWalkPrefixes', () => {
70
+ test('字面目录续推到倒数第二段', () => {
71
+ expect(deriveWalkPrefixes(['docs/issues/*.md'])).toEqual(['docs/issues']);
72
+ });
73
+
74
+ test('通配目录段截断(保守超集)', () => {
75
+ expect(deriveWalkPrefixes(['docs/**/*.md'])).toEqual(['docs']);
76
+ expect(deriveWalkPrefixes(['docs/*abc/x.md'])).toEqual(['docs']);
77
+ });
78
+
79
+ test('首段即通配贡献全仓标记', () => {
80
+ expect(deriveWalkPrefixes(['*.md'])).toEqual(['']);
81
+ });
82
+
83
+ test('大括号分叉(字面候选续推,通配候选截断)', () => {
84
+ expect(deriveWalkPrefixes(['{docs,apps}/issues/*.md'])).toEqual(['docs/issues', 'apps/issues']);
85
+ expect(deriveWalkPrefixes(['docs/{*a,b}/x.md'])).toEqual(['docs']);
86
+ });
87
+
88
+ test('包含关系裁剪(宽前缀吞并窄前缀)', () => {
89
+ expect(deriveWalkPrefixes(['docs/*.md', 'docs/issues/*.md'])).toEqual(['docs']);
90
+ });
91
+
92
+ test('空模式集产出空前缀集(无扫描面)', () => {
93
+ expect(deriveWalkPrefixes([])).toEqual([]);
94
+ });
95
+ });
96
+
97
+ describe('walkFilesUnderPrefixes', () => {
98
+ test('前缀定向只收前缀下文件,.git 恒跳过', () => {
99
+ const files = walkFilesUnderPrefixes(FIXTURE_ROOT, ['docs/issues']);
100
+
101
+ expect(files).toEqual(['docs/issues/0001-x.md', 'docs/issues/README.md']);
102
+ });
103
+
104
+ test('排除 glob 整树剪枝(子树口径)', () => {
105
+ const files = walkFilesUnderPrefixes(FIXTURE_ROOT, ['docs'], {
106
+ excludeGlobs: ['docs/generated/**'],
107
+ });
108
+
109
+ expect(files.includes('docs/generated/g.md')).toBe(false);
110
+ expect(files.includes('docs/b.md')).toBe(true);
111
+ });
112
+
113
+ test('符号链接跳过', () => {
114
+ const files = walkFilesUnderPrefixes(FIXTURE_ROOT, ['']);
115
+
116
+ expect(files.some((file) => file.startsWith('docs-link'))).toBe(false);
117
+ });
118
+
119
+ test('前缀目录缺失静默跳过(未来目录合法)', () => {
120
+ expect(walkFilesUnderPrefixes(FIXTURE_ROOT, ['future/registry'])).toEqual([]);
121
+ });
122
+ });
123
+
124
+ describe('walkFilesUnderRoots', () => {
125
+ test('缺根抛错(防扫描面缩水)', () => {
126
+ expect(() => walkFilesUnderRoots(join(FIXTURE_ROOT, 'missing'))).toThrow('扫描面目录缺失');
127
+ });
128
+
129
+ test('全仓严格遍历收齐全部文件(.git 除外)', () => {
130
+ const files = walkFilesUnderRoots(FIXTURE_ROOT);
131
+
132
+ expect(files).toEqual([
133
+ 'a.md',
134
+ 'apps/c.md',
135
+ 'apps/cli/main.ts',
136
+ 'docs/b.md',
137
+ 'docs/generated/g.md',
138
+ 'docs/issues/0001-x.md',
139
+ 'docs/issues/README.md',
140
+ 'docs/other/deep/d.md',
141
+ 'root.txt',
142
+ ]);
143
+ });
144
+ });
145
+
146
+ describe('前缀推导与全仓口径等价守卫(常驻)', () => {
147
+ test('推导前缀收集 ≡ 全仓收集(按 include 过滤后)', () => {
148
+ /**
149
+ * 等价性守卫用的 include glob 集(覆盖字面/通配/大括号/全仓四形态)
150
+ */
151
+ const includeGlobs = ['docs/**/*.md', '*.md', '{apps,docs}/*.md'];
152
+
153
+ /**
154
+ * 推导前缀收集结果
155
+ */
156
+ const viaPrefixes = walkFilesUnderPrefixes(
157
+ FIXTURE_ROOT,
158
+ deriveWalkPrefixes(includeGlobs),
159
+ ).filter((file) => includeGlobs.some((glob) => matchMiniGlob(glob, file)));
160
+
161
+ /**
162
+ * 全仓收集结果
163
+ */
164
+ const viaWhole = walkFilesUnderRoots(FIXTURE_ROOT).filter((file) =>
165
+ includeGlobs.some((glob) => matchMiniGlob(glob, file)),
166
+ );
167
+
168
+ expect(viaPrefixes).toEqual(viaWhole);
169
+ });
170
+ });
@@ -0,0 +1,131 @@
1
+ import { parseLziIssuesConfig } from '@longzai-intelligence-issues/config';
2
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ import { runHealth, runTriage } from '@/health/health.commands';
7
+
8
+ /**
9
+ * 测试夹具根(包内临时目录)
10
+ */
11
+ const FIXTURE_ROOT = join(import.meta.dir, '__tmp-health__');
12
+
13
+ /**
14
+ * 解析后的测试配置
15
+ */
16
+ const CONFIG = parseLziIssuesConfig({
17
+ schemaVersion: 1,
18
+ scopes: [{ key: 'root', title: '全局', paths: ['.'], registryDir: 'docs/issues' }],
19
+ });
20
+
21
+ /**
22
+ * 写入夹具
23
+ */
24
+ function writeFixture(): void {
25
+ rmSync(FIXTURE_ROOT, { recursive: true, force: true });
26
+ mkdirSync(join(FIXTURE_ROOT, 'docs/issues'), { recursive: true });
27
+
28
+ /**
29
+ * 黄灯三类样例 + 红灯
30
+ */
31
+ writeFileSync(
32
+ join(FIXTURE_ROOT, 'docs/issues/0001-yellow-a.md'),
33
+ '**状态**: 🟡 处理中(判据补跑中)\n\n```criteria\nsummary: s\n单包测试|test|pkg|bun run test\n```\n',
34
+ );
35
+ writeFileSync(
36
+ join(FIXTURE_ROOT, 'docs/issues/0002-yellow-b.md'),
37
+ '**状态**: 🟡 处理中(等待真机回归)\n',
38
+ );
39
+ writeFileSync(
40
+ join(FIXTURE_ROOT, 'docs/issues/0003-yellow-c.md'),
41
+ '**状态**: 🟡 处理中(被并行线阻断)\n',
42
+ );
43
+ writeFileSync(join(FIXTURE_ROOT, 'docs/issues/0004-red.md'), '**状态**: 🔴 待处理\n');
44
+ }
45
+
46
+ /**
47
+ * 清理夹具
48
+ */
49
+ function cleanFixture(): void {
50
+ rmSync(FIXTURE_ROOT, { recursive: true, force: true });
51
+ }
52
+
53
+ beforeEach(writeFixture);
54
+
55
+ afterEach(cleanFixture);
56
+
57
+ describe('runHealth', () => {
58
+ test('状态分布、黄灯总数与 A-B-C 分类', () => {
59
+ const report = runHealth({ root: FIXTURE_ROOT, config: CONFIG });
60
+
61
+ expect(report.statusCounts['yellow']).toBe(3);
62
+ expect(report.statusCounts['red']).toBe(1);
63
+ expect(report.yellowTotal).toBe(3);
64
+ expect(report.categoryCounts['A']).toBe(1);
65
+ expect(report.categoryCounts['B']).toBe(1);
66
+ expect(report.categoryCounts['C']).toBe(1);
67
+ });
68
+
69
+ test('最老黄灯账龄按 mtime 天计算', () => {
70
+ const report = runHealth({ root: FIXTURE_ROOT, config: CONFIG });
71
+
72
+ expect(report.oldestYellowAgeDays).toBeGreaterThanOrEqual(0);
73
+ });
74
+
75
+ test('无黄灯时账龄为 0', () => {
76
+ rmSync(join(FIXTURE_ROOT, 'docs/issues/0001-yellow-a.md'));
77
+ rmSync(join(FIXTURE_ROOT, 'docs/issues/0002-yellow-b.md'));
78
+ rmSync(join(FIXTURE_ROOT, 'docs/issues/0003-yellow-c.md'));
79
+
80
+ const report = runHealth({ root: FIXTURE_ROOT, config: CONFIG });
81
+
82
+ expect(report.yellowTotal).toBe(0);
83
+ expect(report.oldestYellowAgeDays).toBe(0);
84
+ });
85
+ });
86
+
87
+ describe('runTriage', () => {
88
+ test('台账只收黄灯,append-only 落盘且结构完整', () => {
89
+ const { ledgerPath, ledger } = runTriage({ root: FIXTURE_ROOT, config: CONFIG });
90
+
91
+ expect(existsSync(ledgerPath)).toBe(true);
92
+ expect(ledgerPath).toContain(join('.lzi', 'issues', 'runs', 'ledger'));
93
+
94
+ expect(ledger.items).toHaveLength(3);
95
+
96
+ /**
97
+ * B/C 类理由与判据命令
98
+ */
99
+ const bItem = ledger.items.find((item) => item.category === 'B');
100
+ const aItem = ledger.items.find((item) => item.category === 'A');
101
+
102
+ expect(bItem?.reason).toContain('真机');
103
+ expect(aItem?.criteriaCommands).toEqual(['bun run test']);
104
+
105
+ /**
106
+ * 落盘内容可解析且与返回一致
107
+ */
108
+ const persisted = JSON.parse(readFileSync(ledgerPath, 'utf8')) as typeof ledger;
109
+
110
+ expect(persisted.items).toHaveLength(3);
111
+ });
112
+
113
+ test('再次运行产生新台账文件(append-only 不覆盖)', () => {
114
+ const first = runTriage({ root: FIXTURE_ROOT, config: CONFIG });
115
+
116
+ const second = runTriage({ root: FIXTURE_ROOT, config: CONFIG });
117
+
118
+ expect(first.ledgerPath).not.toBe(second.ledgerPath);
119
+ });
120
+
121
+ test('outDir 覆盖生效', () => {
122
+ /**
123
+ * 自定义输出目录
124
+ */
125
+ const outDir = join(FIXTURE_ROOT, 'custom-ledger');
126
+
127
+ const { ledgerPath } = runTriage({ root: FIXTURE_ROOT, config: CONFIG, outDir });
128
+
129
+ expect(ledgerPath.startsWith(outDir)).toBe(true);
130
+ });
131
+ });