Skip to content
Open
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: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"cli": "tsx src/bin/index.ts",
"cli:index": "tsx src/bin/index.ts index",
"cli:search": "tsx src/bin/index.ts search",
"test:integration": "tsx test/augment-provider.ts && tsx test/cli-agent.ts",
"test:integration": "tsx test/augment-provider.ts && tsx test/cli-agent.ts && tsx test/resolved-ref.ts",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test:integration now runs scripts that execute/import from dist/, but the script itself doesn’t ensure a build has happened first (fresh checkouts will fail with missing dist). Consider making the integration script self-contained by ensuring npm run build is run as a pre-step.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

"format": "biome format --write .",
"lint": "biome check .",
"lint:fix": "biome check --write ."
Expand Down
115 changes: 115 additions & 0 deletions src/clients/multi-index-runner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Tests for createSourceFromState
*
* These tests verify that createSourceFromState correctly uses resolvedRef
* from state metadata when creating source instances.
*
* We mock GitHub and Website sources to capture what config gets passed
* to the constructors, without needing API credentials.
*
* Since all VCS sources (GitHub, GitLab, BitBucket) use the same getRef() logic,
* we only test GitHub as the representative case.
*/

import { describe, it, expect, vi, beforeEach } from "vitest";
import type { IndexStateSearchOnly, SourceMetadata } from "../core/types.js";

// Mock only the sources we actually test
vi.mock("../sources/github.js", () => ({
GitHubSource: vi.fn().mockImplementation((config) => ({
type: "github" as const,
config,
})),
}));

vi.mock("../sources/website.js", () => ({
WebsiteSource: vi.fn().mockImplementation((config) => ({
type: "website" as const,
config,
})),
}));

// Import the function under test and mocked sources
import { createSourceFromState } from "./multi-index-runner.js";
import { GitHubSource } from "../sources/github.js";
import { WebsiteSource } from "../sources/website.js";

// Create mock state with specific source metadata
const createMockState = (source: SourceMetadata): IndexStateSearchOnly => ({
version: 1,
contextState: {
version: 1,
} as any,
source,
});

describe("createSourceFromState", () => {
beforeEach(() => {
vi.clearAllMocks();
});

// All VCS sources (GitHub, GitLab, BitBucket) use the same getRef() logic:
// resolvedRef ?? config.ref
// We test this once with GitHub as the representative case.

it("uses resolvedRef when present", async () => {
const state = createMockState({
type: "github",
config: { owner: "test-owner", repo: "test-repo", ref: "main" },
resolvedRef: "abc123sha",
syncedAt: new Date().toISOString(),
});

await createSourceFromState(state);

expect(GitHubSource).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
ref: "abc123sha",
});
});

it("falls back to config.ref when resolvedRef is missing", async () => {
const state = createMockState({
type: "github",
config: { owner: "test-owner", repo: "test-repo", ref: "main" },
// No resolvedRef
syncedAt: new Date().toISOString(),
});

await createSourceFromState(state);

expect(GitHubSource).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
ref: "main",
});
});

it("website source works without resolvedRef", async () => {
const state = createMockState({
type: "website",
config: { url: "https://example.com", maxDepth: 2 },
syncedAt: new Date().toISOString(),
});

await createSourceFromState(state);

expect(WebsiteSource).toHaveBeenCalledWith({
url: "https://example.com",
maxDepth: 2,
});
});

it("throws error for unknown source type", async () => {
const state = createMockState({
type: "unknown" as any,
config: {} as any,
syncedAt: new Date().toISOString(),
});

await expect(createSourceFromState(state)).rejects.toThrow(
"Unknown source type: unknown"
);
});
});
30 changes: 25 additions & 5 deletions src/clients/multi-index-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,38 @@ export interface MultiIndexRunnerConfig {
searchOnly?: boolean;
}

/** Create a Source from index state metadata */
async function createSourceFromState(state: IndexStateSearchOnly): Promise<Source> {
/**
* Create a Source from index state metadata.
*
* For VCS sources (GitHub, GitLab, BitBucket), uses `resolvedRef` (the indexed commit SHA)
* if available, falling back to `config.ref` (branch name) if not.
*
* **Why resolvedRef matters:**
* - `resolvedRef` is the exact commit SHA that was indexed for search
* - Using it ensures `listFiles` and `readFile` return content from the same commit
* that was indexed, so file operations match search results
* - If we used `config.ref` (branch name), the branch might have moved since indexing,
* causing file operations to return different content than what search indexed
*
* @internal Exported for testing
*/
export async function createSourceFromState(state: IndexStateSearchOnly): Promise<Source> {
const meta = state.source;

// For VCS sources, use resolvedRef (indexed commit SHA) if available.
// This ensures file operations (listFiles, readFile) return content from
// the same commit that was indexed, so results match search.
// Falls back to config.ref for backwards compatibility with older indexes.

if (meta.type === "github") {
const { GitHubSource } = await import("../sources/github.js");
return new GitHubSource(meta.config);
return new GitHubSource({ ...meta.config, ref: meta.resolvedRef ?? meta.config.ref });
} else if (meta.type === "gitlab") {
const { GitLabSource } = await import("../sources/gitlab.js");
return new GitLabSource(meta.config);
return new GitLabSource({ ...meta.config, ref: meta.resolvedRef ?? meta.config.ref });
} else if (meta.type === "bitbucket") {
const { BitBucketSource } = await import("../sources/bitbucket.js");
return new BitBucketSource(meta.config);
return new BitBucketSource({ ...meta.config, ref: meta.resolvedRef ?? meta.config.ref });
} else if (meta.type === "website") {
const { WebsiteSource } = await import("../sources/website.js");
return new WebsiteSource(meta.config);
Expand Down
1 change: 1 addition & 0 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ This directory contains integration tests that require real credentials and make
|------|-------------|
| `augment-provider.ts` | Tests the Augment provider SDK integration (credentials, model, API calls, tool calling) |
| `cli-agent.ts` | Tests the built `ctxc` CLI binary end-to-end (indexes augmentcode/auggie, then runs agent) |
| `resolved-ref.ts` | Tests that GitHubSource operations use the exact commit SHA provided as ref (honors resolvedRef) |

## Note

Expand Down
6 changes: 5 additions & 1 deletion test/cli-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import { spawn } from "child_process";
import { mkdtemp, rm } from "fs/promises";
import { resolve } from "path";
import { tmpdir } from "os";
import { join } from "path";

Expand All @@ -26,12 +27,15 @@ interface TestResult {

let testIndexPath: string | null = null;

// Path to the local CLI build
const CLI_PATH = resolve(import.meta.dirname, "../dist/bin/index.js");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import.meta.dirname isn’t available in all Node/runner versions, and if it’s undefined this test will crash before spawning the CLI. Consider deriving the directory from import.meta.url (or otherwise ensuring the runtime guarantees import.meta.dirname).

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.


async function runCLI(
args: string[],
timeoutMs = 60000
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
return new Promise((resolve) => {
const proc = spawn("npx", ["ctxc", ...args], {
const proc = spawn(process.execPath, [CLI_PATH, ...args], {
cwd: process.cwd(),
env: process.env,
timeout: timeoutMs,
Expand Down
Loading