Skip to content

Commit 50e1929

Browse files
dmartinochoaclaude
andcommitted
test: expand coverage to 187 unit + integration tests (was 134)
Backstop the recent fixes and add coverage for surfaces that had none. The most load-bearing additions: - findScannableFiles: per-pattern findFiles, dedupe-on-toString, first-seen order. Locks the fence against the nested-brace glob that produced "no scannable files found" returning. - scanWorkspace orchestration: no-workspace path, no-files path, quiet vs notification toasts, openTextDocument-failure counting, user cancellation. Previously only formatSummary had tests. - installInTerminal: opens a terminal, types pip command, asserts addNewLine=false so the user's unactivated venv isn't violated. copyInstallCommandToClipboard: clipboard write + status-bar confirmation TTL. The PIP_INSTALL_COMMAND literal is pinned. - setLspReady: exact setContext payload for both true and false transitions. The conditional viewsWelcome panel rides on this. - registerStatusBar visibility latch: hidden until either a CI candidate file or a pipeline-check diagnostic is observed; non- pipeline-check sources don't flip the latch. - manifest.test.ts: viewsWelcome shape (two gated entries, primary CTAs, no "Copy install command" button), every welcome-link command target declared, workspace-trust capability locked. - Integration: runs the actual pipelineCheck.scanWorkspace command against the fixture workspace so a real VS Code findFiles walks the glob — end-to-end regression fence for the nested-brace bug. Shared infra: the __testStubs__/vscode.ts module gains findFiles, createTerminal, createStatusBarItem, openTextDocument, clipboard, setStatusBarMessage, withProgress, configurable workspaceFolders, and a resetStubState() helper that beforeEach hooks use to keep each test's globalThis observations isolated. 134 → 187 vitest assertions; lint, typecheck, integration typecheck, and bundle all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d28652e commit 50e1929

7 files changed

Lines changed: 1101 additions & 94 deletions

File tree

src/__testStubs__/vscode.ts

