@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,136 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { TraceCommands } from '../src/commands';
5
+
6
+ describe('Checkout Tests', () => {
7
+ let tmpDir: string;
8
+ let commands: TraceCommands;
9
+ let memoryDir: string;
10
+
11
+ beforeEach(() => {
12
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-git-'));
13
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'home-'));
14
+ process.env.HOME = homeDir;
15
+ memoryDir = path.join(homeDir, '.openclaw/memory');
16
+ fs.mkdirSync(memoryDir, { recursive: true });
17
+ commands = new TraceCommands(tmpDir);
18
+ });
19
+
20
+ afterEach(() => {
21
+ fs.rmSync(tmpDir, { recursive: true });
22
+ fs.rmSync(process.env.HOME || '', { recursive: true });
23
+ });
24
+
25
+ it('should restore to previous commit', () => {
26
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v1');
27
+ const h1 = commands.commit('First');
28
+
29
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v2');
30
+ commands.commit('Second');
31
+
32
+ commands.checkout(h1);
33
+ const current = commands.getCurrentCommit();
34
+ expect(current?.hash).toBe(h1);
35
+ });
36
+
37
+ it('should update HEAD reference', () => {
38
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v1');
39
+ const h1 = commands.commit('First');
40
+
41
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v2');
42
+ const h2 = commands.commit('Second');
43
+
44
+ commands.checkout(h1);
45
+ const current = commands.getCurrentCommit();
46
+ expect(current?.message).toBe('First');
47
+ });
48
+
49
+ it('should throw on invalid hash', () => {
50
+ expect(() => commands.checkout('nonexistent')).toThrow();
51
+ });
52
+
53
+ it('should handle checkout to root commit', () => {
54
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v1');
55
+ const h1 = commands.commit('Root');
56
+
57
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v2');
58
+ commands.commit('Second');
59
+
60
+ commands.checkout(h1);
61
+ const current = commands.getCurrentCommit();
62
+ expect(current?.parent).toBeNull();
63
+ });
64
+
65
+ it('should restore multiple files', () => {
66
+ fs.writeFileSync(path.join(memoryDir, 'file1.txt'), 'f1');
67
+ fs.writeFileSync(path.join(memoryDir, 'file2.txt'), 'f2');
68
+ const h1 = commands.commit('Multi');
69
+
70
+ fs.unlinkSync(path.join(memoryDir, 'file1.txt'));
71
+ fs.unlinkSync(path.join(memoryDir, 'file2.txt'));
72
+ commands.commit('Deleted');
73
+
74
+ commands.checkout(h1);
75
+ const current = commands.getCurrentCommit();
76
+ expect(current?.message).toBe('Multi');
77
+ });
78
+
79
+ it('should complete checkout quickly', () => {
80
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v1');
81
+ const h1 = commands.commit('First');
82
+
83
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v2');
84
+ commands.commit('Second');
85
+
86
+ const start = Date.now();
87
+ commands.checkout(h1);
88
+ const elapsed = Date.now() - start;
89
+
90
+ expect(elapsed).toBeLessThan(150);
91
+ });
92
+
93
+ it('should invalidate cache on checkout', () => {
94
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v1');
95
+ const h1 = commands.commit('First');
96
+
97
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v2');
98
+ const h2 = commands.commit('Second');
99
+
100
+ commands.checkout(h1);
101
+ commands.checkout(h2);
102
+
103
+ const current = commands.getCurrentCommit();
104
+ expect(current?.hash).toBe(h2);
105
+ });
106
+
107
+ it('should handle checkout chain', () => {
108
+ const hashes = [];
109
+ for (let i = 0; i < 5; i++) {
110
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), `v${i}`);
111
+ hashes.push(commands.commit(`C${i}`));
112
+ }
113
+
114
+ // Checkout backwards
115
+ for (let i = hashes.length - 2; i >= 0; i--) {
116
+ commands.checkout(hashes[i]);
117
+ const current = commands.getCurrentCommit();
118
+ expect(current?.hash).toBe(hashes[i]);
119
+ }
120
+ });
121
+
122
+ it('should restore from nested history', () => {
123
+ // Create deep history
124
+ for (let i = 0; i < 10; i++) {
125
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), `v${i}`);
126
+ commands.commit(`C${i}`);
127
+ }
128
+
129
+ const log = commands.log();
130
+ const targetHash = log[5].hash;
131
+
132
+ commands.checkout(targetHash);
133
+ const current = commands.getCurrentCommit();
134
+ expect(current?.hash).toBe(targetHash);
135
+ });
136
+ });
@@ -0,0 +1,118 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { TraceCommands } from '../src/commands';
5
+
6
+ describe('Commit Tests', () => {
7
+ let tmpDir: string;
8
+ let commands: TraceCommands;
9
+ let memoryDir: string;
10
+
11
+ beforeEach(() => {
12
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-git-'));
13
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'home-'));
14
+ process.env.HOME = homeDir;
15
+ memoryDir = path.join(homeDir, '.openclaw/memory');
16
+ fs.mkdirSync(memoryDir, { recursive: true });
17
+ commands = new TraceCommands(tmpDir);
18
+ });
19
+
20
+ afterEach(() => {
21
+ fs.rmSync(tmpDir, { recursive: true });
22
+ fs.rmSync(process.env.HOME || '', { recursive: true });
23
+ });
24
+
25
+ it('should create valid commit hash', () => {
26
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'content');
27
+ const hash = commands.commit('Test');
28
+ expect(hash).toMatch(/^[a-f0-9]{64}$/);
29
+ });
30
+
31
+ it('should set correct author', () => {
32
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'content');
33
+ const hash = commands.commit('Test', 'custom-author');
34
+ const log = commands.log(1);
35
+ expect(log[0].author).toBe('custom-author');
36
+ });
37
+
38
+ it('should default to agent author', () => {
39
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'content');
40
+ const hash = commands.commit('Test');
41
+ const log = commands.log(1);
42
+ expect(log[0].author).toBe('agent');
43
+ });
44
+
45
+ it('should store message', () => {
46
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'content');
47
+ commands.commit('My message');
48
+ const log = commands.log(1);
49
+ expect(log[0].message).toBe('My message');
50
+ });
51
+
52
+ it('should track commit timestamp', () => {
53
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'content');
54
+ const before = Date.now();
55
+ commands.commit('Test');
56
+ const after = Date.now();
57
+ const log = commands.log(1);
58
+ expect(log[0].timestamp).toBeGreaterThanOrEqual(before);
59
+ expect(log[0].timestamp).toBeLessThanOrEqual(after);
60
+ });
61
+
62
+ it('should link parent commit', () => {
63
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v1');
64
+ const h1 = commands.commit('First');
65
+
66
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v2');
67
+ const h2 = commands.commit('Second');
68
+
69
+ const log = commands.log();
70
+ expect(log[0].hash).toBe(h2);
71
+ expect(log[1].hash).toBe(h1);
72
+ });
73
+
74
+ it('should store metadata', () => {
75
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'content');
76
+ commands.commit('Test', 'agent', { version: '1.0', tag: 'release' });
77
+ const current = commands.getCurrentCommit();
78
+ expect(current?.metadata).toEqual({ version: '1.0', tag: 'release' });
79
+ });
80
+
81
+ it('should commit empty state', () => {
82
+ const hash = commands.commit('Empty state');
83
+ expect(hash).toMatch(/^[a-f0-9]{64}$/);
84
+ });
85
+
86
+ it('should compute consistent hash', () => {
87
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'content');
88
+ // Note: Same content may have different hash due to timestamp
89
+ const h1 = commands.commit('Test');
90
+ expect(h1.length).toBe(64);
91
+ });
92
+
93
+ it('should handle unicode in commit message', () => {
94
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'content');
95
+ commands.commit('Test: привет мир 🌍');
96
+ const log = commands.log(1);
97
+ expect(log[0].message).toBe('Test: привет мир 🌍');
98
+ });
99
+
100
+ it('should reset to new HEAD on commit', () => {
101
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v1');
102
+ const h1 = commands.commit('First');
103
+
104
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'v2');
105
+ const h2 = commands.commit('Second');
106
+
107
+ const current = commands.getCurrentCommit();
108
+ expect(current?.hash).toBe(h2);
109
+ });
110
+
111
+ it('should handle large messages', () => {
112
+ fs.writeFileSync(path.join(memoryDir, 'file.txt'), 'content');
113
+ const largeMsg = 'x'.repeat(1000);
114
+ commands.commit(largeMsg);
115
+ const log = commands.log(1);
116
+ expect(log[0].message.length).toBe(1000);
117
+ });
118
+ });
@@ -0,0 +1,191 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { Differ } from '../src/diff';
5
+ import { Storage } from '../src/storage';
6
+ import { TreeObject, FileEntry } from '../src/types';
7
+
8
+ describe('Differ', () => {
9
+ let tmpDir: string;
10
+ let storage: Storage;
11
+ let differ: Differ;
12
+
13
+ beforeEach(() => {
14
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-git-'));
15
+ storage = new Storage(tmpDir);
16
+ differ = new Differ(storage);
17
+ });
18
+
19
+ afterEach(() => {
20
+ fs.rmSync(tmpDir, { recursive: true });
21
+ });
22
+
23
+ it('should detect added files', () => {
24
+ const oldTree: TreeObject = {
25
+ files: new Map(),
26
+ hash: 'tree1',
27
+ };
28
+
29
+ const newEntry: FileEntry = {
30
+ path: 'new.txt',
31
+ hash: 'hash1',
32
+ size: 100,
33
+ mode: 'file',
34
+ lastModified: Date.now(),
35
+ };
36
+
37
+ const newTree: TreeObject = {
38
+ files: new Map([['new.txt', newEntry]]),
39
+ hash: 'tree2',
40
+ };
41
+
42
+ const diff = differ.diff(oldTree, newTree);
43
+ expect(diff.added.has('new.txt')).toBe(true);
44
+ expect(diff.modified.size).toBe(0);
45
+ expect(diff.deleted.size).toBe(0);
46
+ expect(diff.stats.filesChanged).toBe(1);
47
+ });
48
+
49
+ it('should detect deleted files', () => {
50
+ const oldEntry: FileEntry = {
51
+ path: 'deleted.txt',
52
+ hash: 'hash1',
53
+ size: 100,
54
+ mode: 'file',
55
+ lastModified: 1000,
56
+ };
57
+
58
+ const oldTree: TreeObject = {
59
+ files: new Map([['deleted.txt', oldEntry]]),
60
+ hash: 'tree1',
61
+ };
62
+
63
+ const newTree: TreeObject = {
64
+ files: new Map(),
65
+ hash: 'tree2',
66
+ };
67
+
68
+ const diff = differ.diff(oldTree, newTree);
69
+ expect(diff.deleted.has('deleted.txt')).toBe(true);
70
+ expect(diff.added.size).toBe(0);
71
+ expect(diff.modified.size).toBe(0);
72
+ });
73
+
74
+ it('should detect modified files', () => {
75
+ const oldEntry: FileEntry = {
76
+ path: 'file.txt',
77
+ hash: 'hash1',
78
+ size: 100,
79
+ mode: 'file',
80
+ lastModified: 1000,
81
+ };
82
+
83
+ const newEntry: FileEntry = {
84
+ path: 'file.txt',
85
+ hash: 'hash2',
86
+ size: 150,
87
+ mode: 'file',
88
+ lastModified: 2000,
89
+ };
90
+
91
+ const oldTree: TreeObject = {
92
+ files: new Map([['file.txt', oldEntry]]),
93
+ hash: 'tree1',
94
+ };
95
+
96
+ const newTree: TreeObject = {
97
+ files: new Map([['file.txt', newEntry]]),
98
+ hash: 'tree2',
99
+ };
100
+
101
+ const diff = differ.diff(oldTree, newTree);
102
+ expect(diff.modified.has('file.txt')).toBe(true);
103
+ expect(diff.added.size).toBe(0);
104
+ expect(diff.deleted.size).toBe(0);
105
+ });
106
+
107
+ it('should handle complex diffs', () => {
108
+ const oldFile: FileEntry = {
109
+ path: 'keep.txt',
110
+ hash: 'hash1',
111
+ size: 100,
112
+ mode: 'file',
113
+ lastModified: 1000,
114
+ };
115
+
116
+ const modFile: FileEntry = {
117
+ path: 'modify.txt',
118
+ hash: 'hash2',
119
+ size: 150,
120
+ mode: 'file',
121
+ lastModified: 1000,
122
+ };
123
+
124
+ const oldTree: TreeObject = {
125
+ files: new Map([
126
+ ['keep.txt', oldFile],
127
+ ['delete.txt', { path: 'delete.txt', hash: 'hash3', size: 50, mode: 'file', lastModified: 1000 }],
128
+ ['modify.txt', { path: 'modify.txt', hash: 'hashOld', size: 50, mode: 'file', lastModified: 1000 }],
129
+ ]),
130
+ hash: 'tree1',
131
+ };
132
+
133
+ const newTree: TreeObject = {
134
+ files: new Map([
135
+ ['keep.txt', oldFile],
136
+ ['modify.txt', modFile],
137
+ ['new.txt', { path: 'new.txt', hash: 'hash4', size: 75, mode: 'file', lastModified: 2000 }],
138
+ ]),
139
+ hash: 'tree2',
140
+ };
141
+
142
+ const diff = differ.diff(oldTree, newTree);
143
+ expect(diff.added.size).toBe(1);
144
+ expect(diff.modified.size).toBe(1);
145
+ expect(diff.deleted.size).toBe(1);
146
+ expect(diff.stats.filesChanged).toBe(3);
147
+ });
148
+
149
+ it('should format diff output', () => {
150
+ const oldTree: TreeObject = {
151
+ files: new Map([['old.txt', { path: 'old.txt', hash: 'h1', size: 100, mode: 'file', lastModified: 1000 }]]),
152
+ hash: 'tree1',
153
+ };
154
+
155
+ const newTree: TreeObject = {
156
+ files: new Map([['new.txt', { path: 'new.txt', hash: 'h2', size: 100, mode: 'file', lastModified: 2000 }]]),
157
+ hash: 'tree2',
158
+ };
159
+
160
+ const diff = differ.diff(oldTree, newTree);
161
+ const formatted = differ.format(diff);
162
+ expect(formatted).toContain('deleted');
163
+ expect(formatted).toContain('new file');
164
+ expect(formatted).toContain('2 files changed');
165
+ });
166
+
167
+ it('should compute stats', () => {
168
+ const oldTree: TreeObject = {
169
+ files: new Map(),
170
+ hash: 'tree1',
171
+ };
172
+
173
+ const newEntry: FileEntry = {
174
+ path: 'file.txt',
175
+ hash: 'h1',
176
+ size: 100,
177
+ mode: 'file',
178
+ lastModified: Date.now(),
179
+ };
180
+
181
+ const newTree: TreeObject = {
182
+ files: new Map([['file.txt', newEntry]]),
183
+ hash: 'tree2',
184
+ };
185
+
186
+ const diff = differ.diff(oldTree, newTree);
187
+ const stats = differ.getStats(diff);
188
+ expect(stats.filesChanged).toBe(1);
189
+ expect(stats.totalChanges).toBe(1);
190
+ });
191
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * GitHub Integration Tests
3
+ */
4
+
5
+ import { TraceSync } from '../src/github';
6
+ import { mkdtempSync, rmSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { execSync } from 'child_process';
9
+
10
+ describe('TraceSync', () => {
11
+ let testDir: string;
12
+ let github: TraceSync;
13
+
14
+ beforeEach(() => {
15
+ testDir = mkdtempSync(join('/tmp', 'trace-test-'));
16
+ github = new TraceSync(testDir);
17
+ github.initRepo();
18
+ });
19
+
20
+ afterEach(() => {
21
+ rmSync(testDir, { recursive: true });
22
+ });
23
+
24
+ test('should initialize a git repository', () => {
25
+ expect(() => {
26
+ execSync('git rev-parse --git-dir', { cwd: testDir, stdio: 'ignore' });
27
+ }).not.toThrow();
28
+ });
29
+
30
+ test('should add a remote', () => {
31
+ github.addRemote('origin', 'https://github.com/test/repo.git');
32
+ const remotes = github.listRemotes();
33
+ expect(remotes.length).toBeGreaterThan(0);
34
+ expect(remotes.some(r => r.name === 'origin')).toBe(true);
35
+ });
36
+
37
+ test('should list remotes', () => {
38
+ github.addRemote('origin', 'https://github.com/test/repo.git');
39
+ github.addRemote('backup', 'https://github.com/test/backup.git');
40
+
41
+ const remotes = github.listRemotes();
42
+ expect(remotes.length).toBeGreaterThanOrEqual(2);
43
+ });
44
+
45
+ test('should stage and commit changes', () => {
46
+ // Create a test file
47
+ execSync(`echo "test" > ${join(testDir, 'test.txt')}`);
48
+
49
+ const result = github.stageAndCommit('test commit');
50
+ expect(result.success).toBe(true);
51
+ expect(result.hash.length).toBeGreaterThan(0);
52
+ });
53
+
54
+ test('should handle nothing to commit gracefully', () => {
55
+ // Make an initial commit first
56
+ execSync(`echo "test" > ${join(testDir, 'test.txt')}`);
57
+ const init = github.stageAndCommit('initial');
58
+ expect(init.success).toBe(true);
59
+
60
+ // Now try to commit with no changes
61
+ const result = github.stageAndCommit('empty commit');
62
+ // Git returns non-zero exit code for "nothing to commit"
63
+ // So success should be false, or we can accept either behavior
64
+ // Let's just test that it doesn't crash
65
+ expect(result).toHaveProperty('success');
66
+ expect(result).toHaveProperty('hash');
67
+ });
68
+
69
+ test('should commit with auto-push flag', () => {
70
+ execSync(`echo "test" > ${join(testDir, 'test.txt')}`);
71
+
72
+ const result = github.commitWithAutoPush('test message', false);
73
+ expect(result.success).toBe(true);
74
+ expect(result.hash).toBeDefined();
75
+ });
76
+
77
+ test('should handle push timeout gracefully', () => {
78
+ github.addRemote('origin', 'https://github.com/nonexistent/repo.git');
79
+ execSync(`echo "test" > ${join(testDir, 'test.txt')}`);
80
+ github.stageAndCommit('test');
81
+
82
+ // This will timeout or fail, which is OK - we just check error handling
83
+ const result = github.push('origin', 1000);
84
+ // Should either fail or timeout - both are acceptable
85
+ expect(result).toHaveProperty('success');
86
+ expect(result).toHaveProperty('message');
87
+ });
88
+
89
+ test('should handle missing remote gracefully', () => {
90
+ const result = github.pull('nonexistent');
91
+ expect(result.success).toBe(false);
92
+ expect(result.message).toContain('failed');
93
+ });
94
+ });