Skip to content
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
188 changes: 188 additions & 0 deletions src/core/webview/__tests__/diagnosticsHandler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// npx vitest src/core/webview/__tests__/diagnosticsHandler.spec.ts

import * as path from "path"

// Mock vscode first
vi.mock("vscode", () => {
const showErrorMessage = vi.fn()
const openTextDocument = vi.fn().mockResolvedValue({})
const showTextDocument = vi.fn().mockResolvedValue(undefined)

return {
window: {
showErrorMessage,
showTextDocument,
},
workspace: {
openTextDocument,
},
}
})

// Mock storage utilities
vi.mock("../../../utils/storage", () => ({
getTaskDirectoryPath: vi.fn(async () => "/mock/task-dir"),
}))

// Mock fs utilities
vi.mock("../../../utils/fs", () => ({
fileExistsAtPath: vi.fn(),
}))

// Mock fs/promises
vi.mock("fs/promises", () => {
const mockReadFile = vi.fn()
const mockWriteFile = vi.fn().mockResolvedValue(undefined)

return {
default: {
readFile: mockReadFile,
writeFile: mockWriteFile,
},
readFile: mockReadFile,
writeFile: mockWriteFile,
}
})

import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as fsUtils from "../../../utils/fs"
import { generateErrorDiagnostics } from "../diagnosticsHandler"

describe("generateErrorDiagnostics", () => {
const mockLog = vi.fn()

beforeEach(() => {
vi.clearAllMocks()
})

it("generates a diagnostics file with error metadata and history", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true as any)
vi.mocked(fs.readFile).mockResolvedValue('[{"role": "user", "content": "test"}]' as any)

const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.2.3",
provider: "test-provider",
model: "test-model",
details: "Sample error details",
},
log: mockLog,
})

expect(result.success).toBe(true)
expect(result.filePath).toContain("roo-diagnostics-")

// Verify we attempted to read API history
expect(fs.readFile).toHaveBeenCalledWith(path.join("/mock/task-dir", "api_conversation_history.json"), "utf8")

// Verify we wrote a diagnostics file with the expected content
expect(fs.writeFile).toHaveBeenCalledTimes(1)
const [writtenPath, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
// taskId.slice(0, 8) = "test-tas" from "test-task-id"
expect(String(writtenPath)).toContain("roo-diagnostics-test-tas")
expect(String(writtenContent)).toContain(
"// Please share this file with Roo Code Support ([email protected]) to diagnose the issue faster",
)
expect(String(writtenContent)).toContain('"error":')
expect(String(writtenContent)).toContain('"history":')
expect(String(writtenContent)).toContain('"version": "1.2.3"')
expect(String(writtenContent)).toContain('"provider": "test-provider"')
expect(String(writtenContent)).toContain('"model": "test-model"')
expect(String(writtenContent)).toContain('"details": "Sample error details"')

// Verify VS Code APIs were used to open the generated file
expect(vscode.workspace.openTextDocument).toHaveBeenCalledTimes(1)
expect(vscode.window.showTextDocument).toHaveBeenCalledTimes(1)
})

it("uses empty history when API history file does not exist", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false as any)

const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.0.0",
provider: "test",
model: "test",
details: "error",
},
log: mockLog,
})

expect(result.success).toBe(true)

// Should not attempt to read file when it doesn't exist
expect(fs.readFile).not.toHaveBeenCalled()

// Verify empty history in output
const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
expect(String(writtenContent)).toContain('"history": []')
})

it("uses default values when values are not provided", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false as any)

const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
log: mockLog,
})

expect(result.success).toBe(true)

// Verify defaults in output
const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
expect(String(writtenContent)).toContain('"version": ""')
expect(String(writtenContent)).toContain('"provider": ""')
expect(String(writtenContent)).toContain('"model": ""')
expect(String(writtenContent)).toContain('"details": ""')
})

it("handles JSON parse error gracefully", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true as any)
vi.mocked(fs.readFile).mockResolvedValue("invalid json" as any)

const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.0.0",
provider: "test",
model: "test",
details: "error",
},
log: mockLog,
})

// Should still succeed but with empty history
expect(result.success).toBe(true)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to parse api_conversation_history.json")

// Verify empty history in output
const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
expect(String(writtenContent)).toContain('"history": []')
})

it("returns error result when file write fails", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false as any)
vi.mocked(fs.writeFile).mockRejectedValue(new Error("Write failed"))

