Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/build-extensions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,69 @@ jobs:
name: vsix
path: src/vscode-*-extension/*.vsix

vscode-e2e:
# Deliberately a separate job from `extensions`, not a step inside it.
# This one is the only thing in CI that downloads a VS Code build at run
# time, so it is also the only thing that can fail for reasons that have
# nothing to do with the code (update.code.visualstudio.com being down, a
# slow CDN, a runner with no egress). Keeping it separate means such a
# failure shows up as "vscode-e2e red, extensions green" instead of
# masking — or being mistaken for — a real unit-test regression.
name: VS Code E2E (spike)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"

- name: Install dependencies
run: npm ci

# esbuild/vitest never typecheck; the E2E sources sit outside every
# workspace tsconfig, so they need their own pass like the others do.
- name: Typecheck E2E suite
run: npm run typecheck:e2e

# The suite loads the extensions' real bundles via
# --extensionDevelopmentPath, so `dist/extension.js` has to exist for
# both. Never the production build: the remote-gate seam the suite
# depends on is only honored outside ExtensionMode.Production, and a
# minified bundle makes failures unreadable for no benefit here.
- name: Build UI Extension
run: npm run build -w src/vscode-ui-extension

- name: Build Workspace Extension
run: npm run build -w src/vscode-workspace-extension

- name: Build E2E suite
run: npm run build:e2e

- name: Generate test certificate fixture
run: npm run gen:test-cert

# ~200ms, and it runs BEFORE the VS Code download on purpose. A bundle
# that throws during module init (a missing reflect-metadata polyfill is
# the one that already bit us) fails identically inside the extension
# host — but only after npm ci, two builds and a ~110MB download. Cheapest
# failure, earliest.
- name: Check E2E suite bundle loads
run: npm run check:e2e-load

# VS Code is an Electron app and needs an X server even with
# --disable-gpu. The hosted ubuntu images ship xvfb, but installing it
# explicitly costs a few seconds and removes a dependency on the runner
# image's contents staying the way they are.
- name: Install xvfb
run: sudo apt-get update -y && sudo apt-get install -y --no-install-recommends xvfb

# `xvfb-run -a` picks a free display number. DEVCERTS_E2E_VSCODE_VERSION
# pins a specific VS Code build if `stable` ever regresses; unset means
# stable, which is what we want CI tracking by default.
- name: Run VS Code E2E suite
run: xvfb-run -a npm run test:e2e

windows-certificate-validation:
name: Windows Certificate Validation
runs-on: windows-latest
Expand Down
33 changes: 33 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,39 @@ These decisions were made deliberately. Do not change them without discussion.
- **Extension testing**: F5 launches an Extension Development Host. The `build-extensions` task hydrates a test project at `.out/test-project/` from the template at `test/sample-project/`. The workspace extension VSIX is staged in `.out/test-project/.devcontainer/` and referenced via `${containerWorkspaceFolder}` in `customizations.vscode.extensions`.
- **The `trust` operation generates the cert if it doesn't exist.** This is intentional — it's the single entry point for provisioning.

### VS Code E2E harness (`test/vscode-e2e/`) — spike status

`@vscode/test-electron` launches a real VS Code with **both** extensions loaded into one window (`--extensionDevelopmentPath` twice) and runs the suite in the extension host. Scripts, from the repo root:

```
npm run typecheck:e2e # tsc pass; esbuild and the runner never typecheck
npm run build:e2e # bundles the suite to .out/vscode-e2e/suite.cjs
npm run check:e2e-load # ~200ms: does the bundle even load? (see below)
npm run test:e2e # on Linux: xvfb-run -a npm run test:e2e
```

**Run `check:e2e-load` before `test:e2e`, and keep it ahead of the download step in CI.** It stubs `vscode` and `require()`s the built bundle, which is where module-init bugs surface. That is not hypothetical: the suite bundles the shared package, which pulls in `@peculiar/x509` → tsyringe, and a missing `import "reflect-metadata"` in the entry point threw inside the extension host before a single test ran. The load check reproduces that in ~200ms; discovering it the other way costs `npm ci`, two extension builds, a ~110MB VS Code download and an Electron launch. If you add an import to the suite that reaches new runtime machinery, this is the check that tells you cheaply.

It needs `dist/extension.js` for both extensions and the `.out/test-fixtures/` cert (the launcher runs `gen:test-cert` itself if it's missing). CI runs it as the separate `vscode-e2e` job in `build-extensions.yml` — separate so a VS Code download failure can't be confused with a unit-test regression.

**What it covers:** activation of both extensions without throwing, command registration on both sides, and one vertical slice — `getAllCertMaterialV3` driven from the workspace extension to the UI extension, with the container-side install asserted on disk (PEM present and matching, `{hash}.N` symlink resolving, .NET Root store PFX written, My-store PFX correctly *absent*). All writes are redirected into a `mkdtemp` sandbox via `HOME` and `DOTNET_DEV_CERTS_OPENSSL_CERTIFICATE_DIRECTORY`, so the runner's real `~/.aspnet` and `~/.dotnet` are untouched.

**What it does NOT cover — do not oversell this as full E2E:**

- **The actual host↔server hop.** Both extensions share one extension host, so `executeCommand` passes objects **by reference**. In production the payload is serialized. This is the approach's central fidelity gap, and `src/wireGuard.ts` is the compensation: every cross-host payload is walked for non-JSON values (Buffer, Date, Map, class instances, functions, bigint, non-finite numbers, cycles, own `toJSON`) and round-tripped through `structuredClone` and JSON. Its negative self-tests are not optional decoration — without them a guard that silently accepted everything would produce an identical green run. `undefined`-valued object properties are the one tolerated difference (VS Code's RPC drops them exactly as JSON does); `undefined` inside an *array* is still rejected, because it becomes `null`.
- **The container filesystem.** The "container" is a temp dir on the same machine. Nothing exercises a real container's users, permissions, mounts, or a genuinely separate `$HOME`.
- **The dotnet dev-cert path.** The slice runs a *user* certificate. Generating the dev cert would write to the runner's real OS trust store and raise a modal consent dialog nothing headless can dismiss, so `generateDotNetCert` is off for the run. Reverse-sync (`acceptContainerDevCert`) is likewise uncovered.

### The remote-gate seam (`DEVCONTAINER_DEV_CERTS_TEST_REMOTE`) — test-only

`src/vscode-workspace-extension/src/extension.ts` no-ops unless `vscode.env.remoteName` is set. That property is read-only and is populated only by a resolver extension that has claimed an authority, so nothing inside a test can set it — hence `isRemoteContext()`, which also accepts `DEVCONTAINER_DEV_CERTS_TEST_REMOTE=1`.

**This is a test-only code path in production code, and the gating is the whole reason it's acceptable.** The env var alone does nothing. It is honored only when `context.extensionMode !== vscode.ExtensionMode.Production`, and `Production` is what VS Code assigns to *every* installed extension — the marketplace VSIX, a sideloaded VSIX, a `--install-extension` copy. Reaching the override requires launching VS Code with `--extensionDevelopmentPath` or `--extensionTestsPath` pointed at a source checkout. So the branch is not merely unlikely in a shipped build, it is unreachable: someone who can set environment variables still cannot flip it on.

If you change this, keep both halves. An env-var-only check would be a genuine escape hatch in shipped code and should be rejected in review. `tests/remoteGate.test.ts` pins exactly that — it asserts the override is refused under `ExtensionMode.Production` for every truthy spelling of the variable — and it runs in the fast vitest suite, not only in the E2E job, so the guarantee doesn't depend on a VS Code download succeeding.

The alternative that avoids a product-code seam entirely is a resolver extension implementing `resolveAuthority` to populate `remoteName` for real. It is more faithful and would also unlock testing against a real container — but it rides a **proposed API**, so it needs `--enable-proposed-api` and can break between VS Code releases. Choosing between the two is the open question this spike exists to inform.

## File Paths That Matter

| Path (in container) | Purpose |
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default tseslint.config(
"./src/shared/tsconfig.json",
"./src/vscode-ui-extension/tsconfig.lint.json",
"./src/vscode-workspace-extension/tsconfig.lint.json",
"./test/vscode-e2e/tsconfig.json",
],
tsconfigRootDir: import.meta.dirname,
},
Expand Down
Loading
Loading