Files
codegraph/__tests__/migrations-synthesis-json.test.ts
Colby MchenryandClaude Opus 5.5 71985f2620 fix(db): guard synthesis metadata JSON and keep calls ahead of imports (#2038)
* fix(db): guard synthesis metadata and migrate the expression index

Bisect identifies 2a2f71ec (#2033) as the malformed JSON regression. Guard index expressions, ownership predicates, wiring lookups, and the legacy Go backfill; schema v11 replaces existing v10 indexes. Cover malformed base edges, staged publication, upgrade replay, and indexed range lookup.

* fix(db): preserve call precedence in deterministic edge traversal

Independent bisect identifies 2a2f71ec (#2033): source-first ordering puts importing file IDs ahead of function callers and consumes the default caller limit. Restore kind-first incoming and outgoing traversal with deterministic endpoint/position ties. Keep both original regression tests unchanged and cover filtered, cached, and provenance-filtered queries across reverse insertion order.

Validation: 25 test files passed (564 tests, 3 skipped), including security, MCP caller truncation, sync, migration, graph, and DB/query coverage; npx tsc --noEmit passed. Both bisects rebuilt tsc and copied assets at every step with CODEGRAPH_KERNEL=0 to avoid using the HEAD kernel on historical extraction code. Final validation used the normal kernel setting.

* chore: no changelog entry for an unreleased regression

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:54:58 +00:00

122 lines
6.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { DatabaseConnection } from '../src/db';
import { CURRENT_SCHEMA_VERSION, getCurrentVersion, runMigrations } from '../src/db/migrations';
import { QueryBuilder } from '../src/db/queries';
import { SynthesisStage } from '../src/db/synthesis-stage';
describe('synthesis metadata JSON boundaries and migration', () => {
let dir: string;
let connection: DatabaseConnection;
let queries: QueryBuilder;
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-synthesis-json-'));
connection = DatabaseConnection.initialize(path.join(dir, 'test.db'));
queries = new QueryBuilder(connection.getDb());
queries.insertNodes(['a', 'b'].map(id => ({
id, name: id, qualifiedName: id, kind: 'function', language: 'typescript',
filePath: `${id}.ts`, startLine: 1, endLine: 1, startColumn: 0, endColumn: 1,
updatedAt: 0,
})));
});
afterEach(() => {
vi.restoreAllMocks();
connection.close();
fs.rmSync(dir, { recursive: true, force: true });
});
const insertRaw = (metadata: string | null, line = 1) => connection.getDb().prepare(
"INSERT INTO edges(source, target, kind, metadata, line) VALUES ('a', 'b', 'calls', ?, ?)"
).run(metadata, line);
it.each([null, 'broken json {{{', 'null', '[]', '{}', '{"registeredAt":"wiring.ts:1"}'])
('treats unowned metadata %s as an ordinary edge', metadata => {
insertRaw(metadata);
expect(queries.getOutgoingEdges('a')).toHaveLength(1);
expect(queries.hasSynthesizedEdgesTouchingFile('a.ts')).toBe(false);
expect(queries.hasSynthesizedEdgesTouchingFile('b.ts')).toBe(false);
expect(queries.hasSynthesizedEdgesTouchingFile('wiring.ts')).toBe(false);
});
it('keeps the third-file range lookup indexed while tolerating malformed neighbors', () => {
insertRaw('broken json {{{');
queries.insertEdge({ source: 'a', target: 'b', kind: 'calls', line: 2,
metadata: { synthesizedBy: 'event-emitter', registeredAt: 'wiring.ts:12' } });
expect(queries.hasSynthesizedEdgesTouchingFile('a.ts')).toBe(true);
expect(queries.hasSynthesizedEdgesTouchingFile('b.ts')).toBe(true);
const prepare = vi.spyOn(connection.getDb(), 'prepare');
expect(queries.hasSynthesizedEdgesTouchingFile('wiring.ts')).toBe(true);
const sql = prepare.mock.calls.map(([sql]) => sql).find(sql => sql.includes('FROM edges e WHERE'))!;
expect(sql).toBeDefined();
prepare.mockRestore();
const plan = connection.getDb().prepare(`EXPLAIN QUERY PLAN ${sql}`).all('wiring.ts:', 'wiring.ts;');
expect(plan.map(row => row.detail).join('\n'))
.toMatch(/SEARCH e USING INDEX idx_edges_synthesis_site/);
expect(queries.hasSynthesizedEdgesTouchingFile('wiring.tsx')).toBe(false);
});
it('preserves malformed base edges while staging and publishing owned replacements', async () => {
insertRaw('broken json {{{');
queries.insertEdge({ source: 'a', target: 'b', kind: 'calls', line: 2,
metadata: { synthesizedBy: 'old-pass', registeredAt: 'old.ts:1' } });
const stage = new SynthesisStage(connection.getPath());
try {
expect(stage.queries.getOutgoingEdges('a').map(edge => edge.line)).toEqual([1]);
// The overlay must not shadow an existing base edge, even if its JSON is bad.
stage.queries.insertEdge({ source: 'a', target: 'b', kind: 'calls', line: 1,
metadata: { synthesizedBy: 'new-pass' } });
stage.queries.insertEdge({ source: 'a', target: 'b', kind: 'calls', line: 3,
metadata: { synthesizedBy: 'new-pass', registeredAt: 'new.ts:1' } });
await stage.publish();
} finally { stage.close(); }
expect(connection.getDb().prepare('SELECT line, metadata FROM edges ORDER BY line').all()).toEqual([
{ line: 1, metadata: 'broken json {{{' },
{ line: 3, metadata: JSON.stringify({ synthesizedBy: 'new-pass', registeredAt: 'new.ts:1' }) },
]);
});
it('upgrades pre-v10 malformed metadata, including legacy Go containment', () => {
const raw = connection.getDb();
raw.exec(`DROP INDEX idx_edges_synthesis_site;
DROP TABLE synthesis_inputs;
DELETE FROM schema_versions WHERE version >= 10;
UPDATE nodes SET language = 'go', kind = CASE id WHEN 'a' THEN 'struct' ELSE 'method' END;`);
insertRaw('broken json {{{');
raw.prepare("INSERT INTO edges(source, target, kind, metadata) VALUES ('a', 'b', 'contains', ?)")
.run('broken containment {{{');
runMigrations(raw, 9);
expect(getCurrentVersion(raw)).toBe(CURRENT_SCHEMA_VERSION);
expect(queries.getOutgoingEdges('a', ['calls'])[0].metadata).toBeUndefined();
expect(queries.getOutgoingEdges('a', ['contains'])[0].metadata?.synthesizedBy).toBe('go-method-contains');
expect(queries.hasSynthesizedEdgesTouchingFile('a.ts')).toBe(true);
insertRaw('still malformed', 2);
});
it('replaces the v10 index on open and safely replays the replacement', () => {
const raw = connection.getDb();
raw.exec(`DROP INDEX idx_edges_synthesis_site;
CREATE INDEX idx_edges_synthesis_site ON edges(json_extract(metadata, '$.registeredAt'))
WHERE json_extract(metadata, '$.synthesizedBy') IS NOT NULL;
DELETE FROM schema_versions WHERE version >= 11;
INSERT OR IGNORE INTO schema_versions(version, applied_at, description) VALUES (10, 0, 'legacy fixture');`);
queries.insertEdge({ source: 'a', target: 'b', kind: 'calls', line: 2,
metadata: { synthesizedBy: 'event-emitter', registeredAt: 'wiring.ts:12' } });
connection.close();
connection = DatabaseConnection.open(path.join(dir, 'test.db'));
queries = new QueryBuilder(connection.getDb());
expect(getCurrentVersion(connection.getDb())).toBe(CURRENT_SCHEMA_VERSION);
insertRaw('broken json {{{');
const before = connection.getDb().prepare('SELECT * FROM edges ORDER BY id').all();
// Replay the DDL over its own output with bad JSON already present.
connection.getDb().exec('DELETE FROM schema_versions WHERE version >= 11');
runMigrations(connection.getDb(), 10);
runMigrations(connection.getDb(), getCurrentVersion(connection.getDb()));
expect(connection.getDb().prepare('SELECT * FROM edges ORDER BY id').all()).toEqual(before);
expect(queries.hasSynthesizedEdgesTouchingFile('wiring.ts')).toBe(true);
});
});