Skip to content

Commit 73d4587

Browse files
committed
refactor: extract error diagnostics logic from webviewMessageHandler
1 parent 59e77aa commit 73d4587

File tree

4 files changed

+317
-85
lines changed

4 files changed

+317
-85
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// npx vitest src/core/webview/__tests__/diagnosticsHandler.spec.ts
2+
3+
import * as path from "path"
4+
5+
// Mock vscode first
6+
vi.mock("vscode", () => {
7+
const showErrorMessage = vi.fn()
8+
const openTextDocument = vi.fn().mockResolvedValue({})
9+
const showTextDocument = vi.fn().mockResolvedValue(undefined)
10+
11+
return {
12+
window: {
13+
showErrorMessage,
14+
showTextDocument,
15+
},
16+
workspace: {
17+
openTextDocument,
18+
},
19+
}
20+
})
21+
22+
// Mock storage utilities
23+
vi.mock("../../../utils/storage", () => ({
24+
getTaskDirectoryPath: vi.fn(async () => "/mock/task-dir"),
25+
}))
26+
27+
// Mock fs utilities
28+
vi.mock("../../../utils/fs", () => ({
29+
fileExistsAtPath: vi.fn(),
30+
}))
31+
32+
// Mock fs/promises
33+
vi.mock("fs/promises", () => {
34+
const mockReadFile = vi.fn()
35+
const mockWriteFile = vi.fn().mockResolvedValue(undefined)
36+
37+
return {
38+
default: {
39+
readFile: mockReadFile,
40+
writeFile: mockWriteFile,
41+
},
42+
readFile: mockReadFile,
43+
writeFile: mockWriteFile,
44+
}
45+
})
46+
47+
import * as vscode from "vscode"
48+
import * as fs from "fs/promises"
49+
import * as fsUtils from "../../../utils/fs"
50+
import { generateErrorDiagnostics } from "../diagnosticsHandler"
51+
52+
describe("generateErrorDiagnostics", () => {
53+
const mockLog = vi.fn()
54+
55+
beforeEach(() => {
56+
vi.clearAllMocks()
57+
})
58+
59+
it("generates a diagnostics file with error metadata and history", async () => {
60+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true as any)
61+
vi.mocked(fs.readFile).mockResolvedValue('[{"role": "user", "content": "test"}]' as any)
62+
63+
const result = await generateErrorDiagnostics({
64+
taskId: "test-task-id",
65+
globalStoragePath: "/mock/global/storage",
66+
values: {
67+
timestamp: "2025-01-01T00:00:00.000Z",
68+
version: "1.2.3",
69+
provider: "test-provider",
70+
model: "test-model",
71+
details: "Sample error details",
72+
},
73+
log: mockLog,
74+
})
75+
76+
expect(result.success).toBe(true)
77+
expect(result.filePath).toContain("roo-diagnostics-")
78+
79+
// Verify we attempted to read API history
80+
expect(fs.readFile).toHaveBeenCalledWith(path.join("/mock/task-dir", "api_conversation_history.json"), "utf8")
81+
82+
// Verify we wrote a diagnostics file with the expected content
83+
expect(fs.writeFile).toHaveBeenCalledTimes(1)
84+
const [writtenPath, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
85+
// taskId.slice(0, 8) = "test-tas" from "test-task-id"
86+
expect(String(writtenPath)).toContain("roo-diagnostics-test-tas")
87+
expect(String(writtenContent)).toContain(
88+
"// Please share this file with Roo Code Support (support@roocode.com) to diagnose the issue faster",
89+
)
90+
expect(String(writtenContent)).toContain('"error":')
91+
expect(String(writtenContent)).toContain('"history":')
92+
expect(String(writtenContent)).toContain('"version": "1.2.3"')
93+
expect(String(writtenContent)).toContain('"provider": "test-provider"')
94+
expect(String(writtenContent)).toContain('"model": "test-model"')
95+
expect(String(writtenContent)).toContain('"details": "Sample error details"')
96+
97+
// Verify VS Code APIs were used to open the generated file
98+
expect(vscode.workspace.openTextDocument).toHaveBeenCalledTimes(1)
99+
expect(vscode.window.showTextDocument).toHaveBeenCalledTimes(1)
100+
})
101+
102+
it("uses empty history when API history file does not exist", async () => {
103+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false as any)
104+
105+
const result = await generateErrorDiagnostics({
106+
taskId: "test-task-id",
107+
globalStoragePath: "/mock/global/storage",
108+
values: {
109+
timestamp: "2025-01-01T00:00:00.000Z",
110+
version: "1.0.0",
111+
provider: "test",
112+
model: "test",
113+
details: "error",
114+
},
115+
log: mockLog,
116+
})
117+
118+
expect(result.success).toBe(true)
119+
120+
// Should not attempt to read file when it doesn't exist
121+
expect(fs.readFile).not.toHaveBeenCalled()
122+
123+
// Verify empty history in output
124+
const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
125+
expect(String(writtenContent)).toContain('"history": []')
126+
})
127+
128+
it("uses default values when values are not provided", async () => {
129+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false as any)
130+
131+
const result = await generateErrorDiagnostics({
132+
taskId: "test-task-id",
133+
globalStoragePath: "/mock/global/storage",
134+
log: mockLog,
135+
})
136+
137+
expect(result.success).toBe(true)
138+
139+
// Verify defaults in output
140+
const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
141+
expect(String(writtenContent)).toContain('"version": ""')
142+
expect(String(writtenContent)).toContain('"provider": ""')
143+
expect(String(writtenContent)).toContain('"model": ""')
144+
expect(String(writtenContent)).toContain('"details": ""')
145+
})
146+
147+
it("handles JSON parse error gracefully", async () => {
148+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true as any)
149+
vi.mocked(fs.readFile).mockResolvedValue("invalid json" as any)
150+
151+
const result = await generateErrorDiagnostics({
152+
taskId: "test-task-id",
153+
globalStoragePath: "/mock/global/storage",
154+
values: {
155+
timestamp: "2025-01-01T00:00:00.000Z",
156+
version: "1.0.0",
157+
provider: "test",
158+
model: "test",
159+
details: "error",
160+
},
161+
log: mockLog,
162+
})
163+
164+
// Should still succeed but with empty history
165+
expect(result.success).toBe(true)
166+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to parse api_conversation_history.json")
167+
168+
// Verify empty history in output
169+
const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
170+
expect(String(writtenContent)).toContain('"history": []')
171+
})
172+
173+
it("returns error result when file write fails", async () => {
174+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false as any)
175+
vi.mocked(fs.writeFile).mockRejectedValue(new Error("Write failed"))
176+
177+
const result = await generateErrorDiagnostics({
178+
taskId: "test-task-id",
179+
globalStoragePath: "/mock/global/storage",
180+
log: mockLog,
181+
})
182+
183+
expect(result.success).toBe(false)
184+
expect(result.error).toBe("Write failed")
185+
expect(mockLog).toHaveBeenCalledWith("Error generating diagnostics: Write failed")
186+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to generate diagnostics: Write failed")
187+
})
188+
})

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import type { Mock } from "vitest"
55
// Mock dependencies - must come before imports
66
vi.mock("../../../api/providers/fetchers/modelCache")
77

