Skip to content

Commit 111fbdb

Browse files
committed
test: increasing coverage of touched files
1 parent f166679 commit 111fbdb

4 files changed

Lines changed: 196 additions & 0 deletions

File tree

src/api/providers/__tests__/unbound.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,24 @@ describe("UnboundHandler", () => {
181181
}),
182182
)
183183
})
184+
185+
it("completePrompt returns the response text", async () => {
186+
const mockCreate = (OpenAI as unknown as any)().chat.completions.create
187+
mockCreate.mockResolvedValue({
188+
choices: [{ message: { content: "completed text" } }],
189+
})
190+
191+
const handler = new UnboundHandler({
192+
unboundApiKey: "test-key",
193+
unboundModelId: "openai/gpt-4o",
194+
})
195+
196+
const result = await handler.completePrompt("Write a haiku")
197+
expect(result).toBe("completed text")
198+
expect(mockCreate).toHaveBeenCalledWith(
199+
expect.objectContaining({
200+
messages: [{ role: "system", content: "Write a haiku" }],
201+
}),
202+
)
203+
})
184204
})
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { FileContextTracker } from "../FileContextTracker"
3+
4+
vi.mock("vscode", () => ({
5+
workspace: {
6+
workspaceFolders: [{ uri: { fsPath: "/workspace" } }],
7+
createFileSystemWatcher: vi.fn().mockReturnValue({
8+
onDidChange: vi.fn(),
9+
onDidCreate: vi.fn(),
10+
onDidDelete: vi.fn(),
11+
dispose: vi.fn(),
12+
}),
13+
},
14+
Uri: { file: vi.fn((p: string) => ({ fsPath: p })) },
15+
RelativePattern: vi.fn(),
16+
}))
17+
18+
vi.mock("../../../utils/storage", () => ({
19+
getTaskDirectoryPath: vi.fn().mockResolvedValue("/storage/task-1"),
20+
}))
21+
22+
vi.mock("../../../utils/fs", () => ({
23+
fileExistsAtPath: vi.fn().mockResolvedValue(false),
24+
}))
25+
26+
vi.mock("../../../utils/safeWriteJson", () => ({
27+
safeWriteJson: vi.fn().mockResolvedValue(undefined),
28+
}))
29+
30+
vi.mock("fs/promises", () => ({
31+
default: { readFile: vi.fn() },
32+
}))
33+
34+
vi.mock("path", async () => {
35+
const actual = await vi.importActual<typeof import("path")>("path")
36+
return { ...actual, default: actual }
37+
})
38+
39+
describe("FileContextTracker.addFileToFileContextTracker", () => {
40+
let tracker: FileContextTracker
41+
const mockProvider = {
42+
contextProxy: {
43+
globalStorageUri: { fsPath: "/storage" },
44+
},
45+
} as any
46+
47+
beforeEach(() => {
48+
vi.clearAllMocks()
49+
tracker = new FileContextTracker(mockProvider, "task-1")
50+
})
51+
52+
it("creates a new active entry with record_source set to the given source", async () => {
53+
const { safeWriteJson } = await import("../../../utils/safeWriteJson")
54+
const mockWrite = vi.mocked(safeWriteJson)
55+
56+
await tracker.addFileToFileContextTracker("task-1", "/workspace/foo.ts", "read_tool")
57+
58+
expect(mockWrite).toHaveBeenCalledOnce()
59+
const written = mockWrite.mock.calls[0][1] as any
60+
const entry = written.files_in_context[0]
61+
expect(entry.path).toBe("/workspace/foo.ts")
62+
expect(entry.record_state).toBe("active")
63+
expect(entry.record_source).toBe("read_tool")
64+
expect(entry.roo_read_date).toBeTypeOf("number")
65+
})
66+
67+
it("marks existing active entries as stale before adding the new entry", async () => {
68+
const { fileExistsAtPath } = await import("../../../utils/fs")
69+
const fs = await import("fs/promises")
70+
vi.mocked(fileExistsAtPath).mockResolvedValue(true)
71+
vi.mocked(fs.default.readFile).mockResolvedValue(
72+
JSON.stringify({
73+
files_in_context: [{ path: "/workspace/foo.ts", record_state: "active", record_source: "read_tool" }],
74+
}) as any,
75+
)
76+
77+
const { safeWriteJson } = await import("../../../utils/safeWriteJson")
78+
const mockWrite = vi.mocked(safeWriteJson)
79+
80+
await tracker.addFileToFileContextTracker("task-1", "/workspace/foo.ts", "roo_edited")
81+
82+
const written = mockWrite.mock.calls[0][1] as any
83+
expect(written.files_in_context[0].record_state).toBe("stale")
84+
expect(written.files_in_context[1].record_state).toBe("active")
85+
})
86+
})

