fix(tests): use fs.cpSync for fixture copy to handle subdirectories#584
fix(tests): use fs.cpSync for fixture copy to handle subdirectories#584carlos-alm merged 5 commits intomainfrom
Conversation
copyFileSync throws EPERM when readdirSync encounters .codegraph/
(a directory). Replace the shallow loop with fs.cpSync({ recursive: true }).
|
Claude finished @carlos-alm's task —— View job PR Review CompleteTodo List:
Root Cause AnalysisProblem Being Solved: The manual file copying approach ( Why This Approach: Using Risk Assessment: ✅ LOW RISK - This is a pure improvement that fixes a real bug in test infrastructure without changing any production code or test logic. Backlog Compliance
Technical AssessmentCode Quality: ✅ EXCELLENT
Test Impact: ✅ POSITIVE
Security: ✅ NO CONCERNS — Only test code changes, uses standard Node.js API Performance & Reliability
Minor Observations
Final Recommendation
This is exactly the type of incremental improvement that strengthens the codebase — fixing real issues with minimal, focused changes. |
Greptile SummaryThis PR fixes a test setup bug in Key changes:
All previous review feedback (adding the filter, switching to Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Test as Test (beforeAll / copyFixture)
participant FS as node:fs
participant TmpDir as tmpDir (os.tmpdir())
participant Fixture as FIXTURE_DIR (sample-project/)
Test->>FS: mkdtempSync("codegraph-*")
FS-->>Test: tmpDir path
Test->>FS: cpSync(FIXTURE_DIR, tmpDir, {recursive, filter})
Note over FS,Fixture: filter: path.basename(src) !== '.codegraph'
FS->>Fixture: read entries
Fixture-->>FS: [math.js, utils.js, index.js, .codegraph/]
FS->>TmpDir: copy math.js ✓
FS->>TmpDir: copy utils.js ✓
FS->>TmpDir: copy index.js ✓
FS-xTmpDir: skip .codegraph/ (filtered)
Test->>Test: buildGraph(tmpDir, ...)
Note over Test,TmpDir: Builds fresh .codegraph/graph.db in tmpDir
Test->>FS: rmSync(tmpDir, {recursive, force}) [afterAll]
Reviews (4): Last reviewed commit: "fix: format cpSync calls to satisfy biom..." | Re-trigger Greptile |
tests/builder/pipeline.test.js
Outdated
| for (const file of fs.readdirSync(FIXTURE_DIR)) { | ||
| fs.copyFileSync(path.join(FIXTURE_DIR, file), path.join(tmpDir, file)); | ||
| } | ||
| fs.cpSync(FIXTURE_DIR, tmpDir, { recursive: true }); |
There was a problem hiding this comment.
Consider filtering
.codegraph/ from the copy
fs.cpSync with { recursive: true } will copy any .codegraph/ directory present in the fixture (which is exactly what triggered this bug). That means tmpDir starts with whatever stale database files were in the fixture rather than a clean slate.
Tests currently survive this because all builds use incremental: false, which resets the DB — but future tests that omit that flag could pick up stale state from the fixture's .codegraph/. Adding a filter makes the intent explicit and guards against that class of issue:
| fs.cpSync(FIXTURE_DIR, tmpDir, { recursive: true }); | |
| fs.cpSync(FIXTURE_DIR, tmpDir, { recursive: true, filter: (src) => !src.includes('.codegraph') }); |
There was a problem hiding this comment.
Done — added the filter option to exclude .codegraph/ from the copy in 6e80058.
| for (const file of fs.readdirSync(FIXTURE_DIR)) { | ||
| fs.copyFileSync(path.join(FIXTURE_DIR, file), path.join(dir, file)); | ||
| } | ||
| fs.cpSync(FIXTURE_DIR, dir, { recursive: true }); |
There was a problem hiding this comment.
Same
.codegraph/ filter suggestion applies here
Same concern as pipeline.test.js: if the fixture directory happens to contain a .codegraph/ subdirectory it will be copied into every copyFixture() temp dir, potentially carrying stale graph data into fresh test runs. The filter option keeps temp dirs clean:
| fs.cpSync(FIXTURE_DIR, dir, { recursive: true }); | |
| fs.cpSync(FIXTURE_DIR, dir, { recursive: true, filter: (src) => !src.includes('.codegraph') }); |
There was a problem hiding this comment.
Done — same filter applied here in 6e80058.
1 similar comment
tests/builder/pipeline.test.js
Outdated
| for (const file of fs.readdirSync(FIXTURE_DIR)) { | ||
| fs.copyFileSync(path.join(FIXTURE_DIR, file), path.join(tmpDir, file)); | ||
| } | ||
| fs.cpSync(FIXTURE_DIR, tmpDir, { recursive: true, filter: (src) => !src.includes('.codegraph') }); |
There was a problem hiding this comment.
Filter could match parent path components
src.includes('.codegraph') checks the full source path string, not just the entry's basename. If the fixture is ever located under a parent directory whose name happens to contain .codegraph (e.g. /home/user/.codegraph-tools/...), the filter would silently skip every file in the copy, producing an empty tmpDir and silent test failures.
Using path.basename(src) makes the intent precise and path-independent:
| fs.cpSync(FIXTURE_DIR, tmpDir, { recursive: true, filter: (src) => !src.includes('.codegraph') }); | |
| fs.cpSync(FIXTURE_DIR, tmpDir, { recursive: true, filter: (src) => path.basename(src) !== '.codegraph' }); |
There was a problem hiding this comment.
Done — switched to path.basename(src) !== '.codegraph' in 8abeb83. This ensures the filter only matches the directory entry itself, not parent path components.
| for (const file of fs.readdirSync(FIXTURE_DIR)) { | ||
| fs.copyFileSync(path.join(FIXTURE_DIR, file), path.join(dir, file)); | ||
| } | ||
| fs.cpSync(FIXTURE_DIR, dir, { recursive: true, filter: (src) => !src.includes('.codegraph') }); |
There was a problem hiding this comment.
Same issue as pipeline.test.js: if any ancestor directory of FIXTURE_DIR contains .codegraph in its name, the filter returns false for every path and the temp directory is left empty. Prefer matching on path.basename for safety:
| fs.cpSync(FIXTURE_DIR, dir, { recursive: true, filter: (src) => !src.includes('.codegraph') }); | |
| fs.cpSync(FIXTURE_DIR, dir, { recursive: true, filter: (src) => path.basename(src) !== '.codegraph' }); |
There was a problem hiding this comment.
Done — same path.basename fix applied here in 8abeb83.
|
Fixed formatting in both test files — biome required the \ options object to be broken across multiple lines. This was the cause of the CI lint failure (the only 2 errors in the biome run were format errors in the PR's test files). |
Summary
pipeline.test.jsandscoped-rebuild.test.jsusedreaddirSync+copyFileSyncto copy the fixture project into a temp dir. When.codegraph/exists in the fixture directory,copyFileSyncthrowsEPERMbecause it can't copy directories.fs.cpSync(src, dest, { recursive: true })which handles nested directories correctly.Test plan
npx vitest run tests/builder/pipeline.test.js tests/integration/scoped-rebuild.test.js— 9/9 pass