@dommaker/harness 1.5.0 → 1.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.
Files changed (209) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +0 -2
  3. package/dist/cli/commands/definitions.d.ts.map +1 -1
  4. package/dist/cli/commands/definitions.js +1 -32
  5. package/dist/cli/commands/definitions.js.map +1 -1
  6. package/dist/cli/commands/passes-gate.d.ts +0 -2
  7. package/dist/cli/commands/passes-gate.d.ts.map +1 -1
  8. package/dist/cli/commands/passes-gate.js +0 -1
  9. package/dist/cli/commands/passes-gate.js.map +1 -1
  10. package/dist/cli/commands/performance.d.ts +0 -4
  11. package/dist/cli/commands/performance.d.ts.map +1 -1
  12. package/dist/cli/commands/performance.js +10 -20
  13. package/dist/cli/commands/performance.js.map +1 -1
  14. package/dist/cli/commands/report.d.ts +1 -1
  15. package/dist/cli/commands/report.d.ts.map +1 -1
  16. package/dist/cli/commands/report.js +2 -55
  17. package/dist/cli/commands/report.js.map +1 -1
  18. package/dist/cli/commands/status.d.ts.map +1 -1
  19. package/dist/cli/commands/status.js +4 -8
  20. package/dist/cli/commands/status.js.map +1 -1
  21. package/dist/context/index.d.ts +2 -5
  22. package/dist/context/index.d.ts.map +1 -1
  23. package/dist/context/index.js +2 -8
  24. package/dist/context/index.js.map +1 -1
  25. package/dist/context/token-budget.d.ts +0 -29
  26. package/dist/context/token-budget.d.ts.map +1 -1
  27. package/dist/context/token-budget.js +1 -65
  28. package/dist/context/token-budget.js.map +1 -1
  29. package/dist/context/types.d.ts +0 -1
  30. package/dist/context/types.d.ts.map +1 -1
  31. package/dist/context/types.js +0 -9
  32. package/dist/context/types.js.map +1 -1
  33. package/dist/core/constraints/checker.d.ts +14 -0
  34. package/dist/core/constraints/checker.d.ts.map +1 -1
  35. package/dist/core/constraints/checker.js +22 -2
  36. package/dist/core/constraints/checker.js.map +1 -1
  37. package/dist/core/constraints/git-evidence.d.ts +6 -3
  38. package/dist/core/constraints/git-evidence.d.ts.map +1 -1
  39. package/dist/core/constraints/git-evidence.js +6 -3
  40. package/dist/core/constraints/git-evidence.js.map +1 -1
  41. package/dist/core/index.d.ts +1 -2
  42. package/dist/core/index.d.ts.map +1 -1
  43. package/dist/core/index.js +1 -9
  44. package/dist/core/index.js.map +1 -1
  45. package/dist/core/spec/validator.d.ts +2 -0
  46. package/dist/core/spec/validator.d.ts.map +1 -1
  47. package/dist/core/spec/validator.js +9 -11
  48. package/dist/core/spec/validator.js.map +1 -1
  49. package/dist/core/validators/index.d.ts +1 -1
  50. package/dist/core/validators/index.d.ts.map +1 -1
  51. package/dist/core/validators/passes-gate.d.ts +1 -15
  52. package/dist/core/validators/passes-gate.d.ts.map +1 -1
  53. package/dist/core/validators/passes-gate.js +2 -127
  54. package/dist/core/validators/passes-gate.js.map +1 -1
  55. package/dist/failure/index.d.ts +0 -1
  56. package/dist/failure/index.d.ts.map +1 -1
  57. package/dist/failure/index.js +1 -7
  58. package/dist/failure/index.js.map +1 -1
  59. package/dist/gates/definitions.d.ts.map +1 -1
  60. package/dist/gates/definitions.js +0 -4
  61. package/dist/gates/definitions.js.map +1 -1
  62. package/dist/gates/performance.d.ts +7 -22
  63. package/dist/gates/performance.d.ts.map +1 -1
  64. package/dist/gates/performance.js +7 -89
  65. package/dist/gates/performance.js.map +1 -1
  66. package/dist/gates/types.d.ts +2 -8
  67. package/dist/gates/types.d.ts.map +1 -1
  68. package/dist/index.d.ts +6 -8
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -22
  71. package/dist/index.js.map +1 -1
  72. package/dist/knowledge/index.d.ts +0 -1
  73. package/dist/knowledge/index.d.ts.map +1 -1
  74. package/dist/knowledge/index.js +1 -3
  75. package/dist/knowledge/index.js.map +1 -1
  76. package/dist/monitoring/trace-analyzer.d.ts +25 -10
  77. package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
  78. package/dist/monitoring/trace-analyzer.js +147 -116
  79. package/dist/monitoring/trace-analyzer.js.map +1 -1
  80. package/dist/types/passes-gate.d.ts +0 -32
  81. package/dist/types/passes-gate.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/src/__tests__/passes-gate-check.test.ts +20 -4
  84. package/src/__tests__/passes-gate.test.ts +5 -99
  85. package/src/__tests__/performance-gate.test.ts +3 -11
  86. package/src/__tests__/public-exports.test.ts +68 -16
  87. package/src/__tests__/public-type-surface.test.ts +359 -0
  88. package/src/__tests__/trace-analyzer.test.ts +52 -2
  89. package/src/cli/commands/CONTEXT.md +3 -2
  90. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +158 -0
  91. package/src/cli/commands/__tests__/passes-gate.test.ts +7 -3
  92. package/src/cli/commands/__tests__/performance.test.ts +5 -16
  93. package/src/cli/commands/__tests__/registry.test.ts +46 -3
  94. package/src/cli/commands/__tests__/report-extra.test.ts +0 -19
  95. package/src/cli/commands/__tests__/report-false-green.test.ts +46 -0
  96. package/src/cli/commands/__tests__/report.test.ts +0 -20
  97. package/src/cli/commands/__tests__/status-analysis-wiring.test.ts +68 -0
  98. package/src/cli/commands/__tests__/status-extra.test.ts +63 -116
  99. package/src/cli/commands/__tests__/status.test.ts +66 -148
  100. package/src/cli/commands/definitions.ts +1 -32
  101. package/src/cli/commands/passes-gate.ts +0 -3
  102. package/src/cli/commands/performance.ts +13 -27
  103. package/src/cli/commands/report.ts +3 -61
  104. package/src/cli/commands/status.ts +5 -10
  105. package/src/context/CONTEXT.md +2 -3
  106. package/src/context/__tests__/token-budget.test.ts +1 -81
  107. package/src/context/index.ts +2 -7
  108. package/src/context/token-budget.ts +0 -73
  109. package/src/context/types.ts +0 -9
  110. package/src/core/CONTEXT.md +5 -6
  111. package/src/core/constraints/__tests__/collect-constraints.test.ts +66 -0
  112. package/src/core/constraints/checker.ts +33 -2
  113. package/src/core/constraints/git-evidence.ts +6 -3
  114. package/src/core/index.ts +0 -12
  115. package/src/core/spec/__tests__/validator.test.ts +24 -0
  116. package/src/core/spec/validator.ts +9 -10
  117. package/src/core/validators/__tests__/passes-gate.test.ts +2 -9
  118. package/src/core/validators/index.ts +0 -2
  119. package/src/core/validators/passes-gate.ts +3 -152
  120. package/src/failure/CONTEXT.md +8 -6
  121. package/src/failure/index.ts +0 -10
  122. package/src/gates/CONTEXT.md +3 -2
  123. package/src/gates/__tests__/performance.test.ts +6 -74
  124. package/src/gates/definitions.ts +0 -4
  125. package/src/gates/performance.ts +9 -111
  126. package/src/gates/types.ts +2 -8
  127. package/src/index.ts +1 -37
  128. package/src/knowledge/CONTEXT.md +1 -2
  129. package/src/knowledge/index.ts +0 -1
  130. package/src/monitoring/CONTEXT.md +2 -2
  131. package/src/monitoring/trace-analyzer.ts +160 -126
  132. package/src/types/passes-gate.ts +0 -43
  133. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +0 -1
  134. package/dist/cli/commands/analyze-sessions.d.ts +0 -20
  135. package/dist/cli/commands/analyze-sessions.d.ts.map +0 -1
  136. package/dist/cli/commands/analyze-sessions.js +0 -290
  137. package/dist/cli/commands/analyze-sessions.js.map +0 -1
  138. package/dist/cli/commands/update-user-model.d.ts +0 -30
  139. package/dist/cli/commands/update-user-model.d.ts.map +0 -1
  140. package/dist/cli/commands/update-user-model.js +0 -406
  141. package/dist/cli/commands/update-user-model.js.map +0 -1
  142. package/dist/cli/session-mining/corrections.d.ts +0 -17
  143. package/dist/cli/session-mining/corrections.d.ts.map +0 -1
  144. package/dist/cli/session-mining/corrections.js +0 -58
  145. package/dist/cli/session-mining/corrections.js.map +0 -1
  146. package/dist/cli/session-mining/index.d.ts +0 -13
  147. package/dist/cli/session-mining/index.d.ts.map +0 -1
  148. package/dist/cli/session-mining/index.js +0 -29
  149. package/dist/cli/session-mining/index.js.map +0 -1
  150. package/dist/cli/session-mining/text.d.ts +0 -31
  151. package/dist/cli/session-mining/text.d.ts.map +0 -1
  152. package/dist/cli/session-mining/text.js +0 -126
  153. package/dist/cli/session-mining/text.js.map +0 -1
  154. package/dist/cli/session-mining/transcript.d.ts +0 -34
  155. package/dist/cli/session-mining/transcript.d.ts.map +0 -1
  156. package/dist/cli/session-mining/transcript.js +0 -130
  157. package/dist/cli/session-mining/transcript.js.map +0 -1
  158. package/dist/context/compaction.d.ts +0 -57
  159. package/dist/context/compaction.d.ts.map +0 -1
  160. package/dist/context/compaction.js +0 -165
  161. package/dist/context/compaction.js.map +0 -1
  162. package/dist/core/session/clean-state.d.ts +0 -17
  163. package/dist/core/session/clean-state.d.ts.map +0 -1
  164. package/dist/core/session/clean-state.js +0 -192
  165. package/dist/core/session/clean-state.js.map +0 -1
  166. package/dist/core/session/index.d.ts +0 -6
  167. package/dist/core/session/index.d.ts.map +0 -1
  168. package/dist/core/session/index.js +0 -15
  169. package/dist/core/session/index.js.map +0 -1
  170. package/dist/core/session/startup.d.ts +0 -47
  171. package/dist/core/session/startup.d.ts.map +0 -1
  172. package/dist/core/session/startup.js +0 -263
  173. package/dist/core/session/startup.js.map +0 -1
  174. package/dist/failure/constraint-handler.d.ts +0 -83
  175. package/dist/failure/constraint-handler.d.ts.map +0 -1
  176. package/dist/failure/constraint-handler.js +0 -138
  177. package/dist/failure/constraint-handler.js.map +0 -1
  178. package/dist/knowledge/lifecycle-hooks.d.ts +0 -62
  179. package/dist/knowledge/lifecycle-hooks.d.ts.map +0 -1
  180. package/dist/knowledge/lifecycle-hooks.js +0 -143
  181. package/dist/knowledge/lifecycle-hooks.js.map +0 -1
  182. package/dist/types/session.d.ts +0 -113
  183. package/dist/types/session.d.ts.map +0 -1
  184. package/dist/types/session.js +0 -6
  185. package/dist/types/session.js.map +0 -1
  186. package/src/__tests__/clean-state-extra.test.ts +0 -301
  187. package/src/__tests__/clean-state.test.ts +0 -169
  188. package/src/__tests__/constraint-handler.test.ts +0 -132
  189. package/src/__tests__/passes-gate-extra.test.ts +0 -93
  190. package/src/__tests__/session-startup.test.ts +0 -334
  191. package/src/cli/commands/__tests__/analyze-sessions.test.ts +0 -184
  192. package/src/cli/commands/__tests__/update-user-model.test.ts +0 -268
  193. package/src/cli/commands/analyze-sessions.ts +0 -335
  194. package/src/cli/commands/update-user-model.ts +0 -476
  195. package/src/cli/session-mining/__tests__/transcript.test.ts +0 -118
  196. package/src/cli/session-mining/corrections.ts +0 -56
  197. package/src/cli/session-mining/index.ts +0 -13
  198. package/src/cli/session-mining/text.ts +0 -117
  199. package/src/cli/session-mining/transcript.ts +0 -125
  200. package/src/context/__tests__/compaction.test.ts +0 -169
  201. package/src/context/compaction.ts +0 -201
  202. package/src/core/session/__tests__/startup.test.ts +0 -281
  203. package/src/core/session/clean-state.ts +0 -178
  204. package/src/core/session/index.ts +0 -15
  205. package/src/core/session/startup.ts +0 -253
  206. package/src/failure/constraint-handler.ts +0 -171
  207. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +0 -140
  208. package/src/knowledge/lifecycle-hooks.ts +0 -202
  209. package/src/types/session.ts +0 -132