8-
// Mock storage utilities used by debug/diagnostics handlers
9-
vi.mock("../../../utils/storage", () => ({
10-
getTaskDirectoryPath: vi.fn(async () => "/mock/task-dir"),
8+
// Mock the diagnosticsHandler module
9+
vi.mock("../diagnosticsHandler", () => ({
10+
generateErrorDiagnostics: vi.fn().mockResolvedValue({ success: true, filePath: "/tmp/diagnostics.json" }),
1111
}))
1212

1313
import { webviewMessageHandler } from "../webviewMessageHandler"
@@ -110,6 +110,7 @@ import * as path from "path"
110110
import * as fsUtils from "../../../utils/fs"
111111
import { getWorkspacePath } from "../../../utils/path"
112112
import { ensureSettingsDirectoryExists } from "../../../utils/globalContext"
113+
import { generateErrorDiagnostics } from "../diagnosticsHandler"
113114
import type { ModeConfig } from "@roo-code/types"
114115

115116
vi.mock("../../../utils/fs")
@@ -771,18 +772,9 @@ describe("webviewMessageHandler - downloadErrorDiagnostics", () => {
771772
vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue({
772773
taskId: "test-task-id",
773774
} as any)
774-
775-
// fileExistsAtPath should report that the history file exists
776-
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true as any)
777775
})
778776

779-
it("generates a diagnostics file with error metadata and history", async () => {
780-
const readFileSpy = vi.spyOn(fs, "readFile").mockResolvedValue("[{}]" as any)
781-
const writeFileSpy = vi.spyOn(fs, "writeFile").mockResolvedValue(undefined as any)
782-
783-
const openTextDocumentSpy = vi.spyOn(vscode.workspace, "openTextDocument")
784-
const showTextDocumentSpy = vi.spyOn(vscode.window, "showTextDocument")
785-
777+
it("calls generateErrorDiagnostics with correct parameters", async () => {
786778
await webviewMessageHandler(mockClineProvider, {
787779
type: "downloadErrorDiagnostics",
788780
values: {
@@ -794,25 +786,31 @@ describe("webviewMessageHandler - downloadErrorDiagnostics", () => {
794786
},
795787
} as any)
796788

797-
// Ensure we attempted to read API history
798-
expect(readFileSpy).toHaveBeenCalledWith(path.join("/mock/task-dir", "api_conversation_history.json"), "utf8")
789+
// Verify generateErrorDiagnostics was called with the correct parameters
790+
expect(generateErrorDiagnostics).toHaveBeenCalledTimes(1)
791+
expect(generateErrorDiagnostics).toHaveBeenCalledWith({
792+
taskId: "test-task-id",
793+
globalStoragePath: "/mock/global/storage",
794+
values: {
795+
timestamp: "2025-01-01T00:00:00.000Z",
796+
version: "1.2.3",
797+
provider: "test-provider",
798+
model: "test-model",
799+
details: "Sample error details",
800+
},
801+
log: expect.any(Function),
802+
})
803+
})
799804

800-
// Ensure we wrote a diagnostics file with the expected header and JSON content
801-
expect(writeFileSpy).toHaveBeenCalledTimes(1)
802-
const [writtenPath, writtenContent] = writeFileSpy.mock.calls[0]
803-
expect(String(writtenPath)).toContain("roo-diagnostics-")
804-
expect(String(writtenContent)).toContain(
805-
"// Please share this file with Roo Code Support (support@roocode.com) to diagnose the issue faster",
806-
)
807-
expect(String(writtenContent)).toContain('"error":')
808-
expect(String(writtenContent)).toContain('"history":')
809-
expect(String(writtenContent)).toContain('"version": "1.2.3"')
810-
expect(String(writtenContent)).toContain('"provider": "test-provider"')
811-
expect(String(writtenContent)).toContain('"model": "test-model"')
812-
expect(String(writtenContent)).toContain('"details": "Sample error details"')
813-
814-
// Ensure VS Code APIs were used to open the generated file
815-
expect(openTextDocumentSpy).toHaveBeenCalledTimes(1)
816-
expect(showTextDocumentSpy).toHaveBeenCalledTimes(1)
805+
it("shows error when no active task", async () => {
806+
vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue(null as any)
807+
808+
await webviewMessageHandler(mockClineProvider, {
809+
type: "downloadErrorDiagnostics",
810+
values: {},
811+
} as any)
812+
813+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("No active task to generate diagnostics for")
814+
expect(generateErrorDiagnostics).not.toHaveBeenCalled()
817815
})
818816
})
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as path from "path"
2+
import * as os from "os"
3+
import * as fs from "fs/promises"
4+
import * as vscode from "vscode"
5+
6+
import { getTaskDirectoryPath } from "../../utils/storage"
7+
import { fileExistsAtPath } from "../../utils/fs"
8+
9+
export interface ErrorDiagnosticsValues {
10+
timestamp?: string
11+
version?: string
12+
provider?: string
13+
model?: string
14+
details?: string
15+
}
16+
17+
export interface GenerateDiagnosticsParams {
18+
taskId: string
19+
globalStoragePath: string
20+
values?: ErrorDiagnosticsValues
21+
log: (message: string) => void
22+
}
23+
24+
export interface GenerateDiagnosticsResult {
25+
success: boolean
26+
filePath?: string
27+
error?: string
28+
}
29+
30+
/**
31+
* Generates an error diagnostics file containing error metadata and API conversation history.
32+
* The file is created in the system temp directory and opened in VS Code for the user to review
33+
* before sharing with support.
34+
*/
35+
export async function generateErrorDiagnostics(params: GenerateDiagnosticsParams): Promise<GenerateDiagnosticsResult> {
36+
const { taskId, globalStoragePath, values, log } = params
37+
38+
try {
39+
const taskDirPath = await getTaskDirectoryPath(globalStoragePath, taskId)
40+
41+
// Load API conversation history from the same file used by openDebugApiHistory
42+
const apiHistoryPath = path.join(taskDirPath, "api_conversation_history.json")
43+
let history: unknown = []
44+
45+
if (await fileExistsAtPath(apiHistoryPath)) {
46+
const content = await fs.readFile(apiHistoryPath, "utf8")
47+
try {
48+
history = JSON.parse(content)
49+
} catch {
50+
// If parsing fails, fall back to empty history but still generate diagnostics file
51+
vscode.window.showErrorMessage("Failed to parse api_conversation_history.json")
52+
}
53+
}
54+
55+
const diagnostics = {
56+
error: {
57+
timestamp: values?.timestamp ?? new Date().toISOString(),
58+
version: values?.version ?? "",
59+
provider: values?.provider ?? "",
60+
model: values?.model ?? "",
61+
details: values?.details ?? "",
62+
},
63+
history,
64+
}
65+
66+
// Prepend human-readable guidance comments before the JSON payload
67+
const headerComment =
68+
"// Please share this file with Roo Code Support (support@roocode.com) to diagnose the issue faster\n" +
69+
"// Just make sure you're OK sharing the contents of the conversation below.\n\n"
70+
const jsonContent = JSON.stringify(diagnostics, null, 2)
71+
const fullContent = headerComment + jsonContent
72+
73+
// Create a temporary diagnostics file
74+
const tmpDir = os.tmpdir()
75+
const timestamp = Date.now()
76+
const tempFileName = `roo-diagnostics-${taskId.slice(0, 8)}-${timestamp}.json`
77+
const tempFilePath = path.join(tmpDir, tempFileName)
78+
79+
await fs.writeFile(tempFilePath, fullContent, "utf8")
80+
81+
// Open the diagnostics file in VS Code
82+
const doc = await vscode.workspace.openTextDocument(tempFilePath)
83+
await vscode.window.showTextDocument(doc, { preview: true })
84+
85+
return { success: true, filePath: tempFilePath }
86+
} catch (error) {
87+
const errorMessage = error instanceof Error ? error.message : String(error)
88+
log(`Error generating diagnostics: ${errorMessage}`)
89+
vscode.window.showErrorMessage(`Failed to generate diagnostics: ${errorMessage}`)
90+
return { success: false, error: errorMessage }
91+
}
92+
}

0 commit comments

Comments
 (0)