From 3b99ca7baf9e36a52b7bd6eada405313d7c456d8 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 25 Feb 2026 09:18:14 -0600 Subject: [PATCH 1/4] feat(vscode-web): enhance settings management and testing for VS Code Web module - Added functionality to merge new settings with existing machine settings during startup. - Updated tests to validate settings merging and error handling for invalid configurations. - Improved test structure with before/after hooks for better resource management. - Enhanced README to clarify settings merging requirements and behavior. --- registry/coder/modules/vscode-web/README.md | 6 +- .../coder/modules/vscode-web/main.test.ts | 170 +++++++++++++++--- registry/coder/modules/vscode-web/main.tf | 9 +- registry/coder/modules/vscode-web/run.sh | 53 +++++- 4 files changed, 204 insertions(+), 34 deletions(-) diff --git a/registry/coder/modules/vscode-web/README.md b/registry/coder/modules/vscode-web/README.md index 43b1eb9d0..8c0a83ece 100644 --- a/registry/coder/modules/vscode-web/README.md +++ b/registry/coder/modules/vscode-web/README.md @@ -51,9 +51,9 @@ module "vscode-web" { } ``` -### Pre-configure Settings +### Pre-configure Machine Settings -Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file: +Configure VS Code's [Machine settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file). These settings are merged with any existing machine settings on startup: ```tf module "vscode-web" { @@ -69,6 +69,8 @@ module "vscode-web" { } ``` +> **Note:** Merging settings requires `jq` or `python3`. If neither is available, existing machine settings will be preserved. User settings configured through the VS Code UI are stored in browser local storage and will not persist across different browsers or devices. + ### Pin a specific VS Code Web version By default, this module installs the latest. To pin a specific version, retrieve the commit ID from the [VS Code Update API](https://update.code.visualstudio.com/api/commits/stable/server-linux-x64-web) and verify its corresponding release on the [VS Code GitHub Releases](https://github.com/microsoft/vscode/releases). diff --git a/registry/coder/modules/vscode-web/main.test.ts b/registry/coder/modules/vscode-web/main.test.ts index 860fc176c..e02fe3c36 100644 --- a/registry/coder/modules/vscode-web/main.test.ts +++ b/registry/coder/modules/vscode-web/main.test.ts @@ -1,42 +1,166 @@ -import { describe, expect, it } from "bun:test"; -import { runTerraformApply, runTerraformInit } from "~test"; +import { + describe, + expect, + it, + beforeAll, + afterEach, + setDefaultTimeout, +} from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + runContainer, + execContainer, + removeContainer, + findResourceInstance, +} from "~test"; + +// Set timeout to 2 minutes for tests that install packages +setDefaultTimeout(2 * 60 * 1000); + +let cleanupContainers: string[] = []; + +afterEach(async () => { + for (const id of cleanupContainers) { + try { + await removeContainer(id); + } catch { + // Ignore cleanup errors + } + } + cleanupContainers = []; +}); describe("vscode-web", async () => { - await runTerraformInit(import.meta.dir); + beforeAll(async () => { + await runTerraformInit(import.meta.dir); + }); - it("accept_license should be set to true", () => { - const t = async () => { + it("accept_license should be set to true", async () => { + try { await runTerraformApply(import.meta.dir, { agent_id: "foo", - accept_license: "false", + accept_license: false, }); - }; - expect(t).toThrow("Invalid value for variable"); + throw new Error("Expected terraform apply to fail"); + } catch (ex) { + expect((ex as Error).message).toContain("Invalid value for variable"); + } }); - it("use_cached and offline can not be used together", () => { - const t = async () => { + it("use_cached and offline can not be used together", async () => { + try { await runTerraformApply(import.meta.dir, { agent_id: "foo", - accept_license: "true", - use_cached: "true", - offline: "true", + accept_license: true, + use_cached: true, + offline: true, }); - }; - expect(t).toThrow("Offline and Use Cached can not be used together"); + throw new Error("Expected terraform apply to fail"); + } catch (ex) { + expect((ex as Error).message).toContain( + "Offline and Use Cached can not be used together", + ); + } }); - it("offline and extensions can not be used together", () => { - const t = async () => { + it("offline and extensions can not be used together", async () => { + try { await runTerraformApply(import.meta.dir, { agent_id: "foo", - accept_license: "true", - offline: "true", - extensions: '["1", "2"]', + accept_license: true, + offline: true, + extensions: '["ms-python.python"]', }); - }; - expect(t).toThrow("Offline mode does not allow extensions to be installed"); + throw new Error("Expected terraform apply to fail"); + } catch (ex) { + expect((ex as Error).message).toContain( + "Offline mode does not allow extensions to be installed", + ); + } }); - // More tests depend on shebang refactors + it("creates settings file with correct content", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + accept_license: true, + settings: '{"editor.fontSize": 14}', + }); + + const containerId = await runContainer("ubuntu:22.04"); + cleanupContainers.push(containerId); + + // Create a mock code-server CLI + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF' +#!/bin/bash +exit 0 +MOCKEOF +chmod +x /tmp/vscode-web/bin/code-server`, + ]); + + const script = findResourceInstance(state, "coder_script"); + + await execContainer(containerId, ["bash", "-c", script.script]); + + // Check that settings file was created + const settingsResult = await execContainer(containerId, [ + "cat", + "/root/.vscode-server/data/Machine/settings.json", + ]); + + expect(settingsResult.exitCode).toBe(0); + expect(settingsResult.stdout).toContain("editor.fontSize"); + expect(settingsResult.stdout).toContain("14"); + }); + + it("merges settings with existing settings file", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + accept_license: true, + settings: '{"new.setting": "new_value"}', + }); + + const containerId = await runContainer("ubuntu:22.04"); + cleanupContainers.push(containerId); + + // Install jq and create mock code-server CLI + await execContainer(containerId, ["apt-get", "update", "-qq"]); + await execContainer(containerId, ["apt-get", "install", "-y", "-qq", "jq"]); + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF' +#!/bin/bash +exit 0 +MOCKEOF +chmod +x /tmp/vscode-web/bin/code-server`, + ]); + + // Pre-create an existing settings file + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /root/.vscode-server/data/Machine && echo '{"existing.setting": "existing_value"}' > /root/.vscode-server/data/Machine/settings.json`, + ]); + + const script = findResourceInstance(state, "coder_script"); + + await execContainer(containerId, ["bash", "-c", script.script]); + + // Check that settings were merged (both existing and new should be present) + const settingsResult = await execContainer(containerId, [ + "cat", + "/root/.vscode-server/data/Machine/settings.json", + ]); + + expect(settingsResult.exitCode).toBe(0); + // Should contain both existing and new settings + expect(settingsResult.stdout).toContain("existing.setting"); + expect(settingsResult.stdout).toContain("existing_value"); + expect(settingsResult.stdout).toContain("new.setting"); + expect(settingsResult.stdout).toContain("new_value"); + }); }); diff --git a/registry/coder/modules/vscode-web/main.tf b/registry/coder/modules/vscode-web/main.tf index 7a2029c87..ff86e455f 100644 --- a/registry/coder/modules/vscode-web/main.tf +++ b/registry/coder/modules/vscode-web/main.tf @@ -105,7 +105,7 @@ variable "group" { variable "settings" { type = any - description = "A map of settings to apply to VS Code web." + description = "A map of settings to apply to VS Code Web's Machine settings. These settings are merged with any existing machine settings on startup." default = {} } @@ -167,6 +167,10 @@ variable "workspace" { data "coder_workspace_owner" "me" {} data "coder_workspace" "me" {} +locals { + settings_b64 = var.settings != {} ? base64encode(jsonencode(var.settings)) : "" +} + resource "coder_script" "vscode-web" { agent_id = var.agent_id display_name = "VS Code Web" @@ -177,8 +181,7 @@ resource "coder_script" "vscode-web" { INSTALL_PREFIX : var.install_prefix, EXTENSIONS : join(",", var.extensions), TELEMETRY_LEVEL : var.telemetry_level, - // This is necessary otherwise the quotes are stripped! - SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""), + SETTINGS_B64 : local.settings_b64, OFFLINE : var.offline, USE_CACHED : var.use_cached, DISABLE_TRUST : var.disable_trust, diff --git a/registry/coder/modules/vscode-web/run.sh b/registry/coder/modules/vscode-web/run.sh index 57bb760f9..fbb584c0d 100644 --- a/registry/coder/modules/vscode-web/run.sh +++ b/registry/coder/modules/vscode-web/run.sh @@ -4,13 +4,54 @@ BOLD='\033[0;1m' EXTENSIONS=("${EXTENSIONS}") VSCODE_WEB="${INSTALL_PREFIX}/bin/code-server" +# Merge settings from module with existing settings file +# Uses jq if available, falls back to Python3 for deep merge +merge_settings() { + local new_settings="$1" + local settings_file="$2" + + if [ -z "$new_settings" ] || [ "$new_settings" = "{}" ]; then + return 0 + fi + + if [ ! -f "$settings_file" ]; then + mkdir -p "$(dirname "$settings_file")" + printf '%s\n' "$new_settings" > "$settings_file" + printf "⚙️ Creating settings file...\n" + return 0 + fi + + local tmpfile + tmpfile="$(mktemp)" + + if command -v jq > /dev/null 2>&1; then + if jq -s '.[0] * .[1]' "$settings_file" <(printf '%s\n' "$new_settings") > "$tmpfile" 2> /dev/null; then + mv "$tmpfile" "$settings_file" + printf "⚙️ Merging settings...\n" + return 0 + fi + fi + + if command -v python3 > /dev/null 2>&1; then + if python3 -c "import json,sys;m=lambda a,b:{**a,**{k:m(a[k],v)if k in a and type(a[k])==type(v)==dict else v for k,v in b.items()}};print(json.dumps(m(json.load(open(sys.argv[1])),json.loads(sys.argv[2])),indent=2))" "$settings_file" "$new_settings" > "$tmpfile" 2> /dev/null; then + mv "$tmpfile" "$settings_file" + printf "⚙️ Merging settings...\n" + return 0 + fi + fi + + rm -f "$tmpfile" + printf "Warning: Could not merge settings (jq or python3 required). Keeping existing settings.\n" + return 0 +} + # Set extension directory EXTENSION_ARG="" if [ -n "${EXTENSIONS_DIR}" ]; then EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}" fi -# Set extension directory +# Set server base path SERVER_BASE_PATH_ARG="" if [ -n "${SERVER_BASE_PATH}" ]; then SERVER_BASE_PATH_ARG="--server-base-path=${SERVER_BASE_PATH}" @@ -28,11 +69,11 @@ run_vscode_web() { "$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" "$DISABLE_TRUST_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 & } -# Check if the settings file exists... -if [ ! -f ~/.vscode-server/data/Machine/settings.json ]; then - echo "⚙️ Creating settings file..." - mkdir -p ~/.vscode-server/data/Machine - echo "${SETTINGS}" > ~/.vscode-server/data/Machine/settings.json +# Apply machine settings (merge with existing if present) +SETTINGS_B64='${SETTINGS_B64}' +if [ -n "$SETTINGS_B64" ]; then + SETTINGS_JSON="$(echo -n "$SETTINGS_B64" | base64 -d)" + merge_settings "$SETTINGS_JSON" ~/.vscode-server/data/Machine/settings.json fi # Check if vscode-server is already installed for offline or cached mode From 19cf6ba573a2748b664285987702ec5c4ca1e8e9 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 25 Feb 2026 10:29:07 -0600 Subject: [PATCH 2/4] feat(vscode-web): improve test execution and mock setup for code-server --- .../coder/modules/vscode-web/main.test.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/registry/coder/modules/vscode-web/main.test.ts b/registry/coder/modules/vscode-web/main.test.ts index e02fe3c36..9c63a8361 100644 --- a/registry/coder/modules/vscode-web/main.test.ts +++ b/registry/coder/modules/vscode-web/main.test.ts @@ -84,18 +84,20 @@ describe("vscode-web", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", accept_license: true, + use_cached: true, settings: '{"editor.fontSize": 14}', }); const containerId = await runContainer("ubuntu:22.04"); cleanupContainers.push(containerId); - // Create a mock code-server CLI + // Create a mock code-server CLI that the script expects await execContainer(containerId, [ "bash", "-c", `mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF' #!/bin/bash +echo "Mock code-server running" exit 0 MOCKEOF chmod +x /tmp/vscode-web/bin/code-server`, @@ -103,7 +105,12 @@ chmod +x /tmp/vscode-web/bin/code-server`, const script = findResourceInstance(state, "coder_script"); - await execContainer(containerId, ["bash", "-c", script.script]); + const scriptResult = await execContainer(containerId, [ + "bash", + "-c", + script.script, + ]); + expect(scriptResult.exitCode).toBe(0); // Check that settings file was created const settingsResult = await execContainer(containerId, [ @@ -120,6 +127,7 @@ chmod +x /tmp/vscode-web/bin/code-server`, const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", accept_license: true, + use_cached: true, settings: '{"new.setting": "new_value"}', }); @@ -134,6 +142,7 @@ chmod +x /tmp/vscode-web/bin/code-server`, "-c", `mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF' #!/bin/bash +echo "Mock code-server running" exit 0 MOCKEOF chmod +x /tmp/vscode-web/bin/code-server`, @@ -148,7 +157,12 @@ chmod +x /tmp/vscode-web/bin/code-server`, const script = findResourceInstance(state, "coder_script"); - await execContainer(containerId, ["bash", "-c", script.script]); + const scriptResult = await execContainer(containerId, [ + "bash", + "-c", + script.script, + ]); + expect(scriptResult.exitCode).toBe(0); // Check that settings were merged (both existing and new should be present) const settingsResult = await execContainer(containerId, [ From c44e9b72f592c4f0fa307dce2f0c2559a4a018bd Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Wed, 25 Feb 2026 12:10:03 -0600 Subject: [PATCH 3/4] feat(vscode-web): add warn message when merging fails and add tests for fallback scenarios - Added tests to verify settings merging behavior under different conditions, including when neither jq nor Python3 is installed. - Updated run.sh to handle settings decoding errors gracefully, providing warnings when settings cannot be applied. --- .../coder/modules/vscode-web/main.test.ts | 118 ++++++++++++++++++ registry/coder/modules/vscode-web/run.sh | 7 +- 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/vscode-web/main.test.ts b/registry/coder/modules/vscode-web/main.test.ts index 9c63a8361..96c787c86 100644 --- a/registry/coder/modules/vscode-web/main.test.ts +++ b/registry/coder/modules/vscode-web/main.test.ts @@ -177,4 +177,122 @@ chmod +x /tmp/vscode-web/bin/code-server`, expect(settingsResult.stdout).toContain("new.setting"); expect(settingsResult.stdout).toContain("new_value"); }); + + it("merges settings using python3 fallback when jq unavailable", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + accept_license: true, + use_cached: true, + settings: '{"new.setting": "new_value"}', + }); + + const containerId = await runContainer("ubuntu:22.04"); + cleanupContainers.push(containerId); + + // Install python3 (ubuntu:22.04 doesn't have it by default) + await execContainer(containerId, ["apt-get", "update", "-qq"]); + await execContainer(containerId, [ + "apt-get", + "install", + "-y", + "-qq", + "python3", + ]); + + // Create mock code-server CLI (no jq installed) + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF' +#!/bin/bash +echo "Mock code-server running" +exit 0 +MOCKEOF +chmod +x /tmp/vscode-web/bin/code-server`, + ]); + + // Pre-create an existing settings file + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /root/.vscode-server/data/Machine && echo '{"existing.setting": "existing_value"}' > /root/.vscode-server/data/Machine/settings.json`, + ]); + + const script = findResourceInstance(state, "coder_script"); + + const scriptResult = await execContainer(containerId, [ + "bash", + "-c", + script.script, + ]); + expect(scriptResult.exitCode).toBe(0); + + // Check that settings were merged using python3 fallback + const settingsResult = await execContainer(containerId, [ + "cat", + "/root/.vscode-server/data/Machine/settings.json", + ]); + + expect(settingsResult.exitCode).toBe(0); + // Should contain both existing and new settings + expect(settingsResult.stdout).toContain("existing.setting"); + expect(settingsResult.stdout).toContain("existing_value"); + expect(settingsResult.stdout).toContain("new.setting"); + expect(settingsResult.stdout).toContain("new_value"); + }); + + it("preserves existing settings when neither jq nor python3 available", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + accept_license: true, + use_cached: true, + settings: '{"new.setting": "new_value"}', + }); + + // Use ubuntu without installing jq or python3 (neither available by default) + const containerId = await runContainer("ubuntu:22.04"); + cleanupContainers.push(containerId); + + // Create mock code-server CLI + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF' +#!/bin/bash +echo "Mock code-server running" +exit 0 +MOCKEOF +chmod +x /tmp/vscode-web/bin/code-server`, + ]); + + // Pre-create an existing settings file + await execContainer(containerId, [ + "bash", + "-c", + `mkdir -p /root/.vscode-server/data/Machine && echo '{"existing.setting": "existing_value"}' > /root/.vscode-server/data/Machine/settings.json`, + ]); + + const script = findResourceInstance(state, "coder_script"); + + // Run script - should warn but not fail + const scriptResult = await execContainer(containerId, [ + "bash", + "-c", + script.script, + ]); + expect(scriptResult.exitCode).toBe(0); + expect(scriptResult.stdout).toContain("Could not merge settings"); + + // Existing settings should be preserved (not overwritten) + const settingsResult = await execContainer(containerId, [ + "cat", + "/root/.vscode-server/data/Machine/settings.json", + ]); + + expect(settingsResult.exitCode).toBe(0); + expect(settingsResult.stdout).toContain("existing.setting"); + expect(settingsResult.stdout).toContain("existing_value"); + expect(settingsResult.stdout).not.toContain("new.setting"); + expect(settingsResult.stdout).not.toContain("new_value"); + }); }); diff --git a/registry/coder/modules/vscode-web/run.sh b/registry/coder/modules/vscode-web/run.sh index fbb584c0d..dea8e5853 100644 --- a/registry/coder/modules/vscode-web/run.sh +++ b/registry/coder/modules/vscode-web/run.sh @@ -72,8 +72,11 @@ run_vscode_web() { # Apply machine settings (merge with existing if present) SETTINGS_B64='${SETTINGS_B64}' if [ -n "$SETTINGS_B64" ]; then - SETTINGS_JSON="$(echo -n "$SETTINGS_B64" | base64 -d)" - merge_settings "$SETTINGS_JSON" ~/.vscode-server/data/Machine/settings.json + if SETTINGS_JSON="$(echo -n "$SETTINGS_B64" | base64 -d 2> /dev/null)" && [ -n "$SETTINGS_JSON" ]; then + merge_settings "$SETTINGS_JSON" ~/.vscode-server/data/Machine/settings.json + else + printf "Warning: Failed to decode settings. Skipping settings configuration.\n" + fi fi # Check if vscode-server is already installed for offline or cached mode From 53fe87ec0f37ac8a57fa2f7ed0e97ea4fd99ce45 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 27 Feb 2026 14:04:57 -0600 Subject: [PATCH 4/4] chore(vscode-web): update version to 1.5.0 and change Note to GFM Warning --- registry/coder/modules/vscode-web/README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/registry/coder/modules/vscode-web/README.md b/registry/coder/modules/vscode-web/README.md index 8c0a83ece..35cad1adb 100644 --- a/registry/coder/modules/vscode-web/README.md +++ b/registry/coder/modules/vscode-web/README.md @@ -14,7 +14,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/ module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.4.3" + version = "1.5.0" agent_id = coder_agent.example.id accept_license = true } @@ -30,7 +30,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.4.3" + version = "1.5.0" agent_id = coder_agent.example.id install_prefix = "/home/coder/.vscode-web" folder = "/home/coder" @@ -44,7 +44,7 @@ module "vscode-web" { module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.4.3" + version = "1.5.0" agent_id = coder_agent.example.id extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"] accept_license = true @@ -59,7 +59,7 @@ Configure VS Code's [Machine settings.json](https://code.visualstudio.com/docs/g module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.4.3" + version = "1.5.0" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -69,7 +69,8 @@ module "vscode-web" { } ``` -> **Note:** Merging settings requires `jq` or `python3`. If neither is available, existing machine settings will be preserved. User settings configured through the VS Code UI are stored in browser local storage and will not persist across different browsers or devices. +> [!WARNING] +> Merging settings requires `jq` or `python3`. If neither is available, existing machine settings will be preserved. User settings configured through the VS Code UI are stored in browser local storage and will not persist across different browsers or devices. ### Pin a specific VS Code Web version @@ -79,7 +80,7 @@ By default, this module installs the latest. To pin a specific version, retrieve module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.4.3" + version = "1.5.0" agent_id = coder_agent.example.id commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447" accept_license = true @@ -95,7 +96,7 @@ Note: Either `workspace` or `folder` can be used, but not both simultaneously. T module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" - version = "1.4.3" + version = "1.5.0" agent_id = coder_agent.example.id workspace = "/home/coder/coder.code-workspace" }