Skip to content

fix: refresh codebase index on config change #6131

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

Merged
merged 3 commits into from
Jul 16, 2025
Merged
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
305 changes: 295 additions & 10 deletions core/indexing/CodebaseIndexer.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable max-lines-per-function */
/* lint is not useful for test classes */
import { jest } from "@jest/globals";
import { execSync } from "node:child_process";
import fs from "node:fs";
Expand All @@ -14,8 +16,11 @@ import {
} from "../test/testDir.js";
import { getIndexSqlitePath } from "../util/paths.js";

import { ConfigResult } from "@continuedev/config-yaml";
import CodebaseContextProvider from "../context/providers/CodebaseContextProvider.js";
import { ContinueConfig } from "../index.js";
import { localPathToUri } from "../util/pathToUri.js";
import { CodebaseIndexer, PauseToken } from "./CodebaseIndexer.js";
import { CodebaseIndexer } from "./CodebaseIndexer.js";
import { getComputeDeleteAddRemove } from "./refreshIndex.js";
import { TestCodebaseIndex } from "./TestCodebaseIndex.js";
import { CodebaseIndex } from "./types.js";
Expand Down Expand Up @@ -57,6 +62,17 @@ class TestCodebaseIndexer extends CodebaseIndexer {
protected async getIndexesToBuild(): Promise<CodebaseIndex[]> {
return [new TestCodebaseIndex()];
}

// Add public methods to test private methods
public testHasCodebaseContextProvider() {
return (this as any).hasCodebaseContextProvider();
}

public async testHandleConfigUpdate(
configResult: ConfigResult<ContinueConfig>,
) {
return (this as any).handleConfigUpdate({ config: configResult.config });
}
}

