@zoebuildsai/trace 1.5.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/.gitignore +115 -0
- package/.trace/progress.json +22 -0
- package/README.md +466 -0
- package/RELEASE-NOTES-1.5.0.md +410 -0
- package/STATUS.md +245 -0
- package/dist/auto-commit.d.ts +66 -0
- package/dist/auto-commit.d.ts.map +1 -0
- package/dist/auto-commit.js +180 -0
- package/dist/auto-commit.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +246 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +46 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +256 -0
- package/dist/commands.js.map +1 -0
- package/dist/diff.d.ts +23 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +106 -0
- package/dist/diff.js.map +1 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js.map +1 -0
- package/dist/index-cache.d.ts +35 -0
- package/dist/index-cache.d.ts.map +1 -0
- package/dist/index-cache.js +114 -0
- package/dist/index-cache.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/storage.d.ts +45 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +151 -0
- package/dist/storage.js.map +1 -0
- package/dist/sync.d.ts +60 -0
- package/dist/sync.js +184 -0
- package/dist/tags.d.ts +85 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +219 -0
- package/dist/tags.js.map +1 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +73 -0
- package/docs/_config.yml +2 -0
- package/docs/index.html +960 -0
- package/docs-website/package.json +20 -0
- package/jest.config.js +21 -0
- package/package.json +50 -0
- package/scripts/init.ts +290 -0
- package/src/agent-audit.ts +270 -0
- package/src/agent-checkout.ts +227 -0
- package/src/agent-coordination.ts +318 -0
- package/src/async-queue.ts +203 -0
- package/src/auto-branching.ts +279 -0
- package/src/auto-commit.ts +166 -0
- package/src/cherry-pick.ts +252 -0
- package/src/chunked-upload.ts +224 -0
- package/src/cli-v2.ts +335 -0
- package/src/cli.ts +318 -0
- package/src/cliff-detection.ts +232 -0
- package/src/commands.ts +267 -0
- package/src/commit-hash-system.ts +351 -0
- package/src/compression.ts +176 -0
- package/src/conflict-resolution-ui.ts +277 -0
- package/src/conflict-visualization.ts +238 -0
- package/src/diff-formatter.ts +184 -0
- package/src/diff.ts +124 -0
- package/src/distributed-coordination.ts +273 -0
- package/src/git-interop.ts +316 -0
- package/src/index-cache.ts +88 -0
- package/src/index.ts +38 -0
- package/src/merge-engine.ts +143 -0
- package/src/message-search.ts +370 -0
- package/src/performance-monitoring.ts +236 -0
- package/src/rebase.ts +327 -0
- package/src/rollback.ts +215 -0
- package/src/semantic-grouping.ts +245 -0
- package/src/stage-area.ts +324 -0
- package/src/stash.ts +278 -0
- package/src/storage.ts +131 -0
- package/src/sync.ts +205 -0
- package/src/tags.ts +244 -0
- package/src/types.ts +119 -0
- package/src/webhooks.ts +119 -0
- package/src/workspace-isolation.ts +298 -0
- package/tests/auto-commit.test.ts +308 -0
- package/tests/checkout.test.ts +136 -0
- package/tests/commit.test.ts +118 -0
- package/tests/diff.test.ts +191 -0
- package/tests/github.test.ts +94 -0
- package/tests/integration.test.ts +267 -0
- package/tests/log.test.ts +125 -0
- package/tests/phase2-integration.test.ts +370 -0
- package/tests/storage.test.ts +167 -0
- package/tests/tags.test.ts +477 -0
- package/tests/types.test.ts +75 -0
- package/tests/v1.1/agent-audit.test.ts +472 -0
- package/tests/v1.1/agent-coordination.test.ts +308 -0
- package/tests/v1.1/async-queue.test.ts +253 -0
- package/tests/v1.1/comprehensive.test.ts +521 -0
- package/tests/v1.1/diff-formatter.test.ts +238 -0
- package/tests/v1.1/integration.test.ts +389 -0
- package/tests/v1.1/onboarding.test.ts +365 -0
- package/tests/v1.1/rollback.test.ts +370 -0
- package/tests/v1.1/semantic-grouping.test.ts +230 -0
- package/tests/v1.2/chunked-upload.test.ts +301 -0
- package/tests/v1.2/cliff-detection.test.ts +272 -0
- package/tests/v1.2/commit-hash-system.test.ts +288 -0
- package/tests/v1.2/compression.test.ts +220 -0
- package/tests/v1.2/conflict-visualization.test.ts +263 -0
- package/tests/v1.2/distributed.test.ts +261 -0
- package/tests/v1.2/performance-monitoring.test.ts +328 -0
- package/tests/v1.3/auto-branching.test.ts +270 -0
- package/tests/v1.3/message-search.test.ts +264 -0
- package/tests/v1.3/stage-area.test.ts +330 -0
- package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
- package/tests/v1.4/cli.test.ts +171 -0
- package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
- package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
- package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
- package/tests/v1.4/workspace-isolation.test.ts +268 -0
- package/tests/v1.5/agent-coordination.real.test.ts +401 -0
- package/tests/v1.5/cli-v2.test.ts +354 -0
- package/tests/v1.5/git-interop.real.test.ts +358 -0
- package/tests/v1.5/integration-testing.real.test.ts +440 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2 Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Full workflow tests combining auto-commit and tags:
|
|
5
|
+
* - Auto-commit + tags workflow
|
|
6
|
+
* - Manual + auto-commits mixed
|
|
7
|
+
* - Branching with auto-commits
|
|
8
|
+
* - Tag-based workflows
|
|
9
|
+
* - Complex state transitions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { AutoCommitter } from '../src/auto-commit';
|
|
13
|
+
import { TagManager } from '../src/tags';
|
|
14
|
+
import { TraceCommands } from '../src/commands';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
|
|
19
|
+
describe('Phase 2 Integration - Auto-Commit + Tags', () => {
|
|
20
|
+
let testDir: string;
|
|
21
|
+
let homeDir: string;
|
|
22
|
+
let memoryDir: string;
|
|
23
|
+
let commands: TraceCommands;
|
|
24
|
+
let autoCommitter: AutoCommitter;
|
|
25
|
+
let tagManager: TagManager;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-git-phase2-'));
|
|
29
|
+
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'home-'));
|
|
30
|
+
process.env.HOME = homeDir;
|
|
31
|
+
memoryDir = path.join(homeDir, '.openclaw/memory');
|
|
32
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
commands = new TraceCommands(testDir);
|
|
35
|
+
autoCommitter = new AutoCommitter(commands, memoryDir);
|
|
36
|
+
tagManager = new TagManager(commands, testDir);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
if (autoCommitter) {
|
|
41
|
+
autoCommitter.stop();
|
|
42
|
+
}
|
|
43
|
+
if (fs.existsSync(testDir)) {
|
|
44
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
if (fs.existsSync(homeDir)) {
|
|
47
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('Auto-commit + manual commits', () => {
|
|
52
|
+
it('should handle mixed auto and manual commits', () => {
|
|
53
|
+
const file = path.join(memoryDir, 'mixed.md');
|
|
54
|
+
fs.writeFileSync(file, 'start');
|
|
55
|
+
|
|
56
|
+
autoCommitter.start({ interval: 100 });
|
|
57
|
+
|
|
58
|
+
// Manual commit
|
|
59
|
+
const manualHash = commands.commit('manual commit 1');
|
|
60
|
+
|
|
61
|
+
// Modify file
|
|
62
|
+
fs.writeFileSync(file, 'auto change 1');
|
|
63
|
+
|
|
64
|
+
// Manual commit
|
|
65
|
+
commands.commit('manual commit 2');
|
|
66
|
+
|
|
67
|
+
const log = commands.log(10);
|
|
68
|
+
expect(log.length).toBeGreaterThanOrEqual(3);
|
|
69
|
+
|
|
70
|
+
const hasManual = log.some(entry =>
|
|
71
|
+
entry.message === 'manual commit 1' ||
|
|
72
|
+
entry.message === 'manual commit 2'
|
|
73
|
+
);
|
|
74
|
+
expect(hasManual).toBe(true);
|
|
75
|
+
|
|
76
|
+
autoCommitter.stop();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should distinguish auto from manual in history', () => {
|
|
80
|
+
const file = path.join(memoryDir, 'distinguish.md');
|
|
81
|
+
|
|
82
|
+
autoCommitter.start({ interval: 100 });
|
|
83
|
+
|
|
84
|
+
// Manual
|
|
85
|
+
commands.commit('manual work');
|
|
86
|
+
|
|
87
|
+
// Change file
|
|
88
|
+
fs.writeFileSync(file, 'auto work');
|
|
89
|
+
|
|
90
|
+
const log = commands.log(10);
|
|
91
|
+
const manualEntry = log.find(e => e.message === 'manual work');
|
|
92
|
+
|
|
93
|
+
expect(manualEntry).toBeDefined();
|
|
94
|
+
|
|
95
|
+
autoCommitter.stop();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should allow tagging both auto and manual commits', () => {
|
|
99
|
+
const file = path.join(memoryDir, 'tag-both.md');
|
|
100
|
+
fs.writeFileSync(file, 'start');
|
|
101
|
+
|
|
102
|
+
autoCommitter.start({ interval: 100 });
|
|
103
|
+
|
|
104
|
+
// Tag manual
|
|
105
|
+
commands.commit('manual checkpoint');
|
|
106
|
+
tagManager.create('manual-tag');
|
|
107
|
+
|
|
108
|
+
// Change and auto-commit
|
|
109
|
+
fs.writeFileSync(file, 'changed');
|
|
110
|
+
|
|
111
|
+
// Tag
|
|
112
|
+
tagManager.create('auto-tag');
|
|
113
|
+
|
|
114
|
+
const tags = tagManager.list();
|
|
115
|
+
expect(tags.length).toBeGreaterThanOrEqual(2);
|
|
116
|
+
expect(tags.map(t => t.name)).toContain('manual-tag');
|
|
117
|
+
expect(tags.map(t => t.name)).toContain('auto-tag');
|
|
118
|
+
|
|
119
|
+
autoCommitter.stop();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Tag-based workflow', () => {
|
|
124
|
+
it('should support snapshot-based development', () => {
|
|
125
|
+
const file = path.join(memoryDir, 'snapshot.md');
|
|
126
|
+
fs.writeFileSync(file, 'v1');
|
|
127
|
+
|
|
128
|
+
autoCommitter.start({ interval: 100 });
|
|
129
|
+
|
|
130
|
+
// Create snapshots
|
|
131
|
+
commands.commit('v1');
|
|
132
|
+
tagManager.create('v1-initial');
|
|
133
|
+
|
|
134
|
+
fs.writeFileSync(file, 'v2-experiment');
|
|
135
|
+
commands.commit('v2');
|
|
136
|
+
tagManager.create('v2-experiment');
|
|
137
|
+
|
|
138
|
+
// List all experiments
|
|
139
|
+
const tags = tagManager.list();
|
|
140
|
+
expect(tags.length).toBeGreaterThanOrEqual(2);
|
|
141
|
+
|
|
142
|
+
// Verify tags exist
|
|
143
|
+
expect(tagManager.get('v1-initial')).toBeDefined();
|
|
144
|
+
expect(tagManager.get('v2-experiment')).toBeDefined();
|
|
145
|
+
|
|
146
|
+
autoCommitter.stop();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should support milestone tracking', () => {
|
|
150
|
+
const file = path.join(memoryDir, 'milestones.md');
|
|
151
|
+
fs.writeFileSync(file, 'start');
|
|
152
|
+
|
|
153
|
+
autoCommitter.start({ interval: 100 });
|
|
154
|
+
|
|
155
|
+
const milestones = [
|
|
156
|
+
{ name: 'm1-learning', description: 'Learned basics' },
|
|
157
|
+
{ name: 'm2-integration', description: 'Integrated systems' },
|
|
158
|
+
{ name: 'm3-optimization', description: 'Optimized performance' },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
for (const m of milestones) {
|
|
162
|
+
fs.writeFileSync(file, m.name);
|
|
163
|
+
commands.commit(m.name);
|
|
164
|
+
tagManager.create(m.name, { description: m.description });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const tags = tagManager.list();
|
|
168
|
+
expect(tags.length).toBeGreaterThanOrEqual(milestones.length);
|
|
169
|
+
|
|
170
|
+
autoCommitter.stop();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('Error handling & recovery', () => {
|
|
175
|
+
it('should handle permission errors gracefully', () => {
|
|
176
|
+
const file = path.join(memoryDir, 'readonly.md');
|
|
177
|
+
fs.writeFileSync(file, 'content');
|
|
178
|
+
|
|
179
|
+
autoCommitter.start({ interval: 100 });
|
|
180
|
+
|
|
181
|
+
// Make file readonly
|
|
182
|
+
fs.chmodSync(file, 0o444);
|
|
183
|
+
|
|
184
|
+
// Try to modify - auto-commit should handle it
|
|
185
|
+
try {
|
|
186
|
+
fs.writeFileSync(file, 'new', { flag: 'w' });
|
|
187
|
+
} catch (e) {
|
|
188
|
+
// Expected to fail
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Auto-committer should still be running
|
|
192
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
193
|
+
|
|
194
|
+
fs.chmodSync(file, 0o644);
|
|
195
|
+
autoCommitter.stop();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should recover from corrupt tag files', () => {
|
|
199
|
+
tagManager.create('good-tag');
|
|
200
|
+
|
|
201
|
+
// Corrupt a tag file
|
|
202
|
+
const tagsDir = path.join(testDir, 'refs', 'tags');
|
|
203
|
+
const badFile = path.join(tagsDir, 'bad-tag');
|
|
204
|
+
fs.writeFileSync(badFile, 'invalid json data');
|
|
205
|
+
|
|
206
|
+
// Should still work
|
|
207
|
+
const newManager = new TagManager(commands, testDir);
|
|
208
|
+
const tags = newManager.list();
|
|
209
|
+
|
|
210
|
+
expect(tags.find(t => t.name === 'good-tag')).toBeDefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle cleanup on error', () => {
|
|
214
|
+
autoCommitter.start({ interval: 100 });
|
|
215
|
+
|
|
216
|
+
const file = path.join(memoryDir, 'cleanup.md');
|
|
217
|
+
fs.writeFileSync(file, 'test');
|
|
218
|
+
|
|
219
|
+
autoCommitter.stop();
|
|
220
|
+
|
|
221
|
+
// Verify no dangling processes
|
|
222
|
+
expect(autoCommitter.isRunning_()).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('Complex state transitions', () => {
|
|
227
|
+
it('should handle: create → tag → modify → tag → restore', () => {
|
|
228
|
+
const file = path.join(memoryDir, 'complex.md');
|
|
229
|
+
fs.writeFileSync(file, 'v1');
|
|
230
|
+
|
|
231
|
+
autoCommitter.start({ interval: 100 });
|
|
232
|
+
|
|
233
|
+
commands.commit('v1');
|
|
234
|
+
tagManager.create('v1-stable');
|
|
235
|
+
|
|
236
|
+
fs.writeFileSync(file, 'v2-beta');
|
|
237
|
+
commands.commit('v2');
|
|
238
|
+
tagManager.create('v2-beta');
|
|
239
|
+
|
|
240
|
+
fs.writeFileSync(file, 'v3-alpha');
|
|
241
|
+
commands.commit('v3');
|
|
242
|
+
tagManager.create('v3-alpha');
|
|
243
|
+
|
|
244
|
+
// Restore to v2
|
|
245
|
+
tagManager.checkout('v2-beta');
|
|
246
|
+
|
|
247
|
+
const currentCommit = commands.getCurrentCommit();
|
|
248
|
+
expect(currentCommit?.message).toBe('v2');
|
|
249
|
+
|
|
250
|
+
// Forward to v3
|
|
251
|
+
tagManager.checkout('v3-alpha');
|
|
252
|
+
const currentCommit2 = commands.getCurrentCommit();
|
|
253
|
+
expect(currentCommit2?.message).toBe('v3');
|
|
254
|
+
|
|
255
|
+
autoCommitter.stop();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should track changes across sessions', () => {
|
|
259
|
+
const file = path.join(memoryDir, 'multi-session.md');
|
|
260
|
+
fs.writeFileSync(file, 'session 1');
|
|
261
|
+
|
|
262
|
+
// Session 1
|
|
263
|
+
autoCommitter.start({ interval: 100 });
|
|
264
|
+
|
|
265
|
+
fs.writeFileSync(file, 'session 2');
|
|
266
|
+
commands.commit('session 2');
|
|
267
|
+
tagManager.create('end-of-session');
|
|
268
|
+
|
|
269
|
+
// Stop watching
|
|
270
|
+
autoCommitter.stop();
|
|
271
|
+
|
|
272
|
+
// Session 2: restart
|
|
273
|
+
const ac2 = new AutoCommitter(commands, memoryDir);
|
|
274
|
+
ac2.start({ interval: 100 });
|
|
275
|
+
|
|
276
|
+
fs.writeFileSync(file, 'session 3');
|
|
277
|
+
commands.commit('session 3');
|
|
278
|
+
|
|
279
|
+
const log = commands.log(20);
|
|
280
|
+
expect(log.length).toBeGreaterThanOrEqual(3);
|
|
281
|
+
|
|
282
|
+
ac2.stop();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should support rollback via tags', () => {
|
|
286
|
+
const work = path.join(memoryDir, 'work.md');
|
|
287
|
+
fs.writeFileSync(work, '1');
|
|
288
|
+
|
|
289
|
+
autoCommitter.start({ interval: 100 });
|
|
290
|
+
|
|
291
|
+
commands.commit('checkpoint 1');
|
|
292
|
+
tagManager.create('checkpoint-good');
|
|
293
|
+
|
|
294
|
+
fs.writeFileSync(work, '2');
|
|
295
|
+
fs.writeFileSync(path.join(memoryDir, 'newfile'), 'bad');
|
|
296
|
+
commands.commit('checkpoint 2');
|
|
297
|
+
|
|
298
|
+
// Rollback
|
|
299
|
+
tagManager.checkout('checkpoint-good');
|
|
300
|
+
|
|
301
|
+
const current = commands.getCurrentCommit();
|
|
302
|
+
expect(current?.message).toBe('checkpoint 1');
|
|
303
|
+
|
|
304
|
+
autoCommitter.stop();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('Initialization and setup', () => {
|
|
309
|
+
it('should initialize auto-committer', () => {
|
|
310
|
+
autoCommitter.start({ interval: 200 });
|
|
311
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
312
|
+
expect(autoCommitter.getInterval()).toBe(200);
|
|
313
|
+
autoCommitter.stop();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should initialize tag manager', () => {
|
|
317
|
+
const tags = tagManager.list();
|
|
318
|
+
expect(tags).toBeDefined();
|
|
319
|
+
expect(Array.isArray(tags)).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should work with both systems together', () => {
|
|
323
|
+
autoCommitter.start({ interval: 100 });
|
|
324
|
+
|
|
325
|
+
const file = path.join(memoryDir, 'work.md');
|
|
326
|
+
fs.writeFileSync(file, 'initial');
|
|
327
|
+
commands.commit('initial');
|
|
328
|
+
|
|
329
|
+
tagManager.create('start');
|
|
330
|
+
|
|
331
|
+
fs.writeFileSync(file, 'modified');
|
|
332
|
+
commands.commit('modified');
|
|
333
|
+
|
|
334
|
+
tagManager.create('modified-tag');
|
|
335
|
+
|
|
336
|
+
const tags = tagManager.list();
|
|
337
|
+
expect(tags.length).toBeGreaterThanOrEqual(2);
|
|
338
|
+
|
|
339
|
+
autoCommitter.stop();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('Performance under reasonable load', () => {
|
|
344
|
+
it('should handle tag creation quickly', () => {
|
|
345
|
+
for (let i = 0; i < 10; i++) {
|
|
346
|
+
const file = path.join(memoryDir, `file${i}.md`);
|
|
347
|
+
fs.writeFileSync(file, `content ${i}`);
|
|
348
|
+
commands.commit(`commit ${i}`);
|
|
349
|
+
const tag = tagManager.create(`tag-${i}`);
|
|
350
|
+
expect(tag).toBeDefined();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const tags = tagManager.list();
|
|
354
|
+
expect(tags.length).toBe(10);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should maintain 100+ commits', () => {
|
|
358
|
+
const file = path.join(memoryDir, 'many.md');
|
|
359
|
+
fs.writeFileSync(file, 'start');
|
|
360
|
+
|
|
361
|
+
for (let i = 0; i < 20; i++) {
|
|
362
|
+
fs.writeFileSync(file, `content ${i}`);
|
|
363
|
+
commands.commit(`commit ${i}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const log = commands.log(100);
|
|
367
|
+
expect(log.length).toBeGreaterThanOrEqual(20);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { Storage } from '../src/storage';
|
|
5
|
+
import { CommitObject, TreeObject } from '../src/types';
|
|
6
|
+
|
|
7
|
+
describe('Storage', () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let storage: Storage;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-git-'));
|
|
13
|
+
storage = new Storage(tmpDir);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should hash content consistently', () => {
|
|
21
|
+
const content = 'test content';
|
|
22
|
+
const hash1 = storage.hash(content);
|
|
23
|
+
const hash2 = storage.hash(content);
|
|
24
|
+
expect(hash1).toBe(hash2);
|
|
25
|
+
expect(hash1.length).toBe(64); // SHA-256 = 64 hex chars
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should save and load commit', () => {
|
|
29
|
+
const tree: TreeObject = {
|
|
30
|
+
files: new Map(),
|
|
31
|
+
hash: 'tree123',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const commit: CommitObject = {
|
|
35
|
+
hash: 'commit123',
|
|
36
|
+
message: 'test commit',
|
|
37
|
+
timestamp: 1000,
|
|
38
|
+
author: 'test',
|
|
39
|
+
parent: null,
|
|
40
|
+
tree,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
storage.saveCommit(commit);
|
|
44
|
+
const loaded = storage.loadCommit('commit123');
|
|
45
|
+
|
|
46
|
+
expect(loaded).not.toBeNull();
|
|
47
|
+
expect(loaded!.message).toBe('test commit');
|
|
48
|
+
expect(loaded!.author).toBe('test');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return null for non-existent commit', () => {
|
|
52
|
+
const loaded = storage.loadCommit('nonexistent');
|
|
53
|
+
expect(loaded).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should save and load refs', () => {
|
|
57
|
+
storage.saveRef('main', 'commit123');
|
|
58
|
+
const ref = storage.loadRef('main');
|
|
59
|
+
expect(ref).toBe('commit123');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should list all commits', () => {
|
|
63
|
+
const tree: TreeObject = { files: new Map(), hash: 'tree1' };
|
|
64
|
+
const commit1: CommitObject = {
|
|
65
|
+
hash: 'c1',
|
|
66
|
+
message: 'msg1',
|
|
67
|
+
timestamp: 1000,
|
|
68
|
+
author: 'test',
|
|
69
|
+
parent: null,
|
|
70
|
+
tree,
|
|
71
|
+
};
|
|
72
|
+
const commit2: CommitObject = {
|
|
73
|
+
hash: 'c2',
|
|
74
|
+
message: 'msg2',
|
|
75
|
+
timestamp: 2000,
|
|
76
|
+
author: 'test',
|
|
77
|
+
parent: 'c1',
|
|
78
|
+
tree,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
storage.saveCommit(commit1);
|
|
82
|
+
storage.saveCommit(commit2);
|
|
83
|
+
|
|
84
|
+
const commits = storage.listCommits();
|
|
85
|
+
expect(commits).toContain('c1');
|
|
86
|
+
expect(commits).toContain('c2');
|
|
87
|
+
expect(commits.length).toBeGreaterThanOrEqual(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should delete commit', () => {
|
|
91
|
+
const tree: TreeObject = { files: new Map(), hash: 'tree1' };
|
|
92
|
+
const commit: CommitObject = {
|
|
93
|
+
hash: 'commit-to-delete',
|
|
94
|
+
message: 'test',
|
|
95
|
+
timestamp: 1000,
|
|
96
|
+
author: 'test',
|
|
97
|
+
parent: null,
|
|
98
|
+
tree,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
storage.saveCommit(commit);
|
|
102
|
+
expect(storage.loadCommit('commit-to-delete')).not.toBeNull();
|
|
103
|
+
|
|
104
|
+
storage.deleteCommit('commit-to-delete');
|
|
105
|
+
expect(storage.loadCommit('commit-to-delete')).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should get history chain', () => {
|
|
109
|
+
const tree: TreeObject = { files: new Map(), hash: 'tree1' };
|
|
110
|
+
const c1: CommitObject = {
|
|
111
|
+
hash: 'c1',
|
|
112
|
+
message: 'first',
|
|
113
|
+
timestamp: 1000,
|
|
114
|
+
author: 'test',
|
|
115
|
+
parent: null,
|
|
116
|
+
tree,
|
|
117
|
+
};
|
|
118
|
+
const c2: CommitObject = {
|
|
119
|
+
hash: 'c2',
|
|
120
|
+
message: 'second',
|
|
121
|
+
timestamp: 2000,
|
|
122
|
+
author: 'test',
|
|
123
|
+
parent: 'c1',
|
|
124
|
+
tree,
|
|
125
|
+
};
|
|
126
|
+
const c3: CommitObject = {
|
|
127
|
+
hash: 'c3',
|
|
128
|
+
message: 'third',
|
|
129
|
+
timestamp: 3000,
|
|
130
|
+
author: 'test',
|
|
131
|
+
parent: 'c2',
|
|
132
|
+
tree,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
storage.saveCommit(c1);
|
|
136
|
+
storage.saveCommit(c2);
|
|
137
|
+
storage.saveCommit(c3);
|
|
138
|
+
|
|
139
|
+
const history = storage.getHistory('c3');
|
|
140
|
+
expect(history.length).toBe(3);
|
|
141
|
+
expect(history[0].hash).toBe('c3');
|
|
142
|
+
expect(history[1].hash).toBe('c2');
|
|
143
|
+
expect(history[2].hash).toBe('c1');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle empty history', () => {
|
|
147
|
+
const tree: TreeObject = { files: new Map(), hash: 'tree1' };
|
|
148
|
+
const commit: CommitObject = {
|
|
149
|
+
hash: 'root',
|
|
150
|
+
message: 'root commit',
|
|
151
|
+
timestamp: 1000,
|
|
152
|
+
author: 'test',
|
|
153
|
+
parent: null,
|
|
154
|
+
tree,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
storage.saveCommit(commit);
|
|
158
|
+
const history = storage.getHistory('root');
|
|
159
|
+
expect(history.length).toBe(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should hash different content differently', () => {
|
|
163
|
+
const hash1 = storage.hash('content1');
|
|
164
|
+
const hash2 = storage.hash('content2');
|
|
165
|
+
expect(hash1).not.toBe(hash2);
|
|
166
|
+
});
|
|
167
|
+
});
|