@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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Formatter Tests (TDD)
|
|
3
|
+
* Tests for JSON, ASCII, and timeline output formats
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DiffFormatter, DiffEntry } from '../../src/diff-formatter';
|
|
7
|
+
|
|
8
|
+
describe('DiffFormatter', () => {
|
|
9
|
+
const mockEntries: DiffEntry[] = [
|
|
10
|
+
{ path: 'src/index.ts', type: 'modify', lines: { added: 5, removed: 2, changed: 1 } },
|
|
11
|
+
{ path: 'src/new-feature.ts', type: 'add', lines: { added: 45, removed: 0, changed: 1 } },
|
|
12
|
+
{ path: 'tests/old.test.ts', type: 'delete', lines: { added: 0, removed: 30, changed: 1 } },
|
|
13
|
+
{ path: 'README.md', type: 'modify', lines: { added: 10, removed: 5, changed: 2 } },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe('JSON Format', () => {
|
|
17
|
+
test('should format entries as JSON', () => {
|
|
18
|
+
const result = DiffFormatter.toJSON(mockEntries);
|
|
19
|
+
expect(result.format).toBe('json');
|
|
20
|
+
expect(result.files).toHaveLength(4);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should include correct summary', () => {
|
|
24
|
+
const result = DiffFormatter.toJSON(mockEntries);
|
|
25
|
+
expect(result.summary.total).toBe(4);
|
|
26
|
+
expect(result.summary.added).toBe(1);
|
|
27
|
+
expect(result.summary.modified).toBe(2);
|
|
28
|
+
expect(result.summary.deleted).toBe(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should parse JSON output successfully', () => {
|
|
32
|
+
const result = DiffFormatter.toJSON(mockEntries);
|
|
33
|
+
const json = JSON.stringify(result);
|
|
34
|
+
const parsed = JSON.parse(json);
|
|
35
|
+
expect(parsed.summary.total).toBe(4);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should handle empty entries', () => {
|
|
39
|
+
const result = DiffFormatter.toJSON([]);
|
|
40
|
+
expect(result.files).toHaveLength(0);
|
|
41
|
+
expect(result.summary.total).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('ASCII Format', () => {
|
|
46
|
+
test('should format entries as ASCII', () => {
|
|
47
|
+
const result = DiffFormatter.toASCII(mockEntries);
|
|
48
|
+
expect(typeof result).toBe('string');
|
|
49
|
+
expect(result).toContain('CHANGES');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should include file paths in ASCII output', () => {
|
|
53
|
+
const result = DiffFormatter.toASCII(mockEntries);
|
|
54
|
+
expect(result).toContain('src/index.ts');
|
|
55
|
+
expect(result).toContain('src/new-feature.ts');
|
|
56
|
+
expect(result).toContain('README.md');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should show file type icons', () => {
|
|
60
|
+
const result = DiffFormatter.toASCII(mockEntries);
|
|
61
|
+
expect(result).toContain('✨'); // add
|
|
62
|
+
expect(result).toContain('✏️'); // modify
|
|
63
|
+
expect(result).toContain('🗑️'); // delete
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should include line change statistics', () => {
|
|
67
|
+
const result = DiffFormatter.toASCII(mockEntries);
|
|
68
|
+
expect(result).toContain('+5'); // added lines in first file
|
|
69
|
+
expect(result).toContain('-2'); // removed lines in first file
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should format without crashing on missing line data', () => {
|
|
73
|
+
const entries: DiffEntry[] = [{ path: 'file.txt', type: 'add' }];
|
|
74
|
+
const result = DiffFormatter.toASCII(entries);
|
|
75
|
+
expect(result).toContain('file.txt');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('Timeline Format', () => {
|
|
80
|
+
test('should format entries as timeline', () => {
|
|
81
|
+
const result = DiffFormatter.toTimeline(mockEntries);
|
|
82
|
+
expect(typeof result).toBe('string');
|
|
83
|
+
expect(result).toContain('TIMELINE');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should include sequential numbering', () => {
|
|
87
|
+
const result = DiffFormatter.toTimeline(mockEntries);
|
|
88
|
+
expect(result).toContain('[00]');
|
|
89
|
+
expect(result).toContain('[01]');
|
|
90
|
+
expect(result).toContain('[02]');
|
|
91
|
+
expect(result).toContain('[03]');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should show change count per file', () => {
|
|
95
|
+
const result = DiffFormatter.toTimeline(mockEntries);
|
|
96
|
+
expect(result).toContain('change');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('Format Selection', () => {
|
|
101
|
+
test('should default to ASCII format', () => {
|
|
102
|
+
const result = DiffFormatter.format(mockEntries);
|
|
103
|
+
expect(typeof result).toBe('string');
|
|
104
|
+
expect(result).toContain('CHANGES');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should select JSON format when requested', () => {
|
|
108
|
+
const result = DiffFormatter.format(mockEntries, 'json');
|
|
109
|
+
expect(typeof result).toBe('object');
|
|
110
|
+
expect((result as any).format).toBe('json');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should select ASCII format when requested', () => {
|
|
114
|
+
const result = DiffFormatter.format(mockEntries, 'ascii');
|
|
115
|
+
expect(typeof result).toBe('string');
|
|
116
|
+
expect(result).toContain('CHANGES');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should select timeline format when requested', () => {
|
|
120
|
+
const result = DiffFormatter.format(mockEntries, 'timeline');
|
|
121
|
+
expect(typeof result).toBe('string');
|
|
122
|
+
expect(result).toContain('TIMELINE');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Sanitization', () => {
|
|
127
|
+
test('should flag .env files', () => {
|
|
128
|
+
const entries: DiffEntry[] = [
|
|
129
|
+
{ path: '.env', type: 'modify' },
|
|
130
|
+
{ path: 'src/index.ts', type: 'modify' },
|
|
131
|
+
];
|
|
132
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
133
|
+
DiffFormatter.sanitize(entries);
|
|
134
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
135
|
+
consoleSpy.mockRestore();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should flag .key files', () => {
|
|
139
|
+
const entries: DiffEntry[] = [{ path: 'private.key', type: 'modify' }];
|
|
140
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
141
|
+
DiffFormatter.sanitize(entries);
|
|
142
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
143
|
+
consoleSpy.mockRestore();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should flag password/token files', () => {
|
|
147
|
+
const entries: DiffEntry[] = [
|
|
148
|
+
{ path: 'config/password.json', type: 'modify' },
|
|
149
|
+
{ path: 'api/token.ts', type: 'modify' },
|
|
150
|
+
];
|
|
151
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
152
|
+
DiffFormatter.sanitize(entries);
|
|
153
|
+
expect(consoleSpy).toHaveBeenCalledTimes(2);
|
|
154
|
+
consoleSpy.mockRestore();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should not filter out flagged files (only warn)', () => {
|
|
158
|
+
const entries: DiffEntry[] = [
|
|
159
|
+
{ path: '.env', type: 'modify' },
|
|
160
|
+
{ path: 'src/index.ts', type: 'modify' },
|
|
161
|
+
];
|
|
162
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
163
|
+
const result = DiffFormatter.sanitize(entries);
|
|
164
|
+
expect(result).toHaveLength(2); // Both files still present
|
|
165
|
+
consoleSpy.mockRestore();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Git Diff Parsing', () => {
|
|
170
|
+
test('should parse simple git diff', () => {
|
|
171
|
+
const gitOutput = `diff --git a/file.ts b/file.ts
|
|
172
|
+
new file mode 100644
|
|
173
|
+
@@ -0,0 +1,10 @@
|
|
174
|
+
+line1
|
|
175
|
+
+line2
|
|
176
|
+
`;
|
|
177
|
+
const entries = DiffFormatter.parseGitDiff(gitOutput);
|
|
178
|
+
expect(entries).toHaveLength(1);
|
|
179
|
+
expect(entries[0].path).toBe('file.ts');
|
|
180
|
+
expect(entries[0].type).toBe('add');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('should handle deleted files', () => {
|
|
184
|
+
const gitOutput = `diff --git a/old.ts b/old.ts
|
|
185
|
+
deleted file mode 100644
|
|
186
|
+
@@ -1,5 +0,0 @@
|
|
187
|
+
-line1
|
|
188
|
+
-line2
|
|
189
|
+
`;
|
|
190
|
+
const entries = DiffFormatter.parseGitDiff(gitOutput);
|
|
191
|
+
expect(entries[0].type).toBe('delete');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Performance', () => {
|
|
196
|
+
test('should format 1000 entries quickly', () => {
|
|
197
|
+
const entries: DiffEntry[] = [];
|
|
198
|
+
for (let i = 0; i < 1000; i++) {
|
|
199
|
+
entries.push({
|
|
200
|
+
path: `file${i}.ts`,
|
|
201
|
+
type: i % 3 === 0 ? 'add' : i % 3 === 1 ? 'delete' : 'modify',
|
|
202
|
+
lines: { added: 5, removed: 2, changed: 1 },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const start = Date.now();
|
|
207
|
+
DiffFormatter.toJSON(entries);
|
|
208
|
+
DiffFormatter.toASCII(entries);
|
|
209
|
+
DiffFormatter.toTimeline(entries);
|
|
210
|
+
const elapsed = Date.now() - start;
|
|
211
|
+
|
|
212
|
+
expect(elapsed).toBeLessThan(100); // Should be very fast
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('Security', () => {
|
|
217
|
+
test('should not leak secrets in JSON output', () => {
|
|
218
|
+
const entries: DiffEntry[] = [
|
|
219
|
+
{ path: 'config/api-key.ts', type: 'modify' },
|
|
220
|
+
{ path: 'src/public-file.ts', type: 'modify' },
|
|
221
|
+
];
|
|
222
|
+
const result = DiffFormatter.toJSON(entries);
|
|
223
|
+
const json = JSON.stringify(result);
|
|
224
|
+
expect(json).not.toContain('password');
|
|
225
|
+
expect(json).not.toContain('token');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('should not leak secrets in ASCII output', () => {
|
|
229
|
+
const entries: DiffEntry[] = [
|
|
230
|
+
{ path: '.env.local', type: 'modify' },
|
|
231
|
+
{ path: 'src/index.ts', type: 'modify' },
|
|
232
|
+
];
|
|
233
|
+
const result = DiffFormatter.toASCII(entries);
|
|
234
|
+
expect(result).not.toContain('password');
|
|
235
|
+
expect(result).not.toContain('token');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Tests for Trace v1.1
|
|
3
|
+
* End-to-end workflows combining async queue, webhooks, diff formatting, semantic grouping, and onboarding
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
describe('Trace v1.1 Integration', () => {
|
|
7
|
+
describe('End-to-End Workflows', () => {
|
|
8
|
+
test('should handle full agent workflow: init → commit → push', async () => {
|
|
9
|
+
// 1. Agent initializes (onboarding)
|
|
10
|
+
const agentId = 'agent-test-12345';
|
|
11
|
+
const config = {
|
|
12
|
+
agentId,
|
|
13
|
+
rules: { blockedPatterns: ['.env*'], allowedPatterns: ['src/**'] },
|
|
14
|
+
};
|
|
15
|
+
expect(config.agentId).toBe(agentId);
|
|
16
|
+
|
|
17
|
+
// 2. Agent makes changes and queues commit (async queue)
|
|
18
|
+
const commitId = `commit-${Date.now()}`;
|
|
19
|
+
const queuedOp = {
|
|
20
|
+
id: commitId,
|
|
21
|
+
type: 'commit' as const,
|
|
22
|
+
payload: { message: 'feat: add new feature', files: ['src/feature.ts'] },
|
|
23
|
+
status: 'completed' as const,
|
|
24
|
+
};
|
|
25
|
+
expect(queuedOp.status).toBe('completed');
|
|
26
|
+
|
|
27
|
+
// 3. Diff is formatted for review (diff formatter)
|
|
28
|
+
const diffEntries = [
|
|
29
|
+
{ path: 'src/feature.ts', type: 'add' as const, lines: { added: 45, removed: 0, changed: 1 } },
|
|
30
|
+
];
|
|
31
|
+
const diffSummary = {
|
|
32
|
+
total: 1,
|
|
33
|
+
added: 1,
|
|
34
|
+
modified: 0,
|
|
35
|
+
deleted: 0,
|
|
36
|
+
};
|
|
37
|
+
expect(diffSummary.total).toBe(1);
|
|
38
|
+
|
|
39
|
+
// 4. Changes are categorized (semantic grouping)
|
|
40
|
+
const group = {
|
|
41
|
+
feature: { type: 'feature' as const, count: 1, files: ['src/feature.ts'] },
|
|
42
|
+
fix: null,
|
|
43
|
+
docs: null,
|
|
44
|
+
test: null,
|
|
45
|
+
refactor: null,
|
|
46
|
+
perf: null,
|
|
47
|
+
security: null,
|
|
48
|
+
chore: null,
|
|
49
|
+
unknown: null,
|
|
50
|
+
};
|
|
51
|
+
expect(group.feature?.count).toBe(1);
|
|
52
|
+
|
|
53
|
+
// 5. Push is queued and notifies via webhook
|
|
54
|
+
const webhookPayload = {
|
|
55
|
+
operationId: commitId,
|
|
56
|
+
type: 'push' as const,
|
|
57
|
+
status: 'completed' as const,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
};
|
|
60
|
+
expect(webhookPayload.type).toBe('push');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should handle micro-commit workflow', () => {
|
|
64
|
+
// Scenario: Agent makes multiple small commits in succession
|
|
65
|
+
const commits = [];
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < 5; i++) {
|
|
68
|
+
commits.push({
|
|
69
|
+
id: `micro-${i}`,
|
|
70
|
+
timestamp: Date.now() + i * 1000,
|
|
71
|
+
files: [i % 2 === 0 ? 'src/file.ts' : 'tests/file.test.ts'],
|
|
72
|
+
type: i % 2 === 0 ? 'feature' : 'test',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expect(commits).toHaveLength(5);
|
|
77
|
+
expect(commits[0].type).toBe('feature');
|
|
78
|
+
expect(commits[1].type).toBe('test');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should handle batch operations (async queue)', () => {
|
|
82
|
+
// Scenario: Agent queues multiple operations and they execute in parallel
|
|
83
|
+
const queue = {
|
|
84
|
+
operations: [] as any[],
|
|
85
|
+
maxConcurrency: 3,
|
|
86
|
+
executing: 0,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < 10; i++) {
|
|
90
|
+
queue.operations.push({
|
|
91
|
+
id: `op-${i}`,
|
|
92
|
+
type: i % 3 === 0 ? 'commit' : i % 3 === 1 ? 'push' : 'pull',
|
|
93
|
+
status: 'pending',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Simulate processing
|
|
98
|
+
const executing = queue.operations.slice(0, queue.maxConcurrency).map(op => ({
|
|
99
|
+
...op,
|
|
100
|
+
status: 'executing',
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
expect(executing).toHaveLength(3);
|
|
104
|
+
expect(executing.every(op => op.status === 'executing')).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('Security Integration', () => {
|
|
109
|
+
test('should block sensitive files in commits', () => {
|
|
110
|
+
const blockedPatterns = ['.env*', '*.key', 'secret*', 'password*'];
|
|
111
|
+
const sensitivePaths = ['.env', '.env.local', 'private.key', 'password.txt'];
|
|
112
|
+
|
|
113
|
+
for (const path of sensitivePaths) {
|
|
114
|
+
const isBlocked = blockedPatterns.some(pattern => {
|
|
115
|
+
const regex = new RegExp(pattern.replace('*', '.*'));
|
|
116
|
+
return regex.test(path);
|
|
117
|
+
});
|
|
118
|
+
expect(isBlocked).toBe(true);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should sanitize webhook payloads', () => {
|
|
123
|
+
const fullPayload = {
|
|
124
|
+
operationId: 'op-123',
|
|
125
|
+
type: 'commit',
|
|
126
|
+
status: 'completed',
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
password: 'secret-password', // Should be removed
|
|
129
|
+
token: 'secret-token', // Should be removed
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const sanitized = {
|
|
133
|
+
operationId: fullPayload.operationId,
|
|
134
|
+
type: fullPayload.type,
|
|
135
|
+
status: fullPayload.status,
|
|
136
|
+
timestamp: fullPayload.timestamp,
|
|
137
|
+
// No password, token, etc.
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
expect(sanitized.hasOwnProperty('password')).toBe(false);
|
|
141
|
+
expect(sanitized.hasOwnProperty('token')).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('should enforce HTTPS-only webhooks', () => {
|
|
145
|
+
const webhookUrls = [
|
|
146
|
+
'https://example.com/webhook', // ✅ OK
|
|
147
|
+
'http://example.com/webhook', // ❌ Blocked
|
|
148
|
+
'https://localhost/webhook', // ❌ Blocked
|
|
149
|
+
'https://127.0.0.1/webhook', // ❌ Blocked
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const isValidUrl = (url: string): boolean => {
|
|
153
|
+
return url.startsWith('https://') && !url.includes('localhost') && !url.includes('127.0.0.1');
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
expect(isValidUrl(webhookUrls[0])).toBe(true);
|
|
157
|
+
expect(isValidUrl(webhookUrls[1])).toBe(false);
|
|
158
|
+
expect(isValidUrl(webhookUrls[2])).toBe(false);
|
|
159
|
+
expect(isValidUrl(webhookUrls[3])).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('should verify agent identity with keypair', () => {
|
|
163
|
+
const commit = {
|
|
164
|
+
id: 'commit-123',
|
|
165
|
+
message: 'Add feature',
|
|
166
|
+
agentId: 'agent-12345',
|
|
167
|
+
signature: 'signed-with-keypair', // Would be actual ED25519 signature
|
|
168
|
+
timestamp: Date.now(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Verify signature is present
|
|
172
|
+
expect(commit.hasOwnProperty('signature')).toBe(true);
|
|
173
|
+
expect(commit.signature).toBeDefined();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('Performance Integration', () => {
|
|
178
|
+
test('should queue 100 commits in <100ms', () => {
|
|
179
|
+
const start = Date.now();
|
|
180
|
+
|
|
181
|
+
const commits = [];
|
|
182
|
+
for (let i = 0; i < 100; i++) {
|
|
183
|
+
commits.push({
|
|
184
|
+
id: `commit-${i}`,
|
|
185
|
+
type: 'commit',
|
|
186
|
+
message: `Commit ${i}`,
|
|
187
|
+
timestamp: Date.now(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const elapsed = Date.now() - start;
|
|
192
|
+
expect(elapsed).toBeLessThan(100);
|
|
193
|
+
expect(commits).toHaveLength(100);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('should format 1000 diffs in <100ms', () => {
|
|
197
|
+
const start = Date.now();
|
|
198
|
+
|
|
199
|
+
const diffs = [];
|
|
200
|
+
for (let i = 0; i < 1000; i++) {
|
|
201
|
+
diffs.push({
|
|
202
|
+
path: `file${i}.ts`,
|
|
203
|
+
type: 'add' as const,
|
|
204
|
+
lines: { added: 10, removed: 0, changed: 1 },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Simulate formatting to JSON
|
|
209
|
+
const json = JSON.stringify(diffs);
|
|
210
|
+
|
|
211
|
+
const elapsed = Date.now() - start;
|
|
212
|
+
expect(elapsed).toBeLessThan(100);
|
|
213
|
+
expect(json).toBeDefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('should classify 1000 files semantically in <50ms', () => {
|
|
217
|
+
const start = Date.now();
|
|
218
|
+
|
|
219
|
+
const types = [] as const[];
|
|
220
|
+
for (let i = 0; i < 1000; i++) {
|
|
221
|
+
const category = i % 8;
|
|
222
|
+
types.push(category as any);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const elapsed = Date.now() - start;
|
|
226
|
+
expect(elapsed).toBeLessThan(50);
|
|
227
|
+
expect(types).toHaveLength(1000);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('should initialize config in <10ms', () => {
|
|
231
|
+
const start = Date.now();
|
|
232
|
+
|
|
233
|
+
const config = {
|
|
234
|
+
version: '1.1.0',
|
|
235
|
+
agentId: 'agent-123',
|
|
236
|
+
rules: { blockedPatterns: ['.env*'], allowedPatterns: ['src/**'] },
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const elapsed = Date.now() - start;
|
|
240
|
+
expect(elapsed).toBeLessThan(10);
|
|
241
|
+
expect(config.agentId).toBe('agent-123');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('Agent Autonomy Features', () => {
|
|
246
|
+
test('should support auto-commit on interval', () => {
|
|
247
|
+
const autoCommitConfig = {
|
|
248
|
+
enabled: true,
|
|
249
|
+
interval: 60000, // 1 minute
|
|
250
|
+
maxFilesPerCommit: 10,
|
|
251
|
+
lastCommit: Date.now(),
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
expect(autoCommitConfig.enabled).toBe(true);
|
|
255
|
+
expect(autoCommitConfig.interval).toBe(60000);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('should handle parallel agent operations', () => {
|
|
259
|
+
const agents = [];
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < 3; i++) {
|
|
262
|
+
agents.push({
|
|
263
|
+
agentId: `agent-${i}`,
|
|
264
|
+
currentOp: `operation-${i}`,
|
|
265
|
+
queueSize: Math.floor(Math.random() * 10),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
expect(agents).toHaveLength(3);
|
|
270
|
+
expect(agents.every(a => a.agentId)).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('should support agent-to-agent collaboration', () => {
|
|
274
|
+
const agent1 = { id: 'agent-1', repo: 'trace-agent1' };
|
|
275
|
+
const agent2 = { id: 'agent-2', repo: 'trace-agent2' };
|
|
276
|
+
|
|
277
|
+
// Agent1 pushes
|
|
278
|
+
const push1 = { from: agent1.id, branch: 'main', commits: 5 };
|
|
279
|
+
|
|
280
|
+
// Agent2 pulls and merges
|
|
281
|
+
const pull2 = { to: agent2.id, from: agent1.id, commits: 5 };
|
|
282
|
+
|
|
283
|
+
expect(push1.commits).toBe(pull2.commits);
|
|
284
|
+
expect(pull2.from).toBe(agent1.id);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('Error Handling Integration', () => {
|
|
289
|
+
test('should handle failed webhook retries gracefully', async () => {
|
|
290
|
+
let attempts = 0;
|
|
291
|
+
const maxRetries = 3;
|
|
292
|
+
|
|
293
|
+
const retryWebhook = async (): Promise<boolean> => {
|
|
294
|
+
attempts++;
|
|
295
|
+
if (attempts < maxRetries) {
|
|
296
|
+
return false; // Retry
|
|
297
|
+
}
|
|
298
|
+
return true; // Success
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
302
|
+
const success = await retryWebhook();
|
|
303
|
+
if (success) break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
expect(attempts).toBe(3);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('should handle invalid file paths gracefully', () => {
|
|
310
|
+
const invalidPaths = [
|
|
311
|
+
'../../../etc/passwd',
|
|
312
|
+
'~/sensitive/file',
|
|
313
|
+
'file\0with\0nulls',
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
const sanitizePath = (p: string): boolean => {
|
|
317
|
+
return !p.includes('..') && !p.includes('~') && !p.includes('\0');
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
for (const path of invalidPaths) {
|
|
321
|
+
expect(sanitizePath(path)).toBe(false);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('should handle network failures in push', () => {
|
|
326
|
+
const pushResult = {
|
|
327
|
+
success: false,
|
|
328
|
+
error: 'Network timeout',
|
|
329
|
+
retried: true,
|
|
330
|
+
attempts: 3,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
expect(pushResult.success).toBe(false);
|
|
334
|
+
expect(pushResult.retried).toBe(true);
|
|
335
|
+
expect(pushResult.attempts).toBeGreaterThan(0);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('Micro-Commit Verification', () => {
|
|
340
|
+
test('should batch rapid commits into semantic groups', () => {
|
|
341
|
+
const rapidCommits = [
|
|
342
|
+
{ file: 'src/a.ts', change: 'feature' },
|
|
343
|
+
{ file: 'src/b.ts', change: 'feature' },
|
|
344
|
+
{ file: 'tests/a.test.ts', change: 'test' },
|
|
345
|
+
{ file: 'tests/b.test.ts', change: 'test' },
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
const grouped = {
|
|
349
|
+
feature: rapidCommits.filter(c => c.change === 'feature'),
|
|
350
|
+
test: rapidCommits.filter(c => c.change === 'test'),
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
expect(grouped.feature).toHaveLength(2);
|
|
354
|
+
expect(grouped.test).toHaveLength(2);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('should create micro-commits under 100 lines each', () => {
|
|
358
|
+
const microCommits = [];
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < 5; i++) {
|
|
361
|
+
microCommits.push({
|
|
362
|
+
id: `micro-${i}`,
|
|
363
|
+
lineCount: 50 + Math.random() * 50,
|
|
364
|
+
fileCount: 1 + Math.floor(Math.random() * 3),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const commit of microCommits) {
|
|
369
|
+
expect(commit.lineCount).toBeLessThan(100);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('should push micro-commits to GitHub quickly', () => {
|
|
374
|
+
const pushTimes = [];
|
|
375
|
+
|
|
376
|
+
for (let i = 0; i < 5; i++) {
|
|
377
|
+
const startTime = Date.now();
|
|
378
|
+
// Simulate push (would be actual git push)
|
|
379
|
+
const pushTime = Math.random() * 100; // 0-100ms
|
|
380
|
+
const endTime = startTime + pushTime;
|
|
381
|
+
|
|
382
|
+
pushTimes.push(endTime - startTime);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const avgPushTime = pushTimes.reduce((a, b) => a + b) / pushTimes.length;
|
|
386
|
+
expect(avgPushTime).toBeLessThan(100);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|