Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = {
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
setupFilesAfterEnv: ["<rootDir>/src/test/setup.ts"],
reporters: ["default", ["summary", { summaryThreshold: 1 }]],
collectCoverageFrom: [
"src/**/*.{ts,tsx}",
"!src/test/**",
Expand Down
33 changes: 30 additions & 3 deletions src/test/mock/vscode.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { jest } from "@jest/globals";
import { Uri } from "vscode";

// Export VSCode types that were previously defined
export const ExtensionKind = {
UI: 1,
Workspace: 2,
};

export { Uri };
export const Uri = {
file: jest.fn((f: string) => ({ fsPath: f })),
parse: jest.fn(),
};

export class Position {
constructor(
Expand Down Expand Up @@ -110,6 +112,9 @@ export const window = {
hide: jest.fn(),
dispose: jest.fn(),
}),
withProgress: jest
.fn()
.mockImplementation((_options: any, task: any) => task()),
Comment on lines +115 to +117
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The window.withProgress mock is incomplete. It fails to pass the required token argument to the task function, which will cause a crash in production code that handles cancellation.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The mock implementation of window.withProgress in src/test/mock/vscode.ts calls the provided task function without the required arguments. Production code in src/dbt_client/dbtProject.ts expects this task to receive a token object as its second argument and subsequently calls token.onCancellationRequested. Because the mock passes undefined instead of a token object, any operation that uses this progress indicator (e.g., runModel, buildModel) will crash with a "Cannot read property 'onCancellationRequested' of undefined" error. This issue is not detected by the test suite because the specific code paths are not tested.

💡 Suggested Fix

Update the withProgress mock to call the task function with the two expected arguments: a mock progress object and a mock token object. The token object should have a mock onCancellationRequested function. For example: task({ report: jest.fn() }, { onCancellationRequested: jest.fn() }).

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/test/mock/vscode.ts#L115-L117

Potential issue: The mock implementation of `window.withProgress` in
`src/test/mock/vscode.ts` calls the provided `task` function without the required
arguments. Production code in `src/dbt_client/dbtProject.ts` expects this `task` to
receive a `token` object as its second argument and subsequently calls
`token.onCancellationRequested`. Because the mock passes `undefined` instead of a
`token` object, any operation that uses this progress indicator (e.g., `runModel`,
`buildModel`) will crash with a "Cannot read property 'onCancellationRequested' of
undefined" error. This issue is not detected by the test suite because the specific code
paths are not tested.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 7737468

};

export const workspace = {
Expand All @@ -119,6 +124,12 @@ export const workspace = {
update: jest.fn(),
}),
workspaceFolders: [],
getWorkspaceFolder: jest.fn((uri: typeof Uri) => {
if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
return workspace.workspaceFolders[0];
}
return undefined;
}),
onDidChangeConfiguration: jest.fn().mockReturnValue({ dispose: jest.fn() }),
onDidChangeWorkspaceFolders: jest
.fn()
Expand All @@ -129,11 +140,12 @@ export const workspace = {
onDidDelete: jest.fn().mockReturnValue({ dispose: jest.fn() }),
dispose: jest.fn(),
}),
};
} as any;

export const languages = {
createDiagnosticCollection: jest.fn().mockReturnValue({
set: jest.fn(),
get: jest.fn(),
delete: jest.fn(),
clear: jest.fn(),
dispose: jest.fn(),
Expand All @@ -152,6 +164,21 @@ export const languages = {
registerCodeLensProvider: jest.fn().mockReturnValue({ dispose: jest.fn() }),
};

export const EventEmitter = jest.fn().mockImplementation(() => ({
event: jest.fn().mockReturnValue({ dispose: jest.fn() }),
fire: jest.fn(),
dispose: jest.fn(),
}));

export const ProgressLocation = {
Notification: 15,
};

export const RelativePattern = jest.fn();
export const ViewColumn = {};
export const Disposable = jest.fn();
export const Event = jest.fn();

export const resetMocks = () => {
jest.clearAllMocks();
};
113 changes: 0 additions & 113 deletions src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1 @@
import "reflect-metadata";

// Set up the container before tests
import "../inversify.config";
import { MockEventEmitter } from "./common";

// Mock VS Code APIs before any imports
jest.mock("vscode", () => ({
EventEmitter: jest.fn().mockImplementation(() => new MockEventEmitter()),
workspace: {
getConfiguration: jest.fn().mockReturnValue({
get: jest.fn(),
update: jest.fn(),
}),
workspaceFolders: [],
onDidChangeConfiguration: jest.fn(),
onDidChangeWorkspaceFolders: jest.fn().mockImplementation((callback) => ({
dispose: jest.fn(),
})),
createFileSystemWatcher: jest.fn().mockReturnValue({
onDidChange: jest.fn(),
onDidCreate: jest.fn(),
onDidDelete: jest.fn(),
dispose: jest.fn(),
}),
},
commands: {
getCommands: jest.fn().mockResolvedValue([]),
registerCommand: jest.fn(),
executeCommand: jest.fn(),
},
window: {
showInformationMessage: jest.fn(),
showErrorMessage: jest.fn(),
createTerminal: jest.fn().mockReturnValue({
dispose: jest.fn(),
hide: jest.fn(),
show: jest.fn(),
sendText: jest.fn(),
}),
createOutputChannel: jest.fn().mockReturnValue({
appendLine: jest.fn(),
show: jest.fn(),
clear: jest.fn(),
dispose: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
}),
},
languages: {
createDiagnosticCollection: jest.fn().mockReturnValue({
set: jest.fn(),
delete: jest.fn(),
clear: jest.fn(),
dispose: jest.fn(),
}),
},
Uri: {
file: jest.fn((f: string) => ({ fsPath: f })),
parse: jest.fn(),
},
DiagnosticSeverity: {
Error: 0,
Warning: 1,
Information: 2,
Hint: 3,
},
Disposable: {
from: jest.fn(),
},
ExtensionKind: {
UI: 1,
Workspace: 2,
},
Diagnostic: jest.fn().mockImplementation((range, message, severity) => ({
range,
message,
severity,
})),
Range: jest
.fn()
.mockImplementation((startLine, startChar, endLine, endChar) => ({
start: { line: startLine, character: startChar },
end: { line: endLine, character: endChar },
})),
Position: jest.fn().mockImplementation((line, character) => ({
line,
character,
})),
TreeItemCollapsibleState: {
None: 0,
Collapsed: 1,
Expanded: 2,
},
TreeItem: jest.fn().mockImplementation((label, collapsibleState) => ({
label,
collapsibleState,
})),
CancellationTokenSource: jest.fn().mockImplementation(() => ({
token: {
onCancellationRequested: jest.fn(),
isCancellationRequested: false,
},
cancel: jest.fn(),
dispose: jest.fn(),
})),
CancellationToken: {
None: {
onCancellationRequested: jest.fn(),
isCancellationRequested: false,
},
},
}));
Loading