const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
log: mockLog,
})

expect(result.success).toBe(false)
expect(result.error).toBe("Write failed")
expect(mockLog).toHaveBeenCalledWith("Error generating diagnostics: Write failed")
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to generate diagnostics: Write failed")
})
})
93 changes: 84 additions & 9 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import type { Mock } from "vitest"
// Mock dependencies - must come before imports
vi.mock("../../../api/providers/fetchers/modelCache")

// Mock the diagnosticsHandler module
vi.mock("../diagnosticsHandler", () => ({
generateErrorDiagnostics: vi.fn().mockResolvedValue({ success: true, filePath: "/tmp/diagnostics.json" }),
}))

import { webviewMessageHandler } from "../webviewMessageHandler"
import type { ClineProvider } from "../ClineProvider"
import { getModels } from "../../../api/providers/fetchers/modelCache"
Expand Down Expand Up @@ -41,15 +46,24 @@ const mockClineProvider = {

import { t } from "../../../i18n"

vi.mock("vscode", () => ({
window: {
showInformationMessage: vi.fn(),
showErrorMessage: vi.fn(),
},
workspace: {
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
},
}))
vi.mock("vscode", () => {
const showInformationMessage = vi.fn()
const showErrorMessage = vi.fn()
const openTextDocument = vi.fn().mockResolvedValue({})
const showTextDocument = vi.fn().mockResolvedValue(undefined)

return {
window: {
showInformationMessage,
showErrorMessage,
showTextDocument,
},
workspace: {
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
openTextDocument,
},
}
})

vi.mock("../../../i18n", () => ({
t: vi.fn((key: string, args?: Record<string, any>) => {
Expand All @@ -72,14 +86,20 @@ vi.mock("../../../i18n", () => ({
vi.mock("fs/promises", () => {
const mockRm = vi.fn().mockResolvedValue(undefined)
const mockMkdir = vi.fn().mockResolvedValue(undefined)
const mockReadFile = vi.fn().mockResolvedValue("[]")
const mockWriteFile = vi.fn().mockResolvedValue(undefined)

return {
default: {
rm: mockRm,
mkdir: mockMkdir,
readFile: mockReadFile,
writeFile: mockWriteFile,
},
rm: mockRm,
mkdir: mockMkdir,
readFile: mockReadFile,
writeFile: mockWriteFile,
}
})

Expand All @@ -90,6 +110,7 @@ import * as path from "path"
import * as fsUtils from "../../../utils/fs"
import { getWorkspacePath } from "../../../utils/path"
import { ensureSettingsDirectoryExists } from "../../../utils/globalContext"
import { generateErrorDiagnostics } from "../diagnosticsHandler"
import type { ModeConfig } from "@roo-code/types"

vi.mock("../../../utils/fs")
Expand Down Expand Up @@ -739,3 +760,57 @@ describe("webviewMessageHandler - mcpEnabled", () => {
expect(mockClineProvider.postStateToWebview).toHaveBeenCalledTimes(1)
})
})

describe("webviewMessageHandler - downloadErrorDiagnostics", () => {
beforeEach(() => {
vi.clearAllMocks()

// Ensure contextProxy has a globalStorageUri for the handler
;(mockClineProvider as any).contextProxy.globalStorageUri = { fsPath: "/mock/global/storage" }

// Provide a current task with a stable ID
vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue({
taskId: "test-task-id",
} as any)
})

it("calls generateErrorDiagnostics with correct parameters", async () => {
await webviewMessageHandler(mockClineProvider, {
type: "downloadErrorDiagnostics",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.2.3",
provider: "test-provider",
model: "test-model",
details: "Sample error details",
},
} as any)

// Verify generateErrorDiagnostics was called with the correct parameters
expect(generateErrorDiagnostics).toHaveBeenCalledTimes(1)
expect(generateErrorDiagnostics).toHaveBeenCalledWith({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.2.3",
provider: "test-provider",
model: "test-model",
details: "Sample error details",
},
log: expect.any(Function),
})
})

it("shows error when no active task", async () => {
vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue(null as any)

await webviewMessageHandler(mockClineProvider, {
type: "downloadErrorDiagnostics",
values: {},
} as any)

expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("No active task to generate diagnostics for")
expect(generateErrorDiagnostics).not.toHaveBeenCalled()
})
})
Loading
Loading