Lines changed: 286 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,134 @@
1111
// a fresh object per call keeps tests isolated — none of the classes
1212
// or stubs leak state between files.
1313
//
14-
// `getDiagnostics` reads from `globalThis.__stubDiagnostics`, which
15-
// tests populate via the per-file `setStubDiagnostics` helper they
16-
// keep close to their fixtures.
14+
// Tests drive behaviour through `globalThis.__stub*` slots and read
15+
// observations back through `globalThis.__stubCalls`. Per-file
16+
// `beforeEach` is expected to reset both. Each slot is documented
17+
// inline below so a new test can find its hook without re-reading the
18+
// whole stub.
19+
20+
export interface StubFindFilesCall {
21+
readonly include: string;
22+
readonly exclude?: string;
23+
readonly maxResults?: number;
24+
}
25+
26+
export interface StubTerminalCall {
27+
readonly name: string;
28+
readonly shown: boolean;
29+
readonly sent: ReadonlyArray<{ readonly text: string; readonly addNewLine: boolean }>;
30+
}
31+
32+
export interface StubExecuteCommandCall {
33+
readonly command: string;
34+
readonly args: readonly unknown[];
35+
}
36+
37+
export interface StubClipboardWrite {
38+
readonly text: string;
39+
}
40+
41+
export interface StubStatusBarMessage {
42+
readonly text: string;
43+
readonly hideAfterMs?: number;
44+
}
45+
46+
export interface StubStatusBarItem {
47+
text: string;
48+
tooltip: unknown;
49+
command: unknown;
50+
name: string;
51+
backgroundColor: unknown;
52+
accessibilityInformation: { label: string } | undefined;
53+
shown: boolean;
54+
disposed: boolean;
55+
// Observable counters help tests pin "did update() fire?" without
56+
// racing against the implementation's debouncing.
57+
showCount: number;
58+
hideCount: number;
59+
}
60+
61+
interface StubCalls {
62+
readonly findFiles: StubFindFilesCall[];
63+
readonly terminals: StubTerminalCall[];
64+
readonly executeCommand: StubExecuteCommandCall[];
65+
readonly clipboardWrites: StubClipboardWrite[];
66+
readonly statusBarMessages: StubStatusBarMessage[];
67+
readonly statusBarItems: StubStatusBarItem[];
68+
readonly infoMessages: string[];
69+
readonly warningMessages: string[];
70+
readonly errorMessages: string[];
71+
}
72+
73+
declare global {
74+
var __stubConfig: Record<string, unknown> | undefined;
75+
var __stubDiagnostics:
76+
| Array<[{ toString: () => string }, unknown[]]>
77+
| undefined;
78+
// Findings of `findFiles`. Tests may either set a single `__stubFindFiles`
79+
// (used for every include glob) or `__stubFindFilesByPattern` (a map
80+
// from include glob → URI list, queried per call).
81+
var __stubFindFiles: Array<{ toString: () => string; fsPath: string }> | undefined;
82+
var __stubFindFilesByPattern:
83+
| Record<string, Array<{ toString: () => string; fsPath: string }>>
84+
| undefined;
85+
// What `workspace.workspaceFolders` should return for the duration
86+
// of a test. `undefined` mimics "no workspace open".
87+
var __stubWorkspaceFolders:
88+
| Array<{ uri: { toString: () => string; fsPath: string } }>
89+
| undefined;
90+
// URI strings that `openTextDocument` should reject for (simulating
91+
// a read error / unsupported encoding). All other URIs resolve.
92+
var __stubOpenTextDocumentFailures: Set<string> | undefined;
93+
// When true, `withProgress` reports cancellation back to the task
94+
// immediately. Used by the scanWorkspace cancellation test.
95+
var __stubProgressCancelled: boolean | undefined;
96+
var __stubCalls: StubCalls | undefined;
97+
}
98+
99+
function ensureCalls(): StubCalls {
100+
if (!globalThis.__stubCalls) {
101+
globalThis.__stubCalls = {
102+
findFiles: [],
103+
terminals: [],
104+
executeCommand: [],
105+
clipboardWrites: [],
106+
statusBarMessages: [],
107+
statusBarItems: [],
108+
infoMessages: [],
109+
warningMessages: [],
110+
errorMessages: [],
111+
};
112+
}
113+
return globalThis.__stubCalls;
114+
}
115+
116+
/**
117+
* Reset every observable slot in one place. Tests call this in
118+
* `beforeEach` to keep their assertions isolated. The factory itself
119+
* does not reset; calling `vscodeStub()` returns a fresh module shape
120+
* but reuses the global slots so tests across files don't fight.
121+
*/
122+
export function resetStubState(): void {
123+
globalThis.__stubConfig = {};
124+
globalThis.__stubDiagnostics = [];
125+
globalThis.__stubFindFiles = undefined;
126+
globalThis.__stubFindFilesByPattern = undefined;
127+
globalThis.__stubWorkspaceFolders = undefined;
128+
globalThis.__stubOpenTextDocumentFailures = undefined;
129+
globalThis.__stubProgressCancelled = undefined;
130+
globalThis.__stubCalls = {
131+
findFiles: [],
132+
terminals: [],
133+
executeCommand: [],
134+
clipboardWrites: [],
135+
statusBarMessages: [],
136+
statusBarItems: [],
137+
infoMessages: [],
138+
warningMessages: [],
139+
errorMessages: [],
140+
};
141+
}
17142

