test: incremental edge parity CI check#539
Conversation
Four mutation scenarios verify that incremental rebuilds produce identical edges to clean full builds: comment-only touch, body edit, new export addition, and file deletion. Each scenario compares node and edge identity between incremental and full builds.
|
Claude finished @carlos-alm's task —— View job PR Review Complete
|
Greptile SummaryThis PR adds Key observations:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant T as Test (beforeAll)
participant BAC as buildAndCompare()
participant FS as Filesystem (tmpdir)
participant BG as buildGraph()
participant DB as SQLite (graph.db)
T->>BAC: call with fixtureDir + mutate fn
BAC->>FS: mkdtempSync → fullDir / incrDir
BAC->>FS: copyDirSync(fixture → fullDir)
BAC->>FS: copyDirSync(fixture → incrDir)
BAC->>BG: buildGraph(incrDir, incremental=false)
Note over BG: Baseline hashes written to incrDir DB
BAC->>FS: mutate(fullDir)
BAC->>FS: mutate(incrDir)
BAC->>BG: buildGraph(fullDir, incremental=false)
Note over BG: Clean full rebuild on mutated source
BAC->>BG: buildGraph(incrDir, incremental=true)
Note over BG: Incremental rebuild — must match full
BAC->>DB: readEdges(fullDir DB)
BAC->>DB: readEdges(incrDir DB)
BAC->>DB: readNodes(fullDir DB)
BAC->>DB: readNodes(incrDir DB)
BAC->>FS: rmSync(tmpBase) [finally]
BAC-->>T: { fullEdges, incrEdges, fullNodes, incrNodes }
T->>T: it('edge count matches') → length check
T->>T: it('edges are identical') → toEqual check
|
| src = src.replace('return a + b;', 'return b + a;'); | ||
| fs.writeFileSync(p, src); |
There was a problem hiding this comment.
Silent mutation failure could cause false-positive passes
The .replace() call in Scenario 2 (and several others) doesn't verify that it actually found and replaced the target string. If the fixture math.js is ever updated so it no longer contains the literal 'return a + b;' (e.g., reformatted to return a+b;, or the add function is renamed), the mutation silently does nothing. Both fullDir and incrDir then contain identical, untouched source, so the comparison trivially passes — the test now gives a green result while providing zero coverage.
The same silent-failure risk affects:
- Line 148-151:
src.replace('module.exports = { add, multiply, square };', ...)in Scenario 3 - Line 157-160:
indexSrc.replace("const { add } = require('./math');", ...)in Scenario 3 - Line 161-164:
indexSrc.replace('console.log(add(1, 2));', ...)in Scenario 3 - Line 203-206: The four
src.replace(...)calls in Scenario 4
Add a post-replace assertion so that a drift in the fixture is caught immediately rather than silently:
| src = src.replace('return a + b;', 'return b + a;'); | |
| fs.writeFileSync(p, src); | |
| src = src.replace('return a + b;', 'return b + a;'); | |
| if (!src.includes('return b + a;')) throw new Error('Mutation failed: target string not found in math.js'); | |
| fs.writeFileSync(p, src); |
There was a problem hiding this comment.
Fixed — added post-replace assertions to all mutation scenarios (2, 3, and 4). Each replace now validates its result: Scenario 2 checks for the replacement string, Scenario 3 checks each of the three replacements independently, and Scenario 4 compares against a before snapshot to catch the case where none of the four replacements matched.
| function readEdges(dbPath) { | ||
| const db = new Database(dbPath, { readonly: true }); | ||
| const edges = db | ||
| .prepare( | ||
| `SELECT n1.name AS source_name, n2.name AS target_name, e.kind | ||
| FROM edges e | ||
| JOIN nodes n1 ON e.source_id = n1.id | ||
| JOIN nodes n2 ON e.target_id = n2.id | ||
| ORDER BY n1.name, n2.name, e.kind`, | ||
| ) | ||
| .all(); | ||
| db.close(); | ||
| return edges; | ||
| } |
There was a problem hiding this comment.
Missing try/finally for
db.close()
If db.prepare(...).all() throws (e.g., the graph.db file is absent, schema mismatch, or a disk error), the db.close() call on line 44 is skipped and the SQLite handle leaks. The existing readAnalysisTables function in incremental-parity.test.js already uses a try/finally for exactly this reason.
The same issue applies to readNodes (lines 48–53).
| function readEdges(dbPath) { | |
| const db = new Database(dbPath, { readonly: true }); | |
| const edges = db | |
| .prepare( | |
| `SELECT n1.name AS source_name, n2.name AS target_name, e.kind | |
| FROM edges e | |
| JOIN nodes n1 ON e.source_id = n1.id | |
| JOIN nodes n2 ON e.target_id = n2.id | |
| ORDER BY n1.name, n2.name, e.kind`, | |
| ) | |
| .all(); | |
| db.close(); | |
| return edges; | |
| } | |
| function readEdges(dbPath) { | |
| const db = new Database(dbPath, { readonly: true }); | |
| try { | |
| const edges = db | |
| .prepare( | |
| `SELECT n1.name AS source_name, n2.name AS target_name, e.kind | |
| FROM edges e | |
| JOIN nodes n1 ON e.source_id = n1.id | |
| JOIN nodes n2 ON e.target_id = n2.id | |
| ORDER BY n1.name, n2.name, e.kind`, | |
| ) | |
| .all(); | |
| return edges; | |
| } finally { | |
| db.close(); | |
| } | |
| } |
There was a problem hiding this comment.
Fixed — both readEdges and readNodes now wrap the query in try/finally to ensure db.close() runs even if the prepare/all call throws.
| /** | ||
| * Incremental edge parity CI check. | ||
| * | ||
| * Verifies that incremental rebuilds produce exactly the same edges as a | ||
| * clean full build, across multiple mutation scenarios: | ||
| * 1. Comment-only touch (no semantic change) | ||
| * 2. Body edit (change implementation, keep exports) | ||
| * 3. New export added (structural change) | ||
| * | ||
| * Uses the sample-project fixture (CJS, classes, cross-file calls) for | ||
| * broader edge coverage than the barrel-project fixture. | ||
| */ |
There was a problem hiding this comment.
JSDoc header missing Scenario 4 (file deletion)
The module-level JSDoc lists only three mutation scenarios, but the file implements four. Scenario 4 — file deletion — is missing from the list.
| /** | |
| * Incremental edge parity CI check. | |
| * | |
| * Verifies that incremental rebuilds produce exactly the same edges as a | |
| * clean full build, across multiple mutation scenarios: | |
| * 1. Comment-only touch (no semantic change) | |
| * 2. Body edit (change implementation, keep exports) | |
| * 3. New export added (structural change) | |
| * | |
| * Uses the sample-project fixture (CJS, classes, cross-file calls) for | |
| * broader edge coverage than the barrel-project fixture. | |
| */ | |
| /** | |
| * Incremental edge parity CI check. | |
| * | |
| * Verifies that incremental rebuilds produce exactly the same edges as a | |
| * clean full build, across multiple mutation scenarios: | |
| * 1. Comment-only touch (no semantic change) | |
| * 2. Body edit (change implementation, keep exports) | |
| * 3. New export added (structural change) | |
| * 4. File deletion (stale edges must be purged) | |
| * | |
| * Uses the sample-project fixture (CJS, classes, cross-file calls) for | |
| * broader edge coverage than the barrel-project fixture. | |
| */ |
There was a problem hiding this comment.
Fixed — added Scenario 4 (file deletion) to the JSDoc header listing.
|
All three Greptile review items addressed:
All 10 tests pass. Lint clean. |
|
Addressed Greptile's remaining observation: Scenario 4 now uses per-replacement assertions (e4be883) instead of the weaker collective All 10 tests pass. Lint clean. |

Summary
tests/integration/incremental-edge-parity.test.js— a CI gate that verifies incremental rebuilds produce identical edges to clean full buildssample-projectfixture (CJS, classes, cross-file calls):Test plan