runwork 0.2.4 → 0.2.5
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/dist/commands/dev.js +20 -70
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/__tests__/auto-commit.test.d.ts +1 -0
- package/dist/git/__tests__/auto-commit.test.js +373 -0
- package/dist/git/__tests__/manifest.test.d.ts +1 -0
- package/dist/git/__tests__/manifest.test.js +377 -0
- package/dist/git/__tests__/sync.test.d.ts +1 -0
- package/dist/git/__tests__/sync.test.js +403 -0
- package/dist/git/auto-commit.d.ts +1 -0
- package/dist/git/auto-commit.js +15 -5
- package/dist/git/sync.d.ts +15 -0
- package/dist/git/sync.js +116 -0
- package/dist/template/manifest.js +24 -4
- package/package.json +1 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync, existsSync, mkdirSync, symlinkSync, } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { generateManifest, saveManifest, loadManifest, detectUserEdits, } from '../../template/manifest.js';
|
|
7
|
+
const tempDirs = [];
|
|
8
|
+
function makeTempDir(prefix) {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), `runwork-manifest-test-${prefix}-`));
|
|
10
|
+
tempDirs.push(dir);
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
function initGitRepo(dir) {
|
|
14
|
+
execFileSync('git', ['init', dir]);
|
|
15
|
+
execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: dir });
|
|
16
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: dir });
|
|
17
|
+
}
|
|
18
|
+
function commitFile(repoDir, filename, content, message) {
|
|
19
|
+
const fullPath = join(repoDir, filename);
|
|
20
|
+
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
|
|
21
|
+
if (dir !== repoDir && !existsSync(dir)) {
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
writeFileSync(fullPath, content);
|
|
25
|
+
execFileSync('git', ['add', filename], { cwd: repoDir });
|
|
26
|
+
execFileSync('git', ['commit', '-m', message], { cwd: repoDir });
|
|
27
|
+
}
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
for (const dir of tempDirs) {
|
|
30
|
+
try {
|
|
31
|
+
rmSync(dir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
catch { /* best effort */ }
|
|
34
|
+
}
|
|
35
|
+
tempDirs.length = 0;
|
|
36
|
+
});
|
|
37
|
+
describe('generateManifest()', () => {
|
|
38
|
+
it('generates manifest for a simple directory', async () => {
|
|
39
|
+
const dir = makeTempDir('simple');
|
|
40
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
41
|
+
writeFileSync(join(dir, 'b.txt'), 'world');
|
|
42
|
+
const manifest = await generateManifest(dir);
|
|
43
|
+
expect(manifest.version).toBe(1);
|
|
44
|
+
expect(Object.keys(manifest.files)).toHaveLength(2);
|
|
45
|
+
expect(manifest.files['a.txt']).toMatch(/^sha256:/);
|
|
46
|
+
expect(manifest.files['b.txt']).toMatch(/^sha256:/);
|
|
47
|
+
});
|
|
48
|
+
it('skips .git, node_modules, and .runwork directories', async () => {
|
|
49
|
+
const dir = makeTempDir('skip');
|
|
50
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
51
|
+
mkdirSync(join(dir, '.git'));
|
|
52
|
+
writeFileSync(join(dir, '.git', 'config'), 'git config');
|
|
53
|
+
mkdirSync(join(dir, 'node_modules'));
|
|
54
|
+
writeFileSync(join(dir, 'node_modules', 'pkg.js'), 'module');
|
|
55
|
+
mkdirSync(join(dir, '.runwork'));
|
|
56
|
+
writeFileSync(join(dir, '.runwork', 'manifest.json'), '{}');
|
|
57
|
+
const manifest = await generateManifest(dir);
|
|
58
|
+
expect(Object.keys(manifest.files)).toHaveLength(1);
|
|
59
|
+
expect(manifest.files['a.txt']).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
it('skips .dev.vars and .env files', async () => {
|
|
62
|
+
const dir = makeTempDir('skipfiles');
|
|
63
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
64
|
+
writeFileSync(join(dir, '.dev.vars'), 'SECRET=123');
|
|
65
|
+
writeFileSync(join(dir, '.env'), 'OTHER=456');
|
|
66
|
+
const manifest = await generateManifest(dir);
|
|
67
|
+
expect(Object.keys(manifest.files)).toHaveLength(1);
|
|
68
|
+
expect(manifest.files['a.txt']).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
it('handles files with spaces in names', async () => {
|
|
71
|
+
const dir = makeTempDir('spaces');
|
|
72
|
+
writeFileSync(join(dir, 'hello world.txt'), 'content');
|
|
73
|
+
writeFileSync(join(dir, 'normal.txt'), 'content');
|
|
74
|
+
const manifest = await generateManifest(dir);
|
|
75
|
+
expect(Object.keys(manifest.files)).toHaveLength(2);
|
|
76
|
+
expect(manifest.files['hello world.txt']).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
it('handles nested directories', async () => {
|
|
79
|
+
const dir = makeTempDir('nested');
|
|
80
|
+
mkdirSync(join(dir, 'src', 'components'), { recursive: true });
|
|
81
|
+
writeFileSync(join(dir, 'src', 'index.ts'), 'export {}');
|
|
82
|
+
writeFileSync(join(dir, 'src', 'components', 'App.tsx'), '<App/>');
|
|
83
|
+
const manifest = await generateManifest(dir);
|
|
84
|
+
expect(manifest.files['src/index.ts']).toBeDefined();
|
|
85
|
+
expect(manifest.files['src/components/App.tsx']).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('walkDir() symlink edge cases', () => {
|
|
89
|
+
it('BUG PROBE: symlink cycle causes infinite recursion in walkDir/generateManifest', async () => {
|
|
90
|
+
// walkDir uses readdirSync with withFileTypes: true.
|
|
91
|
+
// Dirent.isDirectory() follows symlinks, so a symlink pointing
|
|
92
|
+
// to a parent directory will cause infinite recursion.
|
|
93
|
+
const dir = makeTempDir('symlink-cycle');
|
|
94
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
95
|
+
mkdirSync(join(dir, 'subdir'));
|
|
96
|
+
writeFileSync(join(dir, 'subdir', 'b.txt'), 'world');
|
|
97
|
+
// Create a symlink cycle: subdir/link -> parent dir
|
|
98
|
+
symlinkSync(dir, join(dir, 'subdir', 'link'));
|
|
99
|
+
// This should not hang or crash, but walkDir follows symlinks
|
|
100
|
+
// via isDirectory() which returns true for symlink-to-directory.
|
|
101
|
+
// Expected: either it handles symlinks gracefully, or it crashes
|
|
102
|
+
// with max call stack exceeded.
|
|
103
|
+
let threw = false;
|
|
104
|
+
let errorMessage = '';
|
|
105
|
+
try {
|
|
106
|
+
// Set a timeout to detect infinite loops - if this takes too long, it's a bug
|
|
107
|
+
await generateManifest(dir);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
threw = true;
|
|
111
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
112
|
+
}
|
|
113
|
+
// If we get here without hanging, check if it threw
|
|
114
|
+
// The bug is that walkDir WILL follow the symlink and recurse infinitely
|
|
115
|
+
// until it hits ENAMETOOLONG or max call stack.
|
|
116
|
+
// We expect this to either throw or produce duplicate entries.
|
|
117
|
+
// If it doesn't throw, it somehow handled it (unlikely).
|
|
118
|
+
if (threw) {
|
|
119
|
+
// Bug confirmed: walkDir crashes on symlink cycles
|
|
120
|
+
expect(errorMessage).toBeTruthy();
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// If it somehow succeeds, check that it didn't include
|
|
124
|
+
// infinitely nested paths
|
|
125
|
+
expect(true).toBe(true); // pass - it handled it somehow
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
it('symlinked directories are silently skipped by walkDir (not followed)', async () => {
|
|
129
|
+
// Node.js readdirSync withFileTypes: Dirent.isDirectory() returns false for symlinks.
|
|
130
|
+
// So walkDir silently skips symlinked directories entirely.
|
|
131
|
+
// This is safe (no infinite recursion) but means symlinked content is invisible.
|
|
132
|
+
const dir = makeTempDir('symlink-dir');
|
|
133
|
+
const targetDir = makeTempDir('symlink-target');
|
|
134
|
+
writeFileSync(join(targetDir, 'external.txt'), 'external content');
|
|
135
|
+
writeFileSync(join(dir, 'local.txt'), 'local');
|
|
136
|
+
symlinkSync(targetDir, join(dir, 'linked'));
|
|
137
|
+
const manifest = await generateManifest(dir);
|
|
138
|
+
const keys = Object.keys(manifest.files);
|
|
139
|
+
const hasLinkedFile = keys.some(k => k.includes('linked'));
|
|
140
|
+
// Symlinked directories are NOT followed -- content is invisible
|
|
141
|
+
expect(hasLinkedFile).toBe(false);
|
|
142
|
+
expect(keys).toEqual(['local.txt']);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe('saveManifest() and loadManifest()', () => {
|
|
146
|
+
it('creates .runwork directory if it does not exist', async () => {
|
|
147
|
+
const dir = makeTempDir('norunwork');
|
|
148
|
+
const manifest = { version: 1, files: { 'a.txt': 'sha256:abc' } };
|
|
149
|
+
expect(existsSync(join(dir, '.runwork'))).toBe(false);
|
|
150
|
+
await saveManifest(dir, manifest);
|
|
151
|
+
expect(existsSync(join(dir, '.runwork', 'template-manifest.json'))).toBe(true);
|
|
152
|
+
const loaded = await loadManifest(dir);
|
|
153
|
+
expect(loaded).toEqual(manifest);
|
|
154
|
+
});
|
|
155
|
+
it('returns null when .runwork directory does not exist', async () => {
|
|
156
|
+
const dir = makeTempDir('nomanifest');
|
|
157
|
+
const result = await loadManifest(dir);
|
|
158
|
+
expect(result).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
it('returns null for corrupted manifest file', async () => {
|
|
161
|
+
const dir = makeTempDir('corrupt');
|
|
162
|
+
mkdirSync(join(dir, '.runwork'));
|
|
163
|
+
writeFileSync(join(dir, '.runwork', 'template-manifest.json'), 'not json{{{');
|
|
164
|
+
const result = await loadManifest(dir);
|
|
165
|
+
expect(result).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('detectUserEdits() edge cases', () => {
|
|
169
|
+
it('returns empty array when git diff and ls-files return nothing', async () => {
|
|
170
|
+
// Edge case: empty strings from git commands.
|
|
171
|
+
// "".split('\n') returns [''] (array with one empty string).
|
|
172
|
+
// The code should handle this gracefully.
|
|
173
|
+
const dir = makeTempDir('clean');
|
|
174
|
+
initGitRepo(dir);
|
|
175
|
+
const content = 'hello';
|
|
176
|
+
writeFileSync(join(dir, 'a.txt'), content);
|
|
177
|
+
execFileSync('git', ['add', 'a.txt'], { cwd: dir });
|
|
178
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
179
|
+
// Use correct hash so manifest scan doesn't flag it as modified
|
|
180
|
+
const { createHash } = await import('crypto');
|
|
181
|
+
const hash = 'sha256:' + createHash('sha256').update(Buffer.from(content)).digest('hex');
|
|
182
|
+
const manifest = {
|
|
183
|
+
version: 1,
|
|
184
|
+
files: { 'a.txt': hash },
|
|
185
|
+
};
|
|
186
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
187
|
+
expect(edits).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
it('BUG PROBE: files with spaces in names from git diff output', async () => {
|
|
190
|
+
// git diff --name-only returns filenames with spaces as-is.
|
|
191
|
+
// The code splits by \n and processes each line.
|
|
192
|
+
// This should work unless there's quoting involved.
|
|
193
|
+
const dir = makeTempDir('spaces');
|
|
194
|
+
initGitRepo(dir);
|
|
195
|
+
writeFileSync(join(dir, 'hello world.txt'), 'original');
|
|
196
|
+
execFileSync('git', ['add', 'hello world.txt'], { cwd: dir });
|
|
197
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
198
|
+
// Modify the file with spaces
|
|
199
|
+
writeFileSync(join(dir, 'hello world.txt'), 'modified');
|
|
200
|
+
const manifest = { version: 1, files: {} };
|
|
201
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
202
|
+
// Should contain the file with spaces
|
|
203
|
+
expect(edits).toContain('hello world.txt');
|
|
204
|
+
});
|
|
205
|
+
it('returns correct unicode filenames (not octal-escaped)', async () => {
|
|
206
|
+
const dir = makeTempDir('unicode');
|
|
207
|
+
initGitRepo(dir);
|
|
208
|
+
const filename = 'caf\u00e9.txt';
|
|
209
|
+
writeFileSync(join(dir, filename), 'original');
|
|
210
|
+
execFileSync('git', ['add', filename], { cwd: dir });
|
|
211
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
212
|
+
writeFileSync(join(dir, filename), 'modified');
|
|
213
|
+
const manifest = { version: 1, files: {} };
|
|
214
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
215
|
+
expect(edits).toContain(filename);
|
|
216
|
+
// Should NOT contain octal-escaped garbage
|
|
217
|
+
expect(edits.some(e => e.includes('\\303'))).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
it('BUG PROBE: file deleted by user that was in manifest (untracked section)', async () => {
|
|
220
|
+
// If a user deletes a file that was in the manifest, it won't
|
|
221
|
+
// appear in git ls-files --others (because it doesn't exist on disk).
|
|
222
|
+
// It also won't appear in git diff --name-only HEAD because it was
|
|
223
|
+
// never committed. So detectUserEdits will miss it entirely.
|
|
224
|
+
// This is arguably correct behavior (delete = no edit to detect),
|
|
225
|
+
// but worth documenting.
|
|
226
|
+
const dir = makeTempDir('deleted');
|
|
227
|
+
initGitRepo(dir);
|
|
228
|
+
writeFileSync(join(dir, 'committed.txt'), 'hello');
|
|
229
|
+
execFileSync('git', ['add', '.'], { cwd: dir });
|
|
230
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
231
|
+
// Template manifest includes both a committed file and a template file
|
|
232
|
+
const manifest = {
|
|
233
|
+
version: 1,
|
|
234
|
+
files: {
|
|
235
|
+
'committed.txt': 'sha256:abc',
|
|
236
|
+
'template-only.txt': 'sha256:def',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
// template-only.txt never existed on disk (or was deleted)
|
|
240
|
+
// detectUserEdits should not crash
|
|
241
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
242
|
+
// No crash is the main assertion
|
|
243
|
+
expect(Array.isArray(edits)).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
it('detects untracked files that are not in the manifest', async () => {
|
|
246
|
+
const dir = makeTempDir('newfile');
|
|
247
|
+
initGitRepo(dir);
|
|
248
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
249
|
+
execFileSync('git', ['add', '.'], { cwd: dir });
|
|
250
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
251
|
+
// User creates a new file
|
|
252
|
+
writeFileSync(join(dir, 'user-created.txt'), 'my new file');
|
|
253
|
+
const manifest = {
|
|
254
|
+
version: 1,
|
|
255
|
+
files: { 'a.txt': 'sha256:abc' },
|
|
256
|
+
};
|
|
257
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
258
|
+
expect(edits).toContain('user-created.txt');
|
|
259
|
+
});
|
|
260
|
+
it('skips untracked file that matches template manifest hash (pristine template file)', async () => {
|
|
261
|
+
const dir = makeTempDir('pristine');
|
|
262
|
+
initGitRepo(dir);
|
|
263
|
+
writeFileSync(join(dir, 'committed.txt'), 'hello');
|
|
264
|
+
execFileSync('git', ['add', '.'], { cwd: dir });
|
|
265
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
266
|
+
// Create an untracked file that matches the template hash exactly
|
|
267
|
+
const content = 'template content';
|
|
268
|
+
writeFileSync(join(dir, 'template-file.txt'), content);
|
|
269
|
+
// Generate the correct hash for this content
|
|
270
|
+
const { createHash } = await import('crypto');
|
|
271
|
+
const hash = 'sha256:' + createHash('sha256').update(Buffer.from(content)).digest('hex');
|
|
272
|
+
const manifest = {
|
|
273
|
+
version: 1,
|
|
274
|
+
files: {
|
|
275
|
+
'committed.txt': 'sha256:abc',
|
|
276
|
+
'template-file.txt': hash,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
280
|
+
// Pristine template file should NOT be in edits
|
|
281
|
+
expect(edits).not.toContain('template-file.txt');
|
|
282
|
+
});
|
|
283
|
+
it('BUG PROBE: detects edit when untracked file has different content than manifest hash', async () => {
|
|
284
|
+
const dir = makeTempDir('modified-template');
|
|
285
|
+
initGitRepo(dir);
|
|
286
|
+
writeFileSync(join(dir, 'committed.txt'), 'hello');
|
|
287
|
+
execFileSync('git', ['add', '.'], { cwd: dir });
|
|
288
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
289
|
+
// Create an untracked file with content that differs from manifest
|
|
290
|
+
writeFileSync(join(dir, 'template-file.txt'), 'user modified this');
|
|
291
|
+
const manifest = {
|
|
292
|
+
version: 1,
|
|
293
|
+
files: {
|
|
294
|
+
'committed.txt': 'sha256:abc',
|
|
295
|
+
'template-file.txt': 'sha256:different_hash_from_template',
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
299
|
+
// Modified template file SHOULD be in edits
|
|
300
|
+
expect(edits).toContain('template-file.txt');
|
|
301
|
+
});
|
|
302
|
+
it('does not duplicate files that appear in both git diff and git diff --cached', async () => {
|
|
303
|
+
const dir = makeTempDir('dedup');
|
|
304
|
+
initGitRepo(dir);
|
|
305
|
+
writeFileSync(join(dir, 'a.txt'), 'original');
|
|
306
|
+
execFileSync('git', ['add', '.'], { cwd: dir });
|
|
307
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
308
|
+
// Stage a change AND have unstaged changes to same file
|
|
309
|
+
writeFileSync(join(dir, 'a.txt'), 'staged change');
|
|
310
|
+
execFileSync('git', ['add', 'a.txt'], { cwd: dir });
|
|
311
|
+
writeFileSync(join(dir, 'a.txt'), 'unstaged further change');
|
|
312
|
+
const manifest = { version: 1, files: {} };
|
|
313
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
314
|
+
// File should appear only once even though it's in both diff outputs
|
|
315
|
+
const count = edits.filter(f => f === 'a.txt').length;
|
|
316
|
+
expect(count).toBe(1);
|
|
317
|
+
});
|
|
318
|
+
it('BUG PROBE: git diff --name-only HEAD with no commits yet', async () => {
|
|
319
|
+
// When repo has no commits, git rev-parse HEAD fails.
|
|
320
|
+
// The code wraps this in try/catch, so it should skip the git diff section.
|
|
321
|
+
// But what about git ls-files --others?
|
|
322
|
+
const dir = makeTempDir('nocommits');
|
|
323
|
+
initGitRepo(dir);
|
|
324
|
+
// Create a file but don't commit
|
|
325
|
+
writeFileSync(join(dir, 'new-file.txt'), 'hello');
|
|
326
|
+
const manifest = { version: 1, files: {} };
|
|
327
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
328
|
+
// Should detect the untracked file via ls-files --others
|
|
329
|
+
expect(edits).toContain('new-file.txt');
|
|
330
|
+
});
|
|
331
|
+
it('detects modified gitignored files that are in the template manifest', async () => {
|
|
332
|
+
// Real-world scenario: template ships with a config file, user adds it
|
|
333
|
+
// to .gitignore (e.g., to keep local secrets), then modifies it.
|
|
334
|
+
// detectUserEdits should still detect it by scanning manifest files on disk.
|
|
335
|
+
const dir = makeTempDir('gitignore');
|
|
336
|
+
initGitRepo(dir);
|
|
337
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
338
|
+
writeFileSync(join(dir, '.gitignore'), 'ignored.txt\n');
|
|
339
|
+
execFileSync('git', ['add', '.'], { cwd: dir });
|
|
340
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
341
|
+
// Create a file that's gitignored but was in the template manifest
|
|
342
|
+
writeFileSync(join(dir, 'ignored.txt'), 'user modified this ignored file');
|
|
343
|
+
const manifest = {
|
|
344
|
+
version: 1,
|
|
345
|
+
files: {
|
|
346
|
+
'a.txt': 'sha256:abc',
|
|
347
|
+
'ignored.txt': 'sha256:original_template_hash',
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
351
|
+
// Fixed: gitignored file IS detected because we scan manifest files on disk
|
|
352
|
+
expect(edits).toContain('ignored.txt');
|
|
353
|
+
});
|
|
354
|
+
it('does not flag gitignored file if it matches template manifest hash', async () => {
|
|
355
|
+
// If the gitignored file has NOT been modified (hash matches manifest),
|
|
356
|
+
// it should NOT appear in edits.
|
|
357
|
+
const dir = makeTempDir('gitignore-pristine');
|
|
358
|
+
initGitRepo(dir);
|
|
359
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
360
|
+
writeFileSync(join(dir, '.gitignore'), 'config.txt\n');
|
|
361
|
+
execFileSync('git', ['add', '.'], { cwd: dir });
|
|
362
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: dir });
|
|
363
|
+
const content = 'pristine template content';
|
|
364
|
+
writeFileSync(join(dir, 'config.txt'), content);
|
|
365
|
+
const { createHash } = await import('crypto');
|
|
366
|
+
const hash = 'sha256:' + createHash('sha256').update(Buffer.from(content)).digest('hex');
|
|
367
|
+
const manifest = {
|
|
368
|
+
version: 1,
|
|
369
|
+
files: {
|
|
370
|
+
'a.txt': 'sha256:abc',
|
|
371
|
+
'config.txt': hash,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
375
|
+
expect(edits).not.toContain('config.txt');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|