mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-02 09:45:39 +08:00
* fix(db): guard synthesis metadata and migrate the expression index Bisect identifies2a2f71ec(#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 identifies2a2f71ec(#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>
122 lines
6.3 KiB
TypeScript
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);
|
|
});
|
|
});
|