-
Notifications
You must be signed in to change notification settings - Fork 14
test: incremental edge parity CI check #539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
09d0539
5c5ba8f
e640a4b
83d02c2
434b9ea
e4be883
4f61473
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,250 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from 'node:fs'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os from 'node:os'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from 'node:path'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Database from 'better-sqlite3'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { beforeAll, describe, expect, it } from 'vitest'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { buildGraph } from '../../src/domain/graph/builder.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const FIXTURE_DIR = path.join(import.meta.dirname, '..', 'fixtures', 'sample-project'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function copyDirSync(src, dest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.mkdirSync(dest, { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const entry of fs.readdirSync(src, { withFileTypes: true })) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const s = path.join(src, entry.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const d = path.join(dest, entry.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.isDirectory()) copyDirSync(s, d); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else fs.copyFileSync(s, d); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If The same issue applies to
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — both readEdges and readNodes now wrap the query in try/finally to ensure db.close() runs even if the prepare/all call throws. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function readNodes(dbPath) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const db = new Database(dbPath, { readonly: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nodes = db.prepare('SELECT name, kind, file FROM nodes ORDER BY name, kind, file').all(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nodes; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.close(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function edgeKey(e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `${e.source_name} -[${e.kind}]-> ${e.target_name}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Build a full-build copy and an incremental-build copy after applying | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the same mutation to both, then compare edges. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function buildAndCompare(fixtureDir, mutate) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-edge-parity-')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fullDir = path.join(tmpBase, 'full'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const incrDir = path.join(tmpBase, 'incr'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| copyDirSync(fixtureDir, fullDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| copyDirSync(fixtureDir, incrDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Initial full build on the incr copy (establishes baseline hashes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await buildGraph(incrDir, { incremental: false, skipRegistry: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Apply the mutation to both copies | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mutate(fullDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mutate(incrDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Full build on the full copy (clean, from scratch) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await buildGraph(fullDir, { incremental: false, skipRegistry: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Incremental rebuild on the incr copy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await buildGraph(incrDir, { incremental: true, skipRegistry: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fullEdges = readEdges(path.join(fullDir, '.codegraph', 'graph.db')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const incrEdges = readEdges(path.join(incrDir, '.codegraph', 'graph.db')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fullNodes = readNodes(path.join(fullDir, '.codegraph', 'graph.db')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const incrNodes = readNodes(path.join(incrDir, '.codegraph', 'graph.db')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { fullEdges, incrEdges, fullNodes, incrNodes }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.rmSync(tmpBase, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('Incremental edge parity (CI gate)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Scenario 1: Comment-only touch — edges must be identical | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('comment-only touch', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeAll(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = await buildAndCompare(FIXTURE_DIR, (dir) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const p = path.join(dir, 'math.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.appendFileSync(p, '\n// comment touch\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 60_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('edge count matches', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrEdges.length).toBe(result.fullEdges.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('edges are identical', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrEdges).toEqual(result.fullEdges); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Scenario 2: Body edit — change function implementation, keep exports | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('body edit (same exports)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeAll(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = await buildAndCompare(FIXTURE_DIR, (dir) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const p = path.join(dir, 'math.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let src = fs.readFileSync(p, 'utf-8'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Change add implementation but keep the same signature and exports | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+132
to
+135
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The The same silent-failure risk affects:
Add a post-replace assertion so that a drift in the fixture is caught immediately rather than silently:
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 60_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('edge count matches', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrEdges.length).toBe(result.fullEdges.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('edges are identical', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrEdges).toEqual(result.fullEdges); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Scenario 3: New export added — edges from consumers should resolve | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('new export added', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeAll(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = await buildAndCompare(FIXTURE_DIR, (dir) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mathPath = path.join(dir, 'math.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let src = fs.readFileSync(mathPath, 'utf-8'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add a new function before the module.exports line | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src = src.replace( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'module.exports = { add, multiply, square };', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `function subtract(a, b) {\n return a - b;\n}\n\nmodule.exports = { add, multiply, square, subtract };`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!src.includes('subtract')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Mutation failed: module.exports replacement not applied in math.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(mathPath, src); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Have index.js import and call the new function | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const indexPath = path.join(dir, 'index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let indexSrc = fs.readFileSync(indexPath, 'utf-8'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| indexSrc = indexSrc.replace( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "const { add } = require('./math');", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "const { add, subtract } = require('./math');", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!indexSrc.includes('subtract')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Mutation failed: require replacement not applied in index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| indexSrc = indexSrc.replace( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'console.log(add(1, 2));', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'console.log(add(1, 2));\n console.log(subtract(5, 3));', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!indexSrc.includes('subtract(5, 3)')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Mutation failed: console.log replacement not applied in index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(indexPath, indexSrc); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 60_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('node count matches', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrNodes.length).toBe(result.fullNodes.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('edge count matches', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrEdges.length).toBe(result.fullEdges.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('edges are identical', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result.incrEdges.length !== result.fullEdges.length) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Diagnostic: show which edges differ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fullSet = new Set(result.fullEdges.map(edgeKey)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const incrSet = new Set(result.incrEdges.map(edgeKey)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const missingInIncr = [...fullSet].filter((k) => !incrSet.has(k)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const extraInIncr = [...incrSet].filter((k) => !fullSet.has(k)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect.fail( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Edge mismatch:\n Missing in incremental: ${missingInIncr.join(', ') || 'none'}\n Extra in incremental: ${extraInIncr.join(', ') || 'none'}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrEdges).toEqual(result.fullEdges); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Scenario 4: File deletion — stale edges must be purged | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('file deletion', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeAll(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = await buildAndCompare(FIXTURE_DIR, (dir) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Delete utils.js — edges involving sumOfSquares/Calculator should disappear | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.unlinkSync(path.join(dir, 'utils.js')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Update index.js to remove the require | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const indexPath = path.join(dir, 'index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let src = fs.readFileSync(indexPath, 'utf-8'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let prev = src; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src = src.replace("const { sumOfSquares, Calculator } = require('./utils');\n", ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (src === prev) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Mutation failed: require(./utils) not found in index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev = src; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src = src.replace(' console.log(sumOfSquares(3, 4));\n', ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (src === prev) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Mutation failed: sumOfSquares call not found in index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev = src; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src = src.replace(' const calc = new Calculator();\n', ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (src === prev) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Mutation failed: Calculator instantiation not found in index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev = src; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src = src.replace(' console.log(calc.compute(5, 6));\n', ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (src === prev) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Mutation failed: calc.compute call not found in index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(indexPath, src); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 60_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('node count matches', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrNodes.length).toBe(result.fullNodes.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('edge count matches', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrEdges.length).toBe(result.fullEdges.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('edges are identical', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.incrEdges).toEqual(result.fullEdges); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The module-level JSDoc lists only three mutation scenarios, but the file implements four. Scenario 4 — file deletion — is missing from the list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — added Scenario 4 (file deletion) to the JSDoc header listing.