From 740b196fcff0f78182c8cb00b3453483a3f77731 Mon Sep 17 00:00:00 2001 From: Yazan Amer Date: Fri, 27 Mar 2026 16:41:18 +0300 Subject: [PATCH 1/2] feat: #750 --- package.json | 9 +++++++++ src/commands.ts | 47 ++++++++++++++++++++++++++++++++++++++++++++ src/core/cliUtils.ts | 30 ++++++++++++++++++++++++++++ src/extension.ts | 4 ++++ 4 files changed, 90 insertions(+) diff --git a/package.json b/package.json index a78b2b5b..d2cc9c3d 100644 --- a/package.json +++ b/package.json @@ -313,6 +313,11 @@ "category": "Coder", "icon": "$(refresh)" }, + { + "command": "coder.speedTest", + "title": "Run Speed Test", + "category": "Coder" + }, { "command": "coder.viewLogs", "title": "Coder: View Logs", @@ -371,6 +376,10 @@ "command": "coder.createWorkspace", "when": "coder.authenticated" }, + { + "command": "coder.speedTest", + "when": "coder.workspace.connected" + }, { "command": "coder.navigateToWorkspace", "when": "coder.workspace.connected" diff --git a/src/commands.ts b/src/commands.ts index 3357f456..fe7568bd 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -146,6 +146,53 @@ export class Commands { this.logger.debug("Login complete to deployment:", url); } + /** + * Run a speed test against the currently connected workspace and display the + * results in a new editor document. + */ + public async speedTest(): Promise { + if (!this.workspace) { + vscode.window.showInformationMessage("No workspace connected."); + return; + } + + await withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Running speed test...", + }, + async () => { + const baseUrl = this.requireExtensionBaseUrl(); + const safeHost = toSafeHost(baseUrl); + const binary = await this.cliManager.fetchBinary(this.extensionClient); + const version = semver.parse(await cliUtils.version(binary)); + const featureSet = featureSetForVersion(version); + const configDir = this.pathResolver.getGlobalConfigDir(safeHost); + const configs = vscode.workspace.getConfiguration(); + const auth = resolveCliAuth(configs, featureSet, baseUrl, configDir); + const workspaceName = createWorkspaceIdentifier(this.workspace!); + + try { + const stdout = await cliUtils.speedtest( + binary, + auth, + workspaceName, + ); + const doc = await vscode.workspace.openTextDocument({ + content: stdout, + language: "json", + }); + await vscode.window.showTextDocument(doc); + } catch (error) { + this.logger.error("Speed test failed", error); + vscode.window.showErrorMessage( + `Speed test failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } + /** * View the logs for the currently connected workspace. */ diff --git a/src/core/cliUtils.ts b/src/core/cliUtils.ts index 4d2f7c55..19f229a7 100644 --- a/src/core/cliUtils.ts +++ b/src/core/cliUtils.ts @@ -6,6 +6,8 @@ import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; +import type { CliAuth } from "../cliConfig"; + /** * Custom error thrown when a binary file is locked (typically on Windows). */ @@ -72,6 +74,34 @@ export async function version(binPath: string): Promise { return json.version; } +/** + * Run a speed test against the specified workspace and return the JSON output. + * Throw if unable to execute the binary or parse the output. + */ +export async function speedtest( + binPath: string, + auth: CliAuth, + workspaceName: string, +): Promise { + const result = await promisify(execFile)(binPath, [ + ...authArgs(auth), + "speedtest", + workspaceName, + "--output", + "json", + ]); + return result.stdout; +} + +/** + * Build CLI auth flags for execFile (no shell escaping). + */ +function authArgs(auth: CliAuth): string[] { + return auth.mode === "url" + ? ["--url", auth.url] + : ["--global-config", auth.configDir]; +} + export interface RemovalResult { fileName: string; error: unknown; diff --git a/src/extension.ts b/src/extension.ts index df2ecd6a..71d8dfc2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -281,6 +281,10 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { void myWorkspacesProvider.fetchAndRefresh(); void allWorkspacesProvider.fetchAndRefresh(); }), + vscode.commands.registerCommand( + "coder.speedTest", + commands.speedTest.bind(commands), + ), vscode.commands.registerCommand( "coder.viewLogs", commands.viewLogs.bind(commands), From d167396491b16063464f3553cd112498dcab7fc5 Mon Sep 17 00:00:00 2001 From: Yazan Amer Date: Fri, 27 Mar 2026 16:55:31 +0300 Subject: [PATCH 2/2] feat: #750 --- test/fixtures/scripts/echo-args.bash | 6 +++ test/unit/core/cliUtils.test.ts | 57 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 test/fixtures/scripts/echo-args.bash diff --git a/test/fixtures/scripts/echo-args.bash b/test/fixtures/scripts/echo-args.bash new file mode 100644 index 00000000..5875d319 --- /dev/null +++ b/test/fixtures/scripts/echo-args.bash @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# Prints each argument on its own line, so tests can verify exact args. +for arg in "$@"; do + echo "$arg" +done diff --git a/test/unit/core/cliUtils.test.ts b/test/unit/core/cliUtils.test.ts index dd1c56f0..6bb8038d 100644 --- a/test/unit/core/cliUtils.test.ts +++ b/test/unit/core/cliUtils.test.ts @@ -142,6 +142,63 @@ describe("CliUtils", () => { ]); }); + describe("speedtest", () => { + const echoArgsBin = path.join(tmp, "echo-args"); + + beforeAll(async () => { + const tmpl = await fs.readFile( + getFixturePath("scripts", "echo-args.bash"), + "utf8", + ); + await fs.writeFile(echoArgsBin, tmpl); + await fs.chmod(echoArgsBin, "755"); + }); + + it("passes global-config auth flags", async () => { + const result = await cliUtils.speedtest( + echoArgsBin, + { mode: "global-config", configDir: "/tmp/test-config" }, + "owner/workspace", + ); + const args = result.trim().split("\n"); + expect(args).toEqual([ + "--global-config", + "/tmp/test-config", + "speedtest", + "owner/workspace", + "--output", + "json", + ]); + }); + + it("passes url auth flags", async () => { + const result = await cliUtils.speedtest( + echoArgsBin, + { mode: "url", url: "http://localhost:3000" }, + "owner/workspace", + ); + const args = result.trim().split("\n"); + expect(args).toEqual([ + "--url", + "http://localhost:3000", + "speedtest", + "owner/workspace", + "--output", + "json", + ]); + }); + + it("throws when binary does not exist", async () => { + await expect( + cliUtils.speedtest( + "/nonexistent/binary", + { mode: "global-config", configDir: "/tmp" }, + "owner/workspace", + ), + ).rejects.toThrow("ENOENT"); + }); + }); + it("ETag", async () => { const binPath = path.join(tmp, "hash");