engine: HostExecutor interface for initializeCommand#33
Conversation
… hooks) Adds a caller-supplied HostExecutor interface that the engine dispatches host-side spec hooks to. initializeCommand now routes through it (Single + Parallel forms). secretsCommand will plug into the same interface in a follow-up. Library does NOT ship a default HostExecutor: host execution is security-sensitive (devcontainer.json can declare arbitrary commands), and policy decisions — sandboxing, env filtering, timeouts, working dir — belong to the embedding application. Unconfigured executor + a configured initializeCommand returns a *LifecycleError wrapping a typed sentinel ErrHostExecutorNotConfigured so callers can errors.Is and surface a useful message. Closes #11. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
📝 WalkthroughWalkthroughThe PR implements host-side execution of ChangesHost Executor Integration for initializeCommand
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lifecycle_test.go (1)
302-369: ⚡ Quick winConsider adding test coverage for the Parallel command form.
The tests comprehensively cover the Single command path, but
runInitializeParallel(which handlesinitializeCommandas a map of named commands running concurrently) lacks direct test coverage. Given the concurrency logic involved, a test verifying parallel execution would strengthen confidence.Example test skeleton
func TestInitializeCommand_RoutesToHostExecutorParallel(t *testing.T) { rt := newScriptedRuntime() hx := &fakeHostExecutor{} eng, _ := New(EngineOptions{Runtime: rt, HostExecutor: hx}) ws := writeImageDevcontainer(t, `{ "image":"alpine:3.20", "initializeCommand": { "setup": "echo setup", "prepare": "echo prepare" } }`) if _, err := eng.Up(context.Background(), UpOptions{ LocalWorkspaceFolder: ws, RunInitializeCommand: true, }); err != nil { t.Fatalf("Up: %v", err) } if len(hx.calls) != 2 { t.Fatalf("expected 2 host calls, got %d", len(hx.calls)) } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lifecycle_test.go` around lines 302 - 369, Add a unit test that exercises the parallel form of initializeCommand by invoking eng.Up with RunInitializeCommand: true on a devcontainer.json where "initializeCommand" is a map (e.g. {"setup":"echo setup","prepare":"echo prepare"}), using newScriptedRuntime() and a fakeHostExecutor to capture host calls; assert that runInitializeParallel (indirectly via Up/EngineOptions and HostExecutor) results in two HostExecutor calls (fakeHostExecutor.calls length == 2) and that each call’s Shell matches the expected commands, and ensure the test mirrors existing patterns used in TestInitializeCommand_RoutesToHostExecutorSingle and TestInitializeCommand_SkippedByDefault.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@lifecycle_test.go`:
- Around line 302-369: Add a unit test that exercises the parallel form of
initializeCommand by invoking eng.Up with RunInitializeCommand: true on a
devcontainer.json where "initializeCommand" is a map (e.g. {"setup":"echo
setup","prepare":"echo prepare"}), using newScriptedRuntime() and a
fakeHostExecutor to capture host calls; assert that runInitializeParallel
(indirectly via Up/EngineOptions and HostExecutor) results in two HostExecutor
calls (fakeHostExecutor.calls length == 2) and that each call’s Shell matches
the expected commands, and ensure the test mirrors existing patterns used in
TestInitializeCommand_RoutesToHostExecutorSingle and
TestInitializeCommand_SkippedByDefault.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d54d45e2-4db2-4244-9310-ab89f570351a
📒 Files selected for processing (4)
engine.gohost_executor.golifecycle.golifecycle_test.go
Summary
Closes #11. Unblocks #23 (`secretsCommand`) — same interface; the follow-up just captures stdout and merges into `ContainerEnv`.
API surface
```go
type HostExecutor interface {
ExecHost(ctx context.Context, cmd HostCommand) (HostExecResult, error)
}
type HostCommand struct {
Shell string
Exec []string
Env map[string]string
WorkingDir string
}
type HostExecResult struct {
ExitCode int
Stdout, Stderr string
}
```
Mirrors `runtime.ExecOptions` / `runtime.ExecResult` so consumers building both sides reuse the same mental model. Engine sets `WorkingDir = LocalWorkspaceFolder` for spec hooks.
Why no default impl?
Host execution is the most security-sensitive surface in the spec. `devcontainer.json` is user-supplied; running `exec.Command` from inside a library against it would bake policy (env passthrough, timeouts, sandboxing) into the wrong place. Caller-supplied keeps the library spec-faithful and lets each embedder make the right call:
Test plan
No integration test — host execution is hostile to CI (would need a sandbox), and the contract is fully covered by unit tests via the in-memory `fakeHostExecutor`.
Summary by CodeRabbit
Release Notes
New Features
initializeCommandhooks with output capture and exit code reporting.Tests