cursor-guard 1.4.0 → 2.0.2

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.
@@ -0,0 +1,329 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const {
8
+ globMatch, matchesAny, loadConfig, DEFAULT_CONFIG, DEFAULT_SECRETS,
9
+ filterFiles, buildManifest, manifestChanged, parseArgs, walkDir,
10
+ } = require('./utils');
11
+
12
+ let passed = 0;
13
+ let failed = 0;
14
+
15
+ function test(name, fn) {
16
+ try {
17
+ fn();
18
+ passed++;
19
+ console.log(` \x1b[32m✓\x1b[0m ${name}`);
20
+ } catch (e) {
21
+ failed++;
22
+ console.log(` \x1b[31m✗\x1b[0m ${name}`);
23
+ console.log(` ${e.message}`);
24
+ }
25
+ }
26
+
27
+ // ── globMatch ────────────────────────────────────────────────────
28
+
29
+ console.log('\nglobMatch:');
30
+
31
+ test('exact filename match', () => {
32
+ assert.strictEqual(globMatch('.env', '.env'), true);
33
+ assert.strictEqual(globMatch('.env', '.envx'), false);
34
+ });
35
+
36
+ test('* matches within a single segment', () => {
37
+ assert.strictEqual(globMatch('*.js', 'foo.js'), true);
38
+ assert.strictEqual(globMatch('*.js', 'bar.ts'), false);
39
+ assert.strictEqual(globMatch('*.js', 'dir/foo.js'), false);
40
+ });
41
+
42
+ test('** matches across directories', () => {
43
+ assert.strictEqual(globMatch('**/*.js', 'src/foo.js'), true);
44
+ assert.strictEqual(globMatch('**/*.js', 'a/b/c/foo.js'), true);
45
+ // **/*.js requires a slash — root-level 'foo.js' doesn't match (matchesAny checks leaf separately)
46
+ assert.strictEqual(globMatch('**/*.js', 'foo.js'), false);
47
+ assert.strictEqual(globMatch('**/*.js', 'foo.ts'), false);
48
+ });
49
+
50
+ test('? matches single character', () => {
51
+ assert.strictEqual(globMatch('?.txt', 'a.txt'), true);
52
+ assert.strictEqual(globMatch('?.txt', 'ab.txt'), false);
53
+ });
54
+
55
+ test('.env.* pattern', () => {
56
+ assert.strictEqual(globMatch('.env.*', '.env.local'), true);
57
+ assert.strictEqual(globMatch('.env.*', '.env.production'), true);
58
+ assert.strictEqual(globMatch('.env.*', '.env'), false);
59
+ });
60
+
61
+ test('credentials* pattern', () => {
62
+ assert.strictEqual(globMatch('credentials*', 'credentials'), true);
63
+ assert.strictEqual(globMatch('credentials*', 'credentials.json'), true);
64
+ assert.strictEqual(globMatch('credentials*', 'my-credentials'), false);
65
+ });
66
+
67
+ test('directory pattern src/**', () => {
68
+ assert.strictEqual(globMatch('src/**', 'src/foo.js'), true);
69
+ assert.strictEqual(globMatch('src/**', 'src/a/b.js'), true);
70
+ assert.strictEqual(globMatch('src/**', 'lib/foo.js'), false);
71
+ });
72
+
73
+ test('backslash normalization', () => {
74
+ assert.strictEqual(globMatch('src/**/*.ts', 'src\\components\\App.ts'), true);
75
+ });
76
+
77
+ test('regex special chars in pattern are escaped', () => {
78
+ assert.strictEqual(globMatch('file(1).txt', 'file(1).txt'), true);
79
+ assert.strictEqual(globMatch('file[0].txt', 'file[0].txt'), true); // [] escaped as literals
80
+ });
81
+
82
+ // ── matchesAny ───────────────────────────────────────────────────
83
+
84
+ console.log('\nmatchesAny:');
85
+
86
+ test('matches when any pattern hits', () => {
87
+ assert.strictEqual(matchesAny(['*.js', '*.ts'], 'foo.js'), true);
88
+ assert.strictEqual(matchesAny(['*.js', '*.ts'], 'foo.ts'), true);
89
+ assert.strictEqual(matchesAny(['*.js', '*.ts'], 'foo.py'), false);
90
+ });
91
+
92
+ test('checks leaf filename for deep paths', () => {
93
+ assert.strictEqual(matchesAny(['*.key'], 'secrets/server.key'), true);
94
+ assert.strictEqual(matchesAny(['.env'], 'config/.env'), true);
95
+ });
96
+
97
+ test('empty patterns matches nothing', () => {
98
+ assert.strictEqual(matchesAny([], 'anything'), false);
99
+ });
100
+
101
+ // ── loadConfig ───────────────────────────────────────────────────
102
+
103
+ console.log('\nloadConfig:');
104
+
105
+ test('returns defaults when no config file', () => {
106
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
107
+ try {
108
+ const { cfg, loaded, error } = loadConfig(tmpDir);
109
+ assert.strictEqual(loaded, false);
110
+ assert.strictEqual(error, null);
111
+ assert.deepStrictEqual(cfg.protect, []);
112
+ assert.deepStrictEqual(cfg.ignore, []);
113
+ assert.deepStrictEqual(cfg.secrets_patterns, DEFAULT_SECRETS);
114
+ assert.strictEqual(cfg.backup_strategy, 'git');
115
+ assert.strictEqual(cfg.git_retention.enabled, false);
116
+ } finally {
117
+ fs.rmSync(tmpDir, { recursive: true, force: true });
118
+ }
119
+ });
120
+
121
+ test('loads and merges custom config', () => {
122
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
123
+ try {
124
+ fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
125
+ protect: ['src/**'],
126
+ backup_strategy: 'both',
127
+ retention: { mode: 'count', max_count: 50 },
128
+ }));
129
+ const { cfg, loaded, error } = loadConfig(tmpDir);
130
+ assert.strictEqual(loaded, true);
131
+ assert.strictEqual(error, null);
132
+ assert.deepStrictEqual(cfg.protect, ['src/**']);
133
+ assert.strictEqual(cfg.backup_strategy, 'both');
134
+ assert.strictEqual(cfg.retention.mode, 'count');
135
+ assert.strictEqual(cfg.retention.max_count, 50);
136
+ assert.strictEqual(cfg.retention.days, 30); // default preserved
137
+ } finally {
138
+ fs.rmSync(tmpDir, { recursive: true, force: true });
139
+ }
140
+ });
141
+
142
+ test('handles malformed JSON gracefully', () => {
143
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
144
+ try {
145
+ fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), '{ broken }');
146
+ const { cfg, loaded, error } = loadConfig(tmpDir);
147
+ assert.strictEqual(loaded, false);
148
+ assert.ok(error, 'should have an error message');
149
+ assert.deepStrictEqual(cfg.protect, []);
150
+ } finally {
151
+ fs.rmSync(tmpDir, { recursive: true, force: true });
152
+ }
153
+ });
154
+
155
+ test('secrets_patterns override replaces defaults entirely', () => {
156
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
157
+ try {
158
+ fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
159
+ secrets_patterns: ['my-secret'],
160
+ }));
161
+ const { cfg } = loadConfig(tmpDir);
162
+ assert.deepStrictEqual(cfg.secrets_patterns, ['my-secret']);
163
+ } finally {
164
+ fs.rmSync(tmpDir, { recursive: true, force: true });
165
+ }
166
+ });
167
+
168
+ test('secrets_patterns_extra appends to defaults', () => {
169
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
170
+ try {
171
+ fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
172
+ secrets_patterns_extra: ['*.secret', 'tokens.*'],
173
+ }));
174
+ const { cfg } = loadConfig(tmpDir);
175
+ assert.ok(cfg.secrets_patterns.includes('.env'), 'should keep default .env');
176
+ assert.ok(cfg.secrets_patterns.includes('*.p12'), 'should keep default *.p12');
177
+ assert.ok(cfg.secrets_patterns.includes('*.secret'), 'should include extra *.secret');
178
+ assert.ok(cfg.secrets_patterns.includes('tokens.*'), 'should include extra tokens.*');
179
+ } finally {
180
+ fs.rmSync(tmpDir, { recursive: true, force: true });
181
+ }
182
+ });
183
+
184
+ test('secrets_patterns_extra merges with custom secrets_patterns', () => {
185
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
186
+ try {
187
+ fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
188
+ secrets_patterns: ['.env'],
189
+ secrets_patterns_extra: ['.env', '*.secret'],
190
+ }));
191
+ const { cfg } = loadConfig(tmpDir);
192
+ assert.deepStrictEqual(cfg.secrets_patterns, ['.env', '*.secret']);
193
+ } finally {
194
+ fs.rmSync(tmpDir, { recursive: true, force: true });
195
+ }
196
+ });
197
+
198
+ test('non-string backup_strategy is ignored', () => {
199
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
200
+ try {
201
+ fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
202
+ backup_strategy: 123,
203
+ auto_backup_interval_seconds: 'bad',
204
+ }));
205
+ const { cfg } = loadConfig(tmpDir);
206
+ assert.strictEqual(cfg.backup_strategy, 'git');
207
+ assert.strictEqual(cfg.auto_backup_interval_seconds, 60);
208
+ } finally {
209
+ fs.rmSync(tmpDir, { recursive: true, force: true });
210
+ }
211
+ });
212
+
213
+ // ── filterFiles ──────────────────────────────────────────────────
214
+
215
+ console.log('\nfilterFiles:');
216
+
217
+ const makeFiles = names => names.map(n => ({ full: `/fake/${n}`, rel: n, name: path.basename(n) }));
218
+
219
+ test('no protect/ignore returns all non-secret files', () => {
220
+ const files = makeFiles(['a.js', 'b.ts', '.env', 'credentials.json']);
221
+ const cfg = { ...DEFAULT_CONFIG };
222
+ const result = filterFiles(files, cfg);
223
+ const rels = result.map(f => f.rel);
224
+ assert.ok(!rels.includes('.env'));
225
+ assert.ok(!rels.includes('credentials.json'));
226
+ assert.ok(rels.includes('a.js'));
227
+ assert.ok(rels.includes('b.ts'));
228
+ });
229
+
230
+ test('protect narrows scope', () => {
231
+ const files = makeFiles(['src/a.js', 'lib/b.js', 'README.md']);
232
+ const cfg = { ...DEFAULT_CONFIG, protect: ['src/**'] };
233
+ const result = filterFiles(files, cfg);
234
+ assert.strictEqual(result.length, 1);
235
+ assert.strictEqual(result[0].rel, 'src/a.js');
236
+ });
237
+
238
+ test('ignore excludes files', () => {
239
+ const files = makeFiles(['src/a.js', 'src/a.test.js', 'src/b.js']);
240
+ const cfg = { ...DEFAULT_CONFIG, ignore: ['**/*.test.js'] };
241
+ const result = filterFiles(files, cfg);
242
+ const rels = result.map(f => f.rel);
243
+ assert.ok(!rels.includes('src/a.test.js'));
244
+ assert.ok(rels.includes('src/a.js'));
245
+ });
246
+
247
+ // ── manifestChanged ──────────────────────────────────────────────
248
+
249
+ console.log('\nmanifestChanged:');
250
+
251
+ test('null old manifest means changed', () => {
252
+ assert.strictEqual(manifestChanged(null, { 'a.js': { mtimeMs: 1, size: 100 } }), true);
253
+ });
254
+
255
+ test('identical manifests are not changed', () => {
256
+ const m = { 'a.js': { mtimeMs: 1, size: 100 } };
257
+ assert.strictEqual(manifestChanged(m, { ...m }), false);
258
+ });
259
+
260
+ test('different mtime means changed', () => {
261
+ const old = { 'a.js': { mtimeMs: 1, size: 100 } };
262
+ const nw = { 'a.js': { mtimeMs: 2, size: 100 } };
263
+ assert.strictEqual(manifestChanged(old, nw), true);
264
+ });
265
+
266
+ test('new file means changed', () => {
267
+ const old = { 'a.js': { mtimeMs: 1, size: 100 } };
268
+ const nw = { 'a.js': { mtimeMs: 1, size: 100 }, 'b.js': { mtimeMs: 2, size: 50 } };
269
+ assert.strictEqual(manifestChanged(old, nw), true);
270
+ });
271
+
272
+ // ── parseArgs ────────────────────────────────────────────────────
273
+
274
+ console.log('\nparseArgs:');
275
+
276
+ test('parses --key value pairs', () => {
277
+ const args = parseArgs(['node', 'script', '--path', '/tmp', '--interval', '30']);
278
+ assert.strictEqual(args.path, '/tmp');
279
+ assert.strictEqual(args.interval, '30');
280
+ });
281
+
282
+ test('parses boolean flags', () => {
283
+ const args = parseArgs(['node', 'script', '--verbose']);
284
+ assert.strictEqual(args.verbose, true);
285
+ });
286
+
287
+ test('empty args returns empty object', () => {
288
+ const args = parseArgs(['node', 'script']);
289
+ assert.deepStrictEqual(args, {});
290
+ });
291
+
292
+ // ── walkDir ──────────────────────────────────────────────────────
293
+
294
+ console.log('\nwalkDir:');
295
+
296
+ test('discovers files recursively', () => {
297
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-walk-'));
298
+ try {
299
+ fs.mkdirSync(path.join(tmpDir, 'sub'), { recursive: true });
300
+ fs.writeFileSync(path.join(tmpDir, 'a.txt'), 'a');
301
+ fs.writeFileSync(path.join(tmpDir, 'sub', 'b.txt'), 'b');
302
+ const files = walkDir(tmpDir, tmpDir);
303
+ const rels = files.map(f => f.rel).sort();
304
+ assert.deepStrictEqual(rels, ['a.txt', 'sub/b.txt']);
305
+ } finally {
306
+ fs.rmSync(tmpDir, { recursive: true, force: true });
307
+ }
308
+ });
309
+
310
+ test('skips .git and node_modules', () => {
311
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-walk-'));
312
+ try {
313
+ fs.mkdirSync(path.join(tmpDir, '.git'), { recursive: true });
314
+ fs.mkdirSync(path.join(tmpDir, 'node_modules'), { recursive: true });
315
+ fs.writeFileSync(path.join(tmpDir, '.git', 'HEAD'), 'ref');
316
+ fs.writeFileSync(path.join(tmpDir, 'node_modules', 'x.js'), 'x');
317
+ fs.writeFileSync(path.join(tmpDir, 'real.js'), 'y');
318
+ const files = walkDir(tmpDir, tmpDir);
319
+ assert.strictEqual(files.length, 1);
320
+ assert.strictEqual(files[0].rel, 'real.js');
321
+ } finally {
322
+ fs.rmSync(tmpDir, { recursive: true, force: true });
323
+ }
324
+ });
325
+
326
+ // ── Summary ──────────────────────────────────────────────────────
327
+
328
+ console.log(`\n${passed + failed} tests: \x1b[32m${passed} passed\x1b[0m` + (failed ? `, \x1b[31m${failed} failed\x1b[0m` : ''));
329
+ process.exit(failed > 0 ? 1 : 0);