@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.
Files changed (130) hide show
  1. package/.gitignore +115 -0
  2. package/.trace/progress.json +22 -0
  3. package/README.md +466 -0
  4. package/RELEASE-NOTES-1.5.0.md +410 -0
  5. package/STATUS.md +245 -0
  6. package/dist/auto-commit.d.ts +66 -0
  7. package/dist/auto-commit.d.ts.map +1 -0
  8. package/dist/auto-commit.js +180 -0
  9. package/dist/auto-commit.js.map +1 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +246 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands.d.ts +46 -0
  15. package/dist/commands.d.ts.map +1 -0
  16. package/dist/commands.js +256 -0
  17. package/dist/commands.js.map +1 -0
  18. package/dist/diff.d.ts +23 -0
  19. package/dist/diff.d.ts.map +1 -0
  20. package/dist/diff.js +106 -0
  21. package/dist/diff.js.map +1 -0
  22. package/dist/github.d.ts.map +1 -0
  23. package/dist/github.js.map +1 -0
  24. package/dist/index-cache.d.ts +35 -0
  25. package/dist/index-cache.d.ts.map +1 -0
  26. package/dist/index-cache.js +114 -0
  27. package/dist/index-cache.js.map +1 -0
  28. package/dist/index.d.ts +15 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +25 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/storage.d.ts +45 -0
  33. package/dist/storage.d.ts.map +1 -0
  34. package/dist/storage.js +151 -0
  35. package/dist/storage.js.map +1 -0
  36. package/dist/sync.d.ts +60 -0
  37. package/dist/sync.js +184 -0
  38. package/dist/tags.d.ts +85 -0
  39. package/dist/tags.d.ts.map +1 -0
  40. package/dist/tags.js +219 -0
  41. package/dist/tags.js.map +1 -0
  42. package/dist/types.d.ts +102 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +6 -0
  45. package/dist/types.js.map +1 -0
  46. package/docs/.nojekyll +0 -0
  47. package/docs/README.md +73 -0
  48. package/docs/_config.yml +2 -0
  49. package/docs/index.html +960 -0
  50. package/docs-website/package.json +20 -0
  51. package/jest.config.js +21 -0
  52. package/package.json +50 -0
  53. package/scripts/init.ts +290 -0
  54. package/src/agent-audit.ts +270 -0
  55. package/src/agent-checkout.ts +227 -0
  56. package/src/agent-coordination.ts +318 -0
  57. package/src/async-queue.ts +203 -0
  58. package/src/auto-branching.ts +279 -0
  59. package/src/auto-commit.ts +166 -0
  60. package/src/cherry-pick.ts +252 -0
  61. package/src/chunked-upload.ts +224 -0
  62. package/src/cli-v2.ts +335 -0
  63. package/src/cli.ts +318 -0
  64. package/src/cliff-detection.ts +232 -0
  65. package/src/commands.ts +267 -0
  66. package/src/commit-hash-system.ts +351 -0
  67. package/src/compression.ts +176 -0
  68. package/src/conflict-resolution-ui.ts +277 -0
  69. package/src/conflict-visualization.ts +238 -0
  70. package/src/diff-formatter.ts +184 -0
  71. package/src/diff.ts +124 -0
  72. package/src/distributed-coordination.ts +273 -0
  73. package/src/git-interop.ts +316 -0
  74. package/src/index-cache.ts +88 -0
  75. package/src/index.ts +38 -0
  76. package/src/merge-engine.ts +143 -0
  77. package/src/message-search.ts +370 -0
  78. package/src/performance-monitoring.ts +236 -0
  79. package/src/rebase.ts +327 -0
  80. package/src/rollback.ts +215 -0
  81. package/src/semantic-grouping.ts +245 -0
  82. package/src/stage-area.ts +324 -0
  83. package/src/stash.ts +278 -0
  84. package/src/storage.ts +131 -0
  85. package/src/sync.ts +205 -0
  86. package/src/tags.ts +244 -0
  87. package/src/types.ts +119 -0
  88. package/src/webhooks.ts +119 -0
  89. package/src/workspace-isolation.ts +298 -0
  90. package/tests/auto-commit.test.ts +308 -0
  91. package/tests/checkout.test.ts +136 -0
  92. package/tests/commit.test.ts +118 -0
  93. package/tests/diff.test.ts +191 -0
  94. package/tests/github.test.ts +94 -0
  95. package/tests/integration.test.ts +267 -0
  96. package/tests/log.test.ts +125 -0
  97. package/tests/phase2-integration.test.ts +370 -0
  98. package/tests/storage.test.ts +167 -0
  99. package/tests/tags.test.ts +477 -0
  100. package/tests/types.test.ts +75 -0
  101. package/tests/v1.1/agent-audit.test.ts +472 -0
  102. package/tests/v1.1/agent-coordination.test.ts +308 -0
  103. package/tests/v1.1/async-queue.test.ts +253 -0
  104. package/tests/v1.1/comprehensive.test.ts +521 -0
  105. package/tests/v1.1/diff-formatter.test.ts +238 -0
  106. package/tests/v1.1/integration.test.ts +389 -0
  107. package/tests/v1.1/onboarding.test.ts +365 -0
  108. package/tests/v1.1/rollback.test.ts +370 -0
  109. package/tests/v1.1/semantic-grouping.test.ts +230 -0
  110. package/tests/v1.2/chunked-upload.test.ts +301 -0
  111. package/tests/v1.2/cliff-detection.test.ts +272 -0
  112. package/tests/v1.2/commit-hash-system.test.ts +288 -0
  113. package/tests/v1.2/compression.test.ts +220 -0
  114. package/tests/v1.2/conflict-visualization.test.ts +263 -0
  115. package/tests/v1.2/distributed.test.ts +261 -0
  116. package/tests/v1.2/performance-monitoring.test.ts +328 -0
  117. package/tests/v1.3/auto-branching.test.ts +270 -0
  118. package/tests/v1.3/message-search.test.ts +264 -0
  119. package/tests/v1.3/stage-area.test.ts +330 -0
  120. package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
  121. package/tests/v1.4/cli.test.ts +171 -0
  122. package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
  123. package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
  124. package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
  125. package/tests/v1.4/workspace-isolation.test.ts +268 -0
  126. package/tests/v1.5/agent-coordination.real.test.ts +401 -0
  127. package/tests/v1.5/cli-v2.test.ts +354 -0
  128. package/tests/v1.5/git-interop.real.test.ts +358 -0
  129. package/tests/v1.5/integration-testing.real.test.ts +440 -0
  130. 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
+ });