@@ -1,334 +0,0 @@
1
- /**
2
- * SessionStartup 测试
3
- */
4
-
5
- import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
6
- import { SessionStartup, createSessionStartup, DEFAULT_CODE_CHECKPOINTS, MINIMAL_CHECKPOINTS } from '../core/session/startup';
7
- import * as fs from 'fs';
8
- import * as path from 'path';
9
- import * as yaml from 'js-yaml';
10
-
11
- describe('SessionStartup', () => {
12
- const tempDir = path.join(process.cwd(), 'temp-test-session');
13
-
14
- beforeAll(() => {
15
- fs.mkdirSync(tempDir, { recursive: true });
16
- });
17
-
18
- afterAll(() => {
19
- try {
20
- fs.rmSync(tempDir, { recursive: true, force: true });
21
- } catch {
22
- // ignore
23
- }
24
- });
25
-
26
- describe('run', () => {
27
- it('应该运行 pwd 检查点', async () => {
28
- const startup = new SessionStartup(tempDir, {
29
- required: ['pwd'],
30
- optional: [],
31
- });
32
-
33
- const result = await startup.run();
34
-
35
- expect(result.success).toBe(true);
36
- expect(result.results.length).toBe(1);
37
- expect(result.results[0].type).toBe('pwd');
38
- });
39
-
40
- it('应该运行 git_status 检查点', async () => {
41
- const startup = new SessionStartup(tempDir, {
42
- required: ['git_status'],
43
- optional: [],
44
- });
45
-
46
- const result = await startup.run();
47
-
48
- expect(result.results.length).toBe(1);
49
- expect(result.results[0].type).toBe('git_status');
50
- });
51
-
52
- it('可选检查点失败不应该影响 success', async () => {
53
- const startup = new SessionStartup(tempDir, {
54
- required: ['pwd'],
55
- optional: ['git_log'],
56
- });
57
-
58
- const result = await startup.run();
59
-
60
- expect(result.success).toBe(true);
61
- expect(result.results.length).toBe(2);
62
- });
63
-
64
- it('必要检查点失败应该影响 success', async () => {
65
- const startup = new SessionStartup('/nonexistent/path', {
66
- required: ['pwd'],
67
- optional: [],
68
- });
69
-
70
- const result = await startup.run();
71
-
72
- expect(result.results.length).toBe(1);
73
- });
74
- });
75
-
76
- describe('检查点数据', () => {
77
- it('pwd 应该返回当前目录', async () => {
78
- const startup = new SessionStartup(tempDir, {
79
- required: ['pwd'],
80
- optional: [],
81
- });
82
-
83
- const result = await startup.run();
84
- const pwdResult = result.results.find(r => r.type === 'pwd');
85
-
86
- expect(pwdResult?.data).toBeDefined();
87
- });
88
- });
89
-
90
- describe('git_log 检查点', () => {
91
- it('应该返回 git log 数据', async () => {
92
- const startup = new SessionStartup(tempDir, { required: ['git_log'], optional: [] });
93
- const result = await startup.run();
94
- expect(result.results[0].type).toBe('git_log');
95
- expect(result.results[0].success).toBe(true);
96
- });
97
- });
98
-
99
- describe('read_task_list 检查点', () => {
100
- it('tasks.yml 存在应该解析 YAML', async () => {
101
- const tasksPath = path.join(tempDir, 'tasks.yml');
102
- const taskData = {
103
- tasks: [
104
- { id: 't1', name: 'task 1', passes: true },
105
- { id: 't2', name: 'task 2', passes: false },
106
- ],
107
- };
108
- fs.writeFileSync(tasksPath, yaml.dump(taskData));
109
-
110
- const startup = new SessionStartup(tempDir, { required: ['read_task_list'], optional: [] });
111
- const result = await startup.run();
112
- expect(result.results[0].success).toBe(true);
113
- expect(result.results[0].data).toBeDefined();
114
- expect(result.results[0].data.tasks.length).toBe(2);
115
-
116
- fs.unlinkSync(tasksPath);
117
- });
118
-
119
- it('tasks.json 存在应该回退到 JSON', async () => {
120
- const tasksPath = path.join(tempDir, 'tasks.json');
121
- const taskData = {
122
- tasks: [
123
- { id: 't1', name: 'task 1', passes: true },
124
- ],
125
- };
126
- fs.writeFileSync(tasksPath, JSON.stringify(taskData));
127
-
128
- const startup = new SessionStartup(tempDir, { required: ['read_task_list'], optional: [] });
129
- const result = await startup.run();
130
- expect(result.results[0].success).toBe(true);
131
- expect(result.results[0].data.tasks.length).toBe(1);
132
-
133
- fs.unlinkSync(tasksPath);
134
- });
135
-
136
- it('无任务文件应该返回 null', async () => {
137
- const noTasksDir = path.join(tempDir, 'no-tasks');
138
- fs.mkdirSync(noTasksDir, { recursive: true });
139
-
140
- const startup = new SessionStartup(noTasksDir, { required: ['read_task_list'], optional: [] });
141
- const result = await startup.run();
142
- expect(result.results[0].success).toBe(true);
143
- expect(result.results[0].data).toBeNull();
144
-
145
- fs.rmSync(noTasksDir, { recursive: true, force: true });
146
- });
147
- });
148
-
149
- describe('read_progress 检查点', () => {
150
- it('progress.yml 存在应该解析', async () => {
151
- const harnessDir = path.join(tempDir, '.harness');
152
- fs.mkdirSync(harnessDir, { recursive: true });
153
- const progressPath = path.join(harnessDir, 'progress.yml');
154
- fs.writeFileSync(progressPath, yaml.dump({ step: 3, total: 10 }));
155
-
156
- const startup = new SessionStartup(tempDir, { required: ['read_progress'], optional: [] });
157
- const result = await startup.run();
158
- expect(result.results[0].success).toBe(true);
159
- expect(result.results[0].data).toEqual({ step: 3, total: 10 });
160
-
161
- fs.unlinkSync(progressPath);
162
- fs.rmdirSync(harnessDir);
163
- });
164
-
165
- it('无 progress.yml 应该返回 null', async () => {
166
- const noProgressDir = path.join(tempDir, 'no-progress');
167
- fs.mkdirSync(noProgressDir, { recursive: true });
168
-
169
- const startup = new SessionStartup(noProgressDir, { required: ['read_progress'], optional: [] });
170
- const result = await startup.run();
171
- expect(result.results[0].success).toBe(true);
172
- expect(result.results[0].data).toBeNull();
173
-
174
- fs.rmSync(noProgressDir, { recursive: true, force: true });
175
- });
176
- });
177
-
178
- describe('init_sh 检查点', () => {
179
- it('init.sh 存在应该返回 true', async () => {
180
- const initPath = path.join(tempDir, 'init.sh');
181
- fs.writeFileSync(initPath, '#!/bin/bash\necho "init"');
182
-
183
- const startup = new SessionStartup(tempDir, { required: ['init_sh'], optional: [] });
184
- const result = await startup.run();
185
- expect(result.results[0].success).toBe(true);
186
- expect(result.results[0].data).toBe(true);
187
-
188
- fs.unlinkSync(initPath);
189
- });
190
-
191
- it('init.sh 不存在应该返回 false', async () => {
192
- const noInitDir = path.join(tempDir, 'no-init');
193
- fs.mkdirSync(noInitDir, { recursive: true });
194
-
195
- const startup = new SessionStartup(noInitDir, { required: ['init_sh'], optional: [] });
196
- const result = await startup.run();
197
- expect(result.results[0].success).toBe(true);
198
- expect(result.results[0].data).toBe(false);
199
-
200
- fs.rmSync(noInitDir, { recursive: true, force: true });
201
- });
202
- });
203
-
204
- describe('basic_verification 检查点', () => {
205
- it('应该检测 package.json 和 README.md', async () => {
206
- const verDir = path.join(tempDir, 'verification');
207
- fs.mkdirSync(verDir, { recursive: true });
208
- fs.writeFileSync(path.join(verDir, 'package.json'), '{}');
209
- fs.writeFileSync(path.join(verDir, 'README.md'), '# Test');
210
-
211
- const startup = new SessionStartup(verDir, { required: ['basic_verification'], optional: [] });
212
- const result = await startup.run();
213
- expect(result.results[0].success).toBe(true);
214
- expect(result.results[0].data.hasPackageJson).toBe(true);
215
- expect(result.results[0].data.hasReadme).toBe(true);
216
-
217
- fs.rmSync(verDir, { recursive: true, force: true });
218
- });
219
- });
220
-
221
- describe('load_context 检查点', () => {
222
- it('应该加载存在的上下文文件', async () => {
223
- const ctxDir = path.join(tempDir, 'context-test');
224
- fs.mkdirSync(ctxDir, { recursive: true });
225
- fs.writeFileSync(path.join(ctxDir, 'AGENTS.md'), '# Agents');
226
- fs.writeFileSync(path.join(ctxDir, 'USER.md'), '# User');
227
-
228
- const startup = new SessionStartup(ctxDir, { required: ['load_context'], optional: [], contextFiles: ['AGENTS.md', 'USER.md'] });
229
- const result = await startup.run();
230
- expect(result.results[0].success).toBe(true);
231
- expect(result.results[0].data['AGENTS.md']).toBe('# Agents');
232
- expect(result.results[0].data['USER.md']).toBe('# User');
233
-
234
- fs.rmSync(ctxDir, { recursive: true, force: true });
235
- });
236
- });
237
-
238
- describe('未知检查点类型', () => {
239
- it('应该返回失败结果', async () => {
240
- const startup = new SessionStartup(tempDir, { required: ['unknown_type' as any], optional: [] });
241
- const result = await startup.run();
242
- expect(result.success).toBe(false);
243
- expect(result.results[0].success).toBe(false);
244
- expect(result.results[0].error).toContain('Unknown checkpoint type');
245
- });
246
- });
247
-
248
- describe('getCurrentTask', () => {
249
- it('应该返回第一个未通过的任务', async () => {
250
- const tasksPath = path.join(tempDir, 'tasks.yml');
251
- const taskData = {
252
- tasks: [
253
- { id: 't1', name: 'done', passes: true },
254
- { id: 't2', name: 'current', passes: false },
255
- { id: 't3', name: 'future', passes: false },
256
- ],
257
- };
258
- fs.writeFileSync(tasksPath, yaml.dump(taskData));
259
-
260
- const startup = new SessionStartup(tempDir, { required: [], optional: [] });
261
- const current = await startup.getCurrentTask();
262
- expect(current).not.toBeNull();
263
- expect(current!.task.id).toBe('t2');
264
- expect(current!.index).toBe(1);
265
-
266
- fs.unlinkSync(tasksPath);
267
- });
268
-
269
- it('所有任务通过应该返回 null', async () => {
270
- const tasksPath = path.join(tempDir, 'tasks.yml');
271
- const taskData = {
272
- tasks: [
273
- { id: 't1', name: 'done', passes: true },
274
- ],
275
- };
276
- fs.writeFileSync(tasksPath, yaml.dump(taskData));
277
-
278
- const startup = new SessionStartup(tempDir, { required: [], optional: [] });
279
- const current = await startup.getCurrentTask();
280
- expect(current).toBeNull();
281
-
282
- fs.unlinkSync(tasksPath);
283
- });
284
-
285
- it('无任务文件应该返回 null', async () => {
286
- const noTasksDir = path.join(tempDir, 'no-tasks-get');
287
- fs.mkdirSync(noTasksDir, { recursive: true });
288
-
289
- const startup = new SessionStartup(noTasksDir, { required: [], optional: [] });
290
- const current = await startup.getCurrentTask();
291
- expect(current).toBeNull();
292
-
293
- fs.rmSync(noTasksDir, { recursive: true, force: true });
294
- });
295
- });
296
-
297
- describe('generateReport', () => {
298
- it('应该生成报告', () => {
299
- const startup = new SessionStartup(tempDir, { required: [], optional: [] });
300
- const report = startup.generateReport([
301
- { type: 'pwd', success: true, data: '/tmp', duration: 5 },
302
- { type: 'git_log', success: false, error: 'not a git repo', duration: 10 },
303
- ]);
304
-
305
- expect(report).toContain('Session Startup Report');
306
- expect(report).toContain('pwd');
307
- expect(report).toContain('git_log');
308
- expect(report).toContain('not a git repo');
309
- });
310
- });
311
-
312
- describe('createSessionStartup 工厂函数', () => {
313
- it('应该创建 SessionStartup 实例', () => {
314
- const startup = createSessionStartup(tempDir, { required: ['pwd'], optional: [] });
315
- expect(startup).toBeInstanceOf(SessionStartup);
316
- });
317
- });
318
-
319
- describe('常量', () => {
320
- it('DEFAULT_CODE_CHECKPOINTS 应该有正确的结构', () => {
321
- expect(DEFAULT_CODE_CHECKPOINTS.required).toContain('pwd');
322
- expect(DEFAULT_CODE_CHECKPOINTS.required).toContain('git_log');
323
- expect(DEFAULT_CODE_CHECKPOINTS.required).toContain('read_task_list');
324
- expect(DEFAULT_CODE_CHECKPOINTS.optional).toContain('read_progress');
325
- expect(DEFAULT_CODE_CHECKPOINTS.timeout).toBe(30000);
326
- });
327
-
328
- it('MINIMAL_CHECKPOINTS 应该有正确的结构', () => {
329
- expect(MINIMAL_CHECKPOINTS.required).toContain('pwd');
330
- expect(MINIMAL_CHECKPOINTS.required).toContain('read_task_list');
331
- expect(MINIMAL_CHECKPOINTS.optional).toEqual([]);
332
- });
333
- });
334
- });
@@ -1,184 +0,0 @@
1
- /**
2
- * analyze-sessions 命令测试(O6)
3
- *
4
- * Seam:analyzeSessions(options) 公开入口。
5
- * 隔离面:
6
- * - os.homedir → 测试临时目录(memory 规则库落 TEST_HOME)
7
- * - readTranscriptSessions → fixture 会话(extractCorrectionMatches/tokenize 等纯函数保持真实)
8
- * - CLAUDE_TRANSCRIPTS_DIR → 测试临时目录(决定 transcripts 目录是否存在)
9
- */
10
-
11
- import * as fs from 'fs';
12
- import { captureIO, lastJsonOutput, type CapturingIO } from '../../command-contract';
13
- import * as path from 'path';
14
- import { analyzeSessions } from '../analyze-sessions';
15
- import { readTranscriptSessions, type MinedSession } from '../../session-mining';
16
-
17
- const TEST_HOME = '/tmp/harness-analyze-test-home';
18
-
19
- jest.mock('os', () => ({
20
- ...jest.requireActual('os'),
21
- homedir: () => '/tmp/harness-analyze-test-home',
22
- }));
23
-
24
- jest.mock('../../session-mining', () => ({
25
- ...jest.requireActual('../../session-mining'),
26
- readTranscriptSessions: jest.fn(),
27
- }));
28
-
29
- jest.mock('chalk', () => {
30
- const id = (s: string) => s;
31
- const chalkFn = Object.assign(id, {
32
- gray: id,
33
- blue: id,
34
- green: id,
35
- yellow: id,
36
- cyan: id,
37
- bold: id,
38
- red: id,
39
- });
40
- return {
41
- __esModule: true,
42
- default: chalkFn,
43
- gray: id,
44
- blue: id,
45
- green: id,
46
- yellow: id,
47
- cyan: id,
48
- bold: id,
49
- red: id,
50
- };
51
- });
52
-
53
- const mockReadTranscriptSessions = readTranscriptSessions as jest.MockedFunction<
54
- typeof readTranscriptSessions
55
- >;
56
-
57
- const TRANSCRIPTS_DIR = path.join(TEST_HOME, 'transcripts');
58
-
59
- function mkSession(partial: Partial<MinedSession> = {}): MinedSession {
60
- return {
61
- id: 'session-1',
62
- date: '2026-08-15',
63
- mtimeMs: Date.now(),
64
- turns: [
65
- { role: 'user', content: '我不是说要用中文吗' },
66
- { role: 'assistant', content: '收到,改用中文' },
67
- ],
68
- toolCalls: [],
69
- ...partial,
70
- };
71
- }
72
-
73
- let io: CapturingIO;
74
- beforeEach(() => {
75
- io = captureIO();
76
- });
77
-
78
- describe('analyze-sessions command', () => {
79
-
80
- beforeEach(() => {
81
- jest.clearAllMocks();
82
- fs.rmSync(TEST_HOME, { recursive: true, force: true });
83
- fs.mkdirSync(path.join(TEST_HOME, '.claude', 'projects', '-root-projects', 'memory'), { recursive: true });
84
- fs.mkdirSync(TRANSCRIPTS_DIR, { recursive: true });
85
- process.env.CLAUDE_TRANSCRIPTS_DIR = TRANSCRIPTS_DIR;
86
- });
87
-
88
- afterEach(() => {
89
- delete process.env.CLAUDE_TRANSCRIPTS_DIR;
90
- });
91
-
92
- test('transcripts 目录不存在:提示 No transcripts directory found', async () => {
93
- process.env.CLAUDE_TRANSCRIPTS_DIR = path.join(TEST_HOME, 'missing-dir');
94
-
95
- const result = await analyzeSessions({}, io);
96
-
97
- expect(io.outText()).toContain('No transcripts directory found');
98
- expect(result).toEqual({ kind: 'skip', reason: expect.stringContaining('会话记录目录不存在') });
99
- expect(mockReadTranscriptSessions).not.toHaveBeenCalled();
100
- });
101
-
102
- test('窗口内无会话:提示 No sessions found(默认最近 7 天)', async () => {
103
- const before = Date.now() - 7 * 86_400_000;
104
- mockReadTranscriptSessions.mockReturnValue([]);
105
-
106
- const result = await analyzeSessions({}, io);
107
- const after = Date.now() - 7 * 86_400_000;
108
-
109
- expect(io.outText()).toContain('No sessions found in the last 7 days');
110
- expect(result).toEqual({ kind: 'skip', reason: '最近 7 天没有会话' });
111
- // since 过滤下推到 seam:默认窗口 7 天
112
- const filter = mockReadTranscriptSessions.mock.calls[0][1] as { since?: number };
113
- expect(filter.since).toBeGreaterThanOrEqual(before);
114
- expect(filter.since).toBeLessThanOrEqual(after);
115
- });
116
-
117
- test('--days 1:since 过滤下推到 seam(mtimeMs 窗口)', async () => {
118
- const before = Date.now() - 86_400_000;
119
- mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'today' })]);
120
-
121
- await analyzeSessions({ days: 1 }, io);
122
- const after = Date.now() - 86_400_000;
123
-
124
- expect(io.outText()).toContain('Analyzing 1 sessions (last 1 days)');
125
- const filter = mockReadTranscriptSessions.mock.calls[0][1] as { since?: number };
126
- expect(filter.since).toBeGreaterThanOrEqual(before);
127
- expect(filter.since).toBeLessThanOrEqual(after);
128
- });
129
-
130
- test('缺省 --days:窗口为 7 天,两天前会话仍计入', async () => {
131
- mockReadTranscriptSessions.mockReturnValue([
132
- mkSession({ id: 'today' }),
133
- mkSession({ id: 'two-days-ago', mtimeMs: Date.now() - 2 * 86_400_000 }),
134
- ]);
135
-
136
- await analyzeSessions({}, io);
137
-
138
- expect(io.outText()).toContain('Analyzing 2 sessions (last 7 days)');
139
- });
140
-
141
- test('--json:纠正句跨 3 会话聚合为 correction 候选', async () => {
142
- mockReadTranscriptSessions.mockReturnValue([
143
- mkSession({ id: 's1' }),
144
- mkSession({ id: 's2' }),
145
- mkSession({ id: 's3' }),
146
- ]);
147
-
148
- await analyzeSessions({ json: true }, io);
149
-
150
- const output = lastJsonOutput(io);
151
- expect(output.sessions).toBe(3);
152
- expect(output.corrections).toBe(3);
153
- expect(output.candidates).toEqual(
154
- expect.arrayContaining([
155
- expect.objectContaining({
156
- source: 'correction',
157
- frequency: 3,
158
- pattern: '我不是说',
159
- }),
160
- ]),
161
- );
162
- });
163
-
164
- test('--json:跨会话重复概念产出 ngram 候选', async () => {
165
- const ngramText = { role: 'user', content: '数据库迁移方案的详细讨论' };
166
- mockReadTranscriptSessions.mockReturnValue([
167
- mkSession({
168
- id: 's1',
169
- turns: [ngramText, { role: 'assistant', content: '好的' }],
170
- }),
171
- mkSession({
172
- id: 's2',
173
- turns: [ngramText, { role: 'assistant', content: '明白' }],
174
- }),
175
- ]);
176
-
177
- await analyzeSessions({ json: true }, io);
178
-
179
- const output = lastJsonOutput(io);
180
- const ngramCandidates = (output.candidates as Array<{ source: string }>)
181
- .filter(c => c.source === 'ngram');
182
- expect(ngramCandidates.length).toBeGreaterThan(0);
183
- });
184
- });