@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,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict Visualization Tests
|
|
3
|
+
* Multiple output formats for merge conflicts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ConflictVisualization, ConflictHunk } from '../../src/conflict-visualization';
|
|
7
|
+
|
|
8
|
+
describe('ConflictVisualization', () => {
|
|
9
|
+
const mockHunks: ConflictHunk[] = [
|
|
10
|
+
{
|
|
11
|
+
startLine: 10,
|
|
12
|
+
endLine: 15,
|
|
13
|
+
ours: ['let x = 5', 'console.log(x)'],
|
|
14
|
+
theirs: ['let x = 10', 'console.log(x * 2)'],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
startLine: 20,
|
|
18
|
+
endLine: 25,
|
|
19
|
+
ours: ['return result', 'return result;'],
|
|
20
|
+
theirs: ['return undefined', 'return null;'],
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
describe('ASCII Rendering', () => {
|
|
25
|
+
test('renders conflict in ASCII format', () => {
|
|
26
|
+
const ascii = ConflictVisualization.toASCII('file.ts', mockHunks, 'agent-a', 'agent-b');
|
|
27
|
+
expect(typeof ascii).toBe('string');
|
|
28
|
+
expect(ascii).toContain('CONFLICT');
|
|
29
|
+
expect(ascii).toContain('file.ts');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('includes both agent names', () => {
|
|
33
|
+
const ascii = ConflictVisualization.toASCII('file.ts', mockHunks, 'researcher', 'builder');
|
|
34
|
+
expect(ascii).toContain('researcher');
|
|
35
|
+
expect(ascii).toContain('builder');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('includes conflict content', () => {
|
|
39
|
+
const ascii = ConflictVisualization.toASCII('file.ts', mockHunks, 'a', 'b');
|
|
40
|
+
expect(ascii).toContain('let x = 5');
|
|
41
|
+
expect(ascii).toContain('let x = 10');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('shows line numbers', () => {
|
|
45
|
+
const ascii = ConflictVisualization.toASCII('file.ts', mockHunks, 'a', 'b');
|
|
46
|
+
expect(ascii).toContain('10');
|
|
47
|
+
expect(ascii).toContain('20');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('handles multiple hunks', () => {
|
|
51
|
+
const ascii = ConflictVisualization.toASCII('file.ts', mockHunks, 'a', 'b');
|
|
52
|
+
expect(ascii).toContain('Conflict 1');
|
|
53
|
+
expect(ascii).toContain('Conflict 2');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('handles empty hunks', () => {
|
|
57
|
+
const ascii = ConflictVisualization.toASCII('file.ts', [], 'a', 'b');
|
|
58
|
+
expect(ascii).toContain('CONFLICT');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('JSON Rendering', () => {
|
|
63
|
+
test('renders conflict as JSON', () => {
|
|
64
|
+
const json = ConflictVisualization.toJSON('file.ts', mockHunks, 'agent-a', 'agent-b');
|
|
65
|
+
const parsed = JSON.parse(json);
|
|
66
|
+
expect(parsed.file).toBe('file.ts');
|
|
67
|
+
expect(parsed.agentA).toBe('agent-a');
|
|
68
|
+
expect(parsed.agentB).toBe('agent-b');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('includes hunks in JSON', () => {
|
|
72
|
+
const json = ConflictVisualization.toJSON('file.ts', mockHunks, 'a', 'b');
|
|
73
|
+
const parsed = JSON.parse(json);
|
|
74
|
+
expect(parsed.hunks).toHaveLength(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('JSON includes metrics', () => {
|
|
78
|
+
const json = ConflictVisualization.toJSON('file.ts', mockHunks, 'a', 'b');
|
|
79
|
+
const parsed = JSON.parse(json);
|
|
80
|
+
expect(parsed.totalHunks).toBe(2);
|
|
81
|
+
expect(parsed.timestamp).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('JSON is valid and parseable', () => {
|
|
85
|
+
const json = ConflictVisualization.toJSON('file.ts', mockHunks, 'a', 'b');
|
|
86
|
+
expect(() => JSON.parse(json)).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('HTML Rendering', () => {
|
|
91
|
+
test('renders conflict as HTML', () => {
|
|
92
|
+
const html = ConflictVisualization.toHTML('file.ts', mockHunks, 'agent-a', 'agent-b');
|
|
93
|
+
expect(html).toContain('<html>');
|
|
94
|
+
expect(html).toContain('</html>');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('includes file name in HTML', () => {
|
|
98
|
+
const html = ConflictVisualization.toHTML('myfile.ts', mockHunks, 'a', 'b');
|
|
99
|
+
expect(html).toContain('myfile.ts');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('includes CSS styling', () => {
|
|
103
|
+
const html = ConflictVisualization.toHTML('file.ts', mockHunks, 'a', 'b');
|
|
104
|
+
expect(html).toContain('<style>');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('renders both versions side-by-side', () => {
|
|
108
|
+
const html = ConflictVisualization.toHTML('file.ts', mockHunks, 'agent-a', 'agent-b');
|
|
109
|
+
expect(html).toContain('agent-a');
|
|
110
|
+
expect(html).toContain('agent-b');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('Git Format Rendering', () => {
|
|
115
|
+
test('renders with git conflict markers', () => {
|
|
116
|
+
const git = ConflictVisualization.toGitFormat(mockHunks, 'agent-a', 'agent-b');
|
|
117
|
+
expect(git).toContain('<<<<<<< agent-a');
|
|
118
|
+
expect(git).toContain('=======');
|
|
119
|
+
expect(git).toContain('>>>>>>> agent-b');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('includes all conflict content', () => {
|
|
123
|
+
const git = ConflictVisualization.toGitFormat(mockHunks, 'a', 'b');
|
|
124
|
+
expect(git).toContain('let x = 5');
|
|
125
|
+
expect(git).toContain('let x = 10');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('Git Marker Parsing', () => {
|
|
130
|
+
test('parses git conflict markers', () => {
|
|
131
|
+
const content = `
|
|
132
|
+
before
|
|
133
|
+
<<<<<<< agent-a
|
|
134
|
+
our change
|
|
135
|
+
=======
|
|
136
|
+
their change
|
|
137
|
+
>>>>>>> agent-b
|
|
138
|
+
after`;
|
|
139
|
+
const hunks = ConflictVisualization.parseGitMarkers(content, 'agent-a', 'agent-b');
|
|
140
|
+
expect(hunks).toHaveLength(1);
|
|
141
|
+
expect(hunks[0].ours).toContain('our change');
|
|
142
|
+
expect(hunks[0].theirs).toContain('their change');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('handles multiple conflicts', () => {
|
|
146
|
+
const content = `
|
|
147
|
+
<<<<<<< a
|
|
148
|
+
v1
|
|
149
|
+
=======
|
|
150
|
+
v2
|
|
151
|
+
>>>>>>> b
|
|
152
|
+
text
|
|
153
|
+
<<<<<<< a
|
|
154
|
+
x1
|
|
155
|
+
=======
|
|
156
|
+
x2
|
|
157
|
+
>>>>>>> b`;
|
|
158
|
+
const hunks = ConflictVisualization.parseGitMarkers(content, 'a', 'b');
|
|
159
|
+
expect(hunks.length).toBeGreaterThanOrEqual(1);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('Resolution Suggestions', () => {
|
|
164
|
+
test('suggests resolution based on shorter diff', () => {
|
|
165
|
+
const hunks: ConflictHunk[] = [
|
|
166
|
+
{
|
|
167
|
+
startLine: 1,
|
|
168
|
+
endLine: 5,
|
|
169
|
+
ours: ['short'],
|
|
170
|
+
theirs: ['very', 'long', 'content', 'here'],
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
const winner = ConflictVisualization.suggestResolution(hunks, 'a', 'b');
|
|
174
|
+
expect(winner).toBe('a');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('suggests other agent for longer diff', () => {
|
|
178
|
+
const hunks: ConflictHunk[] = [
|
|
179
|
+
{
|
|
180
|
+
startLine: 1,
|
|
181
|
+
endLine: 5,
|
|
182
|
+
ours: ['very', 'long', 'content'],
|
|
183
|
+
theirs: ['short'],
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const winner = ConflictVisualization.suggestResolution(hunks, 'agent-a', 'agent-b');
|
|
187
|
+
expect(winner).toBe('agent-b');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('Statistics', () => {
|
|
192
|
+
test('calculates conflict stats', () => {
|
|
193
|
+
const stats = ConflictVisualization.getStats(mockHunks);
|
|
194
|
+
expect(stats.totalHunks).toBe(2);
|
|
195
|
+
expect(stats.totalLinesInConflict).toBeGreaterThan(0);
|
|
196
|
+
expect(stats.avgHunkSize).toBeGreaterThan(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('handles empty hunks', () => {
|
|
200
|
+
const stats = ConflictVisualization.getStats([]);
|
|
201
|
+
expect(stats.totalHunks).toBe(0);
|
|
202
|
+
expect(stats.totalLinesInConflict).toBe(0);
|
|
203
|
+
expect(stats.avgHunkSize).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('calculates average hunk size', () => {
|
|
207
|
+
const stats = ConflictVisualization.getStats(mockHunks);
|
|
208
|
+
const expectedAvg = mockHunks.reduce((sum, h) => sum + h.ours.length + h.theirs.length, 0) / mockHunks.length;
|
|
209
|
+
expect(stats.avgHunkSize).toBe(expectedAvg);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('Large Conflict Handling', () => {
|
|
214
|
+
test('handles 100 conflict hunks', () => {
|
|
215
|
+
const hunks: ConflictHunk[] = [];
|
|
216
|
+
for (let i = 0; i < 100; i++) {
|
|
217
|
+
hunks.push({
|
|
218
|
+
startLine: i * 10,
|
|
219
|
+
endLine: (i + 1) * 10,
|
|
220
|
+
ours: [`our${i}`],
|
|
221
|
+
theirs: [`their${i}`],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const ascii = ConflictVisualization.toASCII('large.ts', hunks, 'a', 'b');
|
|
225
|
+
expect(ascii).toContain('Conflict 1');
|
|
226
|
+
expect(ascii).toContain('Conflict 100');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('renders large conflicts to JSON', () => {
|
|
230
|
+
const hunks: ConflictHunk[] = Array(50).fill(null).map((_, i) => ({
|
|
231
|
+
startLine: i,
|
|
232
|
+
endLine: i + 1,
|
|
233
|
+
ours: [`line${i}a`],
|
|
234
|
+
theirs: [`line${i}b`],
|
|
235
|
+
}));
|
|
236
|
+
const json = ConflictVisualization.toJSON('file.ts', hunks, 'a', 'b');
|
|
237
|
+
const parsed = JSON.parse(json);
|
|
238
|
+
expect(parsed.hunks).toHaveLength(50);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('Format Consistency', () => {
|
|
243
|
+
test('all formats include same file name', () => {
|
|
244
|
+
const ascii = ConflictVisualization.toASCII('test.ts', mockHunks, 'a', 'b');
|
|
245
|
+
const json = ConflictVisualization.toJSON('test.ts', mockHunks, 'a', 'b');
|
|
246
|
+
const html = ConflictVisualization.toHTML('test.ts', mockHunks, 'a', 'b');
|
|
247
|
+
|
|
248
|
+
expect(ascii).toContain('test.ts');
|
|
249
|
+
expect(json).toContain('test.ts');
|
|
250
|
+
expect(html).toContain('test.ts');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('all formats include agent names', () => {
|
|
254
|
+
const ascii = ConflictVisualization.toASCII('f.ts', mockHunks, 'researcher', 'builder');
|
|
255
|
+
const json = ConflictVisualization.toJSON('f.ts', mockHunks, 'researcher', 'builder');
|
|
256
|
+
const html = ConflictVisualization.toHTML('f.ts', mockHunks, 'researcher', 'builder');
|
|
257
|
+
|
|
258
|
+
expect(ascii).toContain('researcher');
|
|
259
|
+
expect(json).toContain('researcher');
|
|
260
|
+
expect(html).toContain('researcher');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distributed Coordination Tests
|
|
3
|
+
* Multi-machine agent collaboration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DistributedCoordination } from '../../src/distributed-coordination';
|
|
7
|
+
|
|
8
|
+
describe('DistributedCoordination', () => {
|
|
9
|
+
let coord: DistributedCoordination;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
coord = new DistributedCoordination();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('Remote Agent Registration', () => {
|
|
16
|
+
test('registers remote agent', () => {
|
|
17
|
+
const agent = coord.registerRemoteAgent(
|
|
18
|
+
'agent-1',
|
|
19
|
+
'https://agent1.example.com/api',
|
|
20
|
+
'pubkey123'
|
|
21
|
+
);
|
|
22
|
+
expect(agent.agentId).toBe('agent-1');
|
|
23
|
+
expect(agent.status).toBe('offline');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('stores agent endpoint', () => {
|
|
27
|
+
coord.registerRemoteAgent('a1', 'https://host1/api', 'key1');
|
|
28
|
+
const agent = coord.getAgentStatus('a1');
|
|
29
|
+
expect(agent?.endpoint).toBe('https://host1/api');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('multiple agents', () => {
|
|
33
|
+
coord.registerRemoteAgent('a1', 'https://host1/api', 'key1');
|
|
34
|
+
coord.registerRemoteAgent('a2', 'https://host2/api', 'key2');
|
|
35
|
+
coord.registerRemoteAgent('a3', 'https://host3/api', 'key3');
|
|
36
|
+
const agents = coord.getAllAgents();
|
|
37
|
+
expect(agents).toHaveLength(3);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('Lock Requests', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
coord.registerRemoteAgent('a1', 'https://h1/api', 'k1');
|
|
44
|
+
coord.registerRemoteAgent('a2', 'https://h2/api', 'k2');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('sends lock request', async () => {
|
|
48
|
+
const result = await coord.requestRemoteLock('a1', 'file.ts', 30000, 'sig1');
|
|
49
|
+
expect(result).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('rejects lock for offline agent', async () => {
|
|
53
|
+
const result = await coord.requestRemoteLock('a1', 'file.ts', 30000, 'sig1');
|
|
54
|
+
expect(result).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('grants lock', () => {
|
|
58
|
+
coord.updateHeartbeat('a1');
|
|
59
|
+
const result = coord.grantLock('a1', 'file.ts', 30000, 'sig1');
|
|
60
|
+
expect(result).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('releases lock', () => {
|
|
64
|
+
const result = coord.releaseLock('a1', 'file.ts', 'sig1');
|
|
65
|
+
expect(result).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Commit Broadcasting', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
coord.registerRemoteAgent('a1', 'https://h1/api', 'k1');
|
|
72
|
+
coord.registerRemoteAgent('a2', 'https://h2/api', 'k2');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('broadcasts commit to all agents', () => {
|
|
76
|
+
const count = coord.broadcastCommit('a1', 'c1', ['f1.ts', 'f2.ts'], 'sig1');
|
|
77
|
+
expect(count).toBe(2);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('records broadcast in message log', () => {
|
|
81
|
+
coord.broadcastCommit('a1', 'c1', ['f.ts'], 'sig1');
|
|
82
|
+
const log = coord.getMessageLog();
|
|
83
|
+
expect(log.length).toBeGreaterThan(0);
|
|
84
|
+
expect(log[0].type).toBe('commit');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('Collision Reporting', () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
coord.registerRemoteAgent('a1', 'https://h1/api', 'k1');
|
|
91
|
+
coord.registerRemoteAgent('a2', 'https://h2/api', 'k2');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('reports collision between agents', () => {
|
|
95
|
+
const result = coord.reportCollision('a1', 'a2', 'shared.ts', 'sig1');
|
|
96
|
+
expect(result).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('collision logged in message log', () => {
|
|
100
|
+
coord.reportCollision('a1', 'a2', 'shared.ts', 'sig1');
|
|
101
|
+
const log = coord.getMessageLog();
|
|
102
|
+
expect(log.some(m => m.type === 'collision')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('State Synchronization', () => {
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
coord.registerRemoteAgent('a1', 'https://h1/api', 'k1');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('syncs agent state', () => {
|
|
112
|
+
const state = coord.syncState('a1', 'c1', 5);
|
|
113
|
+
expect(state.agentId).toBe('a1');
|
|
114
|
+
expect(state.filesChanged).toBe(5);
|
|
115
|
+
expect(state.status).toBe('synced');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('tracks sync status per agent', () => {
|
|
119
|
+
coord.syncState('a1', 'c1', 3);
|
|
120
|
+
coord.syncState('a1', 'c2', 4);
|
|
121
|
+
const states = coord.getSyncStatus('a1');
|
|
122
|
+
expect(states.length).toBe(2);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Heartbeat & Liveness', () => {
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
coord.registerRemoteAgent('a1', 'https://h1/api', 'k1');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('updates heartbeat', () => {
|
|
132
|
+
const result = coord.updateHeartbeat('a1');
|
|
133
|
+
expect(result).toBe(true);
|
|
134
|
+
const agent = coord.getAgentStatus('a1');
|
|
135
|
+
expect(agent?.status).toBe('online');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('detects offline agents', () => {
|
|
139
|
+
const before = Date.now();
|
|
140
|
+
// Simulate time passing
|
|
141
|
+
const offline = coord.detectOfflineAgents();
|
|
142
|
+
expect(Array.isArray(offline)).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Message Verification', () => {
|
|
147
|
+
test('verifies message signature', () => {
|
|
148
|
+
const msg = {
|
|
149
|
+
type: 'commit' as const,
|
|
150
|
+
agentId: 'a1',
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
signature: 'sig123',
|
|
153
|
+
payload: {},
|
|
154
|
+
};
|
|
155
|
+
const valid = coord.verifyMessage(msg, 'pubkey');
|
|
156
|
+
expect(typeof valid).toBe('boolean');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Message Log', () => {
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
coord.registerRemoteAgent('a1', 'https://h1/api', 'k1');
|
|
163
|
+
coord.registerRemoteAgent('a2', 'https://h2/api', 'k2');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('logs all messages', () => {
|
|
167
|
+
coord.broadcastCommit('a1', 'c1', ['f.ts'], 'sig1');
|
|
168
|
+
coord.reportCollision('a2', 'a1', 'f.ts', 'sig2');
|
|
169
|
+
const log = coord.getMessageLog();
|
|
170
|
+
expect(log.length).toBe(2);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('filters log by agent', () => {
|
|
174
|
+
coord.broadcastCommit('a1', 'c1', ['f.ts'], 'sig1');
|
|
175
|
+
coord.broadcastCommit('a2', 'c2', ['g.ts'], 'sig2');
|
|
176
|
+
const a1Log = coord.getMessageLog('a1');
|
|
177
|
+
expect(a1Log.every(m => m.agentId === 'a1')).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('Distributed Merge', () => {
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
coord.registerRemoteAgent('a1', 'https://h1/api', 'k1');
|
|
184
|
+
coord.registerRemoteAgent('a2', 'https://h2/api', 'k2');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('resolves merge between agents', () => {
|
|
188
|
+
coord.syncState('a1', 'c1', 5);
|
|
189
|
+
coord.syncState('a2', 'c2', 3);
|
|
190
|
+
const result = coord.resolveMergeDistributed(
|
|
191
|
+
'file.ts',
|
|
192
|
+
'a1',
|
|
193
|
+
'a2',
|
|
194
|
+
'timestamp-priority'
|
|
195
|
+
);
|
|
196
|
+
expect(result.resolved).toBe(true);
|
|
197
|
+
expect(result.winner).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('Performance', () => {
|
|
202
|
+
test('registers 100 agents quickly', () => {
|
|
203
|
+
const start = Date.now();
|
|
204
|
+
for (let i = 0; i < 100; i++) {
|
|
205
|
+
coord.registerRemoteAgent(`a${i}`, `https://h${i}/api`, `k${i}`);
|
|
206
|
+
}
|
|
207
|
+
const elapsed = Date.now() - start;
|
|
208
|
+
expect(elapsed).toBeLessThan(100);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('broadcasts to 100 agents quickly', () => {
|
|
212
|
+
for (let i = 0; i < 100; i++) {
|
|
213
|
+
coord.registerRemoteAgent(`a${i}`, `https://h${i}/api`, `k${i}`);
|
|
214
|
+
}
|
|
215
|
+
const start = Date.now();
|
|
216
|
+
coord.broadcastCommit('a0', 'c1', ['f.ts'], 'sig');
|
|
217
|
+
const elapsed = Date.now() - start;
|
|
218
|
+
expect(elapsed).toBeLessThan(50);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('handles 1000 messages in log', () => {
|
|
222
|
+
coord.registerRemoteAgent('a1', 'https://h1/api', 'k1');
|
|
223
|
+
const start = Date.now();
|
|
224
|
+
for (let i = 0; i < 1000; i++) {
|
|
225
|
+
coord.broadcastCommit('a1', `c${i}`, [`f${i}.ts`], `sig${i}`);
|
|
226
|
+
}
|
|
227
|
+
const elapsed = Date.now() - start;
|
|
228
|
+
expect(elapsed).toBeLessThan(200);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('Multi-Agent Scenarios', () => {
|
|
233
|
+
test('handles 3 agents with collision', () => {
|
|
234
|
+
coord.registerRemoteAgent('a1', 'https://h1/api', 'k1');
|
|
235
|
+
coord.registerRemoteAgent('a2', 'https://h2/api', 'k2');
|
|
236
|
+
coord.registerRemoteAgent('a3', 'https://h3/api', 'k3');
|
|
237
|
+
|
|
238
|
+
coord.updateHeartbeat('a1');
|
|
239
|
+
coord.updateHeartbeat('a2');
|
|
240
|
+
coord.updateHeartbeat('a3');
|
|
241
|
+
|
|
242
|
+
coord.broadcastCommit('a1', 'c1', ['shared.ts'], 'sig1');
|
|
243
|
+
coord.reportCollision('a2', 'a1', 'shared.ts', 'sig2');
|
|
244
|
+
|
|
245
|
+
const log = coord.getMessageLog();
|
|
246
|
+
expect(log.length).toBe(2);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('tracks all agents independently', () => {
|
|
250
|
+
for (let i = 0; i < 5; i++) {
|
|
251
|
+
coord.registerRemoteAgent(`a${i}`, `https://h${i}/api`, `k${i}`);
|
|
252
|
+
coord.syncState(`a${i}`, `c${i}`, i + 1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (let i = 0; i < 5; i++) {
|
|
256
|
+
const states = coord.getSyncStatus(`a${i}`);
|
|
257
|
+
expect(states.length).toBe(1);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|