This directory is the evidence gate for cutting over from the Python cde-plugin/bin/runcode to the Go runcode binary. It contains two things:
- An opt-in live smoke test (
smoke_test.go, build-taggede2e) that drives a real workspace through a full lifecycle and asserts key behaviors. - This parity checklist (sections 5-7) that tells the operator how to verify the Go binary matches the Python binary's observable contract before flipping production traffic.
The cutover itself is out of scope here. Pass this checklist first.
Running the smoke test creates and then deletes a real, billed workspace.
- With
RUNCODE_API_BASEunset the test runs against production. Point it at a non-prod backend by settingRUNCODE_API_BASE. - The test sets
RUNCODE_CACHE_HOMEto an isolated temp dir for the duration of the run. It will NOT touch or corrupt your real~/.cache/runcodesession or stored token. - The delete step runs via
t.Cleanup, so the workspace is removed even if an intermediate step fails. A very early panic (e.g., build failure) could leave it orphaned; check the dashboard if the run crashes hard.
RUNCODE_TOKEN=<real-token> go test -tags e2e ./e2e/ -v
Run from the runcode-cli/ directory.
Required: RUNCODE_TOKEN must be set to a valid API token. Without it the test skips immediately, which is why the default go test ./... never runs it.
Optional: RUNCODE_API_BASE overrides the API endpoint (default: prod). Set it to target a staging or local backend.
The forward probe sub-step needs python3 on the workspace. If it is absent the sub-step is soft-skipped (logged, not failed). Everything else still runs.
Steps run in order; delete is guaranteed via t.Cleanup:
- create -
create --size tiny --json- waits for SSH-ready, parsesworkspace_id+ assertscreated=trueandattached=true. - exec -
exec -- echo <marker>- asserts the marker string appears in stdout. - write + get round-trip - writes a binary-safe file (includes non-ASCII byte) to the remote workdir, reads it back with
get --out, asserts byte-for-byte equality. - forward + probe - starts
python3 -m http.serveron the workspace at port 8099, opens a local forward, HTTP-probes it with retries; cancels the forward on exit (soft-skipped if python3 absent). - delete (cleanup) -
delete <id> --yes --json- assertsdeleted=true.
Method: run the Go binary and the Python cde-plugin/bin/runcode with identical arguments. Where the command supports --json, add it to both invocations. After each run, diff three things:
- error
codestrings (the"code"field in JSON error output) - process exit codes
- the set of JSON keys in the top-level response object
All three must match on every command except the intentional differences listed in section 6. After the final fix wave, the error code strings and exit codes match Python everywhere (soft errors exit 1, operational errors exit 2), and list --json matches Python's bare-array-minus-provider shape.
| # | Command | Go invocation | Python invocation | Diff (codes/exit/keys) | Result |
|---|---|---|---|---|---|
| 1 | --version |
runcode --version |
cde-plugin/bin/runcode --version |
Version STRING differs by build; assert both print a version line and exit 0 | |
| 2 | login / no-token shape |
runcode logout && runcode list --json (no token present) |
same | both should produce no_token error code, same exit |
|
| 3 | list --json |
runcode list --json |
cde-plugin/bin/runcode list --json |
bare array; per-element keys match (Go lists all by default — see §6.5; Python may show fewer rows but the same shape) | |
| 4 | status --json |
runcode status --json |
cde-plugin/bin/runcode status --json |
keys/codes/exit | |
| 5 | current --json (no attachment) |
runcode current --json |
cde-plugin/bin/runcode current --json |
both report "none"/not_found shape | |
| 6 | exec with no attached workspace |
runcode exec -- echo hi |
cde-plugin/bin/runcode exec -- echo hi |
both not_found, exit non-zero |
|
| 7 | delete without --yes |
runcode delete somews |
cde-plugin/bin/runcode delete somews |
both confirm_required, exit non-zero, ZERO destructive API calls |
|
| 8 | get with no attachment |
runcode get /nope |
cde-plugin/bin/runcode get /nope |
both not_found |
|
| 9 | port-forward bad port |
runcode port-forward notaport |
cde-plugin/bin/runcode forward notaport |
both bad_request (Go forward alias also works) |
|
| 10 | doctor --json |
runcode doctor --json |
cde-plugin/bin/runcode doctor --json |
intentional difference - see section 6; do NOT count as failure | |
| 11 | clean |
runcode clean |
cde-plugin/bin/runcode clean |
both preserve the token; compare behavior |
Run both binaries against the same workspace (or sequential workspaces of the same size). Diff codes/exit/keys for each.
| # | Command | Go invocation | Python invocation | Diff (codes/exit/keys) | Result |
|---|---|---|---|---|---|
| 1 | create |
runcode create --size tiny --json |
cde-plugin/bin/runcode create --size tiny --json |
keys/codes/exit | |
| 2 | connect |
runcode connect <id> --json |
cde-plugin/bin/runcode connect <id> --json |
keys/codes/exit | |
| 3 | exec |
runcode exec -- echo hi |
cde-plugin/bin/runcode exec -- echo hi |
keys/codes/exit | |
| 4 | ssh (was run) |
runcode ssh <id> -- ls |
cde-plugin/bin/runcode run <id> -- ls |
keys/codes/exit (Go run alias also works) |
|
| 5 | context |
runcode context --json |
cde-plugin/bin/runcode context --json |
keys/codes/exit | |
| 6 | write |
runcode write test.txt --file /tmp/x |
cde-plugin/bin/runcode write test.txt --file /tmp/x |
keys/codes/exit | |
| 7 | put |
runcode put /tmp/x test.txt |
cde-plugin/bin/runcode put /tmp/x test.txt |
keys/codes/exit | |
| 8 | get |
runcode get test.txt --out /tmp/y |
cde-plugin/bin/runcode get test.txt --out /tmp/y |
keys/codes/exit | |
| 9 | port-forward (was forward) |
runcode port-forward 8080 --local 9090 |
cde-plugin/bin/runcode forward 8080 --local 9090 |
keys/codes/exit | |
| 10 | port-forward --cancel |
runcode port-forward 8080 --local 9090 --cancel |
cde-plugin/bin/runcode forward 8080 --local 9090 --cancel |
keys/codes/exit | |
| 11 | disconnect |
runcode disconnect --json |
cde-plugin/bin/runcode disconnect --json |
keys/codes/exit | |
| 12 | stop |
runcode stop <id> --json |
cde-plugin/bin/runcode stop <id> --json |
keys/codes/exit | |
| 13 | delete --yes |
runcode delete <id> --yes --json |
cde-plugin/bin/runcode delete <id> --yes --json |
keys/codes/exit |
These are the owner-approved, deliberate deviations from the Python cde-plugin/bin/runcode. They are NOT regressions; every other observable behavior must match. The error code strings and process exit codes match Python on every command (soft/user errors exit 1, operational errors exit 2).
doctor --jsoncheck-name set. The Godoctorcheckstoken,api-base,cache,platform,status-line,auth; Python checks for a Python interpreter,ssh, andssh-keygenonPATH. The Go CLI speaks SSH natively (golang.org/x/crypto/ssh) and never shells out, so those binaries are irrelevant; thecache-writability probe replaces them. Thechecks[].nameset therefore differs by design.- Human / progress text goes to stderr (Python prints it to stdout).
--jsonoutput is on stdout in both, so machine consumers see identical streams — parity is preserved for the path that matters. status/current/disconnectaccept--json(Python's argparse for those rejects the flag). A superset, so any Python invocation still works.execwith no attached workspace returns error codenot_found(Python returns a genericerror). More specific, exit code unchanged.listlists ALL workspaces by default and accepts--allas a no-op alias (Python defaults to SSH-connectable-only and uses--allto widen). Scripts passing--allkeep working;list --jsonotherwise emits the same bare array (each element minus the internalproviderfield, plus a computedconnectable).- Loopback HTTP allowance is narrower: only
127.0.0.1/::1/localhostare accepted over plain http, vs Python's whole127.0.0.0/8. Stricter, never looser. - (Known limitation, not yet fixed)
context/write/put/getdo not enforce a per-call timeout (Python caps reads at 30s and writes at 120s). An unreachable box is still bounded by the 20s SSH handshake, so calls cannot hang indefinitely. - Command surface aligned to Coder/Gitpod conventions (the Go CLI is the replacement for the Python
cde-plugin, so it is free to improve once cut over). All Python names still work as hidden aliases, so existing scripts and muscle memory are unaffected:ssh <ws> [-- cmd]is the human verb (bare → interactive shell,-- cmd→ one-shot). It absorbs Python'srunandshell, both kept as hidden aliases. The agent-facingexec(targets the attached workspace, no positional) is unchanged.port-forward <port>renames Python'sforward(kept as a hidden alias).port-forwardis unambiguous and matchescoder port-forward.start <ws>(NEW, no Python equivalent) powers a stopped box on and waits forrunningwithout attaching — the inverse ofstop;--no-waitreturns immediately. (connect --startremains the power-on-AND-attach combo.)open <ws>(NEW, no Python equivalent) opens the workspace browser IDE, or prints the URL with--print(and auto-falls-back to printing on a headless box). It reads the cached session'sweb_urlwhen one is fresh, else mints to learn the current URL. URL is validated to absolute http/https before being handed to the OS opener (security: never afile:///option-like string toxdg-open/open/rundll32, always via argv, never a shell).
Fill this in after the operator run:
Date: ____________________
Operator: ____________________
Backend: prod | ____________________ (RUNCODE_API_BASE)
Workspace id: ____________________
Region/size: ____________________
Go binary ver: ____________________
Smoke lifecycle:
create ........ PASS / FAIL notes:
exec .......... PASS / FAIL notes:
write+get ..... PASS / FAIL notes:
forward+probe . PASS / FAIL / SKIPPED(python3 absent) notes:
delete ........ PASS / FAIL notes:
Parity (Go vs Python --json): codes / exit / keys
Table A offline: PASS / FAIL notes:
Table B live: PASS / FAIL notes:
Overall: PASS / FAIL