docs/internal/FAKE_DEVCONTAINER_TEST_PLAN.md
Companion to FAKE_DEVCONTAINER_CLI.md. Walks
through the devcontainer-related flows that require a human at the
keyboard, using the fake devcontainer / docker shims so no Docker
daemon, image, or @devcontainers/cli install is needed.
Every step lists the expected screen state. Where a step depends on previous state (a popup that only appears once per session, a build log that survives across restarts), that's called out.
The plan was authored by walking each flow in tmux against the just-built
./target/debug/fresh; observed gaps are noted under "Findings."
# Build the editor under test.
cargo build --bin fresh
# Shim the CLIs.
source scripts/fake-devcontainer/activate.sh
# Fresh state every run, so prior breadcrumbs don't bleed in.
rm -rf /tmp/dc-tmux-workspace ~/.local/share/fresh/workspaces \
~/.cache/fake-devcontainer
mkdir -p /tmp/dc-tmux-workspace/.devcontainer
cat > /tmp/dc-tmux-workspace/.devcontainer/devcontainer.json <<'JSON'
{
"name": "fake-tmux-test",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"forwardPorts": [8080, 5432],
"portsAttributes": {
"8080": { "label": "Web", "onAutoForward": "notify" },
"5432": { "label": "Postgres", "onAutoForward": "silent" }
},
"features": {
"ghcr.io/devcontainers/features/rust:1": "1.91.0",
"ghcr.io/devcontainers/features/node:1": "lts"
},
"remoteUser": "vscode",
"initializeCommand": "echo init-host >&2",
"postCreateCommand": "echo postcreate >&2",
"postStartCommand": "echo poststart >&2"
}
JSON
echo hello > /tmp/dc-tmux-workspace/hello.txt
Launch in tmux for an isolated, capture-able session:
tmux new-session -d -s dc -x 200 -y 60 \
"bash -lc 'source scripts/fake-devcontainer/activate.sh && \
cd /tmp/dc-tmux-workspace && \
exec ./target/debug/fresh /tmp/dc-tmux-workspace'"
tmux attach -t dc
Tip: use tmux capture-pane -t dc -p > /tmp/screen.txt to snapshot the
screen for diffing.
Pre-state: No prior session for this workspace; no fake-state dir.
Launch fresh as above. Within ~2s the screen shows file explorer on the left, dashboard center, and a popup in the bottom-right:
┌Dev Container Detected─────────────────────────────[×]┐
│Folder contains a Dev Container configuration │
│('fake-tmux-test'). Reopen in container? │
│ │
│Reopen in Container │
│Ignore │
└──────────────────────────────────────────────────────┘
Trap: focus starts on file explorer. Pressing Enter there collapses the directory; the popup is shadowed. Press Esc first to release explorer focus, then Enter selects "Reopen in Container."
After Enter the build log appears in the right split, streaming
[+] Building 0.0s … lines. Status bar shows
Container:<id> | .fresh-cache/devcontainer-logs/build-<ts>.log.
The build-log buffer auto-reverts on a 2s poll so each new fake
line shows up within a couple of seconds of being emitted.
When the fake emits the success JSON, the editor restarts (one
visible repaint), the status bar gains the [Container:<shortid>]
prefix, and the workspace re-renders.
Pass criteria: status bar prefix changes to [Container:…]; build
log split is still readable; no error popups.
Continues from Flow A.
Dev Container: Run Lifecycle. The first match is "Dev Container:
Run Lifecycle Command." Press Enter.devcontainer.json (with the fixture above: initializeCommand,
postCreateCommand, postStartCommand, plus updateContentCommand
if you add it). Use Down/Up to pick postCreateCommand. Press
Enter.Running postCreateCommand… then
postCreateCommand completed successfully.Pass criteria: the second status message appears. Without the
da4969a3 fix the handler returns early on data.value (undefined) and
the user sees no second message — the run silently no-ops. The fake's
echo postcreate >&2 lets you double-check by re-running with
FAKE_DC_UP_DELAY_MS=0 and watching tail -F .fresh-cache/devcontainer-logs/build-*.log
before triggering the command — the line will appear in the build
log because the fake's stderr is captured there during up, but for
out-of-band lifecycle runs you'd add a sentinel like touch /tmp/marker.
Dev Container: Show Build Logs. The most
recent build-<ts>.log opens in a horizontal split below.
Subsequent invocations focus the existing split rather than
stacking; verify by triggering twice.Dev Container: Show Logs (be careful —
the fuzzy match also surfaces Show Build Logs; type a few more
chars or use Down to disambiguate). A new split opens with the title
*Dev Container Logs* and the contents of fake docker logs
(which is the same stderr text the fake recorded during up, since
we have no separate "container stdout" to record).Pass criteria: both splits show the captured fake-build text, read-only, line-numbers off for Dev Container Logs.
Dev Container: Show Forwarded Ports. A panel opens
listing the configured 8080 + 5432 with their labels and runtime
bindings.docker port <id>. With the fake, those are
populated only if you set FAKE_DC_PORTS=8080,5432 before up
ran. Otherwise the panel shows the configured ports with no runtime
binding — also a valid state to verify (it's the "container started
but didn't bind anything" case).Pre-state: clean fake state.
Launch with FAKE_DC_UP_HANG=1 so up sleeps forever:
FAKE_DC_UP_HANG=1 ./target/debug/fresh /tmp/dc-tmux-workspace
Esc → Enter the attach popup. Build log split appears with
[+] Hanging on purpose for cancel test. Status bar shows
Connecting · Building.
Palette → Dev Container: Cancel Startup. Status bar should flip
to Dev container attach cancelled. The hung fake process is
killed (verify with ps -ef | grep fake — it's gone). No error
popup.
Pass criteria: status flips, no FailedAttach popup (the cancel
short-circuits attachCancelled).
Pre-state: clean fake state.
FAKE_DC_UP_FAIL=1 \
FAKE_DC_UP_FAIL_REASON='image not found: bogus:latest' \
FAKE_DC_UP_DELAY_MS=80 \
./target/debug/fresh /tmp/dc-tmux-workspace
┌Dev Container Attach Failed────────────────────────[×]┐
│Dev container attach failed: error: image not found: bogus:latest │
│ │
│Retry │
│Show Build Logs │
│Reopen Locally │
│Dismiss (ESC) │
└──────────────────────────────────────────────────────┘
Rebuild failed: error: image not found: bogus:latest.Variations to spot-check (one launch each, fresh state in between):
FAKE_DC_UP_BAD_JSON=1 → enterFailedAttach("Failed to parse devcontainer up output") (or i18n equivalent).FAKE_DC_UP_NO_CONTAINER_ID=1 → "missing containerId" failure.Run Flow A first. Then:
Dev Container: Rebuild. The plugin re-invokes
devcontainer up --workspace-folder <cwd> --remove-existing-container.build-<ts>.log (verify the
filename in the title bar changes). The fake stops the previous
container (per --remove-existing-container) and allocates a new
id; the editor restarts again with the new authority.This is the case the user explicitly asked about. Walks through a real multi-buffer state being restored on cold start, with attention to the build-log surfaces.
hello.txt → Enter (opens in the split that has
focus — likely the bottom split next to the build log)devcontainer.json → EnterDev Container: Show Logs → Enter (adds Dev Container
Logs)Quit).
tmux kill-session is not a clean exit and skips the workspace
write — verify by re-running and seeing the previous tabs gone../target/debug/fresh /tmp/dc-tmux-workspace
[Container:<id>] is gone from the status bar — the container
authority is ephemeral by design, so a cold restart drops back to
local.Reopen in Container? popup re-appears (per-process attach
decision was wiped with the process).build-<ts>.log file in a new split).Stale build log on cold restart. When workspace restore brings
back a build-<ts>.log buffer, there's no banner / tab badge marking
it as historical. A user who attaches again gets a second build-log
split below the first (because prepareBuildLogFile mints a new
timestamp); the older split sits there full of detached lines from
the prior process. Suggest: tag the older buffer's tab title (e.g.
…log (prev)) on restore, or close it automatically when a new
attach starts.
Re-prompt on every cold start. readAttachDecision is keyed by
cwd but stored in plugin global state, which is per-workspace JSON,
so it should survive — but the fake-CLI runs above showed the
prompt re-appearing. Worth confirming whether this is "we never
attached because the previous run died unclean" vs "the per-workspace
decision isn't actually being persisted." (See attachDecisionKey
/writeAttachDecision in devcontainer.ts.)
*Dev Container Logs* is virtual and not restorable. That split
silently disappears on restart, but the saved layout also drops
the slot — serialize_split_node_pruned (workspace.rs:1796-1842)
collapses virtual leaves and hoists their sibling. The "empty
bottom slot" I initially flagged was actually the real on-disk
build log from the previous attach, restored as a normal buffer;
see F1 in DEVCONTAINER_REMEDIATION_PLAN.md.
Multiple tabs in the same split aren't visually marked when they collide with the buffer-group ribbon. The active tab title shows in the strip; siblings are only reachable by Ctrl+Tab. With three or four buffers in the same split, the strip just shows one title and it's easy to think the rest were lost.
After a successful attach:
Dev Container: Detach. Status bar loses the
[Container:…] prefix; the file-explorer header drops the
[Container] tag and reverts to File Explorer.FAKE_DC_USER env var is unset in the spawned
shell). Re-attaching with Dev Container: Attach brings everything
back.In a directory without .devcontainer/:
/tmp/empty-workspace. No popup, no devcontainer
commands registered (only Create Config is unconditional).Dev Container: Create Config. A new
.devcontainer/devcontainer.json is written with a minimal
template, opened in a buffer, and the rest of the devcontainer
commands appear in the palette without needing a restart.The fake CLI is deliberately scriptable. A CI smoke job can do:
source scripts/fake-devcontainer/activate.sh
FAKE_DC_UP_DELAY_MS=0 \
FAKE_DC_PORTS=8080,5432 \
cargo nextest run --features plugins -p fresh-editor \
-E 'test(devcontainer)' --no-fail-fast
The existing e2e tests don't currently shell out to a real
devcontainer, so the fake doesn't regress them — but new tests that
do want to drive the attach flow can opt into the fake by setting up
the harness with the bin dir prepended to PATH. Adding a helper
harness.with_fake_devcontainer() is a small follow-up.
docker exec
doesn't change PATH, so any LSP probe sees the host's binaries.terminal_wrapper. It works (it's just
bash -l via fake-docker exec) but it's testing the host shell, not
a container shell.docker logs --follow semantics — fake reads to EOF and exits.If you want exact diffs against this doc:
# Reset
rm -rf ~/.cache/fake-devcontainer ~/.local/share/fresh/workspaces \
/tmp/dc-tmux-workspace/.fresh-cache
# Sanity: shim is on PATH
which devcontainer # → .../scripts/fake-devcontainer/bin/devcontainer
which docker # → .../scripts/fake-devcontainer/bin/docker
# Quick e2e of `up` from the shell
FAKE_DC_UP_DELAY_MS=0 FAKE_DC_PORTS=8080,5432 \
devcontainer up --workspace-folder /tmp/dc-tmux-workspace 2>&1 | tail
# stdout's last line should be the success JSON.