Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- The Claude Code prompt hook no longer runs on the task-notification messages Claude Code injects when a background agent finishes, removing a multi-second stall on every such turn. (#1832)


- Rust calls on `self` now stay with the enclosing type instead of linking to an unrelated type’s same-named method. Thanks @L4XB. (#1861)

- Turning telemetry off now resets its identity and stops running processes from recording, sending, or restoring unsent data. (#1869)
Expand All @@ -405,6 +406,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Zustand actions keep their callers when read through typed stores, destructured from store state, or selected by a hook.
- Direct React Native bridge calls retain their native implementations and cross-platform relationships.
- Dart extension-type getters remain searchable when using the WebAssembly parser.
- `codegraph sync` now refuses outdated extraction indexes instead of reporting them as up to date, and directs users to a full rebuild; with `--quiet` the reason is still printed as one line on stderr so a git hook failure is explainable. (#1798)

- Calling a built-in method on an awaited value no longer records a call into an unrelated class that happens to declare a method of the same name, and a variable bound to an awaited call now resolves methods on the type that call returns. Thanks @maxmilian. (#1840)
- Spring mappings now include every declared path combination and resolve constants declared in the same file, while unresolved paths no longer appear as false root routes. (#1461)
Expand Down
90 changes: 90 additions & 0 deletions __tests__/cli-sync-stale-index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { EXTRACTION_VERSION } from '../src/extraction/extraction-version';

const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
const { DatabaseSync } = require('node:sqlite');
let root: string;

function run(...args: string[]) {
return spawnSync(process.execPath, [BIN, ...args], {
cwd: root,
encoding: 'utf8',
timeout: 30_000,
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', NO_COLOR: '1' },
});
}

function setStamp(stamp: number | null) {
const db = new DatabaseSync(path.join(root, '.codegraph', 'codegraph.db'));
try {
if (stamp === null) {
db.prepare('DELETE FROM project_metadata WHERE key = ?').run('indexed_with_extraction_version');
} else {
db.prepare('UPDATE project_metadata SET value = ? WHERE key = ?')
.run(String(stamp), 'indexed_with_extraction_version');
}
} finally {
db.close();
}
}

beforeEach(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-stale-'));
fs.writeFileSync(path.join(root, 'original.ts'), 'export function original() { return 1; }\n');
const cg = CodeGraph.initSync(root);
try { await cg.indexAll(); } finally { cg.close(); }
});

afterEach(() => fs.rmSync(root, { recursive: true, force: true }));

describe('explicit CLI sync with stale extraction', () => {
for (const stamp of [EXTRACTION_VERSION - 1, null]) {
for (const quiet of [false, true]) {
it(`rejects stamp ${stamp} before syncing, quiet=${quiet}`, () => {
setStamp(stamp);
const args = quiet ? ['sync', '--quiet'] : ['sync'];
for (const changed of [false, true]) {
if (changed) fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n');
const result = run(...args);
expect(result.status).toBe(1);
const output = result.stdout + result.stderr;
expect(output).toContain('Run "codegraph index"');
expect(output).not.toContain('Already up to date');
if (quiet) {
// One explanatory line on stderr, nothing on stdout, so a hook that
// fails the commit on exit 1 still shows the user why. (#1798)
expect(result.stdout).toBe('');
expect(result.stderr.trim().split('\n')).toHaveLength(1);
}
const cg = CodeGraph.openSync(root);
try {
expect(cg.isIndexStale()).toBe(true);
expect(cg.getStats().fileCount).toBe(1);
} finally { cg.close(); }
}
});
}
}

it.each([false, true])('still syncs a current index, quiet=%s', (quiet) => {
fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n');
expect(run('sync', ...(quiet ? ['--quiet'] : [])).status).toBe(0);
const cg = CodeGraph.openSync(root);
try {
expect(cg.isIndexStale()).toBe(false);
expect(cg.getStats().fileCount).toBe(2);
} finally { cg.close(); }
});

it('accepts sync after the prescribed full rebuild', () => {
setStamp(EXTRACTION_VERSION - 1);
expect(run('sync').status).toBe(1);
expect(run('index').status).toBe(0);
expect(run('sync').status).toBe(0);
});
});
13 changes: 13 additions & 0 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,19 @@ program
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);

if (cg.isIndexStale()) {
cg.destroy();
const message = 'Index extraction is stale. Run "codegraph index" for a full rebuild before syncing.';
// A quiet sync is the git-hook path. The exit status still says "no",
// but one stderr line explains why so a hook that fails the commit on a
// non-zero exit does not block it with an invisible reason. (#1798)
if (options.quiet) {
process.stderr.write(`codegraph sync: ${message}\n`);
process.exit(1);
}
throw new Error(message);
}

try {
if (options.quiet) {
await cg.sync();
Expand Down