src/services/code-index/__tests__/orchestrator.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,67 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => {
160160
const lastCall = stateManager.setSystemState.mock.calls[stateManager.setSystemState.mock.calls.length - 1]
161161
expect(lastCall[0]).toBe("Error")
162162
})
163+
164+
it("collects batch errors from full scan and transitions to Error when all blocks fail", async () => {
165+
const batchError = new Error("batch failure")
166+
vectorStore.initialize.mockResolvedValue(false) // existing collection
167+
vectorStore.hasIndexedData.mockResolvedValue(false) // force full scan path
168+
vectorStore.markIndexingIncomplete.mockResolvedValue(undefined)
169+
vectorStore.markIndexingComplete.mockResolvedValue(undefined)
170+
171+
// Report a batch error — no blocks indexed, so orchestrator treats it as complete failure
172+
scanner.scanDirectory.mockImplementation(async (_dir: string, onBatchError: (e: Error) => void) => {
173+
onBatchError(batchError)
174+
return { stats: { processed: 0, skipped: 0 }, totalBlockCount: 0 }
175+
})
176+
177+
const orchestrator = new CodeIndexOrchestrator(
178+
configManager,
179+
stateManager,
180+
workspacePath,
181+
cacheManager,
182+
vectorStore,
183+
scanner,
184+
fileWatcher,
185+
)
186+
187+
await orchestrator.startIndexing()
188+
189+
// With a batch error and zero indexed blocks the orchestrator sets Error state
190+
const calls = stateManager.setSystemState.mock.calls.map((c: any[]) => c[0])
191+
expect(calls[calls.length - 1]).toBe("Error")
192+
})
193+
194+
it("collects batch errors from incremental scan and still completes indexing", async () => {
195+
const batchError = new Error("incremental batch failure")
196+
vectorStore.initialize.mockResolvedValue(false) // existing collection
197+
vectorStore.hasIndexedData.mockResolvedValue(true) // force incremental scan path
198+
vectorStore.markIndexingIncomplete.mockResolvedValue(undefined)
199+
vectorStore.markIndexingComplete.mockResolvedValue(undefined)
200+
201+
// Incremental scan reports a batch error but returns a result — orchestrator completes normally
202+
scanner.scanDirectory.mockImplementation(async (_dir: string, onBatchError: (e: Error) => void) => {
203+
onBatchError(batchError)
204+
return { stats: { processed: 0, skipped: 0 }, totalBlockCount: 0 }
205+
})
206+
207+
const orchestrator = new CodeIndexOrchestrator(
208+
configManager,
209+
stateManager,
210+
workspacePath,
211+
cacheManager,
212+
vectorStore,
213+
scanner,
214+
fileWatcher,
215+
)
216+
217+
await orchestrator.startIndexing()
218+
219+
// Incremental scan doesn't gate on batch errors — Indexed state is still reached
220+
const calls = stateManager.setSystemState.mock.calls.map((c: any[]) => c[0])
221+
expect(calls[calls.length - 1]).toBe("Indexed")
222+
expect(calls).not.toContain("Error")
223+
})
163224
})
164225

165226
describe("CodeIndexOrchestrator - stopIndexing", () => {

src/services/mcp/__tests__/McpHub.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2692,6 +2692,35 @@ describe("McpHub", () => {
26922692
expect(mockAuthProvider.close).toHaveBeenCalled()
26932693
})
26942694

2695+
it("should dispose the cancellation listener when the OAuth flow times out", async () => {
2696+
vi.useFakeTimers()
2697+
const mockDispose = vi.fn()
2698+
vsc.window.withProgress.mockImplementationOnce((_options: any, task: any) => {
2699+
const progress = { report: vi.fn() }
2700+
const cancellationToken = {
2701+
isCancellationRequested: false,
2702+
onCancellationRequested: vi.fn(() => ({ dispose: mockDispose })),
2703+
}
2704+
return task(progress, cancellationToken)
2705+
})
2706+
vsc.window.showInformationMessage.mockImplementation(() => new Promise(() => {}))
2707+
2708+
const flowPromise = (mcpHub as any)._initiateOAuthFlow(
2709+
serverName,
2710+
source,
2711+
config,
2712+
mockAuthProvider,
2713+
mockTransport,
2714+
mockConnection,
2715+
)
2716+
2717+
await vi.advanceTimersByTimeAsync(OAUTH_FLOW_TIMEOUT_MS)
2718+
await flowPromise
2719+
2720+
// cleanup(cancellationDisposable) inside the timeout handler must dispose the listener
2721+
expect(mockDispose).toHaveBeenCalled()
2722+
})
2723+
26952724
it("should resolve without calling _completeOAuthFlow when tokens exist at click time", async () => {
26962725
// Tokens are present when Authenticate is clicked (click-time guard in the loop).
26972726
// First call (pre-withProgress early-return check) returns null so withProgress runs.

0 commit comments

Comments
 (0)