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
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ Rootless Podman e2e tests run in GitHub Actions on Ubuntu alongside Docker. They
cover image startup, Dockerfile builds, mounted workspace edits, and reopening
and editing a container workspace after the extension runtime restarts.

Configured `forwardPorts` are automatically available on host `127.0.0.1` at
those ports, including services listening only on container localhost. Numeric
ports and `"host:port"` destinations are supported. The container must include
socat, Node.js, or Python 3 for the streaming relay. A missing relay or occupied
host port fails startup with an actionable error. Stop, Remove, and disconnecting
the extension close its listeners and active connections; reconnecting restores
the saved forwarding configuration. This does not require Docker `-p` publication.

For local development, use Node from `.nvmrc`, then run `npm ci` and `npm run dev`.
This prepares the local extension in `.tmp/dev` with esbuild and starts the
development server alongside browser bundle watching. The first bundle is ready
Expand Down Expand Up @@ -47,7 +55,13 @@ The desktop extension contributes these commands for the active workspace:

Docker-backed end-to-end tests run on Ubuntu for every pull request and push to `main`. They cover the official JavaScript/Node 24 and Ubuntu 24.04 images, plus a Dockerfile build with a custom workspace mount.

Each test opens a fresh copy of a workspace fixture, starts the container through Quick Pick, checks its runtime and reads the fixture inside the container. It then writes a file from inside Docker and verifies the file in Explorer and its contents in the editor. A subsequent workspace edit must be readable inside the container. Finally, the test stops the container through Quick Pick and verifies that execution is rejected while stopped.
The forwardPorts acceptance test starts the packaged Dev Containers RPC process
with a localhost-only HTTP server and `forwardPorts: [3000]`. It asserts the
response from the host and checks listener cleanup and restoration across repeated
opening, process disconnect/reconnect, Stop, and Remove. It runs before the browser
suite on Ubuntu, with no Docker publication or manual forwarding.

Each browser test opens a fresh copy of a workspace fixture, starts the container through Quick Pick, checks its runtime and reads the fixture inside the container. It then writes a file from inside Docker and verifies the file in Explorer and its contents in the editor. A subsequent workspace edit must be readable inside the container. Finally, the test stops the container through Quick Pick and verifies that execution is rejected while stopped.

The tests use the `Devcontainer` page object from `@lvce-editor/test-worker` for start, stop, execution, output assertions, and cleanup. Dependency installation loads the declared test-worker dependency into the development server.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"build": "node packages/build/src/build.ts",
"build:watch": "npm run build:watch --workspace=packages/build",
"dev": "node packages/build/src/dev.ts",
"e2e:devcontainer": "npm run e2e:headless --prefix packages/e2e",
"e2e:devcontainer": "npm run test:e2e --workspace=packages/devcontainer-node && npm run e2e:headless --prefix packages/e2e",
"format": "prettier --write .",
"lint": "eslint . && knip --no-config-hints",
"test": "npm test --workspaces --if-present",
Expand Down
3 changes: 2 additions & 1 deletion packages/devcontainer-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
},
"scripts": {
"test": "node --unhandled-rejections=warn --experimental-vm-modules ../../node_modules/jest/bin/jest.js --detectOpenHandles --forceExit",
"test:watch": "node --unhandled-rejections=warn --experimental-vm-modules ../../node_modules/jest/bin/jest.js --watch"
"test:watch": "node --unhandled-rejections=warn --experimental-vm-modules ../../node_modules/jest/bin/jest.js --watch",
"test:e2e": "node --test test/ForwardPorts.e2e.ts"
},
"jest": {
"injectGlobals": false,
Expand Down
9 changes: 9 additions & 0 deletions packages/devcontainer-node/src/devcontainerCli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ export function dockerInspectContainer(
): Promise<boolean>

export function getDevcontainerCliPath(): string

export function forwardPorts(
options: ContainerOptions & {
workspaceFolder?: string
forwardPorts?: unknown
remoteUser?: string
},
): Promise<void>
export function disposeForwardPorts(): void
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ErrorResult } from '../SerializeError/SerializeError.ts'
import * as CliError from '../CliError/CliError.ts'
import * as CliJson from '../CliJson/CliJson.ts'
import * as ContainerFileSystem from '../ContainerFileSystem/ContainerFileSystem.ts'
import * as ForwardPorts from '../ForwardPorts/ForwardPorts.ts'
import * as RunProcess from '../RunProcess/RunProcess.ts'

export interface CliCommandSuccess {
Expand All @@ -17,6 +18,7 @@ export interface CliCommandSuccess {
export interface CliCommandError extends ErrorResult {
commandName: string
exitCode?: number | null
json?: unknown
missingExecutable?: string
ok: false
stderr?: string
Expand Down Expand Up @@ -91,6 +93,7 @@ export const getCliUpArgs = ({
'--workspace-folder',
workspaceFolder,
'--no-lockfile',
'--include-merged-configuration',
'--log-format',
'text',
'--log-level',
Expand Down Expand Up @@ -269,15 +272,53 @@ export const cliReadConfiguration = (options: WorkspaceOptions) => {
)
}

export const cliUp = (options: WorkspaceOptions) => {
return runDevcontainerCli(
export const cliUp = async (
options: WorkspaceOptions,
): Promise<CliCommandResult> => {
const result = await runDevcontainerCli(
'DevContainerNode.cliUp',
[getDevcontainerCliPath(), ...getCliUpArgs(options)],
options.containerCli,
options.onOutput,
)
if (!result.ok) return result
const json = result.json as {
containerId: string
remoteUser?: string
mergedConfiguration?: { forwardPorts?: unknown }
}
try {
await ForwardPorts.ensure({
containerCli: options.containerCli ?? dockerPath,
containerId: json.containerId,
forwardPorts: json.mergedConfiguration?.forwardPorts,
remoteUser: json.remoteUser,
workspaceFolder: options.workspaceFolder,
})
return result
} catch (error) {
return {
commandName: result.commandName,
errorCode: 'DEVCONTAINER_FORWARD_PORTS_ERROR',
errorMessage: error instanceof Error ? error.message : String(error),
errorStack: undefined,
json: result.json,
ok: false,
stderr: result.stderr,
stdout: result.stdout,
}
}
}

export const forwardPorts = (
options: ContainerOptions & {
workspaceFolder?: string
forwardPorts?: unknown
remoteUser?: string
},
): Promise<void> => ForwardPorts.ensure(options)
export const disposeForwardPorts = ForwardPorts.dispose

export const cliExec = (options: ExecOptions) => {
return runDevcontainerCommand(
'DevContainerNode.cliExec',
Expand All @@ -286,15 +327,17 @@ export const cliExec = (options: ExecOptions) => {
)
}

export const dockerStopContainer = (options: ContainerOptions) => {
export const dockerStopContainer = async (options: ContainerOptions) => {
await ForwardPorts.remove(options.containerId)
return runDocker(
'DevContainerNode.dockerStopContainer',
getDockerStopArgs(options),
options.containerCli,
)
}

export const dockerRemoveContainer = (options: ContainerOptions) => {
export const dockerRemoveContainer = async (options: ContainerOptions) => {
await ForwardPorts.remove(options.containerId)
return runDocker(
'DevContainerNode.dockerRemoveContainer',
getDockerRemoveArgs(options),
Expand Down
Loading
Loading