@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,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Isolation for Trace
|
|
3
|
+
* Enforce security boundary between project workspace and private data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
export interface WorkspaceBoundary {
|
|
9
|
+
root: string; // Workspace root (where git is initialized)
|
|
10
|
+
allowed: string[]; // Paths allowed in git
|
|
11
|
+
blocked: string[]; // Paths never committed
|
|
12
|
+
isPublic: boolean; // Whether workspace is public repo
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SecurityViolation {
|
|
16
|
+
path: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
severity: 'warning' | 'error' | 'critical';
|
|
19
|
+
suggestion: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class WorkspaceIsolation {
|
|
23
|
+
private boundary: WorkspaceBoundary;
|
|
24
|
+
private violations: SecurityViolation[] = [];
|
|
25
|
+
|
|
26
|
+
constructor(workspaceRoot: string, isPublic: boolean = true) {
|
|
27
|
+
this.boundary = {
|
|
28
|
+
root: workspaceRoot,
|
|
29
|
+
allowed: [
|
|
30
|
+
'src/',
|
|
31
|
+
'tests/',
|
|
32
|
+
'dist/',
|
|
33
|
+
'docs/',
|
|
34
|
+
'package.json',
|
|
35
|
+
'tsconfig.json',
|
|
36
|
+
'README.md',
|
|
37
|
+
'.gitignore',
|
|
38
|
+
'.gitattributes',
|
|
39
|
+
'LICENSE',
|
|
40
|
+
],
|
|
41
|
+
blocked: [
|
|
42
|
+
'.env',
|
|
43
|
+
'.env.local',
|
|
44
|
+
'.env.*.local',
|
|
45
|
+
'*.key',
|
|
46
|
+
'*.pem',
|
|
47
|
+
'secrets.json',
|
|
48
|
+
'config.local.json',
|
|
49
|
+
'.credentials',
|
|
50
|
+
'credentials.json',
|
|
51
|
+
'oauth.json',
|
|
52
|
+
'api-keys.json',
|
|
53
|
+
'tokens.json',
|
|
54
|
+
'passwords.txt',
|
|
55
|
+
'.workspace/',
|
|
56
|
+
'workspace-private/',
|
|
57
|
+
'private/',
|
|
58
|
+
'.local/',
|
|
59
|
+
'agent-*.key',
|
|
60
|
+
'agent-*.pem',
|
|
61
|
+
'agent-keys/',
|
|
62
|
+
'.trace/config.local.json',
|
|
63
|
+
'.trace/keys.json',
|
|
64
|
+
],
|
|
65
|
+
isPublic,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if path is safe to commit
|
|
71
|
+
*/
|
|
72
|
+
isSafeToCommit(filePath: string): { safe: boolean; violations: SecurityViolation[] } {
|
|
73
|
+
const violations: SecurityViolation[] = [];
|
|
74
|
+
|
|
75
|
+
// Normalize path
|
|
76
|
+
const normalized = path.normalize(filePath).replace(/\\/g, '/');
|
|
77
|
+
|
|
78
|
+
// Check against blocked patterns
|
|
79
|
+
for (const blocked of this.boundary.blocked) {
|
|
80
|
+
if (this.matchesPattern(normalized, blocked)) {
|
|
81
|
+
violations.push({
|
|
82
|
+
path: filePath,
|
|
83
|
+
reason: `Matches blocked pattern: ${blocked}`,
|
|
84
|
+
severity: 'critical',
|
|
85
|
+
suggestion: `Add to .gitignore: ${blocked}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check if file is outside workspace root
|
|
91
|
+
const fullPath = path.resolve(this.boundary.root, filePath);
|
|
92
|
+
const relative = path.relative(this.boundary.root, fullPath);
|
|
93
|
+
|
|
94
|
+
if (relative.startsWith('..')) {
|
|
95
|
+
violations.push({
|
|
96
|
+
path: filePath,
|
|
97
|
+
reason: 'File is outside workspace root',
|
|
98
|
+
severity: 'critical',
|
|
99
|
+
suggestion: `Use paths relative to ${this.boundary.root}`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Warn on sensitive filenames
|
|
104
|
+
if (this.hasSensitiveName(normalized)) {
|
|
105
|
+
violations.push({
|
|
106
|
+
path: filePath,
|
|
107
|
+
reason: 'Filename suggests sensitive data',
|
|
108
|
+
severity: 'warning',
|
|
109
|
+
suggestion: 'Verify this file should be committed',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
safe: violations.length === 0,
|
|
115
|
+
violations,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Scan directory for violations
|
|
121
|
+
*/
|
|
122
|
+
scanDirectory(dirPath: string): {
|
|
123
|
+
safeFiles: string[];
|
|
124
|
+
violatingFiles: string[];
|
|
125
|
+
violations: SecurityViolation[];
|
|
126
|
+
} {
|
|
127
|
+
const safeFiles: string[] = [];
|
|
128
|
+
const violatingFiles: string[] = [];
|
|
129
|
+
const allViolations: SecurityViolation[] = [];
|
|
130
|
+
|
|
131
|
+
// In real implementation, would recursively scan directory
|
|
132
|
+
// For now, check common patterns
|
|
133
|
+
|
|
134
|
+
const testPaths = [
|
|
135
|
+
'src/index.ts',
|
|
136
|
+
'.env',
|
|
137
|
+
'secrets.json',
|
|
138
|
+
'.credentials',
|
|
139
|
+
'package.json',
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
for (const testPath of testPaths) {
|
|
143
|
+
const result = this.isSafeToCommit(testPath);
|
|
144
|
+
if (result.safe) {
|
|
145
|
+
safeFiles.push(testPath);
|
|
146
|
+
} else {
|
|
147
|
+
violatingFiles.push(testPath);
|
|
148
|
+
allViolations.push(...result.violations);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { safeFiles, violatingFiles, violations: allViolations };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if git operation would leak private data
|
|
157
|
+
*/
|
|
158
|
+
validateGitPush(filePaths: string[]): {
|
|
159
|
+
canPush: boolean;
|
|
160
|
+
violations: SecurityViolation[];
|
|
161
|
+
} {
|
|
162
|
+
const violations: SecurityViolation[] = [];
|
|
163
|
+
|
|
164
|
+
for (const file of filePaths) {
|
|
165
|
+
const result = this.isSafeToCommit(file);
|
|
166
|
+
if (!result.safe) {
|
|
167
|
+
violations.push(...result.violations);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
canPush: violations.length === 0,
|
|
173
|
+
violations,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get workspace boundary info
|
|
179
|
+
*/
|
|
180
|
+
getBoundary(): WorkspaceBoundary {
|
|
181
|
+
return { ...this.boundary };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Set custom allowed paths
|
|
186
|
+
*/
|
|
187
|
+
setAllowed(paths: string[]): void {
|
|
188
|
+
this.boundary.allowed = paths;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Add path to blocked list
|
|
193
|
+
*/
|
|
194
|
+
blockPath(pattern: string): void {
|
|
195
|
+
if (!this.boundary.blocked.includes(pattern)) {
|
|
196
|
+
this.boundary.blocked.push(pattern);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if filename has sensitive indicators
|
|
202
|
+
*/
|
|
203
|
+
private hasSensitiveName(filePath: string): boolean {
|
|
204
|
+
const sensitive = [
|
|
205
|
+
'password',
|
|
206
|
+
'secret',
|
|
207
|
+
'token',
|
|
208
|
+
'credential',
|
|
209
|
+
'apikey',
|
|
210
|
+
'api_key',
|
|
211
|
+
'private',
|
|
212
|
+
'backup',
|
|
213
|
+
'dump',
|
|
214
|
+
'key',
|
|
215
|
+
'pem',
|
|
216
|
+
'cert',
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
220
|
+
|
|
221
|
+
return sensitive.some(s => fileName.includes(s));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Match path against pattern (supports * wildcards)
|
|
226
|
+
*/
|
|
227
|
+
private matchesPattern(filePath: string, pattern: string): boolean {
|
|
228
|
+
// Simple glob-style matching
|
|
229
|
+
const regex = pattern
|
|
230
|
+
.replace(/\./g, '\\.')
|
|
231
|
+
.replace(/\*/g, '.*')
|
|
232
|
+
.replace(/\?/g, '.');
|
|
233
|
+
|
|
234
|
+
return new RegExp(`^${regex}$`).test(filePath);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Generate security report
|
|
239
|
+
*/
|
|
240
|
+
generateReport(): string {
|
|
241
|
+
let report = `\n🔒 WORKSPACE SECURITY REPORT\n`;
|
|
242
|
+
report += `Workspace Root: ${this.boundary.root}\n`;
|
|
243
|
+
report += `Public Repository: ${this.boundary.isPublic ? 'Yes' : 'No'}\n\n`;
|
|
244
|
+
|
|
245
|
+
report += `✅ ALLOWED PATHS (${this.boundary.allowed.length}):\n`;
|
|
246
|
+
for (const path of this.boundary.allowed) {
|
|
247
|
+
report += ` • ${path}\n`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
report += `\n🚫 BLOCKED PATHS (${this.boundary.blocked.length}):\n`;
|
|
251
|
+
for (const path of this.boundary.blocked) {
|
|
252
|
+
report += ` • ${path}\n`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (this.violations.length > 0) {
|
|
256
|
+
report += `\n⚠️ VIOLATIONS DETECTED (${this.violations.length}):\n`;
|
|
257
|
+
for (const v of this.violations) {
|
|
258
|
+
report += ` [${v.severity.toUpperCase()}] ${v.path}\n`;
|
|
259
|
+
report += ` Reason: ${v.reason}\n`;
|
|
260
|
+
report += ` Fix: ${v.suggestion}\n`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return report;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create pre-commit hook content
|
|
269
|
+
*/
|
|
270
|
+
getPreCommitHook(): string {
|
|
271
|
+
return `#!/bin/bash
|
|
272
|
+
# Trace Pre-Commit Security Hook
|
|
273
|
+
# Prevents accidental commits of private files
|
|
274
|
+
|
|
275
|
+
STAGED_FILES=$(git diff --cached --name-only)
|
|
276
|
+
|
|
277
|
+
for file in $STAGED_FILES; do
|
|
278
|
+
if [[ $file == .env* ]] || [[ $file == *.key ]] || [[ $file == *.pem ]] || [[ $file == *secret* ]] || [[ $file == *credential* ]]; then
|
|
279
|
+
echo "🚫 SECURITY: Cannot commit private file: $file"
|
|
280
|
+
echo " Add to .gitignore if this is intentional"
|
|
281
|
+
exit 1
|
|
282
|
+
fi
|
|
283
|
+
done
|
|
284
|
+
|
|
285
|
+
echo "✅ Pre-commit security check passed"
|
|
286
|
+
exit 0
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Clear violations log
|
|
292
|
+
*/
|
|
293
|
+
clearViolations(): void {
|
|
294
|
+
this.violations = [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export default WorkspaceIsolation;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Commit Feature Tests
|
|
3
|
+
*
|
|
4
|
+
* Core test suite for auto-commit functionality:
|
|
5
|
+
* - File watching with configurable intervals
|
|
6
|
+
* - Change detection (skip if no changes)
|
|
7
|
+
* - Auto-commit prefix handling
|
|
8
|
+
* - Concurrent change handling
|
|
9
|
+
* - Performance requirements (<500ms)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { AutoCommitter } from '../src/auto-commit';
|
|
13
|
+
import { TraceCommands } from '../src/commands';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import * as os from 'os';
|
|
17
|
+
|
|
18
|
+
describe('Auto-Commit Feature', () => {
|
|
19
|
+
let testDir: string;
|
|
20
|
+
let homeDir: string;
|
|
21
|
+
let memoryDir: string;
|
|
22
|
+
let commands: TraceCommands;
|
|
23
|
+
let autoCommitter: AutoCommitter;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-git-'));
|
|
27
|
+
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'home-'));
|
|
28
|
+
process.env.HOME = homeDir;
|
|
29
|
+
memoryDir = path.join(homeDir, '.openclaw/memory');
|
|
30
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
31
|
+
|
|
32
|
+
commands = new TraceCommands(testDir);
|
|
33
|
+
autoCommitter = new AutoCommitter(commands, memoryDir);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
if (autoCommitter) {
|
|
38
|
+
autoCommitter.stop();
|
|
39
|
+
}
|
|
40
|
+
if (fs.existsSync(testDir)) {
|
|
41
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
if (fs.existsSync(homeDir)) {
|
|
44
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Interval configuration', () => {
|
|
49
|
+
it('should commit with default 60s interval', () => {
|
|
50
|
+
autoCommitter.start({}); // default interval
|
|
51
|
+
expect(autoCommitter.getInterval()).toBe(60000);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should respect custom interval', () => {
|
|
55
|
+
autoCommitter.start({ interval: 200 });
|
|
56
|
+
expect(autoCommitter.getInterval()).toBe(200);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should handle interval in milliseconds', () => {
|
|
60
|
+
autoCommitter.start({ interval: 1500 });
|
|
61
|
+
expect(autoCommitter.getInterval()).toBe(1500);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should allow interval update while watching', () => {
|
|
65
|
+
autoCommitter.start({ interval: 100 });
|
|
66
|
+
expect(autoCommitter.getInterval()).toBe(100);
|
|
67
|
+
|
|
68
|
+
autoCommitter.setInterval(200);
|
|
69
|
+
expect(autoCommitter.getInterval()).toBe(200);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('Watch control', () => {
|
|
74
|
+
it('should start watching', () => {
|
|
75
|
+
autoCommitter.start({ interval: 100 });
|
|
76
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should stop watching', () => {
|
|
80
|
+
autoCommitter.start({ interval: 100 });
|
|
81
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
82
|
+
|
|
83
|
+
autoCommitter.stop();
|
|
84
|
+
expect(autoCommitter.isRunning_()).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should not throw when stopping if not started', () => {
|
|
88
|
+
expect(() => {
|
|
89
|
+
autoCommitter.stop();
|
|
90
|
+
}).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle restart after stop', () => {
|
|
94
|
+
autoCommitter.start({ interval: 100 });
|
|
95
|
+
autoCommitter.stop();
|
|
96
|
+
autoCommitter.start({ interval: 100 });
|
|
97
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('Core logic - commit detection', () => {
|
|
102
|
+
it('should create initial commit', () => {
|
|
103
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'content');
|
|
104
|
+
const hash = commands.commit('initial');
|
|
105
|
+
expect(hash).toBeDefined();
|
|
106
|
+
expect(hash.length).toBeGreaterThan(0);
|
|
107
|
+
expect(typeof hash).toBe('string');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should detect file changes', () => {
|
|
111
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'content');
|
|
112
|
+
commands.commit('initial');
|
|
113
|
+
|
|
114
|
+
// Make change
|
|
115
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'updated');
|
|
116
|
+
|
|
117
|
+
const status = commands.status();
|
|
118
|
+
expect(status.modified.length).toBeGreaterThan(0);
|
|
119
|
+
expect(status.clean).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should detect added files', () => {
|
|
123
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'content');
|
|
124
|
+
commands.commit('initial');
|
|
125
|
+
|
|
126
|
+
// Add new file
|
|
127
|
+
fs.writeFileSync(path.join(memoryDir, 'new.md'), 'new');
|
|
128
|
+
|
|
129
|
+
const status = commands.status();
|
|
130
|
+
expect(status.added.length).toBeGreaterThan(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should detect deleted files', () => {
|
|
134
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'content');
|
|
135
|
+
commands.commit('initial');
|
|
136
|
+
|
|
137
|
+
// Delete file
|
|
138
|
+
fs.unlinkSync(path.join(memoryDir, 'test.md'));
|
|
139
|
+
|
|
140
|
+
const status = commands.status();
|
|
141
|
+
expect(status.deleted.length).toBeGreaterThan(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should skip commits when no changes', () => {
|
|
145
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'content');
|
|
146
|
+
commands.commit('initial');
|
|
147
|
+
|
|
148
|
+
const status = commands.status();
|
|
149
|
+
expect(status.clean).toBe(true);
|
|
150
|
+
expect(status.modified.length).toBe(0);
|
|
151
|
+
expect(status.added.length).toBe(0);
|
|
152
|
+
expect(status.deleted.length).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('Auto-commit markers', () => {
|
|
157
|
+
it('should mark commits with [auto] prefix', () => {
|
|
158
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'initial');
|
|
159
|
+
commands.commit('initial');
|
|
160
|
+
|
|
161
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'updated');
|
|
162
|
+
|
|
163
|
+
const message = '[auto] Memory snapshot (files: 1)';
|
|
164
|
+
const hash = commands.commit(message, 'agent', { auto: true });
|
|
165
|
+
|
|
166
|
+
const log = commands.log(1);
|
|
167
|
+
expect(log[0].message).toContain('[auto]');
|
|
168
|
+
expect(log[0].message).toContain('Memory snapshot');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should include file count in metadata', () => {
|
|
172
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'content');
|
|
173
|
+
commands.commit('initial');
|
|
174
|
+
|
|
175
|
+
fs.writeFileSync(path.join(memoryDir, 'file1.md'), 'new');
|
|
176
|
+
fs.writeFileSync(path.join(memoryDir, 'file2.md'), 'new');
|
|
177
|
+
|
|
178
|
+
const metadata = { auto: true, filesChanged: 2, timestamp: Date.now() };
|
|
179
|
+
commands.commit('[auto] Memory snapshot (files: 2)', 'agent', metadata);
|
|
180
|
+
|
|
181
|
+
const commit = commands.getCurrentCommit();
|
|
182
|
+
expect(commit?.metadata?.auto).toBe(true);
|
|
183
|
+
expect(commit?.metadata?.filesChanged).toBe(2);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should mark author as agent', () => {
|
|
187
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'content');
|
|
188
|
+
const hash = commands.commit('[auto] test', 'agent');
|
|
189
|
+
|
|
190
|
+
const log = commands.log(1);
|
|
191
|
+
expect(log[0].author).toBe('agent');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Ignore patterns', () => {
|
|
196
|
+
it('should skip node_modules by default', () => {
|
|
197
|
+
fs.mkdirSync(path.join(memoryDir, 'node_modules'), { recursive: true });
|
|
198
|
+
fs.writeFileSync(path.join(memoryDir, 'node_modules', 'pkg.json'), 'data');
|
|
199
|
+
|
|
200
|
+
// The auto-committer should ignore node_modules changes
|
|
201
|
+
autoCommitter.start({ interval: 100, ignorePatterns: ['node_modules'] });
|
|
202
|
+
|
|
203
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
204
|
+
autoCommitter.stop();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('Performance', () => {
|
|
209
|
+
it('should initialize quickly', () => {
|
|
210
|
+
const start = Date.now();
|
|
211
|
+
autoCommitter.start({ interval: 50 });
|
|
212
|
+
const duration = Date.now() - start;
|
|
213
|
+
|
|
214
|
+
expect(duration).toBeLessThan(100);
|
|
215
|
+
autoCommitter.stop();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should commit in under 500ms', () => {
|
|
219
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'x'.repeat(10000));
|
|
220
|
+
commands.commit('initial');
|
|
221
|
+
|
|
222
|
+
fs.writeFileSync(path.join(memoryDir, 'test.md'), 'x'.repeat(10001));
|
|
223
|
+
|
|
224
|
+
const start = Date.now();
|
|
225
|
+
const message = '[auto] Memory snapshot (files: 1)';
|
|
226
|
+
commands.commit(message, 'agent', { auto: true });
|
|
227
|
+
const duration = Date.now() - start;
|
|
228
|
+
|
|
229
|
+
expect(duration).toBeLessThan(500);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should handle many commits quickly', () => {
|
|
233
|
+
for (let i = 0; i < 50; i++) {
|
|
234
|
+
fs.writeFileSync(path.join(memoryDir, `file-${i}.txt`), `content ${i}`);
|
|
235
|
+
commands.commit(`commit ${i}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const log = commands.log(100);
|
|
239
|
+
expect(log.length).toBeGreaterThanOrEqual(50);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('Edge cases', () => {
|
|
244
|
+
it('should handle empty directory', () => {
|
|
245
|
+
autoCommitter.start({ interval: 100 });
|
|
246
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
247
|
+
autoCommitter.stop();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should handle special characters in filenames', () => {
|
|
251
|
+
fs.writeFileSync(path.join(memoryDir, 'initial.md'), 'start');
|
|
252
|
+
commands.commit('initial');
|
|
253
|
+
|
|
254
|
+
fs.writeFileSync(path.join(memoryDir, 'file-with-dash.md'), 'content');
|
|
255
|
+
fs.writeFileSync(path.join(memoryDir, 'file_with_underscore.md'), 'content');
|
|
256
|
+
|
|
257
|
+
const status = commands.status();
|
|
258
|
+
expect(status.added.length).toBeGreaterThanOrEqual(2);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should handle deeply nested files', () => {
|
|
262
|
+
const deep = path.join(memoryDir, 'a', 'b', 'c', 'd');
|
|
263
|
+
fs.mkdirSync(deep, { recursive: true });
|
|
264
|
+
fs.writeFileSync(path.join(deep, 'file.md'), 'content');
|
|
265
|
+
|
|
266
|
+
commands.commit('nested');
|
|
267
|
+
const log = commands.log(1);
|
|
268
|
+
expect(log[0].message).toBe('nested');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should not throw on permission errors', () => {
|
|
272
|
+
const file = path.join(memoryDir, 'readonly.md');
|
|
273
|
+
fs.writeFileSync(file, 'content');
|
|
274
|
+
commands.commit('initial');
|
|
275
|
+
|
|
276
|
+
autoCommitter.start({ interval: 100 });
|
|
277
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
278
|
+
|
|
279
|
+
autoCommitter.stop();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('Multi-interval scenarios', () => {
|
|
284
|
+
it('should handle interval changes mid-run', () => {
|
|
285
|
+
autoCommitter.start({ interval: 100 });
|
|
286
|
+
autoCommitter.setInterval(200);
|
|
287
|
+
autoCommitter.setInterval(50);
|
|
288
|
+
|
|
289
|
+
expect(autoCommitter.getInterval()).toBe(50);
|
|
290
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
291
|
+
|
|
292
|
+
autoCommitter.stop();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should stop and restart cleanly', () => {
|
|
296
|
+
autoCommitter.start({ interval: 100 });
|
|
297
|
+
autoCommitter.stop();
|
|
298
|
+
|
|
299
|
+
expect(autoCommitter.isRunning_()).toBe(false);
|
|
300
|
+
|
|
301
|
+
autoCommitter.start({ interval: 150 });
|
|
302
|
+
expect(autoCommitter.isRunning_()).toBe(true);
|
|
303
|
+
expect(autoCommitter.getInterval()).toBe(150);
|
|
304
|
+
|
|
305
|
+
autoCommitter.stop();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|