Skip to content

Commit ff69f62

Browse files
authored
Fix search/list/read operations to use indexed commit ref (#13)
Updated the createSourceFromState function to override the ref in the config with meta.resolvedRef when available for GitHub, GitLab, and BitBucket sources. This ensures that listFiles and readFile operations use the exact commit SHA that was indexed, rather than the branch name that may have moved since indexing.
1 parent ef6a46a commit ff69f62

File tree

6 files changed

+388
-7
lines changed

6 files changed

+388
-7
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"cli": "tsx src/bin/index.ts",
2121
"cli:index": "tsx src/bin/index.ts index",
2222
"cli:search": "tsx src/bin/index.ts search",
23-
"test:integration": "tsx test/augment-provider.ts && tsx test/cli-agent.ts",
23+
"test:integration": "tsx test/augment-provider.ts && tsx test/cli-agent.ts && tsx test/resolved-ref.ts",
2424
"format": "biome format --write .",
2525
"lint": "biome check .",
2626
"lint:fix": "biome check --write ."
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Tests for createSourceFromState
3+
*
4+
* These tests verify that createSourceFromState correctly uses resolvedRef
5+
* from state metadata when creating source instances.
6+
*
7+
* We mock GitHub and Website sources to capture what config gets passed
8+
* to the constructors, without needing API credentials.
9+
*
10+
* Since all VCS sources (GitHub, GitLab, BitBucket) use the same getRef() logic,
11+
* we only test GitHub as the representative case.
12+
*/
13+
14+
import { describe, it, expect, vi, beforeEach } from "vitest";
15+
import type { IndexStateSearchOnly, SourceMetadata } from "../core/types.js";
16+
17+
// Mock only the sources we actually test
18+
vi.mock("../sources/github.js", () => ({
19+
GitHubSource: vi.fn().mockImplementation((config) => ({
20+
type: "github" as const,
21+
config,
22+
})),
23+
}));
24+
25+
vi.mock("../sources/website.js", () => ({
26+
WebsiteSource: vi.fn().mockImplementation((config) => ({
27+
type: "website" as const,
28+
config,
29+
})),
30+
}));
31+
32+
// Import the function under test and mocked sources
33+
import { createSourceFromState } from "./multi-index-runner.js";
34+
import { GitHubSource } from "../sources/github.js";
35+
import { WebsiteSource } from "../sources/website.js";
36+
37+
// Create mock state with specific source metadata
38+
const createMockState = (source: SourceMetadata): IndexStateSearchOnly => ({
39+
version: 1,
40+
contextState: {
41+
version: 1,
42+
} as any,
43+
source,
44+
});
45+
46+
describe("createSourceFromState", () => {
47+
beforeEach(() => {
48+
vi.clearAllMocks();
49+
});
50+
51+
// All VCS sources (GitHub, GitLab, BitBucket) use the same getRef() logic:
52+
// resolvedRef ?? config.ref
53+
// We test this once with GitHub as the representative case.
54+
55+
it("uses resolvedRef when present", async () => {
56+
const state = createMockState({
57+
type: "github",
58+
config: { owner: "test-owner", repo: "test-repo", ref: "main" },
59+
resolvedRef: "abc123sha",
60+
syncedAt: new Date().toISOString(),
61+
});
62+
63+
await createSourceFromState(state);
64+
65+
expect(GitHubSource).toHaveBeenCalledWith({
66+
owner: "test-owner",
67+
repo: "test-repo",
68+
ref: "abc123sha",
69+
});
70+
});
71+
72+
it("falls back to config.ref when resolvedRef is missing", async () => {
73+
const state = createMockState({
74+
type: "github",
75+
config: { owner: "test-owner", repo: "test-repo", ref: "main" },
76+
// No resolvedRef
77+
syncedAt: new Date().toISOString(),
78+
});
79+
80+
await createSourceFromState(state);
81+
82+
expect(GitHubSource).toHaveBeenCalledWith({
83+
owner: "test-owner",
84+
repo: "test-repo",
85+
ref: "main",
86+
});
87+
});
88+
89+
it("website source works without resolvedRef", async () => {
90+
const state = createMockState({
91+
type: "website",
92+
config: { url: "https://example.com", maxDepth: 2 },
93+
syncedAt: new Date().toISOString(),
94+
});
95+
96+
await createSourceFromState(state);
97+
98+
expect(WebsiteSource).toHaveBeenCalledWith({
99+
url: "https://example.com",
100+
maxDepth: 2,
101+
});
102+
});
103+
104+
it("throws error for unknown source type", async () => {
105+
const state = createMockState({
106+
type: "unknown" as any,
107+
config: {} as any,
108+
syncedAt: new Date().toISOString(),
109+
});
110+
111+
await expect(createSourceFromState(state)).rejects.toThrow(
112+
"Unknown source type: unknown"
113+
);
114+
});
115+
});

src/clients/multi-index-runner.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,38 @@ export interface MultiIndexRunnerConfig {
4343
clientUserAgent?: string;
4444
}
4545

46-
/** Create a Source from index state metadata */
47-
async function createSourceFromState(state: IndexStateSearchOnly): Promise<Source> {
46+
/**
47+
* Create a Source from index state metadata.
48+
*
49+
* For VCS sources (GitHub, GitLab, BitBucket), uses `resolvedRef` (the indexed commit SHA)
50+
* if available, falling back to `config.ref` (branch name) if not.
51+
*
52+
* **Why resolvedRef matters:**
53+
* - `resolvedRef` is the exact commit SHA that was indexed for search
54+
* - Using it ensures `listFiles` and `readFile` return content from the same commit
55+
* that was indexed, so file operations match search results
56+
* - If we used `config.ref` (branch name), the branch might have moved since indexing,
57+
* causing file operations to return different content than what search indexed
58+
*
59+
* @internal Exported for testing
60+
*/
61+
export async function createSourceFromState(state: IndexStateSearchOnly): Promise<Source> {
4862
const meta = state.source;
63+
64+
// For VCS sources, use resolvedRef (indexed commit SHA) if available.
65+
// This ensures file operations (listFiles, readFile) return content from
66+
// the same commit that was indexed, so results match search.
67+
// Falls back to config.ref for backwards compatibility with older indexes.
68+
4969
if (meta.type === "github") {
5070
const { GitHubSource } = await import("../sources/github.js");
51-
return new GitHubSource(meta.config);
71+
return new GitHubSource({ ...meta.config, ref: meta.resolvedRef ?? meta.config.ref });
5272
} else if (meta.type === "gitlab") {
5373
const { GitLabSource } = await import("../sources/gitlab.js");
54-
return new GitLabSource(meta.config);
74+
return new GitLabSource({ ...meta.config, ref: meta.resolvedRef ?? meta.config.ref });
5575
} else if (meta.type === "bitbucket") {
5676
const { BitBucketSource } = await import("../sources/bitbucket.js");
57-
return new BitBucketSource(meta.config);
77+
return new BitBucketSource({ ...meta.config, ref: meta.resolvedRef ?? meta.config.ref });
5878
} else if (meta.type === "website") {
5979
const { WebsiteSource } = await import("../sources/website.js");
6080
return new WebsiteSource(meta.config);

test/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ This directory contains integration tests that require real credentials and make
3636
|------|-------------|
3737
| `augment-provider.ts` | Tests the Augment provider SDK integration (credentials, model, API calls, tool calling) |
3838
| `cli-agent.ts` | Tests the built `ctxc` CLI binary end-to-end (indexes augmentcode/auggie, then runs agent) |
39+
| `resolved-ref.ts` | Tests that GitHubSource operations use the exact commit SHA provided as ref (honors resolvedRef) |
3940

4041
## Note
4142

test/cli-agent.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import { spawn } from "child_process";
1616
import { mkdtemp, rm } from "fs/promises";
17+
import { resolve } from "path";
1718
import { tmpdir } from "os";
1819
import { join } from "path";
1920

@@ -26,12 +27,15 @@ interface TestResult {
2627

2728
let testIndexPath: string | null = null;
2829

30+
// Path to the local CLI build
31+
const CLI_PATH = resolve(import.meta.dirname, "../dist/bin/index.js");
32+
2933
async function runCLI(
3034
args: string[],
3135
timeoutMs = 60000
3236
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
3337
return new Promise((resolve) => {
34-
const proc = spawn("npx", ["ctxc", ...args], {
38+
const proc = spawn(process.execPath, [CLI_PATH, ...args], {
3539
cwd: process.cwd(),
3640
env: process.env,
3741
timeout: timeoutMs,

0 commit comments

Comments
 (0)