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.
- package/RELEASE_NOTES_v0.4.0.md +140 -0
- package/dist/agent/agent.d.ts +26 -4
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +316 -57
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/types.d.ts +20 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/checkpointing/checkpoint-manager.d.ts +24 -0
- package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -1
- package/dist/checkpointing/checkpoint-manager.js +28 -0
- package/dist/checkpointing/checkpoint-manager.js.map +1 -1
- package/dist/cli/components/App.d.ts +8 -0
- package/dist/cli/components/App.d.ts.map +1 -1
- package/dist/cli/components/App.js +493 -45
- package/dist/cli/components/App.js.map +1 -1
- package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
- package/dist/cli/components/CommandSuggestions.js +2 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -1
- package/dist/cli/components/Header.d.ts +6 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/Header.js +3 -3
- package/dist/cli/components/Header.js.map +1 -1
- package/dist/cli/components/Messages.d.ts.map +1 -1
- package/dist/cli/components/Messages.js +8 -10
- package/dist/cli/components/Messages.js.map +1 -1
- package/dist/cli/components/ModelSelector.d.ts +4 -3
- package/dist/cli/components/ModelSelector.d.ts.map +1 -1
- package/dist/cli/components/ModelSelector.js +54 -37
- package/dist/cli/components/ModelSelector.js.map +1 -1
- package/dist/cli/components/ProviderManager.d.ts +2 -2
- package/dist/cli/components/ProviderManager.d.ts.map +1 -1
- package/dist/cli/components/ProviderManager.js +137 -156
- package/dist/cli/components/ProviderManager.js.map +1 -1
- package/dist/cli/index.js +33 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/levels.d.ts +5 -5
- package/dist/config/levels.d.ts.map +1 -1
- package/dist/config/levels.js +20 -20
- package/dist/config/levels.js.map +1 -1
- package/dist/config/merger.js +1 -1
- package/dist/config/merger.js.map +1 -1
- package/dist/config/providers-config.d.ts +8 -5
- package/dist/config/providers-config.d.ts.map +1 -1
- package/dist/config/providers-config.js +19 -22
- package/dist/config/providers-config.js.map +1 -1
- package/dist/config/test-utils.d.ts +2 -2
- package/dist/config/test-utils.d.ts.map +1 -1
- package/dist/config/test-utils.js +4 -4
- package/dist/config/test-utils.js.map +1 -1
- package/dist/config/types.d.ts +42 -17
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +14 -14
- package/dist/config/types.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/input/history-manager.d.ts +78 -0
- package/dist/input/history-manager.d.ts.map +1 -0
- package/dist/input/history-manager.js +224 -0
- package/dist/input/history-manager.js.map +1 -0
- package/dist/input/index.d.ts +6 -0
- package/dist/input/index.d.ts.map +1 -0
- package/dist/input/index.js +5 -0
- package/dist/input/index.js.map +1 -0
- package/dist/memory/memory-manager.d.ts +25 -12
- package/dist/memory/memory-manager.d.ts.map +1 -1
- package/dist/memory/memory-manager.js +241 -112
- package/dist/memory/memory-manager.js.map +1 -1
- package/dist/memory/test-utils.d.ts +1 -1
- package/dist/memory/test-utils.d.ts.map +1 -1
- package/dist/memory/test-utils.js +3 -3
- package/dist/memory/test-utils.js.map +1 -1
- package/dist/memory/types.d.ts +20 -10
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/memory/types.js +13 -13
- package/dist/memory/types.js.map +1 -1
- package/dist/migration/migrate.d.ts +24 -0
- package/dist/migration/migrate.d.ts.map +1 -0
- package/dist/migration/migrate.js +164 -0
- package/dist/migration/migrate.js.map +1 -0
- package/dist/permissions/persistence.d.ts +2 -2
- package/dist/permissions/persistence.js +4 -4
- package/dist/permissions/persistence.js.map +1 -1
- package/dist/planning/plan-file.d.ts +1 -1
- package/dist/planning/plan-file.js +2 -2
- package/dist/planning/plan-file.js.map +1 -1
- package/dist/prompts/index.d.ts +5 -4
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +13 -10
- package/dist/prompts/index.js.map +1 -1
- package/dist/providers/anthropic.d.ts +2 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +7 -0
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts +2 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +40 -2
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/google.d.ts +22 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +297 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/index.d.ts +22 -12
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +56 -32
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +13 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/registry.d.ts +48 -34
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +72 -88
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/store.d.ts +43 -17
- package/dist/providers/store.d.ts.map +1 -1
- package/dist/providers/store.js +112 -19
- package/dist/providers/store.js.map +1 -1
- package/dist/providers/types.d.ts +52 -3
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/vertex-ai.d.ts +15 -7
- package/dist/providers/vertex-ai.d.ts.map +1 -1
- package/dist/providers/vertex-ai.js +46 -13
- package/dist/providers/vertex-ai.js.map +1 -1
- package/dist/session/compression/engine.d.ts +109 -0
- package/dist/session/compression/engine.d.ts.map +1 -0
- package/dist/session/compression/engine.js +311 -0
- package/dist/session/compression/engine.js.map +1 -0
- package/dist/session/compression/index.d.ts +12 -0
- package/dist/session/compression/index.d.ts.map +1 -0
- package/dist/session/compression/index.js +11 -0
- package/dist/session/compression/index.js.map +1 -0
- package/dist/session/compression/types.d.ts +90 -0
- package/dist/session/compression/types.d.ts.map +1 -0
- package/dist/session/compression/types.js +17 -0
- package/dist/session/compression/types.js.map +1 -0
- package/dist/session/manager.d.ts +64 -3
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +254 -2
- package/dist/session/manager.js.map +1 -1
- package/dist/session/types.d.ts +16 -0
- package/dist/session/types.d.ts.map +1 -1
- package/dist/session/types.js +1 -1
- package/dist/session/types.js.map +1 -1
- package/docs/README.md +1 -0
- package/docs/config-system-comparison.md +50 -50
- package/docs/cost-tracking-comparison.md +2 -2
- package/docs/diagrams/compression-decision.mmd +30 -0
- package/docs/diagrams/compression-workflow.mmd +54 -0
- package/docs/diagrams/layer1-pruning.mmd +45 -0
- package/docs/diagrams/layer2-compaction.mmd +42 -0
- package/docs/memory-system.md +124 -31
- package/docs/permissions.md +2 -2
- package/docs/proposals/0006-memory-system.md +4 -4
- package/docs/proposals/0007-context-management.md +252 -2
- package/docs/proposals/0008-checkpointing.md +109 -2
- package/docs/proposals/0011-custom-commands.md +2 -1
- package/docs/proposals/0021-skills-system.md +2 -1
- package/docs/proposals/0023-permission-enhancements.md +2 -2
- package/docs/proposals/0033-enterprise-deployment.md +1 -1
- package/docs/proposals/0041-configuration-system.md +17 -19
- package/docs/proposals/0042-prompt-optimization.md +17 -9
- package/docs/proposals/README.md +8 -7
- package/docs/providers.md +96 -11
- package/docs/session-compression.md +695 -0
- package/examples/agent-demo.ts +23 -1
- package/examples/basic.ts +3 -3
- package/package.json +2 -2
- package/scripts/migrate.ts +449 -0
- package/src/agent/agent.ts +365 -61
- package/src/agent/types.ts +24 -2
- package/src/checkpointing/checkpoint-manager.ts +48 -0
- package/src/cli/components/App.tsx +570 -42
- package/src/cli/components/CommandSuggestions.tsx +2 -0
- package/src/cli/components/Header.tsx +16 -1
- package/src/cli/components/Messages.tsx +21 -15
- package/src/cli/components/ModelSelector.tsx +62 -43
- package/src/cli/components/ProviderManager.tsx +278 -323
- package/src/cli/index.tsx +38 -18
- package/src/config/index.ts +5 -3
- package/src/config/levels.test.ts +22 -22
- package/src/config/levels.ts +22 -22
- package/src/config/loader.test.ts +14 -14
- package/src/config/manager.test.ts +19 -19
- package/src/config/merger.test.ts +23 -23
- package/src/config/merger.ts +1 -1
- package/src/config/providers-config.ts +23 -21
- package/src/config/test-utils.ts +6 -6
- package/src/config/types.ts +55 -20
- package/src/index.ts +3 -3
- package/src/input/history-manager.ts +289 -0
- package/src/input/index.ts +6 -0
- package/src/memory/memory-manager.test.ts +242 -24
- package/src/memory/memory-manager.ts +270 -141
- package/src/memory/test-utils.ts +4 -4
- package/src/memory/types.ts +28 -17
- package/src/permissions/persistence.ts +4 -4
- package/src/planning/plan-file.ts +2 -2
- package/src/prompts/index.test.ts +2 -1
- package/src/prompts/index.ts +15 -11
- package/src/providers/anthropic.ts +9 -0
- package/src/providers/{gemini.ts → google.ts} +75 -15
- package/src/providers/index.ts +85 -42
- package/src/providers/openai.ts +16 -0
- package/src/providers/registry.ts +116 -111
- package/src/providers/store.ts +130 -28
- package/src/providers/types.ts +65 -3
- package/src/providers/vertex-ai.ts +49 -13
- package/src/session/compression/engine.ts +406 -0
- package/src/session/compression/index.ts +18 -0
- package/src/session/compression/types.ts +102 -0
- package/src/session/manager.ts +326 -3
- package/src/session/types.ts +22 -1
- package/tests/input-history-manager.test.ts +335 -0
- 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
|
+
});
|