Skip to content

Commit eed8e6c

Browse files
feat(vscode-web): enhance settings management and testing for VS Code Web (#758)
This pull request enhances the VS Code Web module by improving how machine settings are handled and merged, updating documentation to clarify the settings behavior, and adding robust automated tests for the new functionality. The most significant changes are grouped below. **Machine Settings Handling and Merging:** * Introduced a new `merge_settings` function in `run.sh` that merges provided settings with any existing machine settings using `jq` or `python3` if available, falling back gracefully if neither is present. Settings are now passed as base64-encoded JSON to avoid quoting issues. [[1]](diffhunk://#diff-c6d09ac3d801a2417c0e3cf8c2cd0f093ba2cf245bad8c213f70115c75276323R7-R54) [[2]](diffhunk://#diff-c6d09ac3d801a2417c0e3cf8c2cd0f093ba2cf245bad8c213f70115c75276323L31-R76) [[3]](diffhunk://#diff-0c7f0791e2c2556eb4ed7666ac44534ea3ff5c7f652e01716e5d7b5c31180d92L180-R184) [[4]](diffhunk://#diff-0c7f0791e2c2556eb4ed7666ac44534ea3ff5c7f652e01716e5d7b5c31180d92R170-R173) * Updated the `settings` variable in `main.tf` to clarify that it applies to VS Code Web's Machine settings and will be merged with any existing settings on startup. **Documentation Improvements:** * Updated the README to clarify that settings are merged with existing machine settings, not simply overwritten, and added a note about the requirements (`jq` or `python3`) and limitations regarding persistence of user settings. [[1]](diffhunk://#diff-24e2e305e46a08f8a30243bdc916241586e4561d97861b4397b14e871f9f085dL54-R56) [[2]](diffhunk://#diff-24e2e305e46a08f8a30243bdc916241586e4561d97861b4397b14e871f9f085dR72-R73) **Automated Testing:** * Expanded `main.test.ts` to include integration tests that verify settings file creation and merging behavior inside a container, as well as improved error handling for invalid configuration combinations. These changes collectively make machine settings management more robust, user-friendly, and well-documented.
1 parent 7b24554 commit eed8e6c

4 files changed

Lines changed: 346 additions & 40 deletions

File tree

registry/coder/modules/vscode-web/README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
1414
module "vscode-web" {
1515
count = data.coder_workspace.me.start_count
1616
source = "registry.coder.com/coder/vscode-web/coder"
17-
version = "1.4.3"
17+
version = "1.5.0"
1818
agent_id = coder_agent.example.id
1919
accept_license = true
2020
}
@@ -30,7 +30,7 @@ module "vscode-web" {
3030
module "vscode-web" {
3131
count = data.coder_workspace.me.start_count
3232
source = "registry.coder.com/coder/vscode-web/coder"
33-
version = "1.4.3"
33+
version = "1.5.0"
3434
agent_id = coder_agent.example.id
3535
install_prefix = "/home/coder/.vscode-web"
3636
folder = "/home/coder"
@@ -44,22 +44,22 @@ module "vscode-web" {
4444
module "vscode-web" {
4545
count = data.coder_workspace.me.start_count
4646
source = "registry.coder.com/coder/vscode-web/coder"
47-
version = "1.4.3"
47+
version = "1.5.0"
4848
agent_id = coder_agent.example.id
4949
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
5050
accept_license = true
5151
}
5252
```
5353

54-
### Pre-configure Settings
54+
### Pre-configure Machine Settings
5555

56-
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file:
56+
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:
5757

5858
```tf
5959
module "vscode-web" {
6060
count = data.coder_workspace.me.start_count
6161
source = "registry.coder.com/coder/vscode-web/coder"
62-
version = "1.4.3"
62+
version = "1.5.0"
6363
agent_id = coder_agent.example.id
6464
extensions = ["dracula-theme.theme-dracula"]
6565
settings = {
@@ -69,6 +69,9 @@ module "vscode-web" {
6969
}
7070
```
7171

72+
> [!WARNING]
73+
> 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.
74+
7275
### Pin a specific VS Code Web version
7376

7477
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).
@@ -77,7 +80,7 @@ By default, this module installs the latest. To pin a specific version, retrieve
7780
module "vscode-web" {
7881
count = data.coder_workspace.me.start_count
7982
source = "registry.coder.com/coder/vscode-web/coder"
80-
version = "1.4.3"
83+
version = "1.5.0"
8184
agent_id = coder_agent.example.id
8285
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
8386
accept_license = true
@@ -93,7 +96,7 @@ Note: Either `workspace` or `folder` can be used, but not both simultaneously. T
9396
module "vscode-web" {
9497
count = data.coder_workspace.me.start_count
9598
source = "registry.coder.com/coder/vscode-web/coder"
96-
version = "1.4.3"
99+
version = "1.5.0"
97100
agent_id = coder_agent.example.id
98101
workspace = "/home/coder/coder.code-workspace"
99102
}
Lines changed: 279 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,298 @@
1-
import { describe, expect, it } from "bun:test";
2-
import { runTerraformApply, runTerraformInit } from "~test";
1+
import {
2+
describe,
3+
expect,
4+
it,
5+
beforeAll,
6+
afterEach,
7+
setDefaultTimeout,
8+
} from "bun:test";
9+
import {
10+
runTerraformApply,
11+
runTerraformInit,
12+
runContainer,
13+
execContainer,
14+
removeContainer,
15+
findResourceInstance,
16+
} from "~test";
17+
18+
// Set timeout to 2 minutes for tests that install packages
19+
setDefaultTimeout(2 * 60 * 1000);
20+
21+
let cleanupContainers: string[] = [];
22+
23+
afterEach(async () => {
24+
for (const id of cleanupContainers) {
25+
try {
26+
await removeContainer(id);
27+
} catch {
28+
// Ignore cleanup errors
29+
}
30+
}
31+
cleanupContainers = [];
32+
});
333

434
describe("vscode-web", async () => {
5-
await runTerraformInit(import.meta.dir);
35+
beforeAll(async () => {
36+
await runTerraformInit(import.meta.dir);
37+
});
638

7-
it("accept_license should be set to true", () => {
8-
const t = async () => {
39+
it("accept_license should be set to true", async () => {
40+
try {
941
await runTerraformApply(import.meta.dir, {
1042
agent_id: "foo",
11-
accept_license: "false",
43+
accept_license: false,
1244
});
13-
};
14-
expect(t).toThrow("Invalid value for variable");
45+
throw new Error("Expected terraform apply to fail");
46+
} catch (ex) {
47+
expect((ex as Error).message).toContain("Invalid value for variable");
48+
}
1549
});
1650

17-
it("use_cached and offline can not be used together", () => {
18-
const t = async () => {
51+
it("use_cached and offline can not be used together", async () => {
52+
try {
1953
await runTerraformApply(import.meta.dir, {
2054
agent_id: "foo",
21-
accept_license: "true",
22-
use_cached: "true",
23-
offline: "true",
55+
accept_license: true,
56+
use_cached: true,
57+
offline: true,
2458
});
25-
};
26-
expect(t).toThrow("Offline and Use Cached can not be used together");
59+
throw new Error("Expected terraform apply to fail");
60+
} catch (ex) {
61+
expect((ex as Error).message).toContain(
62+
"Offline and Use Cached can not be used together",
63+
);
64+
}
2765
});
2866

29-
it("offline and extensions can not be used together", () => {
30-
const t = async () => {
67+
it("offline and extensions can not be used together", async () => {
68+
try {
3169
await runTerraformApply(import.meta.dir, {
3270
agent_id: "foo",
33-
accept_license: "true",
34-
offline: "true",
35-
extensions: '["1", "2"]',
71+
accept_license: true,
72+
offline: true,
73+
extensions: '["ms-python.python"]',
3674
});
37-
};
38-
expect(t).toThrow("Offline mode does not allow extensions to be installed");
75+
throw new Error("Expected terraform apply to fail");
76+
} catch (ex) {
77+
expect((ex as Error).message).toContain(
78+
"Offline mode does not allow extensions to be installed",
79+
);
80+
}
81+
});
82+
83+
it("creates settings file with correct content", async () => {
84+
const state = await runTerraformApply(import.meta.dir, {
85+
agent_id: "foo",
86+
accept_license: true,
87+
use_cached: true,
88+
settings: '{"editor.fontSize": 14}',
89+
});
90+
91+
const containerId = await runContainer("ubuntu:22.04");
92+
cleanupContainers.push(containerId);
93+
94+
// Create a mock code-server CLI that the script expects
95+
await execContainer(containerId, [
96+
"bash",
97+
"-c",
98+
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF'
99+
#!/bin/bash
100+
echo "Mock code-server running"
101+
exit 0
102+
MOCKEOF
103+
chmod +x /tmp/vscode-web/bin/code-server`,
104+
]);
105+
106+
const script = findResourceInstance(state, "coder_script");
107+
108+
const scriptResult = await execContainer(containerId, [
109+
"bash",
110+
"-c",
111+
script.script,
112+
]);
113+
expect(scriptResult.exitCode).toBe(0);
114+
115+
// Check that settings file was created
116+
const settingsResult = await execContainer(containerId, [
117+
"cat",
118+
"/root/.vscode-server/data/Machine/settings.json",
119+
]);
120+
121+
expect(settingsResult.exitCode).toBe(0);
122+
expect(settingsResult.stdout).toContain("editor.fontSize");
123+
expect(settingsResult.stdout).toContain("14");
124+
});
125+
126+
it("merges settings with existing settings file", async () => {
127+
const state = await runTerraformApply(import.meta.dir, {
128+
agent_id: "foo",
129+
accept_license: true,
130+
use_cached: true,
131+
settings: '{"new.setting": "new_value"}',
132+
});
133+
134+
const containerId = await runContainer("ubuntu:22.04");
135+
cleanupContainers.push(containerId);
136+
137+
// Install jq and create mock code-server CLI
138+
await execContainer(containerId, ["apt-get", "update", "-qq"]);
139+
await execContainer(containerId, ["apt-get", "install", "-y", "-qq", "jq"]);
140+
await execContainer(containerId, [
141+
"bash",
142+
"-c",
143+
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF'
144+
#!/bin/bash
145+
echo "Mock code-server running"
146+
exit 0
147+
MOCKEOF
148+
chmod +x /tmp/vscode-web/bin/code-server`,
149+
]);
150+
151+
// Pre-create an existing settings file
152+
await execContainer(containerId, [
153+
"bash",
154+
"-c",
155+
`mkdir -p /root/.vscode-server/data/Machine && echo '{"existing.setting": "existing_value"}' > /root/.vscode-server/data/Machine/settings.json`,
156+
]);
157+
158+
const script = findResourceInstance(state, "coder_script");
159+
160+
const scriptResult = await execContainer(containerId, [
161+
"bash",
162+
"-c",
163+
script.script,
164+
]);
165+
expect(scriptResult.exitCode).toBe(0);
166+
167+
// Check that settings were merged (both existing and new should be present)
168+
const settingsResult = await execContainer(containerId, [
169+
"cat",
170+
"/root/.vscode-server/data/Machine/settings.json",
171+
]);
172+
173+
expect(settingsResult.exitCode).toBe(0);
174+
// Should contain both existing and new settings
175+
expect(settingsResult.stdout).toContain("existing.setting");
176+
expect(settingsResult.stdout).toContain("existing_value");
177+
expect(settingsResult.stdout).toContain("new.setting");
178+
expect(settingsResult.stdout).toContain("new_value");
179+
});
180+
181+
it("merges settings using python3 fallback when jq unavailable", async () => {
182+
const state = await runTerraformApply(import.meta.dir, {
183+
agent_id: "foo",
184+
accept_license: true,
185+
use_cached: true,
186+
settings: '{"new.setting": "new_value"}',
187+
});
188+
189+
const containerId = await runContainer("ubuntu:22.04");
190+
cleanupContainers.push(containerId);
191+
192+
// Install python3 (ubuntu:22.04 doesn't have it by default)
193+
await execContainer(containerId, ["apt-get", "update", "-qq"]);
194+
await execContainer(containerId, [
195+
"apt-get",
196+
"install",
197+
"-y",
198+
"-qq",
199+
"python3",
200+
]);
201+
202+
// Create mock code-server CLI (no jq installed)
203+
await execContainer(containerId, [
204+
"bash",
205+
"-c",
206+
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF'
207+
#!/bin/bash
208+
echo "Mock code-server running"
209+
exit 0
210+
MOCKEOF
211+
chmod +x /tmp/vscode-web/bin/code-server`,
212+
]);
213+
214+
// Pre-create an existing settings file
215+
await execContainer(containerId, [
216+
"bash",
217+
"-c",
218+
`mkdir -p /root/.vscode-server/data/Machine && echo '{"existing.setting": "existing_value"}' > /root/.vscode-server/data/Machine/settings.json`,
219+
]);
220+
221+
const script = findResourceInstance(state, "coder_script");
222+
223+
const scriptResult = await execContainer(containerId, [
224+
"bash",
225+
"-c",
226+
script.script,
227+
]);
228+
expect(scriptResult.exitCode).toBe(0);
229+
230+
// Check that settings were merged using python3 fallback
231+
const settingsResult = await execContainer(containerId, [
232+
"cat",
233+
"/root/.vscode-server/data/Machine/settings.json",
234+
]);
235+
236+
expect(settingsResult.exitCode).toBe(0);
237+
// Should contain both existing and new settings
238+
expect(settingsResult.stdout).toContain("existing.setting");
239+
expect(settingsResult.stdout).toContain("existing_value");
240+
expect(settingsResult.stdout).toContain("new.setting");
241+
expect(settingsResult.stdout).toContain("new_value");
39242
});
40243

41-
// More tests depend on shebang refactors
244+
it("preserves existing settings when neither jq nor python3 available", async () => {
245+
const state = await runTerraformApply(import.meta.dir, {
246+
agent_id: "foo",
247+
accept_license: true,
248+
use_cached: true,
249+
settings: '{"new.setting": "new_value"}',
250+
});
251+
252+
// Use ubuntu without installing jq or python3 (neither available by default)
253+
const containerId = await runContainer("ubuntu:22.04");
254+
cleanupContainers.push(containerId);
255+
256+
// Create mock code-server CLI
257+
await execContainer(containerId, [
258+
"bash",
259+
"-c",
260+
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code-server << 'MOCKEOF'
261+
#!/bin/bash
262+
echo "Mock code-server running"
263+
exit 0
264+
MOCKEOF
265+
chmod +x /tmp/vscode-web/bin/code-server`,
266+
]);
267+
268+
// Pre-create an existing settings file
269+
await execContainer(containerId, [
270+
"bash",
271+
"-c",
272+
`mkdir -p /root/.vscode-server/data/Machine && echo '{"existing.setting": "existing_value"}' > /root/.vscode-server/data/Machine/settings.json`,
273+
]);
274+
275+
const script = findResourceInstance(state, "coder_script");
276+
277+
// Run script - should warn but not fail
278+
const scriptResult = await execContainer(containerId, [
279+
"bash",
280+
"-c",
281+
script.script,
282+
]);
283+
expect(scriptResult.exitCode).toBe(0);
284+
expect(scriptResult.stdout).toContain("Could not merge settings");
285+
286+
// Existing settings should be preserved (not overwritten)
287+
const settingsResult = await execContainer(containerId, [
288+
"cat",
289+
"/root/.vscode-server/data/Machine/settings.json",
290+
]);
291+
292+
expect(settingsResult.exitCode).toBe(0);
293+
expect(settingsResult.stdout).toContain("existing.setting");
294+
expect(settingsResult.stdout).toContain("existing_value");
295+
expect(settingsResult.stdout).not.toContain("new.setting");
296+
expect(settingsResult.stdout).not.toContain("new_value");
297+
});
42298
});

0 commit comments

Comments
 (0)