diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index a06c73d8a9..16de5fe8a5 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -1029,121 +1029,6 @@ "count": 5 } }, - "integrations/editor/DiffViewProvider.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "integrations/editor/__tests__/DiffViewProvider.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 310 - } - }, - "integrations/editor/__tests__/EditorUtils.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 3 - } - }, - "integrations/kimi-code/__tests__/oauth.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "integrations/misc/__tests__/export-markdown.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "integrations/misc/__tests__/open-file.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 11 - } - }, - "integrations/misc/__tests__/performance/processCarriageReturns.benchmark.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, - "integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 10 - } - }, - "integrations/terminal/__tests__/OutputInterceptor.test.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 5 - } - }, - "integrations/terminal/__tests__/TerminalProcess.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 7 - } - }, - "integrations/terminal/__tests__/TerminalProcess.test.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, - "integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, - "integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "integrations/terminal/__tests__/TerminalProcessInterpretExitCode.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 3 - } - }, - "integrations/terminal/__tests__/TerminalProfile.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 34 - } - }, - "integrations/terminal/__tests__/TerminalRegistry.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 22 - } - }, - "integrations/terminal/__tests__/setupTerminalTests.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "integrations/terminal/__tests__/streamUtils/bashStream.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "integrations/terminal/__tests__/streamUtils/cmdStream.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "integrations/terminal/__tests__/streamUtils/index.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, - "integrations/terminal/__tests__/streamUtils/pwshStream.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "integrations/theme/getTheme.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 9 - } - }, "services/__tests__/zoo-code-auth.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 3 diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index bb3368f063..3c1d33ae28 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -977,9 +977,10 @@ export class DiffViewProvider { () => { // Command executed successfully, now wait for the editor to appear }, - (err: any) => { + (err: unknown) => { cleanup() - reject(new Error(`Failed to execute diff command for ${uri.fsPath}: ${err.message}`)) + const message = err instanceof Error ? err.message : String(err) + reject(new Error(`Failed to execute diff command for ${uri.fsPath}: ${message}`)) }, ) }) diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index a740ebed5c..856891d4cf 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -1,9 +1,12 @@ import { DiffViewProvider, DIFF_VIEW_URI_SCHEME, DIFF_VIEW_LABEL_CHANGES } from "../DiffViewProvider" +import { DecorationController } from "../DecorationController" +import type { Task } from "../../../core/task/Task" +import type { Mock } from "vitest" import * as vscode from "vscode" import delay from "delay" import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" -import { makeRange, makeTextDocument, makeTextEditor, makeUri } from "../../../test-utils/vscode" +import { makePosition, makeRange, makeTextDocument, makeTextEditor, makeUri } from "../../../test-utils/vscode" // Mock delay vi.mock("delay", () => ({ @@ -136,63 +139,97 @@ vi.mock("../DecorationController", () => ({ describe("DiffViewProvider", () => { let diffViewProvider: DiffViewProvider const mockCwd = "/mock/cwd" - let mockWorkspaceEdit: { replace: any; delete: any } - let mockTask: any + let mockWorkspaceEdit: { replace: Mock; delete: Mock } + let mockTask: Task + + // Tab doubles: the mocked vscode classes ignore constructor arguments, so the + // uri/original/modified members the source reads after `instanceof` checks are + // assigned directly onto real mocked-class instances. + const makeTextTab = (fsPath: string): vscode.TabInputText => + Object.assign(new vscode.TabInputText(makeUri(fsPath)), { uri: makeUri(fsPath) }) + + const makeDiffTabInput = (originalScheme: string, modifiedFsPath: string): vscode.TabInputTextDiff => + Object.assign(new vscode.TabInputTextDiff(makeUri(modifiedFsPath), makeUri(modifiedFsPath)), { + original: { scheme: originalScheme }, + modified: { fsPath: modifiedFsPath }, + }) + + const makeTab = (input: vscode.Tab["input"], label: string, overrides: Partial = {}): vscode.Tab => ({ + input, + label, + isDirty: false, + isPinned: false, + isPreview: false, + isActive: false, + group: { isActive: false, viewColumn: vscode.ViewColumn.One, activeTab: undefined, tabs: [] }, + ...overrides, + }) - beforeEach(() => { - vi.clearAllMocks() - mockWorkspaceEdit = { - replace: vi.fn(), - delete: vi.fn(), - } - vi.mocked(vscode.WorkspaceEdit).mockImplementation(function () { - return mockWorkspaceEdit as any - }) + const makePreviewTab = (fsPath: string, isPreview = true): vscode.Tab => + makeTab(makeTextTab(fsPath), fsPath, { isPreview }) - // Create a mock Task instance - mockTask = { + // Minimal Uri double: provides only the fsPath/scheme members the restore + // snapshot path reads, so toEqual assertions stay exact. + const makeSnapshotUri = (fsPath: string): vscode.Uri => ({ fsPath, scheme: "file" }) as unknown as vscode.Uri + + // Task double provides providerRef.deref() -> getState() with provider settings. + const makeTaskDouble = (stateOverrides: Record = {}): Task => + ({ providerRef: { deref: vi.fn().mockReturnValue({ getState: vi.fn().mockResolvedValue({ includeDiagnosticMessages: true, maxDiagnosticMessages: 50, - // Auto-closing edited tabs is opt-in by default; the legacy - // "close/keep behavior" suite below asserts the close path, so - // enable it here. The opt-in default itself is covered by the - // dedicated "auto-close settings decision table" suite. - autoCloseZooOpenedFiles: true, + ...stateOverrides, }), }), }, + }) as unknown as Task + + beforeEach(() => { + vi.clearAllMocks() + mockWorkspaceEdit = { + replace: vi.fn(), + delete: vi.fn(), } + vi.mocked(vscode.WorkspaceEdit).mockImplementation(function () { + // WorkspaceEdit double provides replace() and delete(), the only members + // update()/revertChanges() call. + return mockWorkspaceEdit as unknown as vscode.WorkspaceEdit + }) + + // Create a mock Task instance; the double provides providerRef.deref() -> + // getState() with provider settings (see makeTaskDouble below). + mockTask = makeTaskDouble({ + // Auto-closing edited tabs is opt-in by default; the legacy + // "close/keep behavior" suite below asserts the close path, so + // enable it here. The opt-in default itself is covered by the + // dedicated "auto-close settings decision table" suite. + autoCloseZooOpenedFiles: true, + }) diffViewProvider = new DiffViewProvider(mockCwd, mockTask) // Mock the necessary properties and methods - ;(diffViewProvider as any).relPath = "test.txt" - ;(diffViewProvider as any).activeDiffEditor = { - document: { - uri: { fsPath: `${mockCwd}/test.txt` }, - getText: vi.fn(), - lineCount: 10, - }, - selection: { - active: { line: 0, character: 0 }, - anchor: { line: 0, character: 0 }, - }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } - ;(diffViewProvider as any).activeLineController = { setActiveLine: vi.fn(), clear: vi.fn() } - ;(diffViewProvider as any).fadedOverlayController = { + diffViewProvider["relPath"] = "test.txt" + diffViewProvider["activeDiffEditor"] = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(`${mockCwd}/test.txt`), lineCount: 10 }), + }) + // Decoration doubles provide setActiveLine/clear and + // updateOverlayAfterLine/addLines/clear respectively. + diffViewProvider["activeLineController"] = { + setActiveLine: vi.fn(), + clear: vi.fn(), + } as unknown as DecorationController + diffViewProvider["fadedOverlayController"] = { updateOverlayAfterLine: vi.fn(), addLines: vi.fn(), clear: vi.fn(), - } + } as unknown as DecorationController }) describe("update method", () => { it("should preserve empty last line when original content has one", async () => { - ;(diffViewProvider as any).originalContent = "Original content\n" + diffViewProvider.originalContent = "Original content\n" await diffViewProvider.update("New content", true) expect(mockWorkspaceEdit.replace).toHaveBeenCalledWith( @@ -203,7 +240,7 @@ describe("DiffViewProvider", () => { }) it("should not add extra newline when accumulated content already ends with one", async () => { - ;(diffViewProvider as any).originalContent = "Original content\n" + diffViewProvider.originalContent = "Original content\n" await diffViewProvider.update("New content\n", true) expect(mockWorkspaceEdit.replace).toHaveBeenCalledWith( @@ -214,7 +251,7 @@ describe("DiffViewProvider", () => { }) it("should not add newline when original content does not end with one", async () => { - ;(diffViewProvider as any).originalContent = "Original content" + diffViewProvider.originalContent = "Original content" await diffViewProvider.update("New content", true) expect(mockWorkspaceEdit.replace).toHaveBeenCalledWith(expect.anything(), expect.anything(), "New content") @@ -224,19 +261,9 @@ describe("DiffViewProvider", () => { describe("open method", () => { it("should pre-open file as text document before executing diff command", async () => { // Setup - const mockEditor = { - document: { - uri: { fsPath: `${mockCwd}/test.md`, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { - active: { line: 0, character: 0 }, - anchor: { line: 0, character: 0 }, - }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } + const mockEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(`${mockCwd}/test.md`) }), + }) // Track the order of calls const callOrder: string[] = [] @@ -245,7 +272,7 @@ describe("DiffViewProvider", () => { vi.mocked(vscode.window.showTextDocument).mockImplementation(async (_uri, options) => { callOrder.push("showTextDocument") expect(options).toEqual({ preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }) - return mockEditor as any + return mockEditor }) // Mock executeCommand to track when it's called @@ -259,16 +286,16 @@ describe("DiffViewProvider", () => { vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { // Trigger the callback immediately with the document setTimeout(() => { - callback({ uri: { fsPath: `${mockCwd}/test.md`, scheme: "file" } } as any) + callback(makeTextDocument({ uri: makeUri(`${mockCwd}/test.md`) })) }, 0) return { dispose: vi.fn() } }) // Mock window.visibleTextEditors to return our editor - vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] + vi.mocked(vscode.window).visibleTextEditors = [mockEditor] // Set up for file - ;(diffViewProvider as any).editType = "modify" + diffViewProvider.editType = "modify" // Execute open await diffViewProvider.open("test.md") @@ -303,7 +330,7 @@ describe("DiffViewProvider", () => { vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) // Set up for file - ;(diffViewProvider as any).editType = "modify" + diffViewProvider.editType = "modify" // Try to open and expect rejection await expect(diffViewProvider.open("test.md")).rejects.toThrow( @@ -312,46 +339,32 @@ describe("DiffViewProvider", () => { }) it("records the pin state of an already-open pinned tab", async () => { - const mockEditor = { - document: { - uri: { fsPath: `${mockCwd}/test.md`, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } + const mockEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(`${mockCwd}/test.md`) }), + }) // An open, pinned, non-dirty tab for the target file. - const pinnedTab = { - input: Object.assign(new (vscode as any).TabInputText(), { - uri: { fsPath: `${mockCwd}/test.md`, scheme: "file" }, - }), - isDirty: false, - isPinned: true, - label: "test.md", - } + const pinnedTab = makeTab(makeTextTab(`${mockCwd}/test.md`), "test.md", { isPinned: true }) Object.defineProperty(vscode.window.tabGroups, "all", { get: () => [{ tabs: [pinnedTab] }], configurable: true, }) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor) vi.mocked(vscode.commands.executeCommand).mockResolvedValue(undefined) vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { setTimeout(() => { - callback({ uri: { fsPath: `${mockCwd}/test.md`, scheme: "file" } } as any) + callback(makeTextDocument({ uri: makeUri(`${mockCwd}/test.md`) })) }, 0) return { dispose: vi.fn() } }) - vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] - ;(diffViewProvider as any).editType = "modify" + vi.mocked(vscode.window).visibleTextEditors = [mockEditor] + diffViewProvider.editType = "modify" await diffViewProvider.open("test.md") - expect((diffViewProvider as any).documentWasPinned).toBe(true) - expect((diffViewProvider as any).documentWasOpen).toBe(true) + expect(diffViewProvider["documentWasPinned"]).toBe(true) + expect(diffViewProvider["documentWasOpen"]).toBe(true) }) }) @@ -367,7 +380,7 @@ describe("DiffViewProvider", () => { visibleRanges: [makeRange()], revealRange, }) - ;(diffViewProvider as any).activeDiffEditor = editor + diffViewProvider["activeDiffEditor"] = editor // Register the editor as the live modified-side editor so resolveLiveEditor // finds it by document identity, mirroring the runtime path. vi.mocked(vscode.window).visibleTextEditors = [editor] @@ -375,7 +388,7 @@ describe("DiffViewProvider", () => { } it("reveals the first changed line for an addition-only diff", () => { - ;(diffViewProvider as any).originalContent = "a\nb\nc\n" + diffViewProvider.originalContent = "a\nb\nc\n" // Insert a new line between b and c (first change is at line index 2). const revealRange = setupEditor("a\nb\nNEW\nc\n") @@ -387,7 +400,7 @@ describe("DiffViewProvider", () => { }) it("reveals the first changed line for a deletion-only diff", () => { - ;(diffViewProvider as any).originalContent = "a\nb\nc\nd\n" + diffViewProvider.originalContent = "a\nb\nc\nd\n" // Remove line c; the first change is the removed block at line index 2. const revealRange = setupEditor("a\nb\nd\n") @@ -402,7 +415,7 @@ describe("DiffViewProvider", () => { // Long file; remove the final lines. The first-change index lands past the // end of the shortened modified document, so it must be clamped to a real // line or the diff widget will not scroll. - ;(diffViewProvider as any).originalContent = "a\nb\nc\nd\ne\nf\n" + diffViewProvider.originalContent = "a\nb\nc\nd\ne\nf\n" const revealRange = setupEditor("a\nb\nc\n") diffViewProvider.scrollToFirstDiff() @@ -416,7 +429,7 @@ describe("DiffViewProvider", () => { }) it("reveals the first changed line for a mixed diff", () => { - ;(diffViewProvider as any).originalContent = "a\nb\nc\nd\n" + diffViewProvider.originalContent = "a\nb\nc\nd\n" // Change line b (index 1) -- first divergence from the original. const revealRange = setupEditor("a\nCHANGED\nc\nd\n") @@ -432,12 +445,15 @@ describe("DiffViewProvider", () => { // and the diff target is inside the current viewport: a bare revealRange is // a no-op, leaving the viewport pinned at the top. Moving the selection to // the target first forces the diff widget to scroll to the change. - ;(diffViewProvider as any).originalContent = "a\nb\nc\nd\n" + diffViewProvider.originalContent = "a\nb\nc\nd\n" const revealRange = setupEditor("a\nCHANGED\nc\nd\n") diffViewProvider.scrollToFirstDiff() - const editor = (diffViewProvider as any).activeDiffEditor + const editor = diffViewProvider["activeDiffEditor"] + if (!editor) { + throw new Error("activeDiffEditor was not set") + } expect(editor.selection.active.line).toBe(1) expect(editor.selection.anchor.line).toBe(1) expect(revealRange).toHaveBeenCalledTimes(1) @@ -450,7 +466,7 @@ describe("DiffViewProvider", () => { // makes the scroll stick. Verify the second reveal fires on a timer. vi.useFakeTimers() try { - ;(diffViewProvider as any).originalContent = "a\nb\nc\nd\n" + diffViewProvider.originalContent = "a\nb\nc\nd\n" const revealRange = setupEditor("a\nCHANGED\nc\nd\n") diffViewProvider.scrollToFirstDiff() @@ -472,19 +488,21 @@ describe("DiffViewProvider", () => { // detached editor whose visibleRanges no longer match the on-screen diff, // making revealRange a no-op. The reveal must target the live editor found // in visibleTextEditors for the same document. - ;(diffViewProvider as any).originalContent = "a\nb\nc\nd\n" + diffViewProvider.originalContent = "a\nb\nc\nd\n" const staleReveal = setupEditor("a\nCHANGED\nc\nd\n") - const staleEditor = (diffViewProvider as any).activeDiffEditor + const staleEditor = diffViewProvider["activeDiffEditor"] + if (!staleEditor) { + throw new Error("activeDiffEditor was not set") + } // A live editor for the SAME document, distinct from the stale capture. const liveReveal = vi.fn() - const liveEditor = { + const liveEditor = makeTextEditor({ document: staleEditor.document, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - visibleRanges: [{ start: { line: 0 }, end: { line: 0 } }], + visibleRanges: [makeRange()], revealRange: liveReveal, - } - vi.mocked(vscode.window).visibleTextEditors = [liveEditor as any] + }) + vi.mocked(vscode.window).visibleTextEditors = [liveEditor] diffViewProvider.scrollToFirstDiff() @@ -498,16 +516,16 @@ describe("DiffViewProvider", () => { // after a subsequent edit swapped in a different active diff editor. vi.useFakeTimers() try { - ;(diffViewProvider as any).originalContent = "a\nb\nc\nd\n" + diffViewProvider.originalContent = "a\nb\nc\nd\n" const firstReveal = setupEditor("a\nCHANGED\nc\nd\n") - const firstEditor = (diffViewProvider as any).activeDiffEditor + const firstEditor = diffViewProvider["activeDiffEditor"] diffViewProvider.scrollToFirstDiff() expect(firstReveal).toHaveBeenCalledTimes(1) // A later edit swaps in a brand new diff editor before the timer fires. const secondReveal = setupEditor("a\nb\nc\nd\n") - expect((diffViewProvider as any).activeDiffEditor).not.toBe(firstEditor) + expect(diffViewProvider["activeDiffEditor"]).not.toBe(firstEditor) vi.advanceTimersByTime(100) @@ -520,22 +538,15 @@ describe("DiffViewProvider", () => { }) it("does nothing when there is no diff editor", () => { - ;(diffViewProvider as any).activeDiffEditor = undefined + diffViewProvider["activeDiffEditor"] = undefined expect(() => diffViewProvider.scrollToFirstDiff()).not.toThrow() }) }) describe("preview tab snapshot and restore", () => { - const makePreviewTab = (fsPath: string, isPreview = true) => { - const input = Object.assign(new (vscode as any).TabInputText(), { - uri: { fsPath, scheme: "file" }, - }) - return { isPreview, input, label: fsPath } - } - - const setTabs = (tabs: any[], viewColumn = vscode.ViewColumn.One) => { + const setTabs = (tabs: vscode.Tab[], viewColumn: vscode.ViewColumn = vscode.ViewColumn.One) => { Object.defineProperty(vscode.window.tabGroups, "all", { - get: () => [{ tabs, viewColumn }], + get: (): vscode.TabGroup[] => [{ isActive: true, viewColumn, activeTab: tabs[0], tabs }], configurable: true, }) } @@ -550,13 +561,13 @@ describe("DiffViewProvider", () => { vscode.ViewColumn.Two, ) vi.mocked(vscode.window).visibleTextEditors = [ - { - document: { uri: { fsPath: "/mock/cwd/file-1.txt", scheme: "file" } }, - visibleRanges: [{ start: { line: 12 } }], - } as any, + makeTextEditor({ + document: makeTextDocument({ uri: makeUri("/mock/cwd/file-1.txt") }), + visibleRanges: [makeRange(12)], + }), ] - const snapshot = (diffViewProvider as any).captureUnrelatedPreviewTabs("/mock/cwd/file-2.txt") + const snapshot = diffViewProvider["captureUnrelatedPreviewTabs"]("/mock/cwd/file-2.txt") expect(snapshot).toHaveLength(1) expect(snapshot[0].uri.fsPath).toBe("/mock/cwd/file-1.txt") @@ -566,18 +577,18 @@ describe("DiffViewProvider", () => { it("restores an evicted preview tab in its original group and reapplies its scroll position", async () => { const revealRange = vi.fn() - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange } as any) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor({ revealRange })) // The captured file is no longer open (evicted by the diff). setTabs([]) - ;(diffViewProvider as any).snapshotPreviewTabs = [ + diffViewProvider["snapshotPreviewTabs"] = [ { - uri: { fsPath: "/mock/cwd/file-1.txt", scheme: "file" }, + uri: makeSnapshotUri("/mock/cwd/file-1.txt"), scrollLine: 12, viewColumn: vscode.ViewColumn.Two, }, ] - await (diffViewProvider as any).restorePreviewTabs() + await diffViewProvider["restorePreviewTabs"]() expect(vscode.window.showTextDocument).toHaveBeenCalledWith( { fsPath: "/mock/cwd/file-1.txt", scheme: "file" }, @@ -587,20 +598,20 @@ describe("DiffViewProvider", () => { expect.objectContaining({ start: { line: 12, character: 0 } }), vscode.TextEditorRevealType.AtTop, ) - expect((diffViewProvider as any).snapshotPreviewTabs).toEqual([]) + expect(diffViewProvider["snapshotPreviewTabs"]).toEqual([]) }) it("does not restore a preview tab that is still open", async () => { setTabs([makePreviewTab("/mock/cwd/file-1.txt")]) - ;(diffViewProvider as any).snapshotPreviewTabs = [ + diffViewProvider["snapshotPreviewTabs"] = [ { - uri: { fsPath: "/mock/cwd/file-1.txt", scheme: "file" }, + uri: makeSnapshotUri("/mock/cwd/file-1.txt"), scrollLine: 0, viewColumn: vscode.ViewColumn.One, }, ] - await (diffViewProvider as any).restorePreviewTabs() + await diffViewProvider["restorePreviewTabs"]() expect(vscode.window.showTextDocument).not.toHaveBeenCalled() }) @@ -609,18 +620,18 @@ describe("DiffViewProvider", () => { setTabs([]) const fs = await import("fs/promises") vi.mocked(fs.access).mockRejectedValueOnce(new Error("ENOENT")) - ;(diffViewProvider as any).snapshotPreviewTabs = [ + diffViewProvider["snapshotPreviewTabs"] = [ { - uri: { fsPath: "/mock/cwd/deleted.txt", scheme: "file" }, + uri: makeSnapshotUri("/mock/cwd/deleted.txt"), scrollLine: 0, viewColumn: vscode.ViewColumn.One, }, ] - await (diffViewProvider as any).restorePreviewTabs() + await diffViewProvider["restorePreviewTabs"]() expect(vscode.window.showTextDocument).not.toHaveBeenCalled() - expect((diffViewProvider as any).snapshotPreviewTabs).toEqual([]) + expect(diffViewProvider["snapshotPreviewTabs"]).toEqual([]) }) }) @@ -628,15 +639,14 @@ describe("DiffViewProvider", () => { it("re-activates the user's editor when they navigated to a different file", async () => { // User is viewing file-1 while the edited file is file-2. Keeping file-2 // open must not yank focus/foreground onto it. - const userActiveEditor = { - document: { uri: { fsPath: "/mock/cwd/file-1.txt", scheme: "file" } }, - viewColumn: 1, - } - ;(vscode.window as any).activeTextEditor = userActiveEditor - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) - ;(diffViewProvider as any).preEditScrollLine = 5 + const userActiveEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri("/mock/cwd/file-1.txt") }), + }) + vscode.window.activeTextEditor = userActiveEditor + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) + diffViewProvider["preEditScrollLine"] = 5 - await (diffViewProvider as any).showEditedFileWithoutDisruptingFocus("/mock/cwd/file-2.txt") + await diffViewProvider["showEditedFileWithoutDisruptingFocus"]("/mock/cwd/file-2.txt") // First call re-shows the edited file (preserveFocus); last call restores // the user's editor with focus. @@ -648,29 +658,27 @@ describe("DiffViewProvider", () => { }) it("does not re-activate when the user is already on the edited file", async () => { - const userActiveEditor = { - document: { uri: { fsPath: "/mock/cwd/file-2.txt", scheme: "file" } }, - viewColumn: 1, - } - ;(vscode.window as any).activeTextEditor = userActiveEditor - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + const userActiveEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri("/mock/cwd/file-2.txt") }), + }) + vscode.window.activeTextEditor = userActiveEditor + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) - await (diffViewProvider as any).showEditedFileWithoutDisruptingFocus("/mock/cwd/file-2.txt") + await diffViewProvider["showEditedFileWithoutDisruptingFocus"]("/mock/cwd/file-2.txt") // Only the single re-show of the edited file; no focus-restore round trip. expect(vscode.window.showTextDocument).toHaveBeenCalledTimes(1) }) it("re-pins the edited file when it was pinned before the diff", async () => { - const userActiveEditor = { - document: { uri: { fsPath: "/mock/cwd/file-2.txt", scheme: "file" } }, - viewColumn: 1, - } - ;(vscode.window as any).activeTextEditor = userActiveEditor - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) - ;(diffViewProvider as any).documentWasPinned = true + const userActiveEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri("/mock/cwd/file-2.txt") }), + }) + vscode.window.activeTextEditor = userActiveEditor + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) + diffViewProvider["documentWasPinned"] = true - await (diffViewProvider as any).showEditedFileWithoutDisruptingFocus("/mock/cwd/file-2.txt") + await diffViewProvider["showEditedFileWithoutDisruptingFocus"]("/mock/cwd/file-2.txt") // The edited file must be focused (preserveFocus false) so pinEditor // targets the correct tab, then the pin command is issued. @@ -680,15 +688,14 @@ describe("DiffViewProvider", () => { }) it("does not pin the edited file when it was not pinned before the diff", async () => { - const userActiveEditor = { - document: { uri: { fsPath: "/mock/cwd/file-2.txt", scheme: "file" } }, - viewColumn: 1, - } - ;(vscode.window as any).activeTextEditor = userActiveEditor - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) - ;(diffViewProvider as any).documentWasPinned = false + const userActiveEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri("/mock/cwd/file-2.txt") }), + }) + vscode.window.activeTextEditor = userActiveEditor + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) + diffViewProvider["documentWasPinned"] = false - await (diffViewProvider as any).showEditedFileWithoutDisruptingFocus("/mock/cwd/file-2.txt") + await diffViewProvider["showEditedFileWithoutDisruptingFocus"]("/mock/cwd/file-2.txt") expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith("workbench.action.pinEditor") }) @@ -697,73 +704,48 @@ describe("DiffViewProvider", () => { describe("closeAllDiffViews method", () => { it("should close diff views including those identified by label", async () => { // Mock tab groups with various types of tabs - const mockTabs = [ + const mockTabs: vscode.Tab[] = [ // Normal diff view - { - input: { - constructor: { name: "TabInputTextDiff" }, - original: { scheme: DIFF_VIEW_URI_SCHEME }, - modified: { fsPath: "/test/file1.ts" }, - }, - label: `file1.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, - isDirty: false, - }, + makeTab( + makeDiffTabInput(DIFF_VIEW_URI_SCHEME, "/test/file1.ts"), + `file1.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, + ), // Diff view identified by label (for pre-opened files) - { - input: { - constructor: { name: "TabInputTextDiff" }, - original: { scheme: "file" }, // Different scheme due to pre-opening - modified: { fsPath: "/test/file2.md" }, - }, - label: `file2.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, - isDirty: false, - }, + makeTab( + // Different scheme due to pre-opening + makeDiffTabInput("file", "/test/file2.md"), + `file2.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, + ), // Regular file tab (should not be closed) - { - input: { - constructor: { name: "TabInputText" }, - uri: { fsPath: "/test/file3.js" }, - }, - label: "file3.js", - isDirty: false, - }, + makeTab(makeTextTab("/test/file3.js"), "file3.js"), // Dirty diff view (should not be closed) - { - input: { - constructor: { name: "TabInputTextDiff" }, - original: { scheme: DIFF_VIEW_URI_SCHEME }, - modified: { fsPath: "/test/file4.ts" }, - }, - label: `file4.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, - isDirty: true, - }, + makeTab( + makeDiffTabInput(DIFF_VIEW_URI_SCHEME, "/test/file4.ts"), + `file4.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, + { isDirty: true }, + ), ] - // Make tabs appear as TabInputTextDiff instances - mockTabs.forEach((tab) => { - if (tab.input.constructor.name === "TabInputTextDiff") { - Object.setPrototypeOf(tab.input, vscode.TabInputTextDiff.prototype) - } - }) - // Mock the tabGroups getter Object.defineProperty(vscode.window.tabGroups, "all", { - get: () => [ - { - tabs: mockTabs as any, - }, + get: (): vscode.TabGroup[] => [ + { isActive: true, viewColumn: vscode.ViewColumn.One, activeTab: undefined, tabs: mockTabs }, ], configurable: true, }) - const closedTabs: any[] = [] + const closedTabs: vscode.Tab[] = [] vi.mocked(vscode.window.tabGroups.close).mockImplementation((tab) => { + // The close API also accepts whole groups; this suite only closes tabs. + if ("length" in tab || "tabs" in tab) { + throw new Error("unexpected tab group close") + } closedTabs.push(tab) return Promise.resolve(true) }) // Execute closeAllDiffViews - await (diffViewProvider as any).closeAllDiffViews() + await diffViewProvider["closeAllDiffViews"]() // Verify that only the appropriate tabs were closed expect(closedTabs).toHaveLength(2) @@ -781,7 +763,7 @@ describe("DiffViewProvider", () => { describe("saveDirectly method", () => { beforeEach(() => { // Mock vscode functions - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) vi.mocked(vscode.languages.getDiagnostics).mockReturnValue([]) }) @@ -853,29 +835,25 @@ describe("DiffViewProvider", () => { await diffViewProvider.saveDirectly("test.ts", "new content", true, true, 1000) // Verify internal state was updated - expect((diffViewProvider as any).newProblemsMessage).toBe("") - expect((diffViewProvider as any).userEdits).toBeUndefined() - expect((diffViewProvider as any).relPath).toBe("test.ts") - expect((diffViewProvider as any).newContent).toBe("new content") + expect(diffViewProvider.newProblemsMessage).toBe("") + expect(diffViewProvider.userEdits).toBeUndefined() + expect(diffViewProvider["relPath"]).toBe("test.ts") + expect(diffViewProvider["newContent"]).toBe("new content") }) }) describe("saveChanges method with diagnostic settings", () => { beforeEach(() => { // Setup common mocks for saveChanges tests - ;(diffViewProvider as any).relPath = "test.ts" - ;(diffViewProvider as any).newContent = "new content" - ;(diffViewProvider as any).activeDiffEditor = { - document: { - getText: vi.fn().mockReturnValue("new content"), - isDirty: false, - save: vi.fn().mockResolvedValue(undefined), - }, - } - ;(diffViewProvider as any).preDiagnostics = [] + diffViewProvider["relPath"] = "test.ts" + diffViewProvider["newContent"] = "new content" + diffViewProvider["activeDiffEditor"] = makeTextEditor({ + document: makeTextDocument({ getText: vi.fn().mockReturnValue("new content") }), + }) + diffViewProvider["preDiagnostics"] = [] // Mock vscode functions - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) vi.mocked(vscode.languages.getDiagnostics).mockReturnValue([]) }) @@ -884,7 +862,7 @@ describe("DiffViewProvider", () => { mockDelay.mockClear() // Mock closeAllDiffViews - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) const result = await diffViewProvider.saveChanges(true, 3000) @@ -899,7 +877,7 @@ describe("DiffViewProvider", () => { mockDelay.mockClear() // Mock closeAllDiffViews - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) const result = await diffViewProvider.saveChanges(false, 2000) @@ -914,7 +892,7 @@ describe("DiffViewProvider", () => { mockDelay.mockClear() // Mock closeAllDiffViews - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) const result = await diffViewProvider.saveChanges() @@ -935,7 +913,7 @@ describe("DiffViewProvider", () => { mockDelay.mockClear() // Mock closeAllDiffViews - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) await diffViewProvider.saveChanges(true, 5000) @@ -947,76 +925,59 @@ describe("DiffViewProvider", () => { describe("preEditScrollLine capture and restore", () => { it("should capture scroll line from visible editor at open() time", async () => { - const mockEditor = { - document: { - uri: { fsPath: `${mockCwd}/scroll.ts`, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - visibleRanges: [{ start: { line: 42 } }], - } + const mockEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(`${mockCwd}/scroll.ts`) }), + visibleRanges: [makeRange(42)], + }) - vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any) + vi.mocked(vscode.window).visibleTextEditors = [mockEditor] + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor) vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { - setTimeout(() => callback({ uri: { fsPath: `${mockCwd}/scroll.ts`, scheme: "file" } } as any), 0) + setTimeout(() => callback(makeTextDocument({ uri: makeUri(`${mockCwd}/scroll.ts`) })), 0) return { dispose: vi.fn() } }) vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - ;(diffViewProvider as any).editType = "modify" + diffViewProvider.editType = "modify" await diffViewProvider.open("scroll.ts") - expect((diffViewProvider as any).preEditScrollLine).toBe(42) + expect(diffViewProvider["preEditScrollLine"]).toBe(42) }) it("should set preEditScrollLine to undefined when the visible editor has no visibleRanges", async () => { - const mockEditorNoRanges = { - document: { - uri: { fsPath: `${mockCwd}/new.ts`, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), + const mockEditorNoRanges = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(`${mockCwd}/new.ts`) }), // No visibleRanges, so the capture in open() yields undefined. - } + visibleRanges: [], + }) - vi.mocked(vscode.window).visibleTextEditors = [mockEditorNoRanges as any] - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditorNoRanges as any) + vi.mocked(vscode.window).visibleTextEditors = [mockEditorNoRanges] + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditorNoRanges) vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { - setTimeout(() => callback({ uri: { fsPath: `${mockCwd}/new.ts`, scheme: "file" } } as any), 0) + setTimeout(() => callback(makeTextDocument({ uri: makeUri(`${mockCwd}/new.ts`) })), 0) return { dispose: vi.fn() } }) vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - ;(diffViewProvider as any).editType = "modify" + diffViewProvider.editType = "modify" await diffViewProvider.open("new.ts") - expect((diffViewProvider as any).preEditScrollLine).toBeUndefined() + expect(diffViewProvider["preEditScrollLine"]).toBeUndefined() }) it("saveChanges() calls revealRange(AtTop) when documentWasOpen and preEditScrollLine is set", async () => { const mockRevealRange = vi.fn() - const mockSavedEditor = { revealRange: mockRevealRange } + const mockSavedEditor = makeTextEditor({ revealRange: mockRevealRange }) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockSavedEditor as any) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).documentWasOpen = true - ;(diffViewProvider as any).preEditScrollLine = 30 + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockSavedEditor) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["documentWasOpen"] = true + diffViewProvider["preEditScrollLine"] = 30 // saveChanges early-exits without relPath, newContent, activeDiffEditor - ;(diffViewProvider as any).newContent = "content" - ;(diffViewProvider as any).activeDiffEditor = { - document: { - getText: vi.fn().mockReturnValue("content"), - isDirty: false, - save: vi.fn().mockResolvedValue(undefined), - }, - } + diffViewProvider["newContent"] = "content" + diffViewProvider["activeDiffEditor"] = makeTextEditor({ + document: makeTextDocument({ getText: vi.fn().mockReturnValue("content") }), + }) await diffViewProvider.saveChanges(false) @@ -1028,20 +989,16 @@ describe("DiffViewProvider", () => { it("saveChanges() does NOT call revealRange when preEditScrollLine is undefined", async () => { const mockRevealRange = vi.fn() - const mockSavedEditor = { revealRange: mockRevealRange } - - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockSavedEditor as any) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).documentWasOpen = true - ;(diffViewProvider as any).preEditScrollLine = undefined - ;(diffViewProvider as any).newContent = "content" - ;(diffViewProvider as any).activeDiffEditor = { - document: { - getText: vi.fn().mockReturnValue("content"), - isDirty: false, - save: vi.fn().mockResolvedValue(undefined), - }, - } + const mockSavedEditor = makeTextEditor({ revealRange: mockRevealRange }) + + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockSavedEditor) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["documentWasOpen"] = true + diffViewProvider["preEditScrollLine"] = undefined + diffViewProvider["newContent"] = "content" + diffViewProvider["activeDiffEditor"] = makeTextEditor({ + document: makeTextDocument({ getText: vi.fn().mockReturnValue("content") }), + }) await diffViewProvider.saveChanges(false) @@ -1054,39 +1011,35 @@ describe("DiffViewProvider", () => { vi.useFakeTimers() try { const deferredReveal = vi.fn() - const liveEditor = { - document: { - uri: { fsPath: `${mockCwd}/race.ts`, scheme: "file" }, + const liveEditor = makeTextEditor({ + document: makeTextDocument({ + uri: makeUri(`${mockCwd}/race.ts`), getText: vi.fn().mockReturnValue("a\nCHANGED\nc\nd\n"), - isDirty: false, - save: vi.fn().mockResolvedValue(undefined), lineCount: 5, - lineAt: vi.fn().mockReturnValue({ text: "" }), - }, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - visibleRanges: [{ start: { line: 0 }, end: { line: 0 } }], + }), + visibleRanges: [makeRange()], revealRange: deferredReveal, - } - ;(diffViewProvider as any).originalContent = "a\nb\nc\nd\n" - ;(diffViewProvider as any).activeDiffEditor = liveEditor - vi.mocked(vscode.window).visibleTextEditors = [liveEditor as any] + }) + diffViewProvider.originalContent = "a\nb\nc\nd\n" + diffViewProvider["activeDiffEditor"] = liveEditor + vi.mocked(vscode.window).visibleTextEditors = [liveEditor] // Schedule the deferred reveal, then accept the edit before it fires. diffViewProvider.scrollToFirstDiff() expect(deferredReveal).toHaveBeenCalledTimes(1) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).documentWasOpen = true - ;(diffViewProvider as any).preEditScrollLine = 0 - ;(diffViewProvider as any).newContent = "a\nCHANGED\nc\nd\n" + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["documentWasOpen"] = true + diffViewProvider["preEditScrollLine"] = 0 + diffViewProvider["newContent"] = "a\nCHANGED\nc\nd\n" await diffViewProvider.saveChanges(false) // The timer was cancelled; advancing past it produces no extra reveal. vi.advanceTimersByTime(100) expect(deferredReveal).toHaveBeenCalledTimes(1) - expect((diffViewProvider as any).deferredScrollTimer).toBeUndefined() + expect(diffViewProvider["deferredScrollTimer"]).toBeUndefined() } finally { vi.useRealTimers() } @@ -1094,23 +1047,21 @@ describe("DiffViewProvider", () => { it("revertChanges() calls revealRange(AtTop) when documentWasOpen and preEditScrollLine is set", async () => { const mockRevealRange = vi.fn() - const mockSavedEditor = { revealRange: mockRevealRange } - - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockSavedEditor as any) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).documentWasOpen = true - ;(diffViewProvider as any).preEditScrollLine = 15 - ;(diffViewProvider as any).editType = "modify" - ;(diffViewProvider as any).originalContent = "original" - ;(diffViewProvider as any).activeDiffEditor = { - document: { - uri: { fsPath: `${mockCwd}/test.txt` }, + const mockSavedEditor = makeTextEditor({ revealRange: mockRevealRange }) + + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockSavedEditor) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["documentWasOpen"] = true + diffViewProvider["preEditScrollLine"] = 15 + diffViewProvider.editType = "modify" + diffViewProvider.originalContent = "original" + diffViewProvider["activeDiffEditor"] = makeTextEditor({ + document: makeTextDocument({ + uri: makeUri(`${mockCwd}/test.txt`), getText: vi.fn().mockReturnValue("modified"), - isDirty: false, - save: vi.fn().mockResolvedValue(undefined), - positionAt: vi.fn().mockReturnValue({ line: 0, character: 0 }), - }, - } + positionAt: vi.fn().mockReturnValue(makePosition()), + }), + }) vi.mocked(vscode.workspace.applyEdit).mockResolvedValue(true) @@ -1126,26 +1077,25 @@ describe("DiffViewProvider", () => { describe("userTouchedDocument close/keep behavior", () => { const mockTargetPath = `${mockCwd}/mock-target-file.ts` - const buildActiveDiffEditor = () => ({ - document: { - uri: { fsPath: mockTargetPath }, - getText: vi.fn().mockReturnValue("content"), - isDirty: false, - save: vi.fn().mockResolvedValue(undefined), - positionAt: vi.fn().mockReturnValue({ line: 0, character: 0 }), - }, - }) + const buildActiveDiffEditor = () => + makeTextEditor({ + document: makeTextDocument({ + uri: makeUri(mockTargetPath), + getText: vi.fn().mockReturnValue("content"), + positionAt: vi.fn().mockReturnValue(makePosition()), + }), + }) it("saveChanges() closes the file tab when the file was not open and untouched", async () => { const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).closeFileTab = closeFileTab - ;(diffViewProvider as any).relPath = "mock-target-file.ts" - ;(diffViewProvider as any).documentWasOpen = false - ;(diffViewProvider as any).userTouchedDocument = false - ;(diffViewProvider as any).preEditScrollLine = undefined - ;(diffViewProvider as any).newContent = "content" - ;(diffViewProvider as any).activeDiffEditor = buildActiveDiffEditor() + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeFileTab"] = closeFileTab + diffViewProvider["relPath"] = "mock-target-file.ts" + diffViewProvider["documentWasOpen"] = false + diffViewProvider["userTouchedDocument"] = false + diffViewProvider["preEditScrollLine"] = undefined + diffViewProvider["newContent"] = "content" + diffViewProvider["activeDiffEditor"] = buildActiveDiffEditor() await diffViewProvider.saveChanges(false) @@ -1155,15 +1105,15 @@ describe("DiffViewProvider", () => { it("saveChanges() keeps the file open when the user touched it", async () => { const closeFileTab = vi.fn().mockResolvedValue(undefined) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).closeFileTab = closeFileTab - ;(diffViewProvider as any).relPath = "mock-target-file.ts" - ;(diffViewProvider as any).documentWasOpen = false - ;(diffViewProvider as any).userTouchedDocument = true - ;(diffViewProvider as any).preEditScrollLine = undefined - ;(diffViewProvider as any).newContent = "content" - ;(diffViewProvider as any).activeDiffEditor = buildActiveDiffEditor() + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeFileTab"] = closeFileTab + diffViewProvider["relPath"] = "mock-target-file.ts" + diffViewProvider["documentWasOpen"] = false + diffViewProvider["userTouchedDocument"] = true + diffViewProvider["preEditScrollLine"] = undefined + diffViewProvider["newContent"] = "content" + diffViewProvider["activeDiffEditor"] = buildActiveDiffEditor() await diffViewProvider.saveChanges(false) @@ -1174,15 +1124,15 @@ describe("DiffViewProvider", () => { it("revertChanges() closes the file tab when the file was not open and untouched", async () => { const closeFileTab = vi.fn().mockResolvedValue(undefined) vi.mocked(vscode.workspace.applyEdit).mockResolvedValue(true) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).closeFileTab = closeFileTab - ;(diffViewProvider as any).relPath = "mock-target-file.ts" - ;(diffViewProvider as any).documentWasOpen = false - ;(diffViewProvider as any).userTouchedDocument = false - ;(diffViewProvider as any).preEditScrollLine = undefined - ;(diffViewProvider as any).editType = "modify" - ;(diffViewProvider as any).originalContent = "original" - ;(diffViewProvider as any).activeDiffEditor = buildActiveDiffEditor() + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeFileTab"] = closeFileTab + diffViewProvider["relPath"] = "mock-target-file.ts" + diffViewProvider["documentWasOpen"] = false + diffViewProvider["userTouchedDocument"] = false + diffViewProvider["preEditScrollLine"] = undefined + diffViewProvider.editType = "modify" + diffViewProvider.originalContent = "original" + diffViewProvider["activeDiffEditor"] = buildActiveDiffEditor() await diffViewProvider.revertChanges() @@ -1193,16 +1143,16 @@ describe("DiffViewProvider", () => { it("revertChanges() keeps the file open when the user touched it", async () => { const closeFileTab = vi.fn().mockResolvedValue(undefined) vi.mocked(vscode.workspace.applyEdit).mockResolvedValue(true) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).closeFileTab = closeFileTab - ;(diffViewProvider as any).relPath = "mock-target-file.ts" - ;(diffViewProvider as any).documentWasOpen = false - ;(diffViewProvider as any).userTouchedDocument = true - ;(diffViewProvider as any).preEditScrollLine = undefined - ;(diffViewProvider as any).editType = "modify" - ;(diffViewProvider as any).originalContent = "original" - ;(diffViewProvider as any).activeDiffEditor = buildActiveDiffEditor() + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeFileTab"] = closeFileTab + diffViewProvider["relPath"] = "mock-target-file.ts" + diffViewProvider["documentWasOpen"] = false + diffViewProvider["userTouchedDocument"] = true + diffViewProvider["preEditScrollLine"] = undefined + diffViewProvider.editType = "modify" + diffViewProvider.originalContent = "original" + diffViewProvider["activeDiffEditor"] = buildActiveDiffEditor() await diffViewProvider.revertChanges() @@ -1211,70 +1161,70 @@ describe("DiffViewProvider", () => { }) it("marks userTouchedDocument when the file editor is activated during the diff", async () => { - let activeCallback: ((editor: any) => void) | undefined - vi.mocked(vscode.window.onDidChangeActiveTextEditor).mockImplementation((cb: any) => { + let activeCallback: ((editor: vscode.TextEditor) => void) | undefined + vi.mocked(vscode.window.onDidChangeActiveTextEditor).mockImplementation((cb) => { activeCallback = cb return { dispose: vi.fn() } }) - const mockEditor = { - document: { - uri: { fsPath: mockTargetPath, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } - vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any) + const mockEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(mockTargetPath) }), + }) + vi.mocked(vscode.window).visibleTextEditors = [mockEditor] + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor) vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { - setTimeout(() => callback({ uri: { fsPath: mockTargetPath, scheme: "file" } } as any), 0) + setTimeout(() => callback(makeTextDocument({ uri: makeUri(mockTargetPath) })), 0) return { dispose: vi.fn() } }) vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - ;(diffViewProvider as any).editType = "modify" - ;(diffViewProvider as any).documentWasOpen = false + diffViewProvider.editType = "modify" + diffViewProvider["documentWasOpen"] = false await diffViewProvider.open("mock-target-file.ts") // Simulate the user activating the plain file editor (no diff tab active). - ;(vscode.window.tabGroups as any).activeTabGroup = { activeTab: { input: {} } } - activeCallback?.({ - document: { uri: { fsPath: mockTargetPath, scheme: "file" } }, + // TabGroup double provides the activeTab.input the touch-guard inspects. + const activeGroup: vscode.TabGroup = { + isActive: true, + viewColumn: vscode.ViewColumn.One, + activeTab: makeTab(makeTextTab(mockTargetPath), "mock-target-file.ts"), + tabs: [], + } + Object.defineProperty(vscode.window.tabGroups, "activeTabGroup", { + value: activeGroup, + configurable: true, }) + activeCallback?.(makeTextEditor({ document: makeTextDocument({ uri: makeUri(mockTargetPath) }) })) - expect((diffViewProvider as any).userTouchedDocument).toBe(true) + expect(diffViewProvider["userTouchedDocument"]).toBe(true) }) }) describe("userTouchedDiffEditor keep/close behavior", () => { const mockTargetPath = `${mockCwd}/mock-target-file.ts` - const buildActiveDiffEditor = () => ({ - document: { - uri: { fsPath: mockTargetPath }, - getText: vi.fn().mockReturnValue("content"), - isDirty: false, - save: vi.fn().mockResolvedValue(undefined), - positionAt: vi.fn().mockReturnValue({ line: 0, character: 0 }), - }, - }) + const buildActiveDiffEditor = () => + makeTextEditor({ + document: makeTextDocument({ + uri: makeUri(mockTargetPath), + getText: vi.fn().mockReturnValue("content"), + positionAt: vi.fn().mockReturnValue(makePosition()), + }), + }) it("saveChanges() keeps the file open when the user clicked inside the diff editor", async () => { const closeFileTab = vi.fn().mockResolvedValue(undefined) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).closeFileTab = closeFileTab - ;(diffViewProvider as any).relPath = "mock-target-file.ts" - ;(diffViewProvider as any).documentWasOpen = false - ;(diffViewProvider as any).userTouchedDocument = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeFileTab"] = closeFileTab + diffViewProvider["relPath"] = "mock-target-file.ts" + diffViewProvider["documentWasOpen"] = false + diffViewProvider["userTouchedDocument"] = false // The user clicked in the diff editor -- the flag is true. - ;(diffViewProvider as any).userTouchedDiffEditor = true - ;(diffViewProvider as any).preEditScrollLine = undefined - ;(diffViewProvider as any).newContent = "content" - ;(diffViewProvider as any).activeDiffEditor = buildActiveDiffEditor() + diffViewProvider["userTouchedDiffEditor"] = true + diffViewProvider["preEditScrollLine"] = undefined + diffViewProvider["newContent"] = "content" + diffViewProvider["activeDiffEditor"] = buildActiveDiffEditor() await diffViewProvider.saveChanges(false) @@ -1284,16 +1234,16 @@ describe("DiffViewProvider", () => { it("saveChanges() closes the file tab when the user only scrolled (not clicked) in the diff", async () => { const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).closeFileTab = closeFileTab - ;(diffViewProvider as any).relPath = "mock-target-file.ts" - ;(diffViewProvider as any).documentWasOpen = false - ;(diffViewProvider as any).userTouchedDocument = false + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeFileTab"] = closeFileTab + diffViewProvider["relPath"] = "mock-target-file.ts" + diffViewProvider["documentWasOpen"] = false + diffViewProvider["userTouchedDocument"] = false // The user only scrolled -- the flag stays false. - ;(diffViewProvider as any).userTouchedDiffEditor = false - ;(diffViewProvider as any).preEditScrollLine = undefined - ;(diffViewProvider as any).newContent = "content" - ;(diffViewProvider as any).activeDiffEditor = buildActiveDiffEditor() + diffViewProvider["userTouchedDiffEditor"] = false + diffViewProvider["preEditScrollLine"] = undefined + diffViewProvider["newContent"] = "content" + diffViewProvider["activeDiffEditor"] = buildActiveDiffEditor() await diffViewProvider.saveChanges(false) @@ -1302,160 +1252,141 @@ describe("DiffViewProvider", () => { }) it("open() registers onDidChangeTextEditorSelection and sets userTouchedDiffEditor on event", async () => { - let selectionCallback: ((event: any) => void) | undefined - vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation((cb: any) => { + let selectionCallback: ((event: vscode.TextEditorSelectionChangeEvent) => void) | undefined + vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation((cb) => { selectionCallback = cb return { dispose: vi.fn() } }) - const mockEditor = { - document: { - uri: { fsPath: `${mockCwd}/sel.ts`, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } - vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any) + const mockEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(`${mockCwd}/sel.ts`) }), + }) + vi.mocked(vscode.window).visibleTextEditors = [mockEditor] + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor) vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { - setTimeout(() => callback({ uri: { fsPath: `${mockCwd}/sel.ts`, scheme: "file" } } as any), 0) + setTimeout(() => callback(makeTextDocument({ uri: makeUri(`${mockCwd}/sel.ts`) })), 0) return { dispose: vi.fn() } }) vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - ;(diffViewProvider as any).editType = "modify" + diffViewProvider.editType = "modify" await diffViewProvider.open("sel.ts") - expect((diffViewProvider as any).userTouchedDiffEditor).toBe(false) + expect(diffViewProvider["userTouchedDiffEditor"]).toBe(false) // Simulate a Mouse selection-change event on the captured diff editor. - const activeDiffEditor = (diffViewProvider as any).activeDiffEditor + const activeDiffEditor = diffViewProvider["activeDiffEditor"] + if (!activeDiffEditor) { + throw new Error("activeDiffEditor was not set") + } selectionCallback?.({ textEditor: activeDiffEditor, + selections: [], kind: vscode.TextEditorSelectionChangeKind.Mouse, }) - expect((diffViewProvider as any).userTouchedDiffEditor).toBe(true) + expect(diffViewProvider["userTouchedDiffEditor"]).toBe(true) }) it("open() does NOT set userTouchedDiffEditor for programmatic selection changes (kind=Command or undefined)", async () => { - let selectionCallback: ((event: any) => void) | undefined - vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation((cb: any) => { + let selectionCallback: ((event: vscode.TextEditorSelectionChangeEvent) => void) | undefined + vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation((cb) => { selectionCallback = cb return { dispose: vi.fn() } }) - const mockEditor = { - document: { - uri: { fsPath: `${mockCwd}/prog.ts`, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } - vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any) + const mockEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(`${mockCwd}/prog.ts`) }), + }) + vi.mocked(vscode.window).visibleTextEditors = [mockEditor] + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor) vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { - setTimeout(() => callback({ uri: { fsPath: `${mockCwd}/prog.ts`, scheme: "file" } } as any), 0) + setTimeout(() => callback(makeTextDocument({ uri: makeUri(`${mockCwd}/prog.ts`) })), 0) return { dispose: vi.fn() } }) vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - ;(diffViewProvider as any).editType = "modify" + diffViewProvider.editType = "modify" await diffViewProvider.open("prog.ts") - const activeDiffEditor = (diffViewProvider as any).activeDiffEditor + const activeDiffEditor = diffViewProvider["activeDiffEditor"] + if (!activeDiffEditor) { + throw new Error("activeDiffEditor was not set") + } // Programmatic change via editor.selection= (kind undefined) -- e.g. revealDiffLine - selectionCallback?.({ textEditor: activeDiffEditor, kind: undefined }) - expect((diffViewProvider as any).userTouchedDiffEditor).toBe(false) + selectionCallback?.({ textEditor: activeDiffEditor, selections: [], kind: undefined }) + expect(diffViewProvider["userTouchedDiffEditor"]).toBe(false) // Programmatic change via command (kind=Command) selectionCallback?.({ textEditor: activeDiffEditor, + selections: [], kind: vscode.TextEditorSelectionChangeKind.Command, }) - expect((diffViewProvider as any).userTouchedDiffEditor).toBe(false) + expect(diffViewProvider["userTouchedDiffEditor"]).toBe(false) }) it("open() sets userTouchedDiffEditor when event comes from a fresh editor instance wrapping the same document (stale ref fix)", async () => { - let selectionCallback: ((event: any) => void) | undefined - vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation((cb: any) => { + let selectionCallback: ((event: vscode.TextEditorSelectionChangeEvent) => void) | undefined + vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation((cb) => { selectionCallback = cb return { dispose: vi.fn() } }) - const sharedDocument = { - uri: { fsPath: `${mockCwd}/stale.ts`, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - } - const originalEditor = { - document: sharedDocument, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } - vi.mocked(vscode.window).visibleTextEditors = [originalEditor as any] - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(originalEditor as any) + const sharedDocument = makeTextDocument({ uri: makeUri(`${mockCwd}/stale.ts`) }) + const originalEditor = makeTextEditor({ document: sharedDocument }) + vi.mocked(vscode.window).visibleTextEditors = [originalEditor] + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(originalEditor) vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { - setTimeout(() => callback({ uri: { fsPath: `${mockCwd}/stale.ts`, scheme: "file" } } as any), 0) + setTimeout(() => callback(makeTextDocument({ uri: makeUri(`${mockCwd}/stale.ts`) })), 0) return { dispose: vi.fn() } }) vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - ;(diffViewProvider as any).editType = "modify" + diffViewProvider.editType = "modify" await diffViewProvider.open("stale.ts") // Simulate VS Code giving us a NEW editor object that wraps the same // document -- this is the real-world stale reference scenario. - const freshEditorSameDoc = { document: sharedDocument } + const freshEditorSameDoc = makeTextEditor({ document: sharedDocument }) selectionCallback?.({ textEditor: freshEditorSameDoc, + selections: [], kind: vscode.TextEditorSelectionChangeKind.Mouse, }) - expect((diffViewProvider as any).userTouchedDiffEditor).toBe(true) + expect(diffViewProvider["userTouchedDiffEditor"]).toBe(true) }) it("open() ignores selection events on editors other than the diff editor", async () => { - let selectionCallback: ((event: any) => void) | undefined - vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation((cb: any) => { + let selectionCallback: ((event: vscode.TextEditorSelectionChangeEvent) => void) | undefined + vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation((cb) => { selectionCallback = cb return { dispose: vi.fn() } }) - const mockEditor = { - document: { - uri: { fsPath: `${mockCwd}/other.ts`, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } - vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any) + const mockEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(`${mockCwd}/other.ts`) }), + }) + vi.mocked(vscode.window).visibleTextEditors = [mockEditor] + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor) vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { - setTimeout(() => callback({ uri: { fsPath: `${mockCwd}/other.ts`, scheme: "file" } } as any), 0) + setTimeout(() => callback(makeTextDocument({ uri: makeUri(`${mockCwd}/other.ts`) })), 0) return { dispose: vi.fn() } }) vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - ;(diffViewProvider as any).editType = "modify" + diffViewProvider.editType = "modify" await diffViewProvider.open("other.ts") // Fire the event with a completely different editor object. - const unrelatedEditor = { document: { uri: { fsPath: "/some/other/file.ts", scheme: "file" } } } - selectionCallback?.({ textEditor: unrelatedEditor }) + const unrelatedEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri("/some/other/file.ts") }), + }) + selectionCallback?.({ textEditor: unrelatedEditor, selections: [], kind: undefined }) - expect((diffViewProvider as any).userTouchedDiffEditor).toBe(false) + expect(diffViewProvider["userTouchedDiffEditor"]).toBe(false) }) it("revertChanges() closes the file tab even when userTouchedDiffEditor is true (deny ignores diff-touch)", async () => { @@ -1466,17 +1397,17 @@ describe("DiffViewProvider", () => { // from accidental regression. const closeFileTab = vi.fn().mockResolvedValue(undefined) vi.mocked(vscode.workspace.applyEdit).mockResolvedValue(true) - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(diffViewProvider as any).closeFileTab = closeFileTab - ;(diffViewProvider as any).relPath = "mock-target-file.ts" - ;(diffViewProvider as any).documentWasOpen = false - ;(diffViewProvider as any).userTouchedDocument = false + diffViewProvider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + diffViewProvider["closeFileTab"] = closeFileTab + diffViewProvider["relPath"] = "mock-target-file.ts" + diffViewProvider["documentWasOpen"] = false + diffViewProvider["userTouchedDocument"] = false // The user clicked inside the diff pane -- but this is a deny, so it must be ignored. - ;(diffViewProvider as any).userTouchedDiffEditor = true - ;(diffViewProvider as any).preEditScrollLine = undefined - ;(diffViewProvider as any).editType = "modify" - ;(diffViewProvider as any).originalContent = "original" - ;(diffViewProvider as any).activeDiffEditor = buildActiveDiffEditor() + diffViewProvider["userTouchedDiffEditor"] = true + diffViewProvider["preEditScrollLine"] = undefined + diffViewProvider.editType = "modify" + diffViewProvider.originalContent = "original" + diffViewProvider["activeDiffEditor"] = buildActiveDiffEditor() await diffViewProvider.revertChanges() @@ -1493,24 +1424,25 @@ describe("DiffViewProvider", () => { preEditScrollLine: number | undefined, ) => { const mockRevealRange = vi.fn() - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: mockRevealRange } as any) - const userActiveEditor = { - document: { uri: { fsPath: `${mockCwd}/target.ts`, scheme: "file" } }, - viewColumn: 1, - } - ;(vscode.window as any).activeTextEditor = userActiveEditor - ;(diffViewProvider as any).lastScrolledSource = lastScrolledSource - ;(diffViewProvider as any).diffScrollLine = diffScrollLine - ;(diffViewProvider as any).targetFileScrollLine = targetFileScrollLine - ;(diffViewProvider as any).preEditScrollLine = preEditScrollLine - ;(diffViewProvider as any).documentWasPinned = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue( + makeTextEditor({ revealRange: mockRevealRange }), + ) + const userActiveEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(`${mockCwd}/target.ts`) }), + }) + vscode.window.activeTextEditor = userActiveEditor + diffViewProvider["lastScrolledSource"] = lastScrolledSource + diffViewProvider["diffScrollLine"] = diffScrollLine + diffViewProvider["targetFileScrollLine"] = targetFileScrollLine + diffViewProvider["preEditScrollLine"] = preEditScrollLine + diffViewProvider["documentWasPinned"] = false return mockRevealRange } it("uses diffScrollLine when lastScrolledSource is 'diff'", async () => { const mockRevealRange = setupForScroll("diff", 20, 5, 0) - await (diffViewProvider as any).showEditedFileWithoutDisruptingFocus(`${mockCwd}/target.ts`) + await diffViewProvider["showEditedFileWithoutDisruptingFocus"](`${mockCwd}/target.ts`) expect(mockRevealRange).toHaveBeenCalledWith( expect.objectContaining({ start: { line: 20, character: 0 } }), @@ -1521,7 +1453,7 @@ describe("DiffViewProvider", () => { it("uses targetFileScrollLine when lastScrolledSource is 'targetFile'", async () => { const mockRevealRange = setupForScroll("targetFile", 20, 8, 0) - await (diffViewProvider as any).showEditedFileWithoutDisruptingFocus(`${mockCwd}/target.ts`) + await diffViewProvider["showEditedFileWithoutDisruptingFocus"](`${mockCwd}/target.ts`) expect(mockRevealRange).toHaveBeenCalledWith( expect.objectContaining({ start: { line: 8, character: 0 } }), @@ -1532,7 +1464,7 @@ describe("DiffViewProvider", () => { it("falls back to preEditScrollLine when lastScrolledSource is undefined", async () => { const mockRevealRange = setupForScroll(undefined, 20, 8, 3) - await (diffViewProvider as any).showEditedFileWithoutDisruptingFocus(`${mockCwd}/target.ts`) + await diffViewProvider["showEditedFileWithoutDisruptingFocus"](`${mockCwd}/target.ts`) expect(mockRevealRange).toHaveBeenCalledWith( expect.objectContaining({ start: { line: 3, character: 0 } }), @@ -1543,7 +1475,7 @@ describe("DiffViewProvider", () => { it("does not call revealRange when all scroll sources are undefined", async () => { const mockRevealRange = setupForScroll(undefined, undefined, undefined, undefined) - await (diffViewProvider as any).showEditedFileWithoutDisruptingFocus(`${mockCwd}/target.ts`) + await diffViewProvider["showEditedFileWithoutDisruptingFocus"](`${mockCwd}/target.ts`) expect(mockRevealRange).not.toHaveBeenCalled() }) @@ -1553,7 +1485,7 @@ describe("DiffViewProvider", () => { // so targetFileScrollLine must win even when diffScrollLine is higher. const mockRevealRange = setupForScroll("targetFile", 100, 2, 0) - await (diffViewProvider as any).showEditedFileWithoutDisruptingFocus(`${mockCwd}/target.ts`) + await diffViewProvider["showEditedFileWithoutDisruptingFocus"](`${mockCwd}/target.ts`) expect(mockRevealRange).toHaveBeenCalledWith( expect.objectContaining({ start: { line: 2, character: 0 } }), @@ -1564,31 +1496,24 @@ describe("DiffViewProvider", () => { describe("diffScrollListener wiring in open()", () => { const openWithScrollListener = async (relPath: string) => { - let scrollCallback: ((event: any) => void) | undefined - vi.mocked(vscode.window.onDidChangeTextEditorVisibleRanges).mockImplementation((cb: any) => { + let scrollCallback: ((event: vscode.TextEditorVisibleRangesChangeEvent) => void) | undefined + vi.mocked(vscode.window.onDidChangeTextEditorVisibleRanges).mockImplementation((cb) => { scrollCallback = cb return { dispose: vi.fn() } }) const fsPath = `${mockCwd}/${relPath}` - const mockEditor = { - document: { - uri: { fsPath, scheme: "file" }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } - vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] - vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any) + const mockEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri(fsPath) }), + }) + vi.mocked(vscode.window).visibleTextEditors = [mockEditor] + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor) vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { - setTimeout(() => callback({ uri: { fsPath, scheme: "file" } } as any), 0) + setTimeout(() => callback(makeTextDocument({ uri: makeUri(fsPath) })), 0) return { dispose: vi.fn() } }) vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - ;(diffViewProvider as any).editType = "modify" + diffViewProvider.editType = "modify" await diffViewProvider.open(relPath) if (!scrollCallback) { @@ -1596,9 +1521,16 @@ describe("DiffViewProvider", () => { "onDidChangeTextEditorVisibleRanges mock was not invoked during open() - scroll listener setup changed", ) } + const activeDiffEditor = diffViewProvider["activeDiffEditor"] + if (!activeDiffEditor) { + throw new Error("activeDiffEditor was not set") + } + if (!activeDiffEditor) { + throw new Error("activeDiffEditor was not set") + } return { scrollCallback, - activeDiffEditor: (diffViewProvider as any).activeDiffEditor, + activeDiffEditor, fsPath, } } @@ -1606,96 +1538,82 @@ describe("DiffViewProvider", () => { it("records diffScrollLine and sets lastScrolledSource to 'diff' on diff editor scroll", async () => { const { scrollCallback, activeDiffEditor } = await openWithScrollListener("scroll-diff.ts") - scrollCallback({ textEditor: activeDiffEditor, visibleRanges: [{ start: { line: 42 } }] }) + scrollCallback({ textEditor: activeDiffEditor, visibleRanges: [makeRange(42)] }) - expect((diffViewProvider as any).diffScrollLine).toBe(42) - expect((diffViewProvider as any).lastScrolledSource).toBe("diff") + expect(diffViewProvider["diffScrollLine"]).toBe(42) + expect(diffViewProvider["lastScrolledSource"]).toBe("diff") }) it("records targetFileScrollLine and sets lastScrolledSource to 'targetFile' on target file scroll", async () => { const { scrollCallback, fsPath } = await openWithScrollListener("scroll-target.ts") - const targetFileEditor = { - document: { uri: { fsPath, scheme: "file" } }, - } - scrollCallback({ textEditor: targetFileEditor, visibleRanges: [{ start: { line: 17 } }] }) + const targetFileEditor = makeTextEditor({ document: makeTextDocument({ uri: makeUri(fsPath) }) }) + scrollCallback({ textEditor: targetFileEditor, visibleRanges: [makeRange(17)] }) - expect((diffViewProvider as any).targetFileScrollLine).toBe(17) - expect((diffViewProvider as any).lastScrolledSource).toBe("targetFile") + expect(diffViewProvider["targetFileScrollLine"]).toBe(17) + expect(diffViewProvider["lastScrolledSource"]).toBe("targetFile") }) it("ignores scroll events from unrelated editors", async () => { const { scrollCallback } = await openWithScrollListener("scroll-unrelated.ts") - const unrelatedEditor = { - document: { uri: { fsPath: "/some/other/file.ts", scheme: "file" } }, - } - scrollCallback({ textEditor: unrelatedEditor, visibleRanges: [{ start: { line: 99 } }] }) + const unrelatedEditor = makeTextEditor({ + document: makeTextDocument({ uri: makeUri("/some/other/file.ts") }), + }) + scrollCallback({ textEditor: unrelatedEditor, visibleRanges: [makeRange(99)] }) - expect((diffViewProvider as any).diffScrollLine).toBeUndefined() - expect((diffViewProvider as any).targetFileScrollLine).toBeUndefined() - expect((diffViewProvider as any).lastScrolledSource).toBeUndefined() + expect(diffViewProvider["diffScrollLine"]).toBeUndefined() + expect(diffViewProvider["targetFileScrollLine"]).toBeUndefined() + expect(diffViewProvider["lastScrolledSource"]).toBeUndefined() }) it("lastScrolledSource reflects the most recent scroll between diff and target file", async () => { const { scrollCallback, activeDiffEditor, fsPath } = await openWithScrollListener("scroll-recency.ts") // First scroll in diff. - scrollCallback({ textEditor: activeDiffEditor, visibleRanges: [{ start: { line: 10 } }] }) - expect((diffViewProvider as any).lastScrolledSource).toBe("diff") + scrollCallback({ textEditor: activeDiffEditor, visibleRanges: [makeRange(10)] }) + expect(diffViewProvider["lastScrolledSource"]).toBe("diff") // Then scroll in the target file -- this must win. - const targetFileEditor = { document: { uri: { fsPath, scheme: "file" } } } - scrollCallback({ textEditor: targetFileEditor, visibleRanges: [{ start: { line: 5 } }] }) - expect((diffViewProvider as any).lastScrolledSource).toBe("targetFile") + const targetFileEditor = makeTextEditor({ document: makeTextDocument({ uri: makeUri(fsPath) }) }) + scrollCallback({ textEditor: targetFileEditor, visibleRanges: [makeRange(5)] }) + expect(diffViewProvider["lastScrolledSource"]).toBe("targetFile") // Back to diff again. - scrollCallback({ textEditor: activeDiffEditor, visibleRanges: [{ start: { line: 20 } }] }) - expect((diffViewProvider as any).lastScrolledSource).toBe("diff") + scrollCallback({ textEditor: activeDiffEditor, visibleRanges: [makeRange(20)] }) + expect(diffViewProvider["lastScrolledSource"]).toBe("diff") }) }) describe("auto-close settings decision table", () => { const mockTargetPath = `${mockCwd}/auto-close-test.ts` - const buildActiveDiffEditor = () => ({ - document: { - uri: { fsPath: mockTargetPath }, - getText: vi.fn().mockReturnValue("content"), - isDirty: false, - save: vi.fn().mockResolvedValue(undefined), - positionAt: vi.fn().mockReturnValue({ line: 0, character: 0 }), - }, - }) + const buildActiveDiffEditor = () => + makeTextEditor({ + document: makeTextDocument({ + uri: makeUri(mockTargetPath), + getText: vi.fn().mockReturnValue("content"), + positionAt: vi.fn().mockReturnValue(makePosition()), + }), + }) const setupProvider = (stateOverrides: Record = {}) => { - const task = { - providerRef: { - deref: vi.fn().mockReturnValue({ - getState: vi.fn().mockResolvedValue({ - includeDiagnosticMessages: true, - maxDiagnosticMessages: 50, - ...stateOverrides, - }), - }), - }, - } - const provider = new DiffViewProvider(mockCwd, task as any) - ;(provider as any).relPath = "auto-close-test.ts" - ;(provider as any).newContent = "content" - ;(provider as any).activeDiffEditor = buildActiveDiffEditor() - ;(provider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - ;(provider as any).preEditScrollLine = undefined + const provider = new DiffViewProvider(mockCwd, makeTaskDouble(stateOverrides)) + provider["relPath"] = "auto-close-test.ts" + provider["newContent"] = "content" + provider["activeDiffEditor"] = buildActiveDiffEditor() + provider["closeAllDiffViews"] = vi.fn().mockResolvedValue(undefined) + provider["preEditScrollLine"] = undefined return provider } it("already-open file is never auto-closed regardless of settings", async () => { const provider = setupProvider({ autoCloseZooOpenedFiles: true }) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = true - ;(provider as any).userTouchedDocument = false - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = true + provider["userTouchedDocument"] = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await provider.saveChanges(false) @@ -1705,11 +1623,11 @@ describe("DiffViewProvider", () => { it("transient tab is kept when autoCloseZooOpenedFiles is false", async () => { const provider = setupProvider({ autoCloseZooOpenedFiles: false }) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = false - ;(provider as any).userTouchedDocument = false - ;(provider as any).userTouchedDiffEditor = false - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = false + provider["userTouchedDocument"] = false + provider["userTouchedDiffEditor"] = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await provider.saveChanges(false) @@ -1722,11 +1640,11 @@ describe("DiffViewProvider", () => { // centralized default (false), so an untouched transient tab is kept. const provider = setupProvider({}) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = false - ;(provider as any).userTouchedDocument = false - ;(provider as any).userTouchedDiffEditor = false - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = false + provider["userTouchedDocument"] = false + provider["userTouchedDiffEditor"] = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await provider.saveChanges(false) @@ -1737,10 +1655,10 @@ describe("DiffViewProvider", () => { it("transient tab is closed when autoCloseZooOpenedFiles is true", async () => { const provider = setupProvider({ autoCloseZooOpenedFiles: true }) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = false - ;(provider as any).userTouchedDocument = false - ;(provider as any).userTouchedDiffEditor = false + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = false + provider["userTouchedDocument"] = false + provider["userTouchedDiffEditor"] = false await provider.saveChanges(false) @@ -1750,11 +1668,11 @@ describe("DiffViewProvider", () => { it("touched tab is kept by default (autoCloseZooOpenedFilesAfterUserEdited unset)", async () => { const provider = setupProvider({}) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = false - ;(provider as any).userTouchedDocument = true - ;(provider as any).userTouchedDiffEditor = false - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = false + provider["userTouchedDocument"] = true + provider["userTouchedDiffEditor"] = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await provider.saveChanges(false) @@ -1769,10 +1687,10 @@ describe("DiffViewProvider", () => { autoCloseZooOpenedFilesAfterUserEdited: true, }) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = false - ;(provider as any).userTouchedDocument = true - ;(provider as any).userTouchedDiffEditor = false + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = false + provider["userTouchedDocument"] = true + provider["userTouchedDiffEditor"] = false await provider.saveChanges(false) @@ -1787,11 +1705,11 @@ describe("DiffViewProvider", () => { autoCloseZooOpenedFilesAfterUserEdited: true, }) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = false - ;(provider as any).userTouchedDocument = true - ;(provider as any).userTouchedDiffEditor = false - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = false + provider["userTouchedDocument"] = true + provider["userTouchedDiffEditor"] = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await provider.saveChanges(false) @@ -1802,11 +1720,11 @@ describe("DiffViewProvider", () => { it("new file tab is closed when autoCloseZooOpenedNewFiles is true (accept path)", async () => { const provider = setupProvider({ autoCloseZooOpenedNewFiles: true }) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = false - ;(provider as any).userTouchedDocument = false - ;(provider as any).userTouchedDiffEditor = false - ;(provider as any).editType = "create" + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = false + provider["userTouchedDocument"] = false + provider["userTouchedDiffEditor"] = false + provider.editType = "create" await provider.saveChanges(false) @@ -1822,12 +1740,12 @@ describe("DiffViewProvider", () => { autoCloseZooOpenedFiles: false, }) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = false - ;(provider as any).userTouchedDocument = false - ;(provider as any).userTouchedDiffEditor = false - ;(provider as any).editType = "create" - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = false + provider["userTouchedDocument"] = false + provider["userTouchedDiffEditor"] = false + provider.editType = "create" + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await provider.saveChanges(false) @@ -1840,11 +1758,11 @@ describe("DiffViewProvider", () => { // untouched transient tab is kept and re-shown (long-standing behavior). const provider = setupProvider({}) const closeFileTab = vi.fn().mockResolvedValue(undefined) - ;(provider as any).closeFileTab = closeFileTab - ;(provider as any).documentWasOpen = false - ;(provider as any).userTouchedDocument = false - ;(provider as any).userTouchedDiffEditor = false - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + provider["closeFileTab"] = closeFileTab + provider["documentWasOpen"] = false + provider["userTouchedDocument"] = false + provider["userTouchedDiffEditor"] = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await provider.saveChanges(false) diff --git a/src/integrations/editor/__tests__/EditorUtils.spec.ts b/src/integrations/editor/__tests__/EditorUtils.spec.ts index 3ee71570bd..a040a651d3 100644 --- a/src/integrations/editor/__tests__/EditorUtils.spec.ts +++ b/src/integrations/editor/__tests__/EditorUtils.spec.ts @@ -1,9 +1,18 @@ // npx vitest src/integrations/editor/__tests__/EditorUtils.spec.ts +import type { Mock } from "vitest" import * as vscode from "vscode" import { EditorUtils } from "../EditorUtils" +import { makeUri } from "../../../test-utils/vscode" + +// TextDocument double provides getText/lineAt mocks plus lineCount and uri.fsPath. +type MockTextDocument = vscode.TextDocument & { + getText: Mock<(range: vscode.Range) => string> + lineAt: Mock<(line: number) => Pick> +} + // Use simple classes to simulate VSCode's Range and Position behavior. vi.mock("vscode", () => { class MockPosition { @@ -35,15 +44,15 @@ vi.mock("vscode", () => { }) describe("EditorUtils", () => { - let mockDocument: any + let mockDocument: MockTextDocument beforeEach(() => { mockDocument = { - getText: vi.fn(), + getText: vi.fn((_range: vscode.Range) => ""), lineAt: vi.fn(), lineCount: 10, - uri: { fsPath: "/test/file.ts" }, - } + uri: makeUri("/test/file.ts"), + } as unknown as MockTextDocument }) describe("getEffectiveRange", () => { @@ -76,7 +85,7 @@ describe("EditorUtils", () => { mockDocument.lineAt.mockImplementation((line: number) => { return { text: `Line ${line} text`, lineNumber: line } }) - mockDocument.getText.mockImplementation((range: any) => { + mockDocument.getText.mockImplementation((range) => { // If the range is exactly the empty initial selection, return an empty string. if ( range.start.line === initialRange.start.line && @@ -124,12 +133,11 @@ describe("EditorUtils", () => { describe("getFilePath", () => { it("should return relative path when in workspace", () => { - const mockWorkspaceFolder = { - uri: { fsPath: "/test" }, + vi.mocked(vscode.workspace.getWorkspaceFolder).mockReturnValue({ + uri: makeUri("/test"), name: "test", index: 0, - } - vi.mocked(vscode.workspace.getWorkspaceFolder).mockReturnValue(mockWorkspaceFolder as any) + }) const result = EditorUtils.getFilePath(mockDocument) diff --git a/src/integrations/kimi-code/__tests__/oauth.spec.ts b/src/integrations/kimi-code/__tests__/oauth.spec.ts index 960777e69c..e5d5a4135f 100644 --- a/src/integrations/kimi-code/__tests__/oauth.spec.ts +++ b/src/integrations/kimi-code/__tests__/oauth.spec.ts @@ -1,16 +1,19 @@ +import type { ExtensionContext } from "vscode" + import { KIMI_CODE_OAUTH_CONFIG, KimiCodeOAuthManager } from "../oauth" const createContext = () => { const values = new Map() return { values, + // ExtensionContext double provides secrets.get/store/delete only. context: { secrets: { get: vi.fn(async (key: string) => values.get(key)), store: vi.fn(async (key: string, value: string) => void values.set(key, value)), delete: vi.fn(async (key: string) => void values.delete(key)), }, - } as any, + } as unknown as ExtensionContext, } } diff --git a/src/integrations/misc/__tests__/export-markdown.spec.ts b/src/integrations/misc/__tests__/export-markdown.spec.ts index 081fb26158..b0cf238fe6 100644 --- a/src/integrations/misc/__tests__/export-markdown.spec.ts +++ b/src/integrations/misc/__tests__/export-markdown.spec.ts @@ -69,7 +69,8 @@ describe("export-markdown", () => { }) it("should handle unexpected content types", () => { - const block = { type: "unknown_type" as const } as any + // Deliberately malformed block exercising the default branch. + const block = { type: "unknown_type" } as unknown as ExtendedContentBlock expect(formatContentBlockToMarkdown(block)).toBe("[Unexpected content type: unknown_type]") }) diff --git a/src/integrations/misc/__tests__/open-file.spec.ts b/src/integrations/misc/__tests__/open-file.spec.ts index 1cdd91d589..e69fe5953b 100644 --- a/src/integrations/misc/__tests__/open-file.spec.ts +++ b/src/integrations/misc/__tests__/open-file.spec.ts @@ -2,6 +2,8 @@ import * as vscode from "vscode" import { openFile } from "../open-file" +import { makeTextDocument, makeTextEditor, makeUri } from "../../../test-utils/vscode" + // Mock vscode module vi.mock("vscode", () => ({ Uri: { @@ -58,7 +60,7 @@ vi.mock("../../utils/path", () => { // Mock i18n vi.mock("../../i18n", () => ({ - t: vi.fn((key: string, _params?: any) => { + t: vi.fn((key: string, _params?: Record) => { // Return the key without namespace prefix to match actual behavior if (key.startsWith("common:")) { return key.replace("common:", "") @@ -80,7 +82,7 @@ describe("openFile", () => { describe("decodeURIComponent error handling", () => { it("should handle invalid URI encoding gracefully", async () => { const invalidPath = "test%ZZinvalid.txt" // Invalid percent encoding - const mockDocument = { uri: { fsPath: invalidPath } } + const mockDocument = makeTextDocument({ uri: makeUri(invalidPath) }) vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({ type: vscode.FileType.File, @@ -88,8 +90,8 @@ describe("openFile", () => { mtime: 0, size: 0, }) - vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await openFile(invalidPath) @@ -106,7 +108,7 @@ describe("openFile", () => { it("should successfully decode valid URI-encoded paths", async () => { const encodedPath = "./%5Btest%5D/file.txt" // [test] encoded const decodedPath = "./[test]/file.txt" - const mockDocument = { uri: { fsPath: decodedPath } } + const mockDocument = makeTextDocument({ uri: makeUri(decodedPath) }) vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({ type: vscode.FileType.File, @@ -114,8 +116,8 @@ describe("openFile", () => { mtime: 0, size: 0, }) - vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await openFile(encodedPath) @@ -132,7 +134,7 @@ describe("openFile", () => { it("should handle paths with special characters that need encoding", async () => { const pathWithSpecialChars = "./[brackets]/file with spaces.txt" - const mockDocument = { uri: { fsPath: pathWithSpecialChars } } + const mockDocument = makeTextDocument({ uri: makeUri(pathWithSpecialChars) }) vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({ type: vscode.FileType.File, @@ -140,8 +142,8 @@ describe("openFile", () => { mtime: 0, size: 0, }) - vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await openFile(pathWithSpecialChars) @@ -153,7 +155,7 @@ describe("openFile", () => { it("should handle already decoded paths without double-decoding", async () => { const normalPath = "./normal/file.txt" - const mockDocument = { uri: { fsPath: normalPath } } + const mockDocument = makeTextDocument({ uri: makeUri(normalPath) }) vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({ type: vscode.FileType.File, @@ -161,8 +163,8 @@ describe("openFile", () => { mtime: 0, size: 0, }) - vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await openFile(normalPath) @@ -223,8 +225,8 @@ describe("openFile", () => { const content = "Hello, world!" vi.mocked(vscode.workspace.fs.stat).mockRejectedValue(new Error("File not found")) - vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue({} as any) - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(makeTextDocument()) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) await openFile(newFilePath, { create: true, content }) diff --git a/src/integrations/misc/__tests__/performance/processCarriageReturns.benchmark.ts b/src/integrations/misc/__tests__/performance/processCarriageReturns.benchmark.ts index 2862c85bcd..35cf49368e 100644 --- a/src/integrations/misc/__tests__/performance/processCarriageReturns.benchmark.ts +++ b/src/integrations/misc/__tests__/performance/processCarriageReturns.benchmark.ts @@ -181,12 +181,12 @@ function calculateStats(durations: number[]) { } // Run performance test for a specific function -function runPerformanceTest( +function runPerformanceTest( name: string, - fn: (input: string, ...args: any[]) => string, + fn: (input: string, ...args: T) => string, input: string, iterations: number, - args: any[] = [], + ...args: T ) { console.log(`\nTesting ${name}...`) @@ -360,7 +360,7 @@ function runBenchmark() { runPerformanceTest("applyRunLengthEncoding", applyRunLengthEncoding, testData, iterations) console.log("\n--- Function 3: truncateOutput ---") - runPerformanceTest("truncateOutput", truncateOutput, testData, iterations, [lineLimit]) + runPerformanceTest("truncateOutput", truncateOutput, testData, iterations) // Run baseline test to measure variance between identical runs runBaselineTest(testData, Math.max(5, Math.floor(iterations / 4))) diff --git a/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts b/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts index a9dfe26cda..e3d994186c 100644 --- a/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts +++ b/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts @@ -4,10 +4,10 @@ const mockPid = 12345 vitest.mock("execa", () => { const mockKill = vitest.fn() - const execa = vitest.fn(function (_options: any) { - return (_template: TemplateStringsArray, ..._args: any[]) => ({ + const execa = vitest.fn(function (_options: Record) { + return (_template: TemplateStringsArray, ..._args: unknown[]) => ({ pid: mockPid, - iterable: (_opts: any) => + iterable: (_opts: Record) => (async function* () { yield "test output\n" })(), @@ -18,7 +18,7 @@ vitest.mock("execa", () => { }) vitest.mock("ps-tree", () => ({ - default: vitest.fn(function (_: number, cb: any) { + default: vitest.fn(function (_: number, cb: (error: Error | null, children: readonly { PID: string }[]) => void) { return cb(null, []) }), })) @@ -109,7 +109,7 @@ describe("ExecaTerminalProcess", () => { terminalProcess = new ExecaTerminalProcess(mockTerminal) await terminalProcess.run("echo test") const execaMock = vitest.mocked(execa) - const calledOptions = execaMock.mock.calls[0][0] as any + const calledOptions = execaMock.mock.calls[0][0] as unknown as { env: NodeJS.ProcessEnv } expect(calledOptions.env.EXISTING_VAR).toBe("existing") }) @@ -120,7 +120,7 @@ describe("ExecaTerminalProcess", () => { terminalProcess = new ExecaTerminalProcess(mockTerminal) await terminalProcess.run("echo test") const execaMock = vitest.mocked(execa) - const calledOptions = execaMock.mock.calls[0][0] as any + const calledOptions = execaMock.mock.calls[0][0] as unknown as { env: NodeJS.ProcessEnv } expect(calledOptions.env.LANG).toBe("en_US.UTF-8") expect(calledOptions.env.LC_ALL).toBe("en_US.UTF-8") }) @@ -186,7 +186,7 @@ describe("ExecaTerminalProcess", () => { terminalProcess["lastRetrievedIndex"] = 16 // Same as fullOutput.length // Access the protected method through type casting - ;(terminalProcess as any).trimRetrievedOutput() + terminalProcess["trimRetrievedOutput"]() expect(terminalProcess["fullOutput"]).toBe("") expect(terminalProcess["lastRetrievedIndex"]).toBe(0) @@ -196,7 +196,7 @@ describe("ExecaTerminalProcess", () => { // Set up a scenario where not all output has been retrieved terminalProcess["fullOutput"] = "test output data" terminalProcess["lastRetrievedIndex"] = 5 // Less than fullOutput.length - ;(terminalProcess as any).trimRetrievedOutput() + terminalProcess["trimRetrievedOutput"]() // Buffer should NOT be cleared - there's still unretrieved content expect(terminalProcess["fullOutput"]).toBe("test output data") @@ -206,7 +206,7 @@ describe("ExecaTerminalProcess", () => { it("does nothing when buffer is already empty", () => { terminalProcess["fullOutput"] = "" terminalProcess["lastRetrievedIndex"] = 0 - ;(terminalProcess as any).trimRetrievedOutput() + terminalProcess["trimRetrievedOutput"]() expect(terminalProcess["fullOutput"]).toBe("") expect(terminalProcess["lastRetrievedIndex"]).toBe(0) @@ -216,7 +216,7 @@ describe("ExecaTerminalProcess", () => { // Edge case: index is greater than current length (could happen if output was modified) terminalProcess["fullOutput"] = "short" terminalProcess["lastRetrievedIndex"] = 100 - ;(terminalProcess as any).trimRetrievedOutput() + terminalProcess["trimRetrievedOutput"]() expect(terminalProcess["fullOutput"]).toBe("") expect(terminalProcess["lastRetrievedIndex"]).toBe(0) diff --git a/src/integrations/terminal/__tests__/OutputInterceptor.test.ts b/src/integrations/terminal/__tests__/OutputInterceptor.test.ts index 2d32f12704..c4b7eacc40 100644 --- a/src/integrations/terminal/__tests__/OutputInterceptor.test.ts +++ b/src/integrations/terminal/__tests__/OutputInterceptor.test.ts @@ -1,5 +1,6 @@ import * as fs from "fs" import * as path from "path" +import type { Mock } from "vitest" import { vi, describe, it, expect, beforeEach, afterEach } from "vitest" import { clearAllMocks, restoreGlobals } from "../../../test-utils/reset" @@ -26,7 +27,9 @@ vi.mock("fs", () => ({ })) describe("OutputInterceptor", () => { - let mockWriteStream: any + // WriteStream double provides write()/end(callback)/on() mocks. + type MockWriteStream = fs.WriteStream & { write: Mock; end: Mock; on: Mock } + let mockWriteStream: MockWriteStream let storageDir: string beforeEach(() => { @@ -42,10 +45,10 @@ describe("OutputInterceptor", () => { if (callback) callback() }), on: vi.fn(), - } + } as unknown as MockWriteStream vi.mocked(fs.existsSync).mockReturnValue(true) - vi.mocked(fs.createWriteStream).mockReturnValue(mockWriteStream as any) + vi.mocked(fs.createWriteStream).mockReturnValue(mockWriteStream) }) afterEach(() => { @@ -356,7 +359,9 @@ describe("OutputInterceptor", () => { describe("Cleanup methods", () => { it("should clean up all artifacts in directory", async () => { const mockFiles = ["cmd-12345.txt", "cmd-67890.txt", "other-file.txt", "cmd-11111.txt"] - vi.mocked(fs.promises.readdir).mockResolvedValue(mockFiles as any) + vi.mocked(fs.promises.readdir).mockResolvedValue( + mockFiles as unknown as Awaited>, + ) vi.mocked(fs.promises.unlink).mockResolvedValue(undefined) await OutputInterceptor.cleanup(storageDir) @@ -378,7 +383,9 @@ describe("OutputInterceptor", () => { it("should clean up specific artifacts by executionIds", async () => { const mockFiles = ["cmd-12345.txt", "cmd-67890.txt", "cmd-11111.txt"] - vi.mocked(fs.promises.readdir).mockResolvedValue(mockFiles as any) + vi.mocked(fs.promises.readdir).mockResolvedValue( + mockFiles as unknown as Awaited>, + ) vi.mocked(fs.promises.unlink).mockResolvedValue(undefined) // Keep 12345 and 67890, delete 11111 @@ -393,7 +400,9 @@ describe("OutputInterceptor", () => { it("should handle unlink errors gracefully", async () => { const mockFiles = ["cmd-12345.txt", "cmd-67890.txt"] - vi.mocked(fs.promises.readdir).mockResolvedValue(mockFiles as any) + vi.mocked(fs.promises.readdir).mockResolvedValue( + mockFiles as unknown as Awaited>, + ) vi.mocked(fs.promises.unlink).mockRejectedValue(new Error("Permission denied")) // Should not throw even if unlink fails diff --git a/src/integrations/terminal/__tests__/TerminalProcess.spec.ts b/src/integrations/terminal/__tests__/TerminalProcess.spec.ts index 31bb806be3..181810ee03 100644 --- a/src/integrations/terminal/__tests__/TerminalProcess.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcess.spec.ts @@ -1,5 +1,6 @@ // npx vitest run src/integrations/terminal/__tests__/TerminalProcess.spec.ts +import type { Mock } from "vitest" import * as vscode from "vscode" import { mergePromise } from "../mergePromise" @@ -19,14 +20,17 @@ vi.mock("execa", () => ({ describe("TerminalProcess", () => { let terminalProcess: TestTerminalProcess - let mockTerminal: any - type TestVscodeTerminal = vscode.Terminal & { - shellIntegration: { - executeCommand: any - } + type TestShellIntegration = vscode.TerminalShellIntegration & { + executeCommand: Mock<(...args: unknown[]) => vscode.TerminalShellExecution> } + type TestVscodeTerminal = Omit & { + shellIntegration: TestShellIntegration | undefined + } + let mockTerminal: TestVscodeTerminal let mockTerminalInfo: Terminal - let mockExecution: any + // TerminalShellExecution double provides read() only. + type MockShellExecution = vscode.TerminalShellExecution & { read: Mock<() => AsyncIterable> } + let mockExecution: MockShellExecution let mockStream: AsyncIterableIterator beforeEach(() => { @@ -110,8 +114,8 @@ describe("TerminalProcess", () => { it("runs command after shell integration activates via onDidChangeTerminalShellIntegration event", async () => { // Cover Terminal.runCommand's waitForShellIntegration resolve path: shell // integration is initially absent but arrives via the VSCode event before timeout. - let shellIntegrationHandler: ((e: any) => void) | undefined - vi.spyOn(vscode.window, "onDidChangeTerminalShellIntegration" as any).mockImplementation((handler: any) => { + let shellIntegrationHandler: ((e: vscode.TerminalShellIntegrationChangeEvent) => void) | undefined + vi.spyOn(vscode.window, "onDidChangeTerminalShellIntegration").mockImplementation((handler) => { shellIntegrationHandler = handler return { dispose: vi.fn() } }) @@ -129,8 +133,9 @@ describe("TerminalProcess", () => { expect(shellIntegrationHandler).toBeDefined() mockTerminal.shellIntegration = { executeCommand: vi.fn().mockReturnValue({ read: vi.fn().mockReturnValue((async function* () {})()) }), - } - shellIntegrationHandler!({ terminal: mockTerminal }) + } as TestShellIntegration + // Event double provides terminal; the source reads shell integration off the terminal. + shellIntegrationHandler!({ terminal: mockTerminal } as vscode.TerminalShellIntegrationChangeEvent) // Give the .then() callback a chance to run process.run() await Promise.resolve() @@ -171,9 +176,9 @@ describe("TerminalProcess", () => { mockExecution = { read: vi.fn().mockReturnValue(mockStream), - } + } as unknown as MockShellExecution - mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution) + mockTerminal.shellIntegration!.executeCommand.mockReturnValue(mockExecution) const runPromise = terminalProcess.run("test command") terminalProcess.emit("stream_available", mockStream) @@ -225,9 +230,9 @@ describe("TerminalProcess", () => { mockExecution = { read: vi.fn().mockReturnValue(mockStream), - } + } as unknown as MockShellExecution - mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution) + mockTerminal.shellIntegration!.executeCommand.mockReturnValue(mockExecution) const runPromise = terminalProcess.run("test command") terminalProcess.emit("stream_available", mockStream) @@ -279,9 +284,9 @@ describe("TerminalProcess", () => { mockExecution = { read: vi.fn().mockReturnValue(mockStream), - } + } as unknown as MockShellExecution - mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution) + mockTerminal.shellIntegration!.executeCommand.mockReturnValue(mockExecution) const runPromise = terminalProcess.run("sleep 60") terminalProcess.emit("stream_available", mockStream) @@ -341,15 +346,15 @@ describe("TerminalProcess", () => { terminalProcess.emit("shell_execution_complete", { exitCode: 0 }) })() - mockTerminal.shellIntegration.executeCommand.mockReturnValue({ + mockTerminal.shellIntegration!.executeCommand.mockReturnValue({ read: vi.fn().mockReturnValue(mockStream), - }) + } as unknown as vscode.TerminalShellExecution) const runPromise = terminalProcess.run(command) terminalProcess.emit("stream_available", mockStream) await runPromise - expect(mockTerminal.shellIntegration.executeCommand).toHaveBeenCalledWith(`{\n${command}\n}`) + expect(mockTerminal.shellIntegration!.executeCommand).toHaveBeenCalledWith(`{\n${command}\n}`) }) it.each([ @@ -367,15 +372,15 @@ describe("TerminalProcess", () => { terminalProcess.emit("shell_execution_complete", { exitCode: 0 }) })() - mockTerminal.shellIntegration.executeCommand.mockReturnValue({ + mockTerminal.shellIntegration!.executeCommand.mockReturnValue({ read: vi.fn().mockReturnValue(mockStream), - }) + } as unknown as vscode.TerminalShellExecution) const runPromise = terminalProcess.run("echo one\necho two") terminalProcess.emit("stream_available", mockStream) await runPromise - expect(mockTerminal.shellIntegration.executeCommand).toHaveBeenCalledWith(expectedCommand) + expect(mockTerminal.shellIntegration!.executeCommand).toHaveBeenCalledWith(expectedCommand) } finally { psSpy.mockRestore() fishSpy.mockRestore() @@ -492,8 +497,8 @@ describe("TerminalProcess", () => { } mockStream = emptyStream() - mockExecution = { read: vi.fn().mockReturnValue(mockStream) } - mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution) + mockExecution = { read: vi.fn().mockReturnValue(mockStream) } as unknown as MockShellExecution + mockTerminal.shellIntegration!.executeCommand.mockReturnValue(mockExecution) terminalProcess.once("no_shell_integration", noShellIntegrationSpy) @@ -527,8 +532,8 @@ describe("TerminalProcess", () => { terminalProcess.emit("shell_execution_complete", { exitCode: 0 }) })() - mockExecution = { read: vi.fn().mockReturnValue(mockStream) } - mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution) + mockExecution = { read: vi.fn().mockReturnValue(mockStream) } as unknown as MockShellExecution + mockTerminal.shellIntegration!.executeCommand.mockReturnValue(mockExecution) terminalProcess.once("no_shell_integration", noShellIntegrationSpy) @@ -564,9 +569,9 @@ describe("TerminalProcess", () => { yield "\x1b]633;D\x07" // The last chunk contains the command end sequence with bell character. })() - mockTerminal.shellIntegration.executeCommand.mockReturnValue({ + mockTerminal.shellIntegration!.executeCommand.mockReturnValue({ read: vi.fn().mockReturnValue(mockStream), - }) + } as unknown as vscode.TerminalShellExecution) const runPromise = terminalProcess.run("npm run build") terminalProcess.emit("stream_available", mockStream) @@ -591,14 +596,14 @@ describe("TerminalProcess", () => { it("resolves waitForShellIntegration immediately when shell integration is already active", async () => { // Cover waitForShellIntegration fast path: shellIntegration already present → resolves synchronously. // Access the private method via bracket notation so we can call it directly. - mockTerminal.shellIntegration = { executeCommand: vi.fn() } - const result = (mockTerminalInfo as any).waitForShellIntegration(5000) + mockTerminal.shellIntegration = { executeCommand: vi.fn() } as TestShellIntegration + const result = mockTerminalInfo["waitForShellIntegration"](5000) // Should resolve immediately (synchronously resolved Promise) without touching timers. await expect(result).resolves.toBeUndefined() }) it("handles executeCommand throwing by propagating the error", async () => { - mockTerminal.shellIntegration.executeCommand.mockImplementation(() => { + mockTerminal.shellIntegration!.executeCommand.mockImplementation(() => { throw new Error("execution failed") }) @@ -680,7 +685,7 @@ describe("TerminalProcess", () => { // executeCommand should be called with a shell + temp-script invocation, not the // raw multiline command string. - const calledWith = mockTerminal.shellIntegration.executeCommand.mock.calls[0][0] as string + const calledWith = mockTerminal.shellIntegration!.executeCommand.mock.calls[0][0] as string expect(calledWith).toMatch(/^"\/bin\/bash" ".*roo-cmd-.*\.sh"$/) }) @@ -709,7 +714,7 @@ describe("TerminalProcess", () => { await runPromise - const calledWith = mockTerminal.shellIntegration.executeCommand.mock.calls[0][0] as string + const calledWith = mockTerminal.shellIntegration!.executeCommand.mock.calls[0][0] as string expect(calledWith).toMatch(/^"\/bin\/bash" ".*roo-cmd-.*\.sh"$/) }) @@ -740,7 +745,7 @@ describe("TerminalProcess", () => { await runPromise - const calledWith = mockTerminal.shellIntegration.executeCommand.mock.calls[0][0] as string + const calledWith = mockTerminal.shellIntegration!.executeCommand.mock.calls[0][0] as string expect(calledWith).toBe(`. {\necho a\necho b\n}`) }) diff --git a/src/integrations/terminal/__tests__/TerminalProcess.test.ts b/src/integrations/terminal/__tests__/TerminalProcess.test.ts index 14b6ff14e2..23263ff4a7 100644 --- a/src/integrations/terminal/__tests__/TerminalProcess.test.ts +++ b/src/integrations/terminal/__tests__/TerminalProcess.test.ts @@ -1,3 +1,5 @@ +import type * as vscode from "vscode" + import { TerminalProcess } from "../TerminalProcess" import { Terminal } from "../Terminal" @@ -15,8 +17,9 @@ vi.mock("vscode", () => ({ })) describe("TerminalProcess ANSI Handling", () => { - let terminalProcess: any // Using any to access private methods - let mockTerminal: any + let terminalProcess: TerminalProcess + // vscode.Terminal double provides shellIntegration/name/processId/exitStatus/state. + let mockTerminal: vscode.Terminal beforeEach(() => { mockTerminal = { @@ -32,7 +35,7 @@ describe("TerminalProcess ANSI Handling", () => { hide: vi.fn(), show: vi.fn(), sendText: vi.fn(), - } + } as unknown as vscode.Terminal const terminalInfo = new Terminal(1, mockTerminal, "/tmp") terminalProcess = new TerminalProcess(terminalInfo) @@ -41,31 +44,31 @@ describe("TerminalProcess ANSI Handling", () => { describe("removeVSCodeShellIntegration", () => { it("should preserve standard ANSI SGR sequences", () => { const input = "\x1B[32mgreen text\x1B[0m" - const result = terminalProcess.removeVSCodeShellIntegration(input) + const result = terminalProcess["removeVSCodeShellIntegration"](input) expect(result).toBe("\x1B[32mgreen text\x1B[0m") }) it("should remove OSC 633 sequences", () => { const input = "\x1B]633;A\x07some text" - const result = terminalProcess.removeVSCodeShellIntegration(input) + const result = terminalProcess["removeVSCodeShellIntegration"](input) expect(result).toBe("some text") }) it("should remove OSC 133 sequences", () => { const input = "\x1B]133;A\x07some text" - const result = terminalProcess.removeVSCodeShellIntegration(input) + const result = terminalProcess["removeVSCodeShellIntegration"](input) expect(result).toBe("some text") }) it("should handle mixed sequences", () => { const input = "\x1B]633;C\x07\x1B[1m\x1B[32m✓\x1B[39m\x1B[22m test passed" - const result = terminalProcess.removeVSCodeShellIntegration(input) + const result = terminalProcess["removeVSCodeShellIntegration"](input) expect(result).toBe("\x1B[1m\x1B[32m✓\x1B[39m\x1B[22m test passed") }) it("should remove other OSC sequences", () => { const input = "\x1B]0;Console Title\x07Content" - const result = terminalProcess.removeVSCodeShellIntegration(input) + const result = terminalProcess["removeVSCodeShellIntegration"](input) expect(result).toBe("Content") }) }) @@ -73,13 +76,13 @@ describe("TerminalProcess ANSI Handling", () => { describe("stripCursorSequences", () => { it("should remove cursor movement codes", () => { const input = "text\x1B[1Aup\x1B[2Kclear" - const result = terminalProcess.stripCursorSequences(input) + const result = terminalProcess["stripCursorSequences"](input) expect(result).toBe("textupclear") }) it("should preserve colors while removing cursor codes", () => { const input = "\x1B[31mred\x1B[1B\x1B[32mgreen" - const result = terminalProcess.stripCursorSequences(input) + const result = terminalProcess["stripCursorSequences"](input) expect(result).toBe("\x1B[31mred\x1B[32mgreen") }) }) diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts index c2cffb8ff0..45481a3110 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts @@ -73,19 +73,21 @@ function createRealCommandStream(command: string): { stream: AsyncIterable = { SIGTERM: 15, SIGSEGV: 11, // Add other signals as needed } - const signalNum = signals[error.signal] + const signalNum = signals[execError.signal] if (signalNum !== undefined) { exitCode = 128 + signalNum // Signal exit codes are 128 + signal number } else { @@ -216,7 +218,10 @@ async function testTerminalCommand( // We've already created the process, so we'll trigger the events manually // Get the event handlers from the mock - const eventHandlers = (vscode as any).__eventHandlers + // The vscode mock exposes its registered event handlers for manual firing. + const eventHandlers = ( + vscode as typeof vscode & { __eventHandlers: Record void) | undefined> } + ).__eventHandlers // Execute the command first to set up the process const runPromise = terminalProcess.run(command) @@ -236,9 +241,10 @@ async function testTerminalCommand( // that (e.g. after the first 'line' event) would cause DONE_SENTINEL to win // the Promise.race and skip unconsumed chunks. if (eventHandlers.endTerminalShellExecution) { + const endHandler = eventHandlers.endTerminalShellExecution const _exitCode = exitCode setTimeout(() => { - eventHandlers.endTerminalShellExecution({ + endHandler({ terminal: mockTerminal, execution: mockExecution, exitCode: _exitCode, diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts index fbbb5befdb..8b4766c9f9 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts @@ -155,7 +155,10 @@ async function testCmdCommand( mockTerminalInfo.process = terminalProcess // Get the event handlers from the mock - const eventHandlers = (vscode as any).__eventHandlers + // The vscode mock exposes its registered event handlers for manual firing. + const eventHandlers = ( + vscode as typeof vscode & { __eventHandlers: Record void) | undefined> } + ).__eventHandlers // Execute the command first to set up the process const runPromise = terminalProcess.run(command) @@ -192,9 +195,10 @@ async function testCmdCommand( // will be consumed before this fires. Firing synchronously would let // DONE_SENTINEL win the Promise.race and skip unconsumed chunks. if (eventHandlers.endTerminalShellExecution) { + const endHandler = eventHandlers.endTerminalShellExecution const _exitCode = exitCode setTimeout(() => { - eventHandlers.endTerminalShellExecution({ + endHandler({ terminal: mockTerminal, execution: mockExecution, exitCode: _exitCode, diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts index c81045470e..75a4d9229b 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts @@ -156,7 +156,10 @@ async function testPowerShellCommand( mockTerminalInfo.process = terminalProcess // Get the event handlers from the mock - const eventHandlers = (vscode as any).__eventHandlers + // The vscode mock exposes its registered event handlers for manual firing. + const eventHandlers = ( + vscode as typeof vscode & { __eventHandlers: Record void) | undefined> } + ).__eventHandlers // Execute the command first to set up the process const runPromise = terminalProcess.run(command) diff --git a/src/integrations/terminal/__tests__/TerminalProcessInterpretExitCode.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessInterpretExitCode.spec.ts index 7129b4363a..107e3cb19e 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessInterpretExitCode.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessInterpretExitCode.spec.ts @@ -90,9 +90,10 @@ describe("TerminalProcess.interpretExitCode with real commands", () => { // If we get here, the command succeeded with exit code 0 const result = TerminalProcess.interpretExitCode(0) expect(result).toEqual({ exitCode: 0 }) - } catch (error: any) { + } catch (error) { // This should not happen for a successful command - throw new Error("Command should have succeeded: " + error.message) + const message = error instanceof Error ? error.message : String(error) + throw new Error("Command should have succeeded: " + message) } }) @@ -101,12 +102,16 @@ describe("TerminalProcess.interpretExitCode with real commands", () => { // Run a command that should fail with exit code 1 or 2 execSync("ls /nonexistent_directory", { stdio: "ignore" }) throw new Error("Command should have failed") - } catch (error: any) { + } catch (error) { + const { status } = error as NodeJS.ErrnoException & { status?: number | null } + if (typeof status !== "number") { + throw new Error("Command should have failed with a numeric status") + } // Verify the exit code is what we expect (can be 1 or 2 depending on the system) - expect(error.status).toBeGreaterThan(0) - expect(error.status).toBeLessThan(128) // Not a signal - const result = TerminalProcess.interpretExitCode(error.status) - expect(result).toEqual({ exitCode: error.status }) + expect(status).toBeGreaterThan(0) + expect(status).toBeLessThan(128) // Not a signal + const result = TerminalProcess.interpretExitCode(status) + expect(result).toEqual({ exitCode: status }) } }) @@ -115,9 +120,13 @@ describe("TerminalProcess.interpretExitCode with real commands", () => { // Run a command that exits with a specific code execSync("exit 42", { stdio: "ignore" }) throw new Error("Command should have exited with code 42") - } catch (error: any) { - expect(error.status).toBe(42) - const result = TerminalProcess.interpretExitCode(error.status) + } catch (error) { + const { status } = error as NodeJS.ErrnoException & { status?: number | null } + if (typeof status !== "number") { + throw new Error("Command should have exited with a numeric status") + } + expect(status).toBe(42) + const result = TerminalProcess.interpretExitCode(status) expect(result).toEqual({ exitCode: 42 }) } }) diff --git a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts index 3d3993af86..dd8de8aac4 100644 --- a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts @@ -2,6 +2,7 @@ import { existsSync } from "fs" +import type { MockInstance } from "vitest" import * as vscode from "vscode" import { restoreGlobals } from "../../../test-utils/reset" @@ -19,12 +20,18 @@ vi.mock("fs", () => ({ const mockedExistsSync = existsSync as unknown as ReturnType +// WorkspaceConfiguration double: provides only the members these suites read. +const makeWorkspaceConfig = (config: { + inspect?: (key: string) => Record | undefined + get?: (key: string, defaultValue?: unknown) => unknown +}): vscode.WorkspaceConfiguration => config as unknown as vscode.WorkspaceConfiguration + describe("Terminal VS Code terminal profile (#277)", () => { - // VS Code's getConfiguration/createTerminal are overloaded, so the precise - // spy MockInstance type isn't worth fighting in a test — `any` keeps it simple. - let createTerminalSpy: any + // createTerminal is overloaded; MockInstance keeps the spy's overload set. + let createTerminalSpy: MockInstance - const mockTerminal = () => + // vscode.Terminal double provides shellIntegration.executeCommand plus lifecycle stubs. + const mockTerminal = (): vscode.Terminal => ({ exitStatus: undefined, name: "Roo Code", @@ -36,7 +43,7 @@ describe("Terminal VS Code terminal profile (#277)", () => { show: vi.fn(), sendText: vi.fn(), shellIntegration: { executeCommand: vi.fn() }, - }) as any + }) as unknown as vscode.Terminal // Helper to stub `terminal.integrated.profiles.` config reads. const stubProfiles = ( @@ -45,18 +52,18 @@ describe("Terminal VS Code terminal profile (#277)", () => { ) => { vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: (platformKey: string) => ({ defaultValue: profilesByPlatform[platformKey], workspaceValue: workspaceProfilesByPlatform[platformKey], }), - } as any + }) } - return { + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue, inspect: () => undefined, - } as any + }) }) } @@ -100,16 +107,16 @@ describe("Terminal VS Code terminal profile (#277)", () => { it("merges default and global profiles while ignoring workspace profiles", () => { vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: () => ({ defaultValue: { bash: { path: "/bin/bash" } }, globalValue: { zsh: { path: "/bin/zsh" } }, workspaceValue: { malicious: { path: "/workspace/malicious-shell" } }, }), - } as any + }) } - return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }) expect(Terminal.getConfiguredProfiles("linux")).toEqual({ @@ -194,20 +201,20 @@ describe("Terminal VS Code terminal profile (#277)", () => { stubProfiles({ windows: { "Command Prompt": { path: "C:\\Windows\\System32\\cmd.exe" } } }) vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: (_key: string) => ({ defaultValue: { "Command Prompt": { path: "C:\\Windows\\System32\\cmd.exe" } }, globalValue: undefined, }), - } as any + }) } if (section === "terminal.integrated") { - return { + return makeWorkspaceConfig({ inspect: (key: string) => key === "defaultProfile.windows" ? { defaultValue: "Command Prompt" } : undefined, - } as any + }) } - return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }) expect(Terminal.isActiveShellCmdExe("win32")).toBe(true) }) @@ -216,20 +223,20 @@ describe("Terminal VS Code terminal profile (#277)", () => { Terminal.setTerminalProfile(undefined) vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: (_key: string) => ({ defaultValue: { PowerShell: { path: "C:\\Program Files\\PowerShell\\pwsh.exe" } }, globalValue: undefined, }), - } as any + }) } if (section === "terminal.integrated") { - return { + return makeWorkspaceConfig({ inspect: (key: string) => key === "defaultProfile.windows" ? { defaultValue: "PowerShell" } : undefined, - } as any + }) } - return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }) expect(Terminal.isActiveShellCmdExe("win32")).toBe(false) }) @@ -244,24 +251,24 @@ describe("Terminal VS Code terminal profile (#277)", () => { Terminal.setTerminalProfile(undefined) vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: () => ({ defaultValue: { PowerShell: { path: "C:\\Program Files\\PowerShell\\pwsh.exe" }, "Command Prompt": { path: "C:\\Windows\\System32\\cmd.exe" }, }, }), - } as any + }) } if (section === "terminal.integrated") { - return { + return makeWorkspaceConfig({ inspect: () => ({ defaultValue: "PowerShell", workspaceValue: "Command Prompt", }), - } as any + }) } - return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }) expect(Terminal.isActiveShellCmdExe("win32")).toBe(false) @@ -271,12 +278,12 @@ describe("Terminal VS Code terminal profile (#277)", () => { Terminal.setTerminalProfile(undefined) vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { inspect: () => ({ defaultValue: {} }) } as any + return makeWorkspaceConfig({ inspect: () => ({ defaultValue: {} }) }) } if (section === "terminal.integrated") { - return { inspect: () => ({ defaultValue: "Deleted Profile" }) } as any + return makeWorkspaceConfig({ inspect: () => ({ defaultValue: "Deleted Profile" }) }) } - return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }) expect(Terminal.isActiveShellCmdExe("win32")).toBe(false) @@ -309,7 +316,7 @@ describe("Terminal VS Code terminal profile (#277)", () => { Terminal.setTerminalProfile(undefined) vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: (_key: string) => ({ defaultValue: { "Custom PS": { @@ -317,15 +324,15 @@ describe("Terminal VS Code terminal profile (#277)", () => { }, }, }), - } as any + }) } if (section === "terminal.integrated") { - return { + return makeWorkspaceConfig({ inspect: (key: string) => key === "defaultProfile.windows" ? { defaultValue: "Custom PS" } : undefined, - } as any + }) } - return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }) expect(Terminal.isActiveShellPowerShell("win32")).toBe(true) @@ -335,19 +342,19 @@ describe("Terminal VS Code terminal profile (#277)", () => { Terminal.setTerminalProfile(undefined) vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: (_key: string) => ({ defaultValue: { PowerShell: { source: "PowerShell" } }, }), - } as any + }) } if (section === "terminal.integrated") { - return { + return makeWorkspaceConfig({ inspect: (key: string) => key === "defaultProfile.windows" ? { defaultValue: "PowerShell" } : undefined, - } as any + }) } - return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }) expect(Terminal.isActiveShellPowerShell("win32")).toBe(true) @@ -369,10 +376,12 @@ describe("Terminal VS Code terminal profile (#277)", () => { it("returns false when no override and no default profile name is configured", () => { Terminal.setTerminalProfile(undefined) - vi.spyOn(vscode.workspace, "getConfiguration").mockReturnValue({ - get: (_key: string, defaultValue?: unknown) => defaultValue, - inspect: () => undefined, - } as any) + vi.spyOn(vscode.workspace, "getConfiguration").mockReturnValue( + makeWorkspaceConfig({ + get: (_key: string, defaultValue?: unknown) => defaultValue, + inspect: () => undefined, + }), + ) expect(Terminal.isActiveShellFish("linux")).toBe(false) }) @@ -380,17 +389,17 @@ describe("Terminal VS Code terminal profile (#277)", () => { Terminal.setTerminalProfile(undefined) vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: (_key: string) => ({ defaultValue: { "Fish Shell": null } }), - } as any + }) } if (section === "terminal.integrated") { - return { + return makeWorkspaceConfig({ inspect: (key: string) => key === "defaultProfile.linux" ? { defaultValue: "Fish Shell" } : undefined, - } as any + }) } - return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }) expect(Terminal.isActiveShellFish("linux")).toBe(false) }) @@ -399,19 +408,19 @@ describe("Terminal VS Code terminal profile (#277)", () => { Terminal.setTerminalProfile(undefined) vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: (_key: string) => ({ defaultValue: { "Fish Shell": { path: "/usr/local/bin/fish" } }, }), - } as any + }) } if (section === "terminal.integrated") { - return { + return makeWorkspaceConfig({ inspect: (key: string) => key === "defaultProfile.linux" ? { defaultValue: "Fish Shell" } : undefined, - } as any + }) } - return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }) expect(Terminal.isActiveShellFish("linux")).toBe(true) }) @@ -572,19 +581,19 @@ describe("Terminal VS Code terminal profile (#277)", () => { it("resolves profiles defined only in user/global settings", () => { vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { if (section === "terminal.integrated.profiles") { - return { + return makeWorkspaceConfig({ inspect: () => ({ defaultValue: undefined, globalValue: { "User Bash": { path: "/usr/bin/bash" } }, workspaceValue: { "User Bash": { path: "/workspace/bash" } }, }), - } as any + }) } - return { + return makeWorkspaceConfig({ get: (_key: string, defaultValue?: unknown) => defaultValue, inspect: () => undefined, - } as any + }) }) Terminal.setTerminalProfile("User Bash") @@ -715,7 +724,7 @@ describe("Terminal VS Code terminal profile (#277)", () => { }) describe("ZDOTDIR injection guard", () => { - let zshInitTmpDirSpy: any + let zshInitTmpDirSpy: MockInstance beforeEach(() => { zshInitTmpDirSpy = vi diff --git a/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts b/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts index b94a7f2ed8..3a29bc4c6e 100644 --- a/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts @@ -1,5 +1,6 @@ // npx vitest run src/integrations/terminal/__tests__/TerminalRegistry.spec.ts +import type { MockInstance } from "vitest" import * as vscode from "vscode" import { restoreGlobals } from "../../../test-utils/reset" @@ -16,13 +17,14 @@ vi.mock("execa", () => ({ })) describe("TerminalRegistry", () => { - let mockCreateTerminal: any + // createTerminal is overloaded; MockInstance keeps the spy's overload set. + let mockCreateTerminal: MockInstance beforeEach(() => { TerminalRegistry["terminals"] = [] Terminal.setTerminalProfile(undefined) mockCreateTerminal = vi.spyOn(vscode.window, "createTerminal").mockImplementation( - (..._args: any[]) => + (..._args: unknown[]) => ({ exitStatus: undefined, name: "Zoo Code", @@ -39,7 +41,7 @@ describe("TerminalRegistry", () => { shellIntegration: { executeCommand: vi.fn(), }, - }) as any, + }) as unknown as vscode.Terminal, ) }) @@ -209,8 +211,8 @@ describe("TerminalRegistry", () => { }) describe("onDidEndTerminalShellExecution race condition (#489, #622)", () => { - let startHandler: (e: any) => Promise - let endHandler: (e: any) => Promise + let startHandler: (e: vscode.TerminalShellExecutionStartEvent) => Promise + let endHandler: (e: vscode.TerminalShellExecutionEndEvent) => Promise beforeEach(() => { // Reset the initialized flag so we can call initialize() in this block. @@ -218,15 +220,21 @@ describe("TerminalRegistry", () => { // The global vscode mock doesn't define shell execution event // methods, so add them before spying. - ;(vscode.window as any).onDidStartTerminalShellExecution ??= () => ({ dispose: () => {} }) - ;(vscode.window as any).onDidEndTerminalShellExecution ??= () => ({ dispose: () => {} }) - - vi.spyOn(vscode.window, "onDidStartTerminalShellExecution" as any).mockImplementation((handler: any) => { + type MockWindow = Omit< + typeof vscode.window, + "onDidStartTerminalShellExecution" | "onDidEndTerminalShellExecution" + > & + Record + const mockWindow = vscode.window as MockWindow + mockWindow.onDidStartTerminalShellExecution ??= () => ({ dispose: () => {} }) + mockWindow.onDidEndTerminalShellExecution ??= () => ({ dispose: () => {} }) + + vi.spyOn(vscode.window, "onDidStartTerminalShellExecution").mockImplementation((handler) => { startHandler = handler return { dispose: vi.fn() } }) - vi.spyOn(vscode.window, "onDidEndTerminalShellExecution" as any).mockImplementation((handler: any) => { + vi.spyOn(vscode.window, "onDidEndTerminalShellExecution").mockImplementation((handler) => { endHandler = handler return { dispose: vi.fn() } }) @@ -241,23 +249,25 @@ describe("TerminalRegistry", () => { it("calls shellExecutionComplete when end event fires before running is set (race)", async () => { const terminal = TerminalRegistry.createTerminal("/test/path", "vscode") as Terminal + // TerminalProcess double provides command/emit/hasUnretrievedOutput. const mockProcess = { command: "echo hello", emit: vi.fn(), hasUnretrievedOutput: vi.fn().mockReturnValue(false), - } as any + } as unknown as TerminalProcess terminal.process = mockProcess // Simulate the race: running is still false (setActiveStream hasn't // been called yet), but the end event fires. expect(terminal.running).toBe(false) - const mockExecution = { commandLine: { value: "echo hello" } } + // TerminalShellExecution double provides commandLine only. + const mockExecution = { commandLine: { value: "echo hello" } } as unknown as vscode.TerminalShellExecution await endHandler({ terminal: terminal.terminal, execution: mockExecution, exitCode: 0, - }) + } as vscode.TerminalShellExecutionEndEvent) // shellExecutionComplete should have been called exactly once, emitting // shell_execution_complete so TerminalProcess.run() unblocks. @@ -280,12 +290,13 @@ describe("TerminalRegistry", () => { expect(terminal.running).toBe(false) - const mockExecution = { commandLine: { value: "echo hello" } } + // TerminalShellExecution double provides commandLine only. + const mockExecution = { commandLine: { value: "echo hello" } } as unknown as vscode.TerminalShellExecution await endHandler({ terminal: terminal.terminal, execution: mockExecution, exitCode: 0, - }) + } as vscode.TerminalShellExecutionEndEvent) expect(terminal.busy).toBe(false) expect(completeSpy).not.toHaveBeenCalled() @@ -304,7 +315,8 @@ describe("TerminalRegistry", () => { // already be pointing at a different (or no) execution by the time A's stale // event finally arrives. const processA = new TerminalProcess(terminal) - const executionA = { commandLine: { value: "command A" } } as any + // TerminalShellExecution double provides commandLine only. + const executionA = { commandLine: { value: "command A" } } as unknown as vscode.TerminalShellExecution processA.ownExecution = executionA const emitA = vi.spyOn(processA, "emit") @@ -312,7 +324,8 @@ describe("TerminalRegistry", () => { // state TerminalRegistry sees by the time A's late event arrives: a live // process with its own, different execution. const processB = new TerminalProcess(terminal) - const executionB = { commandLine: { value: "command B" } } as any + // TerminalShellExecution double provides commandLine only. + const executionB = { commandLine: { value: "command B" } } as unknown as vscode.TerminalShellExecution processB.ownExecution = executionB terminal.process = processB terminal.running = true @@ -323,7 +336,7 @@ describe("TerminalRegistry", () => { terminal: terminal.terminal, execution: executionA, exitCode: 1, - }) + } as vscode.TerminalShellExecutionEndEvent) // B must be completely unaffected: no shell_execution_complete delivered to // either process, and B is still the terminal's live process. @@ -337,7 +350,7 @@ describe("TerminalRegistry", () => { terminal: terminal.terminal, execution: executionB, exitCode: 0, - }) + } as vscode.TerminalShellExecutionEndEvent) expect(emitB).toHaveBeenCalledWith("shell_execution_complete", expect.objectContaining({ exitCode: 0 })) }, @@ -348,7 +361,11 @@ describe("TerminalRegistry", () => { // processA owns executionA — this is the current, live command. const processA = new TerminalProcess(terminal) - const executionA = { commandLine: { value: "command A" }, read: vi.fn() } as any + // TerminalShellExecution double provides commandLine and read(). + const executionA = { + commandLine: { value: "command A" }, + read: vi.fn(), + } as unknown as vscode.TerminalShellExecution processA.ownExecution = executionA terminal.process = processA const setStreamSpy = vi.spyOn(terminal, "setActiveStream") @@ -356,11 +373,15 @@ describe("TerminalRegistry", () => { // A stale start event arrives referencing a *different* execution (executionB). // This can happen when VSCode fires a delayed start event for a prior terminal // session on the same reused terminal object. - const executionB = { commandLine: { value: "stale command" }, read: vi.fn() } as any + // TerminalShellExecution double provides commandLine and read(). + const executionB = { + commandLine: { value: "stale command" }, + read: vi.fn(), + } as unknown as vscode.TerminalShellExecution await startHandler({ terminal: terminal.terminal, execution: executionB, - }) + } as vscode.TerminalShellExecutionStartEvent) // The stale event must be ignored: read() must not be called on it and the // terminal's active stream must not be replaced. @@ -373,7 +394,11 @@ describe("TerminalRegistry", () => { const process = new TerminalProcess(terminal) const mockStream = (async function* () {})() - const execution = { commandLine: { value: "echo hi" }, read: vi.fn().mockReturnValue(mockStream) } as any + // TerminalShellExecution double provides commandLine and read(). + const execution = { + commandLine: { value: "echo hi" }, + read: vi.fn().mockReturnValue(mockStream), + } as unknown as vscode.TerminalShellExecution process.ownExecution = execution terminal.process = process const setStreamSpy = vi.spyOn(terminal, "setActiveStream") @@ -381,7 +406,7 @@ describe("TerminalRegistry", () => { await startHandler({ terminal: terminal.terminal, execution, - }) + } as vscode.TerminalShellExecutionStartEvent) expect(execution.read).toHaveBeenCalledTimes(1) expect(setStreamSpy).toHaveBeenCalledWith(mockStream) @@ -395,7 +420,8 @@ describe("TerminalRegistry", () => { const abort = vi.fn() terminal.taskId = "task-245" terminal.busy = true - terminal.process = { abort } as any + // TerminalProcess double provides abort() only. + terminal.process = { abort } as unknown as TerminalProcess TerminalRegistry.releaseTerminalsForTask("task-245") @@ -408,7 +434,8 @@ describe("TerminalRegistry", () => { const abort = vi.fn() terminal.taskId = "task-idle" terminal.busy = false - terminal.process = { abort } as any + // TerminalProcess double provides abort() only. + terminal.process = { abort } as unknown as TerminalProcess TerminalRegistry.releaseTerminalsForTask("task-idle") @@ -423,10 +450,12 @@ describe("TerminalRegistry", () => { const abortB = vi.fn() a.taskId = "task-A" a.busy = true - a.process = { abort: abortA } as any + // TerminalProcess double provides abort() only. + a.process = { abort: abortA } as unknown as TerminalProcess b.taskId = "task-B" b.busy = true - b.process = { abort: abortB } as any + // TerminalProcess double provides abort() only. + b.process = { abort: abortB } as unknown as TerminalProcess TerminalRegistry.releaseTerminalsForTask("task-A") @@ -440,11 +469,12 @@ describe("TerminalRegistry", () => { const terminal = TerminalRegistry.createTerminal("/test/path", "vscode") terminal.taskId = "task-throw" terminal.busy = true + // TerminalProcess double provides abort() only. terminal.process = { abort: vi.fn(() => { throw new Error("boom") }), - } as any + } as unknown as TerminalProcess expect(() => TerminalRegistry.releaseTerminalsForTask("task-throw")).not.toThrow() expect(terminal.taskId).toBeUndefined() diff --git a/src/integrations/terminal/__tests__/setupTerminalTests.ts b/src/integrations/terminal/__tests__/setupTerminalTests.ts index 332b58228b..1d0fffbb58 100644 --- a/src/integrations/terminal/__tests__/setupTerminalTests.ts +++ b/src/integrations/terminal/__tests__/setupTerminalTests.ts @@ -22,21 +22,18 @@ const hasPwsh = isPowerShellCoreAvailable() // console.log(`Test environment: ${process.platform} ${process.arch}`) // console.log(`PowerShell Core available: ${hasPwsh}`) -// Define interface for global test environment +// Define the global test environment flag declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace NodeJS { - interface Global { - __TEST_ENV__: { + var __TEST_ENV__: + | { platform: string isPowerShellAvailable: boolean - } - } - } + } + | undefined } // Set global flags for tests to use -;(global as any).__TEST_ENV__ = { +global.__TEST_ENV__ = { platform: process.platform, isPowerShellAvailable: hasPwsh, } diff --git a/src/integrations/terminal/__tests__/streamUtils/bashStream.ts b/src/integrations/terminal/__tests__/streamUtils/bashStream.ts index 0f5dab1f44..2eb9a54450 100644 --- a/src/integrations/terminal/__tests__/streamUtils/bashStream.ts +++ b/src/integrations/terminal/__tests__/streamUtils/bashStream.ts @@ -2,6 +2,13 @@ import { execSync } from "child_process" import { CommandStream } from "./index" +// execSync failures carry stdout plus an ExecException shape (signal/status). +type SyncExecError = import("child_process").ExecException & { + status?: number | null + stdout?: string + stderr?: string +} + /** * Creates a stream with real command output using Bash * @param command The bash command to execute @@ -19,24 +26,25 @@ export function createBashCommandStream(command: string): CommandStream { stdio: ["pipe", "pipe", "ignore"], // Redirect stderr to null }) exitCode = 0 // Command succeeded - } catch (error: any) { + } catch (error) { + const { signal, stdout } = error as SyncExecError // Command failed - get output and exit code from error - realOutput = error.stdout?.toString() || "" + realOutput = stdout?.toString() || "" // Handle signal termination - if (error.signal) { + if (signal) { // Convert signal name to number using Node's constants const signals: Record = { SIGTERM: 15, SIGSEGV: 11, // Add other signals as needed } - const signalNum = signals[error.signal] + const signalNum = signals[signal] if (signalNum !== undefined) { exitCode = 128 + signalNum // Signal exit codes are 128 + signal number } else { // Log error and default to 1 if signal not recognized - console.log(`[DEBUG] Unrecognized signal '${error.signal}' from command '${command}'`) + console.log(`[DEBUG] Unrecognized signal '${signal}' from command '${command}'`) exitCode = 1 } } else { diff --git a/src/integrations/terminal/__tests__/streamUtils/cmdStream.ts b/src/integrations/terminal/__tests__/streamUtils/cmdStream.ts index a34e180f91..eb1b6f7540 100644 --- a/src/integrations/terminal/__tests__/streamUtils/cmdStream.ts +++ b/src/integrations/terminal/__tests__/streamUtils/cmdStream.ts @@ -22,10 +22,12 @@ export function createCmdCommandStream(command: string): CommandStream { stdio: ["pipe", "pipe", "ignore"], // Redirect stderr to null }) exitCode = 0 // Command succeeded - } catch (error: any) { + } catch (error) { // Command failed - get output and exit code from error - realOutput = error.stdout?.toString() || "" - exitCode = error.status ?? 1 + // execSync failures carry stdout plus an ExecException shape (signal/status). + const execError = error as import("child_process").ExecException & { status?: number | null; stdout?: string } + realOutput = execError.stdout?.toString() || "" + exitCode = execError.status ?? 1 } // Create an async iterator for the stream diff --git a/src/integrations/terminal/__tests__/streamUtils/index.ts b/src/integrations/terminal/__tests__/streamUtils/index.ts index 58c9d1bd75..9dc92a4e5d 100644 --- a/src/integrations/terminal/__tests__/streamUtils/index.ts +++ b/src/integrations/terminal/__tests__/streamUtils/index.ts @@ -23,7 +23,7 @@ export interface CommandStream { * @returns Boolean indicating whether pwsh is available */ export function isPowerShellCoreAvailable(): boolean { - return (global as any).__TEST_ENV__?.isPowerShellAvailable || false + return global.__TEST_ENV__?.isPowerShellAvailable || false } /** @@ -31,7 +31,7 @@ export function isPowerShellCoreAvailable(): boolean { * @returns The current platform: 'win32', 'darwin', 'linux', etc. */ export function getPlatform(): string { - return (global as any).__TEST_ENV__?.platform || process.platform + return global.__TEST_ENV__?.platform || process.platform } /** diff --git a/src/integrations/terminal/__tests__/streamUtils/pwshStream.ts b/src/integrations/terminal/__tests__/streamUtils/pwshStream.ts index 9496bf7553..305820aaee 100644 --- a/src/integrations/terminal/__tests__/streamUtils/pwshStream.ts +++ b/src/integrations/terminal/__tests__/streamUtils/pwshStream.ts @@ -35,14 +35,20 @@ export function createPowerShellStream(command: string): CommandStream { stdio: ["pipe", "pipe", "pipe"], // Capture stderr for debugging }) exitCode = 0 // Command succeeded - } catch (error: any) { + } catch (error) { // Command failed - get output and exit code from error - realOutput = error.stdout?.toString() || "" - console.error(`PowerShell command failed with status ${error.status ?? "unknown"}:`, error.message) - if (error.stderr) { - console.error(`stderr: ${error.stderr.toString()}`) + // execSync failures carry stdout/stderr plus an ExecException shape (signal/status). + const execError = error as import("child_process").ExecException & { + status?: number | null + stdout?: string + stderr?: string } - exitCode = error.status ?? 1 + realOutput = execError.stdout?.toString() || "" + console.error(`PowerShell command failed with status ${execError.status ?? "unknown"}:`, execError.message) + if (execError.stderr) { + console.error(`stderr: ${execError.stderr.toString()}`) + } + exitCode = execError.status ?? 1 } // Create an async iterator for the stream diff --git a/src/integrations/theme/getTheme.ts b/src/integrations/theme/getTheme.ts index 20171cc304..4d111cc09d 100644 --- a/src/integrations/theme/getTheme.ts +++ b/src/integrations/theme/getTheme.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode" import * as path from "path" import * as fs from "fs/promises" import { convertTheme } from "monaco-vscode-textmate-theme-converter/lib/cjs" +import type { IVSCodeTheme } from "monaco-vscode-textmate-theme-converter/lib/cjs" import { Package } from "../../shared/package" @@ -22,14 +23,20 @@ const defaultThemes: Record = { "Visual Studio Light": "light_vs", } -function parseThemeString(themeString: string | undefined): any { +type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } +type JsonObject = { [key: string]: JsonValue } + +// Theme JSON is free-form VS Code theme data; model the members this module reads. +type ParsedTheme = JsonObject & { include?: string } + +function parseThemeString(themeString: string | undefined): ParsedTheme { themeString = themeString ?.split("\n") .filter((line) => { return !line.trim().startsWith("//") }) .join("\n") - return JSON.parse(themeString ?? "{}") + return JSON.parse(themeString ?? "{}") as ParsedTheme } export async function getTheme() { @@ -70,18 +77,17 @@ export async function getTheme() { "utf-8", ) const includeTheme = parseThemeString(includeThemeString) - parsed = mergeJson(parsed, includeTheme) + parsed = mergeJson(parsed, includeTheme) as ParsedTheme } - const converted = convertTheme(parsed) + // The converter validates and normalizes free-form theme JSON at runtime. + const converted = convertTheme(parsed as unknown as IVSCodeTheme) - converted.base = ( - ["vs", "hc-black"].includes(converted.base) - ? converted.base - : colorTheme.includes("Light") - ? "vs" - : "vs-dark" - ) as any + converted.base = ["vs", "hc-black"].includes(converted.base) + ? converted.base + : colorTheme.includes("Light") + ? "vs" + : "vs-dark" return converted } catch (e) { @@ -90,14 +96,14 @@ export async function getTheme() { return undefined } -type JsonObject = { [key: string]: any } export function mergeJson( first: JsonObject, second: JsonObject, mergeBehavior?: "merge" | "overwrite", - mergeKeys?: { [key: string]: (a: any, b: any) => boolean }, -): any { - const copyOfFirst = JSON.parse(JSON.stringify(first)) + mergeKeys?: { [key: string]: (a: JsonValue, b: JsonValue) => boolean }, +): JsonObject { + // JSON round-trip is the deep clone of free-form theme JSON. + const copyOfFirst = JSON.parse(JSON.stringify(first)) as JsonObject try { for (const key in second) { @@ -114,9 +120,9 @@ export function mergeJson( // Array if (mergeKeys?.[key]) { // Merge keys are used to determine whether an item form the second object should override one from the first - const keptFromFirst: any[] = [] - firstValue.forEach((item: any) => { - if (!secondValue.some((item2: any) => mergeKeys[key](item, item2))) { + const keptFromFirst: JsonValue[] = [] + firstValue.forEach((item) => { + if (!secondValue.some((item2) => mergeKeys[key](item, item2))) { keptFromFirst.push(item) } }) @@ -125,8 +131,8 @@ export function mergeJson( copyOfFirst[key] = [...firstValue, ...secondValue] } } else if (typeof secondValue === "object" && typeof firstValue === "object") { - // Object - copyOfFirst[key] = mergeJson(firstValue, secondValue, mergeBehavior) + // Object; free-form JSON values are plain objects here + copyOfFirst[key] = mergeJson(firstValue as JsonObject, secondValue as JsonObject, mergeBehavior) } else { // Other (boolean, number, string) copyOfFirst[key] = secondValue