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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

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