// Create a mock messenger type that doesn't require actual protocol imports
Expand All @@ -71,8 +87,6 @@ type MockMessengerType = {
// These are more like integration tests, whereas we should separately test
// the individual CodebaseIndex classes
describe("CodebaseIndexer", () => {
const pauseToken = new PauseToken(false);

// Replace mockProgressReporter with mockMessenger
const mockMessenger: MockMessengerType = {
send: jest.fn(),
Expand All @@ -82,13 +96,8 @@ describe("CodebaseIndexer", () => {
onError: jest.fn(),
};

const codebaseIndexer = new TestCodebaseIndexer(
testConfigHandler,
testIde,
mockMessenger as any,
false,
);
const testIndex = new TestCodebaseIndex();
let codebaseIndexer: TestCodebaseIndexer;
let testIndex: TestCodebaseIndex;

beforeAll(async () => {
tearDownTestDir();
Expand All @@ -99,6 +108,15 @@ describe("CodebaseIndexer", () => {
cwd: TEST_DIR_PATH,
});
execSync('git config user.name "Test"', { cwd: TEST_DIR_PATH });

codebaseIndexer = new TestCodebaseIndexer(
testConfigHandler,
testIde,
mockMessenger as any,
false,
);
await codebaseIndexer.initPromise;
testIndex = new TestCodebaseIndex();
});

afterAll(async () => {
Expand Down Expand Up @@ -161,6 +179,7 @@ describe("CodebaseIndexer", () => {
}

test("should index test folder without problem", async () => {
walkDirCache.invalidate();
addToTestDir([
["test.ts", TEST_TS],
["py/main.py", TEST_PY],
Expand Down Expand Up @@ -403,4 +422,270 @@ describe("CodebaseIndexer", () => {
expect(codebaseIndexer.currentIndexingState).toEqual(testState);
});
});

// New describe block for testing handleConfigUpdate functionality
describe("handleConfigUpdate functionality", () => {
let testIndexer: TestCodebaseIndexer;
let mockRefreshCodebaseIndex: jest.MockedFunction<any>;
let mockGetWorkspaceDirs: jest.MockedFunction<any>;

beforeEach(() => {
testIndexer = new TestCodebaseIndexer(
testConfigHandler,
testIde,
mockMessenger as any,
false,
);

// Mock the refreshCodebaseIndex method to avoid actual indexing
mockRefreshCodebaseIndex = jest
.spyOn(testIndexer, "refreshCodebaseIndex")
.mockImplementation(async () => {});

// Mock getWorkspaceDirs to return test directories
mockGetWorkspaceDirs = jest
.spyOn(testIde, "getWorkspaceDirs")
.mockResolvedValue(["/test/workspace"]);
});

afterEach(() => {
jest.clearAllMocks();
});

describe("hasCodebaseContextProvider", () => {
test("should return true when codebase context provider is present", () => {
// Set up config with codebase context provider
(testIndexer as any).config = {
contextProviders: [
{
description: {
title: CodebaseContextProvider.description.title,
},
},
],
};

const result = testIndexer.testHasCodebaseContextProvider();
expect(result).toBe(true);
});

test("should return false when no context providers are configured", () => {
(testIndexer as any).config = {
contextProviders: undefined,
};

const result = testIndexer.testHasCodebaseContextProvider();
expect(result).toBe(false);
});

test("should return false when context providers exist but no codebase provider", () => {
(testIndexer as any).config = {
contextProviders: [
{
description: {
title: "SomeOtherProvider",
},
},
],
};

const result = testIndexer.testHasCodebaseContextProvider();
expect(result).toBe(false);
});

test("should return false when context providers is empty array", () => {
(testIndexer as any).config = {
contextProviders: [],
};

const result = testIndexer.testHasCodebaseContextProvider();
expect(result).toBe(false);
});
});

describe("handleConfigUpdate", () => {
test("should return early when newConfig is null", async () => {
const configResult: ConfigResult<ContinueConfig> = {
config: null as any,
errors: [],
configLoadInterrupted: false,
};

await testIndexer.testHandleConfigUpdate(configResult);

// These get called once on init, so we want them to not get called again
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(1);
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(1);
});

test("should return early when newConfig is undefined", async () => {
const configResult: ConfigResult<ContinueConfig> = {
config: undefined as any,
errors: [],
configLoadInterrupted: false,
};

await testIndexer.testHandleConfigUpdate(configResult);

// These get called once on init, so we want them to not get called again
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(1);
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(1);
});

test("should return early when no codebase context provider is present", async () => {
const configResult: ConfigResult<ContinueConfig> = {
config: {
contextProviders: [
{
description: {
title: "SomeOtherProvider",
},
},
],
selectedModelByRole: {
embed: {
model: "test-model",
provider: "test-provider",
},
},
} as unknown as ContinueConfig,
errors: [],
configLoadInterrupted: false,
};

await testIndexer.testHandleConfigUpdate(configResult);

// These get called once on init, so we want them to not get called again
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(1);
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(1);
});

test("should return early when no embed model is configured", async () => {
const configResult: ConfigResult<ContinueConfig> = {
config: {
contextProviders: [
{
description: {
title: CodebaseContextProvider.description.title,
},
},
],
selectedModelByRole: {
embed: undefined,
},
} as unknown as ContinueConfig,
errors: [],
configLoadInterrupted: false,
};

await testIndexer.testHandleConfigUpdate(configResult);

// These get called once on init, so we want them to not get called again
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(1);
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(1);
});

test("should call refreshCodebaseIndex when all conditions are met", async () => {
const configResult: ConfigResult<ContinueConfig> = {
config: {
contextProviders: [
{
description: {
title: CodebaseContextProvider.description.title,
},
},
],
selectedModelByRole: {
embed: {
model: "test-model",
provider: "test-provider",
},
},
} as unknown as ContinueConfig,
errors: [],
configLoadInterrupted: false,
};

await testIndexer.testHandleConfigUpdate(configResult);

// These get called once on init, and we want them to get called again
expect(mockGetWorkspaceDirs).toHaveBeenCalledTimes(2);
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(2);
expect(mockRefreshCodebaseIndex).toHaveBeenCalledWith([
"/test/workspace",
]);
});

test("should set config property before checking conditions", async () => {
const testConfig = {
contextProviders: [
{
description: {
title: CodebaseContextProvider.description.title,
},
},
],
selectedModelByRole: {
embed: {
model: "test-model",
provider: "test-provider",
},
},
} as unknown as ContinueConfig;

const configResult: ConfigResult<ContinueConfig> = {
config: testConfig,
errors: [],
configLoadInterrupted: false,
};

await testIndexer.testHandleConfigUpdate(configResult);

// Verify that the config was set
expect((testIndexer as any).config).toBe(testConfig);
// These get called once on init, and we want them to get called again
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(2);
});

test("should handle multiple context providers correctly", async () => {
const configResult: ConfigResult<ContinueConfig> = {
config: {
contextProviders: [
{
description: {
title: "SomeOtherProvider",
},
},
{
description: {
title: CodebaseContextProvider.description.title,
},
},
{
description: {
title: "AnotherProvider",
},
},
],
selectedModelByRole: {
embed: {
model: "test-model",
provider: "test-provider",
},
},
} as unknown as ContinueConfig,
errors: [],
configLoadInterrupted: false,
};

await testIndexer.testHandleConfigUpdate(configResult);

// These get called once on init, and we want them to get called again
expect(mockRefreshCodebaseIndex).toHaveBeenCalledTimes(2);
expect(mockRefreshCodebaseIndex).toHaveBeenCalledWith([
"/test/workspace",
]);
});
});
});
});
Loading
Loading