@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,264 @@
1
+ /**
2
+ * Message Search Tests
3
+ * Full-text search across commit history
4
+ */
5
+
6
+ import { MessageSearch } from '../../src/message-search';
7
+
8
+ describe('MessageSearch', () => {
9
+ let search: MessageSearch;
10
+
11
+ beforeEach(() => {
12
+ search = new MessageSearch();
13
+ });
14
+
15
+ describe('Indexing', () => {
16
+ test('indexes commit', () => {
17
+ search.indexCommit(
18
+ 'abc123',
19
+ 'feat: add login button',
20
+ 'agent-a',
21
+ Date.now(),
22
+ ['src/auth.ts'],
23
+ 'feature'
24
+ );
25
+
26
+ const stats = search.getStats();
27
+ expect(stats.totalIndexedCommits).toBe(1);
28
+ });
29
+
30
+ test('indexes multiple commits', () => {
31
+ for (let i = 0; i < 100; i++) {
32
+ search.indexCommit(
33
+ `hash${i}`,
34
+ `commit message ${i}`,
35
+ `agent-${i % 5}`,
36
+ Date.now(),
37
+ [`file${i}.ts`],
38
+ 'chore'
39
+ );
40
+ }
41
+
42
+ const stats = search.getStats();
43
+ expect(stats.totalIndexedCommits).toBe(100);
44
+ });
45
+
46
+ test('builds indices', () => {
47
+ search.indexCommit('h1', 'fix: bug', 'agent-a', Date.now(), ['file.ts'], 'fix');
48
+ search.indexCommit('h2', 'feat: feature', 'agent-b', Date.now(), ['file.ts'], 'feature');
49
+ search.indexCommit('h3', 'docs: update', 'agent-a', Date.now(), ['readme.md'], 'docs');
50
+
51
+ const stats = search.getStats();
52
+ expect(stats.uniqueAuthors).toBe(2);
53
+ expect(stats.uniqueFiles).toBe(2);
54
+ expect(stats.uniqueWords).toBeGreaterThan(0);
55
+ });
56
+ });
57
+
58
+ describe('Text Search', () => {
59
+ test('searches by text', () => {
60
+ search.indexCommit('h1', 'add authentication system', 'agent-a', Date.now());
61
+ search.indexCommit('h2', 'update login form', 'agent-b', Date.now());
62
+ search.indexCommit('h3', 'fix auth bug', 'agent-c', Date.now());
63
+
64
+ const results = search.search({ text: 'auth' });
65
+ expect(results.length).toBeGreaterThan(0);
66
+ expect(results.every(r => r.message.includes('auth'))).toBe(true);
67
+ });
68
+
69
+ test('searches by multiple words', () => {
70
+ search.indexCommit('h1', 'add user authentication system', 'agent-a', Date.now());
71
+ search.indexCommit('h2', 'add payment processing', 'agent-b', Date.now());
72
+ search.indexCommit('h3', 'fix authentication bug', 'agent-c', Date.now());
73
+
74
+ const results = search.search({ text: 'authentication system' });
75
+ expect(results.length).toBeGreaterThan(0);
76
+ });
77
+
78
+ test('ignores stopwords', () => {
79
+ search.indexCommit('h1', 'the quick brown fox', 'agent-a', Date.now());
80
+ search.indexCommit('h2', 'implement feature', 'agent-b', Date.now());
81
+
82
+ const results = search.search({ text: 'the' });
83
+ expect(results.length).toBe(0); // 'the' is stopword
84
+ });
85
+
86
+ test('filters by limit', () => {
87
+ for (let i = 0; i < 50; i++) {
88
+ search.indexCommit(`h${i}`, 'feature implementation', 'agent-a', Date.now());
89
+ }
90
+
91
+ const results = search.search({ text: 'feature', limit: 10 });
92
+ expect(results).toHaveLength(10);
93
+ });
94
+ });
95
+
96
+ describe('Author Search', () => {
97
+ test('searches by author', () => {
98
+ search.indexCommit('h1', 'commit 1', 'agent-a', Date.now());
99
+ search.indexCommit('h2', 'commit 2', 'agent-a', Date.now());
100
+ search.indexCommit('h3', 'commit 3', 'agent-b', Date.now());
101
+
102
+ const results = search.getByAuthor('agent-a');
103
+ expect(results).toHaveLength(2);
104
+ expect(results.every(r => r.author === 'agent-a')).toBe(true);
105
+ });
106
+
107
+ test('returns empty for unknown author', () => {
108
+ search.indexCommit('h1', 'commit', 'agent-a', Date.now());
109
+
110
+ const results = search.getByAuthor('unknown');
111
+ expect(results).toHaveLength(0);
112
+ });
113
+
114
+ test('filters search by author', () => {
115
+ search.indexCommit('h1', 'feature: login', 'agent-a', Date.now());
116
+ search.indexCommit('h2', 'feature: logout', 'agent-b', Date.now());
117
+
118
+ const results = search.search({ text: 'feature', author: 'agent-a' });
119
+ expect(results.every(r => r.author === 'agent-a')).toBe(true);
120
+ });
121
+ });
122
+
123
+ describe('Type Search', () => {
124
+ test('searches by type', () => {
125
+ search.indexCommit('h1', 'msg1', 'agent-a', Date.now(), [], 'feature');
126
+ search.indexCommit('h2', 'msg2', 'agent-a', Date.now(), [], 'fix');
127
+ search.indexCommit('h3', 'msg3', 'agent-a', Date.now(), [], 'feature');
128
+
129
+ const results = search.getByType('feature');
130
+ expect(results).toHaveLength(2);
131
+ });
132
+
133
+ test('filters search by type', () => {
134
+ search.indexCommit('h1', 'add auth', 'agent-a', Date.now(), [], 'feature');
135
+ search.indexCommit('h2', 'fix auth bug', 'agent-b', Date.now(), [], 'fix');
136
+ search.indexCommit('h3', 'refactor auth', 'agent-c', Date.now(), [], 'refactor');
137
+
138
+ const results = search.search({ text: 'auth', type: 'feature' });
139
+ expect(results.every(r => r.message.includes('add auth'))).toBe(true);
140
+ });
141
+ });
142
+
143
+ describe('File Search', () => {
144
+ test('searches by file', () => {
145
+ search.indexCommit('h1', 'msg1', 'agent-a', Date.now(), ['src/auth.ts']);
146
+ search.indexCommit('h2', 'msg2', 'agent-b', Date.now(), ['src/auth.ts', 'src/api.ts']);
147
+ search.indexCommit('h3', 'msg3', 'agent-c', Date.now(), ['src/ui.ts']);
148
+
149
+ const results = search.getByFile('src/auth.ts');
150
+ expect(results).toHaveLength(2);
151
+ });
152
+
153
+ test('filters search by files', () => {
154
+ search.indexCommit('h1', 'auth work', 'agent-a', Date.now(), ['src/auth.ts']);
155
+ search.indexCommit('h2', 'api work', 'agent-b', Date.now(), ['src/api.ts']);
156
+
157
+ const results = search.search({ files: ['src/auth.ts'] });
158
+ expect(results.every(r => r.message.includes('auth'))).toBe(true);
159
+ });
160
+ });
161
+
162
+ describe('Date Range Search', () => {
163
+ test('filters by date range', () => {
164
+ const now = Date.now();
165
+ const yesterday = now - 24 * 60 * 60 * 1000;
166
+
167
+ search.indexCommit('h1', 'old', 'agent-a', yesterday);
168
+ search.indexCommit('h2', 'new', 'agent-b', now);
169
+
170
+ const results = search.search({ dateAfter: yesterday + 1000 });
171
+ expect(results.some(r => r.message === 'new')).toBe(true);
172
+ expect(results.some(r => r.message === 'old')).toBe(false);
173
+ });
174
+ });
175
+
176
+ describe('Pattern Search', () => {
177
+ test('searches by regex pattern', () => {
178
+ search.indexCommit('h1', 'feat: add login', 'agent-a', Date.now());
179
+ search.indexCommit('h2', 'fix: bug', 'agent-b', Date.now());
180
+ search.indexCommit('h3', 'feat: add logout', 'agent-c', Date.now());
181
+
182
+ const results = search.searchByPattern(/feat:/);
183
+ expect(results).toHaveLength(2);
184
+ });
185
+ });
186
+
187
+ describe('Relevance Scoring', () => {
188
+ test('scores by text matches', () => {
189
+ search.indexCommit('h1', 'add auth system', 'agent-a', Date.now());
190
+ search.indexCommit('h2', 'fix auth', 'agent-b', Date.now());
191
+ search.indexCommit('h3', 'auth', 'agent-c', Date.now());
192
+
193
+ const results = search.search({ text: 'auth system' });
194
+ expect(results[0].relevanceScore).toBeGreaterThan(results[1].relevanceScore);
195
+ });
196
+
197
+ test('boosts by type match', () => {
198
+ search.indexCommit('h1', 'add feature', 'agent-a', Date.now(), [], 'feature');
199
+ search.indexCommit('h2', 'add feature', 'agent-b', Date.now(), [], 'chore');
200
+
201
+ const results = search.search({ text: 'add feature', type: 'feature' });
202
+ expect(results[0].commitHash).toBe('h1');
203
+ });
204
+
205
+ test('boosts recent commits', () => {
206
+ const now = Date.now();
207
+ search.indexCommit('h1', 'auth', 'agent-a', now - 30 * 24 * 60 * 60 * 1000); // 30 days old
208
+ search.indexCommit('h2', 'auth', 'agent-b', now); // today
209
+
210
+ const results = search.search({ text: 'auth' });
211
+ expect(results[0].commitHash).toBe('h2'); // Recent one ranked higher
212
+ });
213
+ });
214
+
215
+ describe('Combined Queries', () => {
216
+ test('searches with multiple filters', () => {
217
+ search.indexCommit('h1', 'add auth system', 'agent-a', Date.now(), ['src/auth.ts'], 'feature');
218
+ search.indexCommit('h2', 'fix auth bug', 'agent-a', Date.now(), ['src/auth.ts'], 'fix');
219
+ search.indexCommit('h3', 'add ui', 'agent-b', Date.now(), ['src/ui.ts'], 'feature');
220
+
221
+ const results = search.search({
222
+ text: 'auth',
223
+ author: 'agent-a',
224
+ type: 'feature',
225
+ files: ['src/auth.ts'],
226
+ });
227
+
228
+ expect(results).toHaveLength(1);
229
+ expect(results[0].commitHash).toBe('h1');
230
+ });
231
+ });
232
+
233
+ describe('Real-world Scenarios', () => {
234
+ test('finds all commits by feature', () => {
235
+ for (let i = 0; i < 100; i++) {
236
+ const type = i % 3 === 0 ? 'feature' : i % 3 === 1 ? 'fix' : 'chore';
237
+ search.indexCommit(`h${i}`, `commit ${i}`, `agent-${i % 5}`, Date.now(), [], type);
238
+ }
239
+
240
+ const features = search.getByType('feature');
241
+ expect(features.length).toBeGreaterThan(20);
242
+ expect(features.every(r => r.matchedFields.includes('type'))).toBe(true);
243
+ });
244
+
245
+ test('searches production code changes', () => {
246
+ search.indexCommit('h1', 'add database migration', 'agent-a', Date.now(), ['migrations/001_users.sql'], 'feature');
247
+ search.indexCommit('h2', 'fix race condition', 'agent-b', Date.now(), ['src/concurrent.ts'], 'fix');
248
+ search.indexCommit('h3', 'update docs', 'agent-c', Date.now(), ['docs/api.md'], 'docs');
249
+
250
+ const prodChanges = search.search({ files: ['src/concurrent.ts', 'migrations/001_users.sql'] });
251
+ expect(prodChanges.length).toBe(2);
252
+ });
253
+ });
254
+
255
+ describe('Index Management', () => {
256
+ test('clears index', () => {
257
+ search.indexCommit('h1', 'msg', 'agent-a', Date.now());
258
+ expect(search.getStats().totalIndexedCommits).toBe(1);
259
+
260
+ search.clearIndex();
261
+ expect(search.getStats().totalIndexedCommits).toBe(0);
262
+ });
263
+ });
264
+ });
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Stage Area Tests
3
+ * Smart partial commits (agent-native git add)
4
+ */
5
+
6
+ import { StageArea } from '../../src/stage-area';
7
+
8
+ describe('StageArea', () => {
9
+ let stage: StageArea;
10
+
11
+ beforeEach(() => {
12
+ stage = new StageArea();
13
+ });
14
+
15
+ describe('Staging Changes', () => {
16
+ test('stages a single change', () => {
17
+ const change = stage.stageChange('src/file.ts', 'modify', 'old', 'new');
18
+ expect(change.path).toBe('src/file.ts');
19
+ expect(change.type).toBe('modify');
20
+ expect(stage.isStaged('src/file.ts')).toBe(true);
21
+ });
22
+
23
+ test('stages added file', () => {
24
+ const change = stage.stageChange('src/new.ts', 'add', undefined, 'content');
25
+ expect(change.type).toBe('add');
26
+ expect(change.newContent).toBe('content');
27
+ });
28
+
29
+ test('stages deleted file', () => {
30
+ const change = stage.stageChange('src/removed.ts', 'delete', 'content');
31
+ expect(change.type).toBe('delete');
32
+ expect(change.oldContent).toBe('content');
33
+ });
34
+
35
+ test('stages multiple changes', () => {
36
+ const changes = [
37
+ { path: 'a.ts', type: 'add' as const, newContent: 'a' },
38
+ { path: 'b.ts', type: 'modify' as const, oldContent: 'b1', newContent: 'b2' },
39
+ { path: 'c.ts', type: 'delete' as const, oldContent: 'c' },
40
+ ];
41
+
42
+ stage.stageMultiple(changes.map(c => ({
43
+ ...c,
44
+ timestamp: Date.now(),
45
+ })));
46
+
47
+ expect(stage.getStagedChanges()).toHaveLength(3);
48
+ });
49
+
50
+ test('overwrites previous stage', () => {
51
+ stage.stageChange('file.ts', 'modify', 'v1', 'v2');
52
+ stage.stageChange('file.ts', 'modify', 'v2', 'v3');
53
+
54
+ const staged = stage.getStagedChanges();
55
+ expect(staged).toHaveLength(1);
56
+ expect(staged[0].newContent).toBe('v3');
57
+ });
58
+ });
59
+
60
+ describe('Unstaging', () => {
61
+ test('unstages file', () => {
62
+ stage.stageChange('file.ts', 'modify', 'old', 'new');
63
+ expect(stage.isStaged('file.ts')).toBe(true);
64
+
65
+ stage.unstageFile('file.ts');
66
+ expect(stage.isStaged('file.ts')).toBe(false);
67
+ });
68
+
69
+ test('unstages all', () => {
70
+ stage.stageChange('a.ts', 'add', undefined, 'a');
71
+ stage.stageChange('b.ts', 'add', undefined, 'b');
72
+ stage.stageChange('c.ts', 'add', undefined, 'c');
73
+
74
+ stage.unstageAll();
75
+ expect(stage.getStagedChanges()).toHaveLength(0);
76
+ });
77
+
78
+ test('returns false for unknown file', () => {
79
+ const result = stage.unstageFile('unknown.ts');
80
+ expect(result).toBe(false);
81
+ });
82
+ });
83
+
84
+ describe('Status Tracking', () => {
85
+ test('reports staging status', () => {
86
+ stage.updateWorkingDirectory(new Map([
87
+ ['staged.ts', 'content'],
88
+ ['modified.ts', 'new content'],
89
+ ['new.ts', 'new file'],
90
+ ]));
91
+
92
+ stage.recordCommit(new Map([
93
+ ['old.ts', 'old'],
94
+ ['modified.ts', 'old content'],
95
+ ]));
96
+
97
+ stage.stageChange('staged.ts', 'modify', 'old', 'new');
98
+
99
+ const status = stage.getStatus();
100
+ expect(status.staged).toBe(1);
101
+ expect(status.unstaged).toBe(1); // modified.ts
102
+ expect(status.untracked).toBe(1); // new.ts
103
+ });
104
+ });
105
+
106
+ describe('Auto-Staging', () => {
107
+ test('stages all changes', () => {
108
+ const workingDir = new Map([
109
+ ['a.ts', 'added'],
110
+ ['b.ts', 'modified'],
111
+ ]);
112
+
113
+ stage.recordCommit(new Map([['b.ts', 'old content']]));
114
+
115
+ const changes = stage.stageAll(workingDir);
116
+ expect(changes).toHaveLength(2);
117
+ expect(changes.some(c => c.type === 'add')).toBe(true);
118
+ expect(changes.some(c => c.type === 'modify')).toBe(true);
119
+ });
120
+
121
+ test('detects deleted files', () => {
122
+ const workingDir = new Map();
123
+ stage.recordCommit(new Map([['deleted.ts', 'content']]));
124
+
125
+ const changes = stage.stageAll(workingDir);
126
+ expect(changes).toHaveLength(1);
127
+ expect(changes[0].type).toBe('delete');
128
+ });
129
+ });
130
+
131
+ describe('Commit Creation', () => {
132
+ test('creates commit from staged', () => {
133
+ stage.stageChange('file.ts', 'modify', 'old', 'new');
134
+
135
+ const commit = stage.createCommit('fix: bug', 'fix');
136
+ expect(commit.message).toBe('fix: bug');
137
+ expect(commit.type).toBe('fix');
138
+ expect(commit.stagedChanges).toHaveLength(1);
139
+ });
140
+
141
+ test('throws without staged changes', () => {
142
+ expect(() => {
143
+ stage.createCommit('empty commit');
144
+ }).toThrow('No staged changes');
145
+ });
146
+
147
+ test('sets commit timestamp', () => {
148
+ stage.stageChange('file.ts', 'add', undefined, 'content');
149
+ const commit = stage.createCommit('feat: new');
150
+
151
+ expect(commit.timestamp).toBeGreaterThan(0);
152
+ expect(commit.timestamp).toBeLessThanOrEqual(Date.now());
153
+ });
154
+ });
155
+
156
+ describe('Stage by Type', () => {
157
+ test('stages by semantic type (feature)', () => {
158
+ stage.stageChange('src/feature.ts', 'add', undefined, 'code');
159
+ stage.stageChange('src/utils.ts', 'modify', 'old', 'new');
160
+ stage.stageChange('README.md', 'modify', 'old', 'new');
161
+
162
+ const featured = stage.stageByType('feature');
163
+ expect(featured.some(c => c.path.includes('src/'))).toBe(true);
164
+ expect(featured.some(c => c.path === 'README.md')).toBe(false);
165
+ });
166
+
167
+ test('stages by semantic type (docs)', () => {
168
+ stage.stageChange('README.md', 'modify', 'old', 'new');
169
+ stage.stageChange('docs/api.md', 'add', undefined, 'docs');
170
+ stage.stageChange('src/code.ts', 'add', undefined, 'code');
171
+
172
+ const docs = stage.stageByType('docs');
173
+ expect(docs.every(c => c.path.endsWith('.md'))).toBe(true);
174
+ });
175
+
176
+ test('stages by semantic type (test)', () => {
177
+ stage.stageChange('src/feature.test.ts', 'add', undefined, 'test');
178
+ stage.stageChange('src/feature.ts', 'add', undefined, 'code');
179
+
180
+ const tests = stage.stageByType('test');
181
+ expect(tests.every(c => c.path.includes('.test.'))).toBe(true);
182
+ });
183
+ });
184
+
185
+ describe('Diff', () => {
186
+ test('gets diff of staged changes', () => {
187
+ stage.stageChange('a.ts', 'add', undefined, 'new');
188
+ stage.stageChange('b.ts', 'modify', 'old', 'new');
189
+ stage.stageChange('c.ts', 'delete', 'old');
190
+
191
+ const diff = stage.getDiff();
192
+ expect(diff.get('a.ts')).toEqual({ old: undefined, new: 'new' });
193
+ expect(diff.get('b.ts')).toEqual({ old: 'old', new: 'new' });
194
+ expect(diff.get('c.ts')).toEqual({ old: 'old', new: undefined });
195
+ });
196
+ });
197
+
198
+ describe('Statistics', () => {
199
+ test('calculates staging stats', () => {
200
+ stage.stageChange('a.ts', 'add', undefined, 'content1');
201
+ stage.stageChange('b.ts', 'modify', 'old', 'content2');
202
+ stage.stageChange('c.ts', 'delete', 'content3');
203
+
204
+ const stats = stage.getStats();
205
+ expect(stats.stagedFiles).toBe(3);
206
+ expect(stats.addedFiles).toBe(1);
207
+ expect(stats.modifiedFiles).toBe(1);
208
+ expect(stats.deletedFiles).toBe(1);
209
+ expect(stats.estimatedSize).toBe('content1content2'.length);
210
+ });
211
+ });
212
+
213
+ describe('Type Detection', () => {
214
+ test('detects feature from source files', () => {
215
+ stage.stageChange('src/component.ts', 'add', undefined, 'code');
216
+ const type = stage.detectCommitType();
217
+ expect(type).toBe('feature');
218
+ });
219
+
220
+ test('detects test from test files', () => {
221
+ stage.stageChange('src/component.test.ts', 'add', undefined, 'test');
222
+ stage.stageChange('src/component.ts', 'add', undefined, 'code');
223
+
224
+ const type = stage.detectCommitType();
225
+ expect(type).toBe('test');
226
+ });
227
+
228
+ test('detects docs from markdown', () => {
229
+ stage.stageChange('README.md', 'modify', 'old', 'new');
230
+ const type = stage.detectCommitType();
231
+ expect(type).toBe('docs');
232
+ });
233
+
234
+ test('defaults to chore', () => {
235
+ stage.stageChange('random.txt', 'add', undefined, 'content');
236
+ const type = stage.detectCommitType();
237
+ expect(type).toBe('chore');
238
+ });
239
+ });
240
+
241
+ describe('Validation', () => {
242
+ test('validates valid commit', () => {
243
+ stage.stageChange('file.ts', 'modify', 'old', 'new');
244
+ const result = stage.validateCommit();
245
+
246
+ expect(result.valid).toBe(true);
247
+ expect(result.errors).toHaveLength(0);
248
+ });
249
+
250
+ test('rejects empty stage', () => {
251
+ const result = stage.validateCommit();
252
+ expect(result.valid).toBe(false);
253
+ expect(result.errors).toContain('No staged changes');
254
+ });
255
+
256
+ test('detects conflicting changes', () => {
257
+ stage.stageChange('file.ts', 'add', undefined, 'content');
258
+ stage.stageChange('file.ts', 'delete', 'content');
259
+
260
+ const result = stage.validateCommit();
261
+ expect(result.valid).toBe(false);
262
+ expect(result.errors.some(e => e.includes('multiple staged changes'))).toBe(true);
263
+ });
264
+ });
265
+
266
+ describe('Discard', () => {
267
+ test('discards staged changes', () => {
268
+ stage.stageChange('a.ts', 'add', undefined, 'a');
269
+ stage.stageChange('b.ts', 'add', undefined, 'b');
270
+
271
+ stage.discardStaged();
272
+ expect(stage.getStagedChanges()).toHaveLength(0);
273
+ });
274
+ });
275
+
276
+ describe('Commit Recording', () => {
277
+ test('records commit and clears stage', () => {
278
+ stage.stageChange('file.ts', 'modify', 'old', 'new');
279
+ expect(stage.getStagedChanges()).toHaveLength(1);
280
+
281
+ stage.recordCommit(new Map([['file.ts', 'new']]));
282
+ expect(stage.getStagedChanges()).toHaveLength(0);
283
+ });
284
+ });
285
+
286
+ describe('Real-world Workflow', () => {
287
+ test('complete commit workflow', () => {
288
+ // Setup working directory
289
+ const working = new Map([
290
+ ['src/feature.ts', 'export function feature() {}'],
291
+ ['src/utils.ts', 'export function helper() {}'],
292
+ ['tests/feature.test.ts', 'describe("feature", () => {})'],
293
+ ]);
294
+
295
+ stage.updateWorkingDirectory(working);
296
+
297
+ // Initial commit
298
+ stage.recordCommit(new Map([['src/utils.ts', 'old helper']]));
299
+
300
+ // Agent stages only test changes
301
+ const testChanges = working;
302
+ const tests = Array.from(testChanges.entries())
303
+ .filter(([path]) => path.includes('.test.'))
304
+ .map(([path, content]) => ({
305
+ path,
306
+ type: 'add' as const,
307
+ newContent: content,
308
+ timestamp: Date.now(),
309
+ }));
310
+
311
+ stage.stageMultiple(tests);
312
+
313
+ // Create test commit
314
+ const commit = stage.createCommit('test: add feature tests', 'test');
315
+ expect(commit.stagedChanges).toHaveLength(1);
316
+ expect(commit.stagedChanges[0].path).toContain('.test.ts');
317
+ });
318
+ });
319
+
320
+ describe('Cleanup', () => {
321
+ test('clears all state', () => {
322
+ stage.stageChange('file.ts', 'add', undefined, 'content');
323
+ stage.updateWorkingDirectory(new Map([['file.ts', 'content']]));
324
+ stage.recordCommit(new Map());
325
+
326
+ stage.clear();
327
+ expect(stage.getStagedChanges()).toHaveLength(0);
328
+ });
329
+ });
330
+ });