gencode-ai 0.2.0 → 0.4.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 (218) hide show
  1. package/RELEASE_NOTES_v0.4.0.md +140 -0
  2. package/dist/agent/agent.d.ts +26 -4
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +316 -57
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/types.d.ts +20 -2
  7. package/dist/agent/types.d.ts.map +1 -1
  8. package/dist/checkpointing/checkpoint-manager.d.ts +24 -0
  9. package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -1
  10. package/dist/checkpointing/checkpoint-manager.js +28 -0
  11. package/dist/checkpointing/checkpoint-manager.js.map +1 -1
  12. package/dist/cli/components/App.d.ts +8 -0
  13. package/dist/cli/components/App.d.ts.map +1 -1
  14. package/dist/cli/components/App.js +493 -45
  15. package/dist/cli/components/App.js.map +1 -1
  16. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  17. package/dist/cli/components/CommandSuggestions.js +2 -0
  18. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  19. package/dist/cli/components/Header.d.ts +6 -1
  20. package/dist/cli/components/Header.d.ts.map +1 -1
  21. package/dist/cli/components/Header.js +3 -3
  22. package/dist/cli/components/Header.js.map +1 -1
  23. package/dist/cli/components/Messages.d.ts.map +1 -1
  24. package/dist/cli/components/Messages.js +8 -10
  25. package/dist/cli/components/Messages.js.map +1 -1
  26. package/dist/cli/components/ModelSelector.d.ts +4 -3
  27. package/dist/cli/components/ModelSelector.d.ts.map +1 -1
  28. package/dist/cli/components/ModelSelector.js +54 -37
  29. package/dist/cli/components/ModelSelector.js.map +1 -1
  30. package/dist/cli/components/ProviderManager.d.ts +2 -2
  31. package/dist/cli/components/ProviderManager.d.ts.map +1 -1
  32. package/dist/cli/components/ProviderManager.js +137 -156
  33. package/dist/cli/components/ProviderManager.js.map +1 -1
  34. package/dist/cli/index.js +33 -15
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/config/index.d.ts +2 -2
  37. package/dist/config/index.d.ts.map +1 -1
  38. package/dist/config/index.js +1 -1
  39. package/dist/config/index.js.map +1 -1
  40. package/dist/config/levels.d.ts +5 -5
  41. package/dist/config/levels.d.ts.map +1 -1
  42. package/dist/config/levels.js +20 -20
  43. package/dist/config/levels.js.map +1 -1
  44. package/dist/config/merger.js +1 -1
  45. package/dist/config/merger.js.map +1 -1
  46. package/dist/config/providers-config.d.ts +8 -5
  47. package/dist/config/providers-config.d.ts.map +1 -1
  48. package/dist/config/providers-config.js +19 -22
  49. package/dist/config/providers-config.js.map +1 -1
  50. package/dist/config/test-utils.d.ts +2 -2
  51. package/dist/config/test-utils.d.ts.map +1 -1
  52. package/dist/config/test-utils.js +4 -4
  53. package/dist/config/test-utils.js.map +1 -1
  54. package/dist/config/types.d.ts +42 -17
  55. package/dist/config/types.d.ts.map +1 -1
  56. package/dist/config/types.js +14 -14
  57. package/dist/config/types.js.map +1 -1
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +2 -2
  60. package/dist/input/history-manager.d.ts +78 -0
  61. package/dist/input/history-manager.d.ts.map +1 -0
  62. package/dist/input/history-manager.js +224 -0
  63. package/dist/input/history-manager.js.map +1 -0
  64. package/dist/input/index.d.ts +6 -0
  65. package/dist/input/index.d.ts.map +1 -0
  66. package/dist/input/index.js +5 -0
  67. package/dist/input/index.js.map +1 -0
  68. package/dist/memory/memory-manager.d.ts +25 -12
  69. package/dist/memory/memory-manager.d.ts.map +1 -1
  70. package/dist/memory/memory-manager.js +241 -112
  71. package/dist/memory/memory-manager.js.map +1 -1
  72. package/dist/memory/test-utils.d.ts +1 -1
  73. package/dist/memory/test-utils.d.ts.map +1 -1
  74. package/dist/memory/test-utils.js +3 -3
  75. package/dist/memory/test-utils.js.map +1 -1
  76. package/dist/memory/types.d.ts +20 -10
  77. package/dist/memory/types.d.ts.map +1 -1
  78. package/dist/memory/types.js +13 -13
  79. package/dist/memory/types.js.map +1 -1
  80. package/dist/migration/migrate.d.ts +24 -0
  81. package/dist/migration/migrate.d.ts.map +1 -0
  82. package/dist/migration/migrate.js +164 -0
  83. package/dist/migration/migrate.js.map +1 -0
  84. package/dist/permissions/persistence.d.ts +2 -2
  85. package/dist/permissions/persistence.js +4 -4
  86. package/dist/permissions/persistence.js.map +1 -1
  87. package/dist/planning/plan-file.d.ts +1 -1
  88. package/dist/planning/plan-file.js +2 -2
  89. package/dist/planning/plan-file.js.map +1 -1
  90. package/dist/prompts/index.d.ts +5 -4
  91. package/dist/prompts/index.d.ts.map +1 -1
  92. package/dist/prompts/index.js +13 -10
  93. package/dist/prompts/index.js.map +1 -1
  94. package/dist/providers/anthropic.d.ts +2 -1
  95. package/dist/providers/anthropic.d.ts.map +1 -1
  96. package/dist/providers/anthropic.js +7 -0
  97. package/dist/providers/anthropic.js.map +1 -1
  98. package/dist/providers/gemini.d.ts +2 -1
  99. package/dist/providers/gemini.d.ts.map +1 -1
  100. package/dist/providers/gemini.js +40 -2
  101. package/dist/providers/gemini.js.map +1 -1
  102. package/dist/providers/google.d.ts +22 -0
  103. package/dist/providers/google.d.ts.map +1 -0
  104. package/dist/providers/google.js +297 -0
  105. package/dist/providers/google.js.map +1 -0
  106. package/dist/providers/index.d.ts +22 -12
  107. package/dist/providers/index.d.ts.map +1 -1
  108. package/dist/providers/index.js +56 -32
  109. package/dist/providers/index.js.map +1 -1
  110. package/dist/providers/openai.d.ts +2 -1
  111. package/dist/providers/openai.d.ts.map +1 -1
  112. package/dist/providers/openai.js +13 -0
  113. package/dist/providers/openai.js.map +1 -1
  114. package/dist/providers/registry.d.ts +48 -34
  115. package/dist/providers/registry.d.ts.map +1 -1
  116. package/dist/providers/registry.js +72 -88
  117. package/dist/providers/registry.js.map +1 -1
  118. package/dist/providers/store.d.ts +43 -17
  119. package/dist/providers/store.d.ts.map +1 -1
  120. package/dist/providers/store.js +112 -19
  121. package/dist/providers/store.js.map +1 -1
  122. package/dist/providers/types.d.ts +52 -3
  123. package/dist/providers/types.d.ts.map +1 -1
  124. package/dist/providers/vertex-ai.d.ts +15 -7
  125. package/dist/providers/vertex-ai.d.ts.map +1 -1
  126. package/dist/providers/vertex-ai.js +46 -13
  127. package/dist/providers/vertex-ai.js.map +1 -1
  128. package/dist/session/compression/engine.d.ts +109 -0
  129. package/dist/session/compression/engine.d.ts.map +1 -0
  130. package/dist/session/compression/engine.js +311 -0
  131. package/dist/session/compression/engine.js.map +1 -0
  132. package/dist/session/compression/index.d.ts +12 -0
  133. package/dist/session/compression/index.d.ts.map +1 -0
  134. package/dist/session/compression/index.js +11 -0
  135. package/dist/session/compression/index.js.map +1 -0
  136. package/dist/session/compression/types.d.ts +90 -0
  137. package/dist/session/compression/types.d.ts.map +1 -0
  138. package/dist/session/compression/types.js +17 -0
  139. package/dist/session/compression/types.js.map +1 -0
  140. package/dist/session/manager.d.ts +64 -3
  141. package/dist/session/manager.d.ts.map +1 -1
  142. package/dist/session/manager.js +254 -2
  143. package/dist/session/manager.js.map +1 -1
  144. package/dist/session/types.d.ts +16 -0
  145. package/dist/session/types.d.ts.map +1 -1
  146. package/dist/session/types.js +1 -1
  147. package/dist/session/types.js.map +1 -1
  148. package/docs/README.md +1 -0
  149. package/docs/config-system-comparison.md +50 -50
  150. package/docs/cost-tracking-comparison.md +2 -2
  151. package/docs/diagrams/compression-decision.mmd +30 -0
  152. package/docs/diagrams/compression-workflow.mmd +54 -0
  153. package/docs/diagrams/layer1-pruning.mmd +45 -0
  154. package/docs/diagrams/layer2-compaction.mmd +42 -0
  155. package/docs/memory-system.md +124 -31
  156. package/docs/permissions.md +2 -2
  157. package/docs/proposals/0006-memory-system.md +4 -4
  158. package/docs/proposals/0007-context-management.md +252 -2
  159. package/docs/proposals/0008-checkpointing.md +109 -2
  160. package/docs/proposals/0011-custom-commands.md +2 -1
  161. package/docs/proposals/0021-skills-system.md +2 -1
  162. package/docs/proposals/0023-permission-enhancements.md +2 -2
  163. package/docs/proposals/0033-enterprise-deployment.md +1 -1
  164. package/docs/proposals/0041-configuration-system.md +17 -19
  165. package/docs/proposals/0042-prompt-optimization.md +17 -9
  166. package/docs/proposals/README.md +8 -7
  167. package/docs/providers.md +96 -11
  168. package/docs/session-compression.md +695 -0
  169. package/examples/agent-demo.ts +23 -1
  170. package/examples/basic.ts +3 -3
  171. package/package.json +2 -2
  172. package/scripts/migrate.ts +449 -0
  173. package/src/agent/agent.ts +365 -61
  174. package/src/agent/types.ts +24 -2
  175. package/src/checkpointing/checkpoint-manager.ts +48 -0
  176. package/src/cli/components/App.tsx +570 -42
  177. package/src/cli/components/CommandSuggestions.tsx +2 -0
  178. package/src/cli/components/Header.tsx +16 -1
  179. package/src/cli/components/Messages.tsx +21 -15
  180. package/src/cli/components/ModelSelector.tsx +62 -43
  181. package/src/cli/components/ProviderManager.tsx +278 -323
  182. package/src/cli/index.tsx +38 -18
  183. package/src/config/index.ts +5 -3
  184. package/src/config/levels.test.ts +22 -22
  185. package/src/config/levels.ts +22 -22
  186. package/src/config/loader.test.ts +14 -14
  187. package/src/config/manager.test.ts +19 -19
  188. package/src/config/merger.test.ts +23 -23
  189. package/src/config/merger.ts +1 -1
  190. package/src/config/providers-config.ts +23 -21
  191. package/src/config/test-utils.ts +6 -6
  192. package/src/config/types.ts +55 -20
  193. package/src/index.ts +3 -3
  194. package/src/input/history-manager.ts +289 -0
  195. package/src/input/index.ts +6 -0
  196. package/src/memory/memory-manager.test.ts +242 -24
  197. package/src/memory/memory-manager.ts +270 -141
  198. package/src/memory/test-utils.ts +4 -4
  199. package/src/memory/types.ts +28 -17
  200. package/src/permissions/persistence.ts +4 -4
  201. package/src/planning/plan-file.ts +2 -2
  202. package/src/prompts/index.test.ts +2 -1
  203. package/src/prompts/index.ts +15 -11
  204. package/src/providers/anthropic.ts +9 -0
  205. package/src/providers/{gemini.ts → google.ts} +75 -15
  206. package/src/providers/index.ts +85 -42
  207. package/src/providers/openai.ts +16 -0
  208. package/src/providers/registry.ts +116 -111
  209. package/src/providers/store.ts +130 -28
  210. package/src/providers/types.ts +65 -3
  211. package/src/providers/vertex-ai.ts +49 -13
  212. package/src/session/compression/engine.ts +406 -0
  213. package/src/session/compression/index.ts +18 -0
  214. package/src/session/compression/types.ts +102 -0
  215. package/src/session/manager.ts +326 -3
  216. package/src/session/types.ts +22 -1
  217. package/tests/input-history-manager.test.ts +335 -0
  218. package/tests/session-checkpoint-persistence.test.ts +198 -0
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Unit tests for InputHistoryManager
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
5
+ import { InputHistoryManager } from '../src/input/history-manager.js';
6
+ import * as fs from 'fs/promises';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+
10
+ describe('InputHistoryManager', () => {
11
+ let historyManager: InputHistoryManager;
12
+ let testDir: string;
13
+ let historyPath: string;
14
+
15
+ beforeEach(async () => {
16
+ testDir = path.join(os.tmpdir(), `gencode-history-test-${Date.now()}`);
17
+ historyPath = path.join(testDir, 'input-history.json');
18
+
19
+ historyManager = new InputHistoryManager({
20
+ savePath: historyPath,
21
+ maxSize: 10, // Small size for testing
22
+ deduplicateConsecutive: true,
23
+ });
24
+
25
+ await historyManager.load();
26
+ });
27
+
28
+ afterEach(async () => {
29
+ await historyManager.flush();
30
+ try {
31
+ await fs.rm(testDir, { recursive: true, force: true });
32
+ } catch (error) {
33
+ // Ignore cleanup errors
34
+ }
35
+ });
36
+
37
+ describe('add()', () => {
38
+ it('should add entries to history', () => {
39
+ historyManager.add('hello');
40
+ historyManager.add('world');
41
+
42
+ expect(historyManager.size()).toBe(2);
43
+ const entries = historyManager.getEntries();
44
+ expect(entries[0].text).toBe('hello');
45
+ expect(entries[1].text).toBe('world');
46
+ });
47
+
48
+ it('should trim whitespace from entries', () => {
49
+ historyManager.add(' hello ');
50
+ historyManager.add('world\n');
51
+
52
+ const entries = historyManager.getEntries();
53
+ expect(entries[0].text).toBe('hello');
54
+ expect(entries[1].text).toBe('world');
55
+ });
56
+
57
+ it('should skip empty entries', () => {
58
+ historyManager.add('hello');
59
+ historyManager.add('');
60
+ historyManager.add(' ');
61
+ historyManager.add('world');
62
+
63
+ expect(historyManager.size()).toBe(2);
64
+ const entries = historyManager.getEntries();
65
+ expect(entries[0].text).toBe('hello');
66
+ expect(entries[1].text).toBe('world');
67
+ });
68
+
69
+ it('should deduplicate consecutive entries', () => {
70
+ historyManager.add('hello');
71
+ historyManager.add('hello');
72
+ historyManager.add('world');
73
+ historyManager.add('world');
74
+
75
+ expect(historyManager.size()).toBe(2);
76
+ const entries = historyManager.getEntries();
77
+ expect(entries[0].text).toBe('hello');
78
+ expect(entries[1].text).toBe('world');
79
+ });
80
+
81
+ it('should not deduplicate non-consecutive duplicates', () => {
82
+ historyManager.add('hello');
83
+ historyManager.add('world');
84
+ historyManager.add('hello');
85
+
86
+ expect(historyManager.size()).toBe(3);
87
+ const entries = historyManager.getEntries();
88
+ expect(entries[0].text).toBe('hello');
89
+ expect(entries[1].text).toBe('world');
90
+ expect(entries[2].text).toBe('hello');
91
+ });
92
+
93
+ it('should prune old entries when exceeding maxSize', () => {
94
+ // Add 15 entries (maxSize is 10)
95
+ for (let i = 0; i < 15; i++) {
96
+ historyManager.add(`entry ${i}`);
97
+ }
98
+
99
+ expect(historyManager.size()).toBe(10);
100
+ const entries = historyManager.getEntries();
101
+ expect(entries[0].text).toBe('entry 5'); // First 5 entries pruned
102
+ expect(entries[9].text).toBe('entry 14');
103
+ });
104
+
105
+ it('should add timestamp to entries', () => {
106
+ const beforeAdd = new Date();
107
+ historyManager.add('hello');
108
+ const afterAdd = new Date();
109
+
110
+ const entries = historyManager.getEntries();
111
+ const timestamp = new Date(entries[0].timestamp);
112
+
113
+ expect(timestamp.getTime()).toBeGreaterThanOrEqual(beforeAdd.getTime());
114
+ expect(timestamp.getTime()).toBeLessThanOrEqual(afterAdd.getTime());
115
+ });
116
+
117
+ it('should reset navigation position after adding', () => {
118
+ historyManager.add('hello');
119
+ historyManager.add('world');
120
+
121
+ // Start navigation
122
+ historyManager.previous();
123
+ expect(historyManager.isNavigating()).toBe(true);
124
+
125
+ // Add new entry - should reset navigation
126
+ historyManager.add('test');
127
+ expect(historyManager.isNavigating()).toBe(false);
128
+ });
129
+ });
130
+
131
+ describe('previous()', () => {
132
+ beforeEach(() => {
133
+ historyManager.add('first');
134
+ historyManager.add('second');
135
+ historyManager.add('third');
136
+ });
137
+
138
+ it('should navigate to most recent entry first', () => {
139
+ const entry = historyManager.previous();
140
+ expect(entry).toBe('third');
141
+ });
142
+
143
+ it('should navigate backwards through history', () => {
144
+ expect(historyManager.previous()).toBe('third');
145
+ expect(historyManager.previous()).toBe('second');
146
+ expect(historyManager.previous()).toBe('first');
147
+ });
148
+
149
+ it('should stop at beginning of history', () => {
150
+ expect(historyManager.previous()).toBe('third');
151
+ expect(historyManager.previous()).toBe('second');
152
+ expect(historyManager.previous()).toBe('first');
153
+ expect(historyManager.previous()).toBe('first'); // Stays at first
154
+ });
155
+
156
+ it('should return null for empty history', () => {
157
+ const emptyManager = new InputHistoryManager({ savePath: historyPath + '.empty' });
158
+ expect(emptyManager.previous()).toBeNull();
159
+ });
160
+ });
161
+
162
+ describe('next()', () => {
163
+ beforeEach(() => {
164
+ historyManager.add('first');
165
+ historyManager.add('second');
166
+ historyManager.add('third');
167
+ });
168
+
169
+ it('should navigate forward through history', () => {
170
+ historyManager.previous(); // third
171
+ historyManager.previous(); // second
172
+ historyManager.previous(); // first
173
+
174
+ expect(historyManager.next()).toBe('second');
175
+ expect(historyManager.next()).toBe('third');
176
+ });
177
+
178
+ it('should return null when reaching end', () => {
179
+ historyManager.previous(); // third
180
+ expect(historyManager.next()).toBeNull(); // End of history
181
+ });
182
+
183
+ it('should return null when not navigating', () => {
184
+ expect(historyManager.next()).toBeNull();
185
+ });
186
+
187
+ it('should reset navigation position when reaching end', () => {
188
+ historyManager.previous(); // Start navigation
189
+ historyManager.next(); // Back to end
190
+
191
+ expect(historyManager.isNavigating()).toBe(false);
192
+ expect(historyManager.getPosition()).toBe(-1);
193
+ });
194
+ });
195
+
196
+ describe('reset()', () => {
197
+ it('should reset navigation state', () => {
198
+ historyManager.add('hello');
199
+ historyManager.add('world');
200
+
201
+ historyManager.previous();
202
+ expect(historyManager.isNavigating()).toBe(true);
203
+
204
+ historyManager.reset();
205
+ expect(historyManager.isNavigating()).toBe(false);
206
+ expect(historyManager.getPosition()).toBe(-1);
207
+ });
208
+ });
209
+
210
+ describe('persistence', () => {
211
+ it('should save and load history', async () => {
212
+ historyManager.add('hello');
213
+ historyManager.add('world');
214
+ await historyManager.flush();
215
+
216
+ // Create new manager and load
217
+ const newManager = new InputHistoryManager({ savePath: historyPath });
218
+ await newManager.load();
219
+
220
+ expect(newManager.size()).toBe(2);
221
+ const entries = newManager.getEntries();
222
+ expect(entries[0].text).toBe('hello');
223
+ expect(entries[1].text).toBe('world');
224
+ });
225
+
226
+ it('should handle missing history file gracefully', async () => {
227
+ const missingPath = path.join(testDir, 'missing.json');
228
+ const manager = new InputHistoryManager({ savePath: missingPath });
229
+
230
+ await expect(manager.load()).resolves.not.toThrow();
231
+ expect(manager.size()).toBe(0);
232
+ });
233
+
234
+ it('should handle corrupt history file gracefully', async () => {
235
+ // Write corrupt JSON
236
+ await fs.mkdir(testDir, { recursive: true });
237
+ await fs.writeFile(historyPath, 'invalid json{', 'utf-8');
238
+
239
+ const manager = new InputHistoryManager({ savePath: historyPath });
240
+ await expect(manager.load()).resolves.not.toThrow();
241
+ expect(manager.size()).toBe(0);
242
+ });
243
+
244
+ it('should preserve maxSize setting on save/load', async () => {
245
+ const manager1 = new InputHistoryManager({
246
+ savePath: historyPath,
247
+ maxSize: 5,
248
+ });
249
+ await manager1.load();
250
+
251
+ for (let i = 0; i < 10; i++) {
252
+ manager1.add(`entry ${i}`);
253
+ }
254
+ await manager1.flush();
255
+
256
+ // Load with new manager (different maxSize)
257
+ const manager2 = new InputHistoryManager({
258
+ savePath: historyPath,
259
+ maxSize: 3,
260
+ });
261
+ await manager2.load();
262
+
263
+ // Should prune to new maxSize
264
+ expect(manager2.size()).toBe(3);
265
+ });
266
+ });
267
+
268
+ describe('clear()', () => {
269
+ it('should clear all entries', () => {
270
+ historyManager.add('hello');
271
+ historyManager.add('world');
272
+
273
+ historyManager.clear();
274
+
275
+ expect(historyManager.size()).toBe(0);
276
+ expect(historyManager.isNavigating()).toBe(false);
277
+ });
278
+ });
279
+
280
+ describe('configuration', () => {
281
+ it('should respect enabled flag', () => {
282
+ const disabledManager = new InputHistoryManager({
283
+ enabled: false,
284
+ savePath: historyPath + '.disabled',
285
+ });
286
+
287
+ disabledManager.add('hello');
288
+ expect(disabledManager.size()).toBe(0);
289
+
290
+ expect(disabledManager.previous()).toBeNull();
291
+ expect(disabledManager.next()).toBeNull();
292
+ });
293
+
294
+ it('should respect deduplicateConsecutive flag', () => {
295
+ const noDedupe = new InputHistoryManager({
296
+ savePath: historyPath + '.nodedupe',
297
+ deduplicateConsecutive: false,
298
+ });
299
+
300
+ noDedupe.add('hello');
301
+ noDedupe.add('hello');
302
+ noDedupe.add('hello');
303
+
304
+ expect(noDedupe.size()).toBe(3);
305
+ });
306
+ });
307
+
308
+ describe('navigation scenarios', () => {
309
+ it('should handle up/down/up pattern correctly', () => {
310
+ historyManager.add('first');
311
+ historyManager.add('second');
312
+ historyManager.add('third');
313
+
314
+ expect(historyManager.previous()).toBe('third');
315
+ expect(historyManager.previous()).toBe('second');
316
+ expect(historyManager.next()).toBe('third');
317
+ expect(historyManager.previous()).toBe('second');
318
+ });
319
+
320
+ it('should handle full navigation cycle', () => {
321
+ historyManager.add('first');
322
+ historyManager.add('second');
323
+
324
+ // Navigate all the way back
325
+ expect(historyManager.previous()).toBe('second');
326
+ expect(historyManager.previous()).toBe('first');
327
+ expect(historyManager.previous()).toBe('first');
328
+
329
+ // Navigate all the way forward
330
+ expect(historyManager.next()).toBe('second');
331
+ expect(historyManager.next()).toBeNull();
332
+ expect(historyManager.next()).toBeNull();
333
+ });
334
+ });
335
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Unit tests for checkpoint persistence across session restarts
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
5
+ import { SessionManager } from '../src/session/manager.js';
6
+ import { initCheckpointManager, getCheckpointManager } from '../src/checkpointing/index.js';
7
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+
11
+ describe('Checkpoint Persistence', () => {
12
+ let sessionManager: SessionManager;
13
+ let testDir: string;
14
+
15
+ beforeEach(async () => {
16
+ testDir = path.join(os.tmpdir(), `gencode-test-${Date.now()}`);
17
+ sessionManager = new SessionManager({
18
+ storageDir: testDir,
19
+ maxSessions: 50,
20
+ maxAge: 30,
21
+ autoSave: true,
22
+ });
23
+ await sessionManager.init();
24
+ });
25
+
26
+ afterEach(async () => {
27
+ try {
28
+ await fs.rm(testDir, { recursive: true, force: true });
29
+ } catch (error) {
30
+ // Ignore cleanup errors
31
+ }
32
+ });
33
+
34
+ it('should save checkpoints with session', async () => {
35
+ // Create session
36
+ const session = await sessionManager.create({
37
+ provider: 'anthropic',
38
+ model: 'claude-3-5-sonnet-20241022',
39
+ title: 'Test Session',
40
+ });
41
+
42
+ // Initialize checkpoint manager
43
+ const checkpointMgr = initCheckpointManager(session.metadata.id);
44
+
45
+ // Record some changes
46
+ checkpointMgr.recordChange({
47
+ path: '/test/file1.ts',
48
+ changeType: 'create',
49
+ previousContent: null,
50
+ newContent: 'const x = 1;',
51
+ toolName: 'Write',
52
+ });
53
+
54
+ checkpointMgr.recordChange({
55
+ path: '/test/file2.ts',
56
+ changeType: 'modify',
57
+ previousContent: 'const y = 1;',
58
+ newContent: 'const y = 2;',
59
+ toolName: 'Edit',
60
+ });
61
+
62
+ // Save session
63
+ await sessionManager.save(session);
64
+
65
+ // Read session file
66
+ const filePath = path.join(testDir, `${session.metadata.id}.json`);
67
+ const content = await fs.readFile(filePath, 'utf-8');
68
+ const savedSession = JSON.parse(content);
69
+
70
+ // Verify checkpoints were saved
71
+ expect(savedSession.checkpoints).toBeDefined();
72
+ expect(savedSession.checkpoints).toHaveLength(2);
73
+ expect(savedSession.checkpoints[0].path).toBe('/test/file1.ts');
74
+ expect(savedSession.checkpoints[1].changeType).toBe('modify');
75
+ });
76
+
77
+ it('should restore checkpoints when loading session', async () => {
78
+ // Create and save session with checkpoints
79
+ const session = await sessionManager.create({
80
+ provider: 'anthropic',
81
+ model: 'claude-3-5-sonnet-20241022',
82
+ title: 'Test Session',
83
+ });
84
+
85
+ const checkpointMgr = initCheckpointManager(session.metadata.id);
86
+ checkpointMgr.recordChange({
87
+ path: '/test/file1.ts',
88
+ changeType: 'create',
89
+ previousContent: null,
90
+ newContent: 'const x = 1;',
91
+ toolName: 'Write',
92
+ });
93
+
94
+ await sessionManager.save(session);
95
+
96
+ // Load session in new manager instance
97
+ const newSessionManager = new SessionManager({
98
+ storageDir: testDir,
99
+ maxSessions: 50,
100
+ maxAge: 30,
101
+ autoSave: true,
102
+ });
103
+ await newSessionManager.init();
104
+
105
+ const loadedSession = await newSessionManager.load(session.metadata.id);
106
+ expect(loadedSession).toBeDefined();
107
+
108
+ // Verify checkpoints were restored to manager
109
+ const restoredMgr = getCheckpointManager();
110
+ const checkpoints = restoredMgr.getCheckpoints();
111
+
112
+ expect(checkpoints).toHaveLength(1);
113
+ expect(checkpoints[0].path).toBe('/test/file1.ts');
114
+ expect(checkpoints[0].changeType).toBe('create');
115
+ });
116
+
117
+ it('should handle session fork with checkpoints', async () => {
118
+ // Create parent session with checkpoints
119
+ const parent = await sessionManager.create({
120
+ provider: 'anthropic',
121
+ model: 'claude-3-5-sonnet-20241022',
122
+ title: 'Parent Session',
123
+ });
124
+
125
+ const parentMgr = initCheckpointManager(parent.metadata.id);
126
+ parentMgr.recordChange({
127
+ path: '/test/file1.ts',
128
+ changeType: 'create',
129
+ previousContent: null,
130
+ newContent: 'const x = 1;',
131
+ toolName: 'Write',
132
+ });
133
+
134
+ await sessionManager.save(parent);
135
+
136
+ // Fork session
137
+ const forked = await sessionManager.fork(parent.metadata.id, 'Forked Session');
138
+ expect(forked).toBeDefined();
139
+ expect(forked.checkpoints).toHaveLength(1);
140
+ expect((forked.checkpoints as any)[0].path).toBe('/test/file1.ts');
141
+ });
142
+
143
+ it('should handle sessions without checkpoints', async () => {
144
+ // Create session without checkpoints
145
+ const session = await sessionManager.create({
146
+ provider: 'anthropic',
147
+ model: 'claude-3-5-sonnet-20241022',
148
+ title: 'Test Session',
149
+ });
150
+
151
+ // Don't add any checkpoints, just save
152
+ await sessionManager.save(session);
153
+
154
+ // Load session
155
+ const loaded = await sessionManager.load(session.metadata.id);
156
+ expect(loaded).toBeDefined();
157
+ expect(loaded?.checkpoints).toBeUndefined();
158
+ });
159
+
160
+ it('should preserve checkpoint metadata on save/load cycle', async () => {
161
+ // Create session with checkpoints
162
+ const session = await sessionManager.create({
163
+ provider: 'anthropic',
164
+ model: 'claude-3-5-sonnet-20241022',
165
+ title: 'Test Session',
166
+ });
167
+
168
+ const checkpointMgr = initCheckpointManager(session.metadata.id);
169
+
170
+ // Record a change with specific metadata
171
+ const now = new Date();
172
+ checkpointMgr.recordChange({
173
+ path: '/test/complex.ts',
174
+ changeType: 'modify',
175
+ previousContent: 'old content',
176
+ newContent: 'new content',
177
+ toolName: 'Edit',
178
+ });
179
+
180
+ await sessionManager.save(session);
181
+
182
+ // Load session
183
+ const loaded = await sessionManager.load(session.metadata.id);
184
+ expect(loaded).toBeDefined();
185
+
186
+ // Verify checkpoint metadata preserved
187
+ const restoredMgr = getCheckpointManager();
188
+ const checkpoints = restoredMgr.getCheckpoints();
189
+
190
+ expect(checkpoints).toHaveLength(1);
191
+ expect(checkpoints[0].path).toBe('/test/complex.ts');
192
+ expect(checkpoints[0].changeType).toBe('modify');
193
+ expect(checkpoints[0].previousContent).toBe('old content');
194
+ expect(checkpoints[0].newContent).toBe('new content');
195
+ expect(checkpoints[0].toolName).toBe('Edit');
196
+ expect(checkpoints[0].timestamp).toBeInstanceOf(Date);
197
+ });
198
+ });