Skip to content

Commit 1797956

Browse files
committed
Add tests
1 parent 9f9567d commit 1797956

File tree

4 files changed

+531
-102
lines changed

4 files changed

+531
-102
lines changed

src/remote/sshProcess.ts

Lines changed: 15 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,6 @@ export interface NetworkInfo {
2020
using_coder_connect: boolean;
2121
}
2222

23-
/**
24-
* Process information from find-process.
25-
*/
26-
interface ProcessInfo {
27-
pid: number;
28-
name: string;
29-
cmd: string;
30-
}
31-
3223
/**
3324
* Options for creating an SshProcessMonitor.
3425
*/
@@ -44,27 +35,6 @@ export interface SshProcessMonitorOptions {
4435
remoteSshExtensionId: string;
4536
}
4637

47-
/**
48-
* Checks if a process is an actual SSH process (not a shell wrapper).
49-
* Filters out processes like "sh -c ... | ssh ..." where ssh appears mid-command.
50-
*/
51-
function isActualSshProcess(p: ProcessInfo): boolean {
52-
// Process name is exactly "ssh" or "ssh.exe"
53-
if (p.name === "ssh" || p.name === "ssh.exe") {
54-
return true;
55-
}
56-
// Command starts with ssh binary (not piped through shell)
57-
if (/^(ssh|ssh\.exe)\s/i.test(p.cmd)) {
58-
return true;
59-
}
60-
// Command starts with full path to ssh (Unix or Windows)
61-
// e.g., "/usr/bin/ssh " or "C:\Program Files\OpenSSH\ssh.exe "
62-
if (/^[\w/\\:.-]+([/\\])ssh(\.exe)?\s/i.test(p.cmd)) {
63-
return true;
64-
}
65-
return false;
66-
}
67-
6838
/**
6939
* Finds the Remote SSH extension's log file path.
7040
*/
@@ -139,6 +109,7 @@ export class SshProcessMonitor implements vscode.Disposable {
139109
private currentPid: number | undefined;
140110
private logFilePath: string | undefined;
141111
private pendingTimeout: NodeJS.Timeout | undefined;
112+
private lastStaleSearchTime = 0;
142113

143114
private constructor(options: SshProcessMonitorOptions) {
144115
this.options = {
@@ -216,7 +187,7 @@ export class SshProcessMonitor implements vscode.Disposable {
216187
while (!this.disposed) {
217188
attempt++;
218189

219-
if (attempt % 5 === 0) {
190+
if (attempt % 10 === 0) {
220191
logger.debug(
221192
`SSH process search attempt ${attempt} for host: ${sshHost}`,
222193
);
@@ -231,15 +202,6 @@ export class SshProcessMonitor implements vscode.Disposable {
231202
return;
232203
}
233204

234-
// Fall back to hostname-based search
235-
// const pidByHost = await this.findSshProcessByHost();
236-
// if (pidByHost !== undefined) {
237-
// logger.info(`Found SSH process by hostname (PID: ${pidByHost})`);
238-
// this.setCurrentPid(pidByHost);
239-
// this.startMonitoring();
240-
// return;
241-
// }
242-
243205
await this.delay(pollInterval);
244206
}
245207
}
@@ -262,6 +224,7 @@ export class SshProcessMonitor implements vscode.Disposable {
262224

263225
const logContent = await fs.readFile(logPath, "utf8");
264226
this.options.logger.debug(`Read Remote SSH log file: ${logPath}`);
227+
265228
const port = findPort(logContent);
266229
if (!port) {
267230
return undefined;
@@ -280,45 +243,6 @@ export class SshProcessMonitor implements vscode.Disposable {
280243
}
281244
}
282245

283-
/**
284-
* Attempts to find an SSH process by hostname.
285-
* Less accurate than port-based as multiple windows may connect to same host.
286-
* Returns the PID if found, undefined otherwise.
287-
*/
288-
private async findSshProcessByHost(): Promise<number | undefined> {
289-
const { sshHost, logger } = this.options;
290-
291-
try {
292-
// Find all processes with "ssh" in name
293-
const processes = await find("name", "ssh");
294-
const matches = processes.filter(
295-
(p) => p.cmd.includes(sshHost) && isActualSshProcess(p),
296-
);
297-
298-
if (matches.length === 0) {
299-
return undefined;
300-
}
301-
302-
const preferred = matches.find(
303-
(p) => p.name === "ssh" || p.name === "ssh.exe",
304-
);
305-
if (preferred) {
306-
return preferred.pid;
307-
}
308-
309-
if (matches.length > 1) {
310-
logger.warn(
311-
`Found ${matches.length} SSH processes for host, using first`,
312-
);
313-
}
314-
315-
return matches[0].pid;
316-
} catch (error) {
317-
logger.debug(`Error searching for SSH process: ${error}`);
318-
return undefined;
319-
}
320-
}
321-
322246
/**
323247
* Updates the current PID and fires change events.
324248
*/
@@ -406,15 +330,20 @@ export class SshProcessMonitor implements vscode.Disposable {
406330
const ageMs = Date.now() - stats.mtime.getTime();
407331

408332
if (ageMs > staleThreshold) {
409-
logger.info(
333+
// Prevent tight loop: if we just searched due to stale, wait before searching again
334+
const timeSinceLastSearch = Date.now() - this.lastStaleSearchTime;
335+
if (timeSinceLastSearch < staleThreshold) {
336+
await this.delay(staleThreshold - timeSinceLastSearch);
337+
continue;
338+
}
339+
340+
logger.debug(
410341
`Network info stale (${Math.round(ageMs / 1000)}s old), searching for new SSH process`,
411342
);
412-
this.currentPid = undefined;
413-
this.statusBarItem.hide();
414-
this._onPidChange.fire(undefined);
415-
this.searchForProcess().catch((err) => {
416-
logger.error("Error restarting SSH process search", err);
417-
});
343+
344+
// searchForProcess will update PID if a different process is found
345+
this.lastStaleSearchTime = Date.now();
346+
await this.searchForProcess();
418347
return;
419348
}
420349

test/mocks/testHelpers.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class MockConfigurationProvider {
2929
get<T>(key: string, defaultValue: T): T;
3030
get<T>(key: string, defaultValue?: T): T | undefined {
3131
const value = this.config.get(key);
32-
return value !== undefined ? (value as T) : defaultValue;
32+
return value === undefined ? defaultValue : (value as T);
3333
}
3434

3535
/**
@@ -53,7 +53,7 @@ export class MockConfigurationProvider {
5353
return {
5454
get: vi.fn((key: string, defaultValue?: unknown) => {
5555
const value = snapshot.get(getFullKey(key));
56-
return value !== undefined ? value : defaultValue;
56+
return value === undefined ? defaultValue : value;
5757
}),
5858
has: vi.fn((key: string) => {
5959
return snapshot.has(getFullKey(key));
@@ -141,7 +141,7 @@ export class MockProgressReporter {
141141
* Use this to control user responses in tests.
142142
*/
143143
export class MockUserInteraction {
144-
private responses = new Map<string, string | undefined>();
144+
private readonly responses = new Map<string, string | undefined>();
145145
private externalUrls: string[] = [];
146146

147147
constructor() {
@@ -211,7 +211,7 @@ export class MockUserInteraction {
211211

212212
// Simple in-memory implementation of Memento
213213
export class InMemoryMemento implements vscode.Memento {
214-
private storage = new Map<string, unknown>();
214+
private readonly storage = new Map<string, unknown>();
215215

216216
get<T>(key: string): T | undefined;
217217
get<T>(key: string, defaultValue: T): T;
@@ -235,9 +235,11 @@ export class InMemoryMemento implements vscode.Memento {
235235

236236
// Simple in-memory implementation of SecretStorage
237237
export class InMemorySecretStorage implements vscode.SecretStorage {
238-
private secrets = new Map<string, string>();
238+
private readonly secrets = new Map<string, string>();
239239
private isCorrupted = false;
240-
private listeners: Array<(e: vscode.SecretStorageChangeEvent) => void> = [];
240+
private readonly listeners: Array<
241+
(e: vscode.SecretStorageChangeEvent) => void
242+
> = [];
241243

242244
onDidChange: vscode.Event<vscode.SecretStorageChangeEvent> = (listener) => {
243245
this.listeners.push(listener);
@@ -350,3 +352,50 @@ export function createMockStream(
350352
destroy: vi.fn(),
351353
} as unknown as IncomingMessage;
352354
}
355+
356+
/**
357+
* Mock status bar that integrates with vscode.window.createStatusBarItem.
358+
* Use this to inspect status bar state in tests.
359+
*/
360+
export class MockStatusBar {
361+
text = "";
362+
tooltip: string | vscode.MarkdownString = "";
363+
backgroundColor: vscode.ThemeColor | undefined;
364+
color: string | vscode.ThemeColor | undefined;
365+
command: string | vscode.Command | undefined;
366+
accessibilityInformation: vscode.AccessibilityInformation | undefined;
367+
name: string | undefined;
368+
priority: number | undefined;
369+
alignment: vscode.StatusBarAlignment = vscode.StatusBarAlignment.Left;
370+
371+
readonly show = vi.fn();
372+
readonly hide = vi.fn();
373+
readonly dispose = vi.fn();
374+
375+
constructor() {
376+
this.setupVSCodeMock();
377+
}
378+
379+
/**
380+
* Reset all status bar state
381+
*/
382+
reset(): void {
383+
this.text = "";
384+
this.tooltip = "";
385+
this.backgroundColor = undefined;
386+
this.color = undefined;
387+
this.command = undefined;
388+
this.show.mockClear();
389+
this.hide.mockClear();
390+
this.dispose.mockClear();
391+
}
392+
393+
/**
394+
* Setup the vscode.window.createStatusBarItem mock
395+
*/
396+
private setupVSCodeMock(): void {
397+
vi.mocked(vscode.window.createStatusBarItem).mockReturnValue(
398+
this as unknown as vscode.StatusBarItem,
399+
);
400+
}
401+
}

test/mocks/vscode.runtime.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,28 @@ export class Uri {
5555
}
5656
}
5757

58-
// mini event
59-
const makeEvent = <T>() => {
60-
const listeners = new Set<(e: T) => void>();
61-
const event = (listener: (e: T) => void) => {
62-
listeners.add(listener);
63-
return { dispose: () => listeners.delete(listener) };
58+
/**
59+
* Mock EventEmitter that matches vscode.EventEmitter interface.
60+
*/
61+
export class EventEmitter<T> {
62+
private readonly listeners = new Set<(e: T) => void>();
63+
64+
event = (listener: (e: T) => void) => {
65+
this.listeners.add(listener);
66+
return { dispose: () => this.listeners.delete(listener) };
6467
};
65-
return { event, fire: (e: T) => listeners.forEach((l) => l(e)) };
66-
};
6768

68-
const onDidChangeConfiguration = makeEvent<unknown>();
69-
const onDidChangeWorkspaceFolders = makeEvent<unknown>();
69+
fire(data: T): void {
70+
this.listeners.forEach((l) => l(data));
71+
}
72+
73+
dispose(): void {
74+
this.listeners.clear();
75+
}
76+
}
77+
78+
const onDidChangeConfiguration = new EventEmitter<unknown>();
79+
const onDidChangeWorkspaceFolders = new EventEmitter<unknown>();
7080

7181
export const window = {
7282
showInformationMessage: vi.fn(),
@@ -83,6 +93,7 @@ export const window = {
8393
dispose: vi.fn(),
8494
clear: vi.fn(),
8595
})),
96+
createStatusBarItem: vi.fn(),
8697
};
8798

8899
export const commands = {
@@ -132,6 +143,7 @@ const vscode = {
132143
ExtensionMode,
133144
UIKind,
134145
Uri,
146+
EventEmitter,
135147
window,
136148
commands,
137149
workspace,

0 commit comments

Comments
 (0)