18143
export function vscodeStub(): Record<string, unknown> {
19144
class ThemeIcon {
@@ -72,6 +197,7 @@ export function vscodeStub(): Record<string, unknown> {
72197
};
73198
const TreeItemCollapsibleState = { None: 0, Collapsed: 1, Expanded: 2 };
74199
const StatusBarAlignment = { Left: 1, Right: 2 };
200+
const ProgressLocation = { SourceControl: 1, Window: 10, Notification: 15 };
75201

76202
class Range {
77203
constructor(
@@ -102,51 +228,194 @@ export function vscodeStub(): Record<string, unknown> {
102228
MarkdownString,
103229
TreeItemCollapsibleState,
104230
StatusBarAlignment,
231+
ProgressLocation,
105232
Range,
106233
CodeLens,
107234
Uri,
108235
workspace: {
109236
asRelativePath: (uri: { fsPath?: string; path?: string }) =>
110237
uri.fsPath ?? uri.path ?? "",
238+
// Getter so a single per-test override (writing to
239+
// `globalThis.__stubWorkspaceFolders`) reaches the consumer
240+
// without each test having to mutate the `vscode.workspace`
241+
// module reference.
242+
get workspaceFolders() {
243+
return globalThis.__stubWorkspaceFolders;
244+
},
111245
// `getConfiguration(section).get(key, fallback)` reads from
112246
// `globalThis.__stubConfig`, a `Record<string, unknown>` keyed
113247
// by `<section>.<key>` (or just `<key>` if no section was
114248
// passed). Tests set the dictionary in beforeEach so each
115249
// test's expectations are isolated.
116250
getConfiguration: (section?: string) => ({
117251
get: <T>(key: string, fallback?: T): T => {
118-
const store =
119-
(globalThis as { __stubConfig?: Record<string, unknown> })
120-
.__stubConfig ?? {};
252+
const store = globalThis.__stubConfig ?? {};
121253
const fullKey = section ? `${section}.${key}` : key;
122254
if (fullKey in store) return store[fullKey] as T;
123255
return fallback as T;
124256
},
125257
}),
126258
onDidChangeConfiguration: () => ({ dispose: () => undefined }),
259+
onDidSaveTextDocument: () => ({ dispose: () => undefined }),
260+
// Resolves with whatever the test stashed on
261+
// `globalThis.__stubFindFiles` (a flat URI list reused for every
262+
// include glob) or `globalThis.__stubFindFilesByPattern[include]`
263+
// (when a per-pattern map is set, more useful for asserting that
264+
// each pattern is queried separately). Every call is captured on
265+
// `globalThis.__stubCalls.findFiles` so a test can assert on the
266+
// include/exclude/maxResults the caller passed.
267+
findFiles: (include: string, exclude?: string, maxResults?: number) => {
268+
const calls = ensureCalls();
269+
calls.findFiles.push({ include, exclude, maxResults });
270+
const byPattern = globalThis.__stubFindFilesByPattern;
271+
if (byPattern) {
272+
return Promise.resolve(byPattern[include] ?? []);
273+
}
274+
const flat = globalThis.__stubFindFiles;
275+
return Promise.resolve(flat ?? []);
276+
},
277+
// Resolves with a minimal TextDocument-shaped object for any
278+
// URI not listed in `__stubOpenTextDocumentFailures`, where it
279+
// rejects instead. scanWorkspace counts those rejections as
280+
// `failed` without aborting the rest of the scan.
281+
openTextDocument: (uri: { toString: () => string }) => {
282+
const key = uri.toString();
283+
if (globalThis.__stubOpenTextDocumentFailures?.has(key)) {
284+
return Promise.reject(new Error(`stub: open failed for ${key}`));
285+
}
286+
return Promise.resolve({ uri });
287+
},
127288
},
128289
languages: {
129290
// Two call shapes:
130291
// - `getDiagnostics()` returns every [uri, diagnostic[]] pair
131292
// - `getDiagnostics(uri)` returns just that uri's diagnostics
132293
getDiagnostics: (uri?: { toString: () => string }) => {
133-
const all =
134-
(
135-
globalThis as {
136-
__stubDiagnostics?: Array<[
137-
{ toString: () => string },
138-
unknown[],
139-
]>;
140-
}
141-
).__stubDiagnostics ?? [];
294+
const all = globalThis.__stubDiagnostics ?? [];
142295
if (uri === undefined) return all;
143296
const key = uri.toString();
144297
const match = all.find(([u]) => u.toString() === key);
145298
return match ? match[1] : [];
146299
},
147300
onDidChangeDiagnostics: () => ({ dispose: () => undefined }),
148301
},
149-
commands: { executeCommand: () => Promise.resolve() },
150-
window: {},
302+
commands: {
303+
// Captures every executeCommand invocation on the shared
304+
// `__stubCalls.executeCommand` slot. Tests assert on the call
305+
// history (e.g. setContext / pipelineCheck.lspReady / true).
306+
executeCommand: (command: string, ...args: unknown[]) => {
307+
ensureCalls().executeCommand.push({ command, args });
308+
return Promise.resolve();
309+
},
310+
registerCommand: () => ({ dispose: () => undefined }),
311+
},
312+
env: {
313+
clipboard: {
314+
writeText: (text: string) => {
315+
ensureCalls().clipboardWrites.push({ text });
316+
return Promise.resolve();
317+
},
318+
},
319+
openExternal: () => Promise.resolve(true),
320+
},
321+
window: {
322+
// Terminal factory captures the name and returns a stub whose
323+
// show/sendText calls land on the shared slot. Each call returns
324+
// a fresh terminal with its own observation buffer.
325+
createTerminal: (name: string) => {
326+
const sent: Array<{ text: string; addNewLine: boolean }> = [];
327+
const record = {
328+
name,
329+
shown: false,
330+
sent,
331+
};
332+
ensureCalls().terminals.push(record);
333+
return {
334+
name,
335+
show: () => {
336+
// Mutate the captured record so tests see `shown: true`
337+
// without having to drill into a closure.
338+
(record as { shown: boolean }).shown = true;
339+
},
340+
sendText: (text: string, addNewLine?: boolean) => {
341+
sent.push({ text, addNewLine: addNewLine ?? true });
342+
},
343+
dispose: () => undefined,
344+
};
345+
},
346+
createStatusBarItem: (
347+
_alignment?: number,
348+
_priority?: number,
349+
): StubStatusBarItem & {
350+
show: () => void;
351+
hide: () => void;
352+
dispose: () => void;
353+
} => {
354+
const item: StubStatusBarItem & {
355+
show: () => void;
356+
hide: () => void;
357+
dispose: () => void;
358+
} = {
359+
text: "",
360+
tooltip: undefined,
361+
command: undefined,
362+
name: "",
363+
backgroundColor: undefined,
364+
accessibilityInformation: undefined,
365+
shown: false,
366+
disposed: false,
367+
showCount: 0,
368+
hideCount: 0,
369+
show() {
370+
this.shown = true;
371+
this.showCount += 1;
372+
},
373+
hide() {
374+
this.shown = false;
375+
this.hideCount += 1;
376+
},
377+
dispose() {
378+
this.disposed = true;
379+
},
380+
};
381+
ensureCalls().statusBarItems.push(item);
382+
return item;
383+
},
384+
setStatusBarMessage: (text: string, hideAfterMs?: number) => {
385+
ensureCalls().statusBarMessages.push({ text, hideAfterMs });
386+
return { dispose: () => undefined };
387+
},
388+
showInformationMessage: (message: string) => {
389+
ensureCalls().infoMessages.push(message);
390+
return Promise.resolve(undefined);
391+
},
392+
showWarningMessage: (message: string) => {
393+
ensureCalls().warningMessages.push(message);
394+
return Promise.resolve(undefined);
395+
},
396+
showErrorMessage: (message: string) => {
397+
ensureCalls().errorMessages.push(message);
398+
return Promise.resolve(undefined);
399+
},
400+
// Progress UI: invokes the task immediately with a no-op
401+
// `progress` reporter and a never-cancelled token. Good enough
402+
// to drive scanWorkspace's loop in a unit test without mocking
403+
// out the full Progress API surface.
404+
withProgress: async <T>(
405+
_options: unknown,
406+
task: (
407+
progress: { report: (value: unknown) => void },
408+
token: { isCancellationRequested: boolean },
409+
) => Thenable<T>,
410+
): Promise<T> => {
411+
const progress = { report: () => undefined };
412+
const token = {
413+
get isCancellationRequested() {
414+
return globalThis.__stubProgressCancelled === true;
415+
},
416+
};
417+
return task(progress, token);
418+
},
419+
},
151420
};
152421
}

0 commit comments

Comments
 (0)