Add guest sessions and agent VM defaults

Add daemon-backed workspace and guest-session primitives so host
orchestrators can prepare /root/repo, launch long-lived guest commands,
and attach to pipe-mode sessions over the local stdio mux bridge.

Persist richer session metadata and launch diagnostics, preflight guest
cwd/command requirements, make pipe-mode attach rehydratable from guest
state after daemon restart, and allow submodules when workspace prepare
runs in full_copy mode.

At the same time, stop vm run from auto-attaching opencode, make it
print next-step commands instead, and make glibc guest images more
agent-ready by installing node, opencode, claude, and pi while syncing
opencode/claude/pi auth files into work disks on VM start.

Validation:
- GOCACHE=/tmp/banger-gocache go test ./...
- make build
- banger vm workspace prepare --help
- banger vm session --help
- banger vm session start --help
- banger vm session attach --help
This commit is contained in:
Thales Maciel 2026-04-12 23:48:42 -03:00
parent 497e6dca3d
commit 37c4c091ec
No known key found for this signature in database
GPG key ID: 33112E6833C34679
18 changed files with 3212 additions and 405 deletions

View file

@ -1232,7 +1232,7 @@ func TestInspectVMRunRepoRejectsSubmodules(t *testing.T) {
}
}
func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
func TestRunVMRunCreatesImportsAndPrintsNextSteps(t *testing.T) {
repoRoot := t.TempDir()
repoCopyDir := filepath.Join(t.TempDir(), "repo-copy")
@ -1243,8 +1243,7 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
origGuestDial := guestDialFunc
origPrepareVMRunRepoCopy := prepareVMRunRepoCopyFunc
origBuildVMRunToolingPlan := buildVMRunToolingPlanFunc
origOpencodeExec := opencodeExecFunc
origHostOpencodeAttachSupported := hostOpencodeAttachSupportedFunc
origVMWorkspacePrepare := vmWorkspacePrepareFunc
t.Cleanup(func() {
vmCreateBeginFunc = origBegin
vmCreateStatusFunc = origStatus
@ -1253,8 +1252,7 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
guestDialFunc = origGuestDial
prepareVMRunRepoCopyFunc = origPrepareVMRunRepoCopy
buildVMRunToolingPlanFunc = origBuildVMRunToolingPlan
opencodeExecFunc = origOpencodeExec
hostOpencodeAttachSupportedFunc = origHostOpencodeAttachSupported
vmWorkspacePrepareFunc = origVMWorkspacePrepare
})
vm := model.VMRecord{
@ -1310,22 +1308,21 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
}
return repoCopyDir, func() {}, nil
}
hostOpencodeAttachSupportedFunc = func(context.Context) (bool, error) {
return true, nil
var workspaceParams api.VMWorkspacePrepareParams
vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
workspaceParams = params
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo"}}, nil
}
buildVMRunToolingPlanFunc = func(context.Context, string) toolingplan.Plan {
return toolingplan.Plan{
Steps: []toolingplan.InstallStep{{Tool: "go", Version: "1.25.0", Source: "go.mod"}},
Skips: []toolingplan.SkipNote{{Target: "python", Reason: "no .python-version"}},
RepoManagedTools: []string{"go"},
Steps: []toolingplan.InstallStep{{Tool: "go", Version: "1.25.0", Source: "go.mod"}},
Skips: []toolingplan.SkipNote{{Target: "python", Reason: "no .python-version"}},
}
}
var attachArgs []string
opencodeExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
attachArgs = append([]string(nil), args...)
return nil
}
spec := vmRunRepoSpec{
SourcePath: repoRoot,
RepoRoot: repoRoot,
RepoName: "repo",
HeadCommit: "deadbeef",
@ -1336,13 +1333,15 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
GitUserEmail: "repo@example.com",
OverlayPaths: []string{"tracked.txt", "nested/keep.txt"},
}
var stdout bytes.Buffer
var stderr bytes.Buffer
err := runVMRun(
context.Background(),
"/tmp/bangerd.sock",
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
strings.NewReader(""),
&bytes.Buffer{},
&bytes.Buffer{},
&stdout,
&stderr,
api.VMCreateParams{Name: "devbox"},
spec,
)
@ -1365,29 +1364,20 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
if dialKeyPath != waitKeyPath {
t.Fatalf("dialKeyPath = %q, want %q", dialKeyPath, waitKeyPath)
}
if fakeClient.tarSourceDir != repoCopyDir {
t.Fatalf("tarSourceDir = %q, want %q", fakeClient.tarSourceDir, repoCopyDir)
if workspaceParams.IDOrName != "devbox" {
t.Fatalf("workspaceParams.IDOrName = %q, want devbox", workspaceParams.IDOrName)
}
if fakeClient.tarCommand != "rm -rf '/root/repo' && mkdir -p '/root/repo' && tar -o -C '/root/repo' --strip-components=1 -xf -" {
t.Fatalf("tarCommand = %q", fakeClient.tarCommand)
if workspaceParams.SourcePath != repoRoot {
t.Fatalf("workspaceParams.SourcePath = %q, want %q", workspaceParams.SourcePath, repoRoot)
}
if len(fakeClient.uploads) != 2 {
t.Fatalf("uploads = %d, want 2", len(fakeClient.uploads))
if workspaceParams.GuestPath != "/root/repo" {
t.Fatalf("workspaceParams.GuestPath = %q, want /root/repo", workspaceParams.GuestPath)
}
if fakeClient.uploads[0].path != vmRunToolingHarnessPromptPath("repo") {
t.Fatalf("prompt upload path = %q, want %q", fakeClient.uploads[0].path, vmRunToolingHarnessPromptPath("repo"))
if workspaceParams.Mode != string(model.WorkspacePrepareModeShallowOverlay) {
t.Fatalf("workspaceParams.Mode = %q", workspaceParams.Mode)
}
if fakeClient.uploads[0].mode != 0o644 {
t.Fatalf("prompt upload mode = %v, want 0644", fakeClient.uploads[0].mode)
}
if !strings.Contains(string(fakeClient.uploads[0].data), `Do not edit repository files.`) {
t.Fatalf("prompt upload data = %q, want prompt body", string(fakeClient.uploads[0].data))
}
if !strings.Contains(string(fakeClient.uploads[0].data), `Planned deterministic install: go@1.25.0 from go.mod`) {
t.Fatalf("prompt upload data = %q, want deterministic install summary", string(fakeClient.uploads[0].data))
}
if !strings.Contains(string(fakeClient.uploads[0].data), `Deterministic skip: python (no .python-version)`) {
t.Fatalf("prompt upload data = %q, want deterministic skip summary", string(fakeClient.uploads[0].data))
if len(fakeClient.uploads) != 1 {
t.Fatalf("uploads = %d, want 1", len(fakeClient.uploads))
}
if fakeClient.uploadPath != vmRunToolingHarnessPath("repo") {
t.Fatalf("uploadPath = %q, want %q", fakeClient.uploadPath, vmRunToolingHarnessPath("repo"))
@ -1395,23 +1385,17 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
if fakeClient.uploadMode != 0o755 {
t.Fatalf("uploadMode = %v, want 0755", fakeClient.uploadMode)
}
if !strings.Contains(string(fakeClient.uploadData), `repo-managed mise tools: go`) {
t.Fatalf("uploadData = %q, want repo-managed tool log", string(fakeClient.uploadData))
}
if !strings.Contains(string(fakeClient.uploadData), `run_best_effort "$MISE_BIN" install`) {
t.Fatalf("uploadData = %q, want mise install best-effort step", string(fakeClient.uploadData))
}
if !strings.Contains(string(fakeClient.uploadData), fmt.Sprintf(`INSTALL_TIMEOUT_SECS=%d`, vmRunToolingInstallTimeoutSeconds)) {
t.Fatalf("uploadData = %q, want deterministic install timeout", string(fakeClient.uploadData))
}
if !strings.Contains(string(fakeClient.uploadData), `deterministic install: go@1.25.0 (go.mod)`) {
t.Fatalf("uploadData = %q, want deterministic install log", string(fakeClient.uploadData))
t.Fatalf("uploadData = %q, want repo mise install step", string(fakeClient.uploadData))
}
if !strings.Contains(string(fakeClient.uploadData), `run_bounded_best_effort "$INSTALL_TIMEOUT_SECS" "$MISE_BIN" use -g --pin 'go@1.25.0'`) {
t.Fatalf("uploadData = %q, want deterministic go install step", string(fakeClient.uploadData))
}
if !strings.Contains(string(fakeClient.uploadData), `deterministic skip: python (no .python-version)`) {
t.Fatalf("uploadData = %q, want deterministic skip log", string(fakeClient.uploadData))
}
if !strings.Contains(string(fakeClient.uploadData), `run_best_effort "$MISE_BIN" reshim`) {
t.Fatalf("uploadData = %q, want deterministic reshim step", string(fakeClient.uploadData))
if strings.Contains(string(fakeClient.uploadData), `opencode run`) {
t.Fatalf("uploadData = %q, want no opencode harness run", string(fakeClient.uploadData))
}
if !strings.Contains(fakeClient.launchScript, `nohup bash "$HELPER" >"$LOG" 2>&1 </dev/null &`) {
t.Fatalf("launchScript = %q, want nohup launcher", fakeClient.launchScript)
@ -1419,33 +1403,21 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
if !strings.Contains(fakeClient.launchScript, vmRunToolingHarnessLogPath("repo")) {
t.Fatalf("launchScript = %q, want tooling harness log path", fakeClient.launchScript)
}
if !strings.Contains(fakeClient.script, `git -C "$DIR" checkout -B 'feature' 'cafebabe'`) {
t.Fatalf("script = %q, want guest branch checkout", fakeClient.script)
}
if !strings.Contains(fakeClient.script, `find "$DIR" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +`) {
t.Fatalf("script = %q, want guest worktree reset", fakeClient.script)
}
if !strings.Contains(fakeClient.script, `git config --global --add safe.directory "$DIR"`) {
t.Fatalf("script = %q, want guest safe.directory config", fakeClient.script)
}
if !strings.Contains(fakeClient.script, `git -C "$DIR" config user.name 'Repo User'`) {
t.Fatalf("script = %q, want guest repo user.name config", fakeClient.script)
}
if !strings.Contains(fakeClient.script, `git -C "$DIR" config user.email 'repo@example.com'`) {
t.Fatalf("script = %q, want guest repo user.email config", fakeClient.script)
}
if fakeClient.streamSourceDir != repoRoot {
t.Fatalf("streamSourceDir = %q, want %q", fakeClient.streamSourceDir, repoRoot)
}
if !reflect.DeepEqual(fakeClient.streamEntries, spec.OverlayPaths) {
t.Fatalf("streamEntries = %v, want %v", fakeClient.streamEntries, spec.OverlayPaths)
}
if fakeClient.streamCommand != "tar -o -C '/root/repo' --strip-components=1 -xf -" {
t.Fatalf("streamCommand = %q", fakeClient.streamCommand)
}
wantAttach := []string{"attach", "--dir", "/root/repo", "http://172.16.0.2:4096"}
if !reflect.DeepEqual(attachArgs, wantAttach) {
t.Fatalf("attachArgs = %v, want %v", attachArgs, wantAttach)
output := stdout.String()
for _, want := range []string{
"VM ready.",
"Name: devbox",
"Host: devbox.vm",
"Repo: /root/repo",
"banger vm ssh devbox",
"opencode attach http://devbox.vm:4096 --dir /root/repo",
"banger vm acp devbox",
`banger vm ssh devbox -- "cd /root/repo && claude"`,
`banger vm ssh devbox -- "cd /root/repo && pi"`,
} {
if !strings.Contains(output, want) {
t.Fatalf("stdout = %q, want %q", output, want)
}
}
if !fakeClient.closed {
t.Fatal("guest client should be closed")
@ -1459,8 +1431,7 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
origWaitForSSH := guestWaitForSSHFunc
origGuestDial := guestDialFunc
origPrepareVMRunRepoCopy := prepareVMRunRepoCopyFunc
origOpencodeExec := opencodeExecFunc
origHostOpencodeAttachSupported := hostOpencodeAttachSupportedFunc
origVMWorkspacePrepare := vmWorkspacePrepareFunc
t.Cleanup(func() {
vmCreateBeginFunc = origBegin
vmCreateStatusFunc = origStatus
@ -1468,8 +1439,7 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
guestWaitForSSHFunc = origWaitForSSH
guestDialFunc = origGuestDial
prepareVMRunRepoCopyFunc = origPrepareVMRunRepoCopy
opencodeExecFunc = origOpencodeExec
hostOpencodeAttachSupportedFunc = origHostOpencodeAttachSupported
vmWorkspacePrepareFunc = origVMWorkspacePrepare
})
vm := model.VMRecord{
@ -1509,20 +1479,18 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
prepareVMRunRepoCopyFunc = func(ctx context.Context, spec vmRunRepoSpec) (string, func(), error) {
return t.TempDir(), func() {}, nil
}
hostOpencodeAttachSupportedFunc = func(context.Context) (bool, error) {
return true, nil
}
opencodeExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
return nil
vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo"}}, nil
}
var stdout bytes.Buffer
var stderr bytes.Buffer
err := runVMRun(
context.Background(),
"/tmp/bangerd.sock",
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
strings.NewReader(""),
&bytes.Buffer{},
&stdout,
&stderr,
api.VMCreateParams{Name: "devbox"},
vmRunRepoSpec{RepoRoot: t.TempDir(), RepoName: "repo", HeadCommit: "deadbeef"},
@ -1533,19 +1501,19 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
output := stderr.String()
for _, want := range []string{
"[vm run] preparing guest workspace",
"[vm run] waiting for guest ssh",
"[vm run] preparing shallow repo",
"[vm run] copying repo metadata to guest",
"[vm run] preparing guest checkout",
"[vm run] overlaying host working tree",
"[vm run] starting tooling harness",
"[vm run] tooling harness log: /root/.cache/banger/vm-run-tooling-repo.log",
"[vm run] attaching opencode",
"[vm run] starting guest tooling bootstrap",
"[vm run] guest tooling log: /root/.cache/banger/vm-run-tooling-repo.log",
"[vm run] printing next steps",
} {
if !strings.Contains(output, want) {
t.Fatalf("stderr = %q, want %q", output, want)
}
}
if strings.Contains(output, "[vm run] attaching opencode") {
t.Fatalf("stderr = %q, want no auto-attach progress", output)
}
}
func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
@ -1555,8 +1523,7 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
origWaitForSSH := guestWaitForSSHFunc
origGuestDial := guestDialFunc
origPrepareVMRunRepoCopy := prepareVMRunRepoCopyFunc
origOpencodeExec := opencodeExecFunc
origHostOpencodeAttachSupported := hostOpencodeAttachSupportedFunc
origVMWorkspacePrepare := vmWorkspacePrepareFunc
t.Cleanup(func() {
vmCreateBeginFunc = origBegin
vmCreateStatusFunc = origStatus
@ -1564,8 +1531,7 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
guestWaitForSSHFunc = origWaitForSSH
guestDialFunc = origGuestDial
prepareVMRunRepoCopyFunc = origPrepareVMRunRepoCopy
opencodeExecFunc = origOpencodeExec
hostOpencodeAttachSupportedFunc = origHostOpencodeAttachSupported
vmWorkspacePrepareFunc = origVMWorkspacePrepare
})
vm := model.VMRecord{
@ -1597,22 +1563,18 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
prepareVMRunRepoCopyFunc = func(ctx context.Context, spec vmRunRepoSpec) (string, func(), error) {
return t.TempDir(), func() {}, nil
}
hostOpencodeAttachSupportedFunc = func(context.Context) (bool, error) {
return true, nil
}
attachCalled := false
opencodeExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
attachCalled = true
return nil
vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo"}}, nil
}
var stdout bytes.Buffer
var stderr bytes.Buffer
err := runVMRun(
context.Background(),
"/tmp/bangerd.sock",
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
strings.NewReader(""),
&bytes.Buffer{},
&stdout,
&stderr,
api.VMCreateParams{Name: "devbox"},
vmRunRepoSpec{RepoRoot: t.TempDir(), RepoName: "repo", HeadCommit: "deadbeef"},
@ -1620,147 +1582,38 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
if err != nil {
t.Fatalf("runVMRun: %v", err)
}
if !attachCalled {
t.Fatal("opencode attach should still run when tooling harness launch fails")
if !strings.Contains(stderr.String(), "[vm run] warning: guest tooling bootstrap start failed: launch guest tooling bootstrap") {
t.Fatalf("stderr = %q, want tooling bootstrap warning", stderr.String())
}
if !strings.Contains(stderr.String(), "[vm run] warning: tooling harness start failed: launch tooling harness: launch failed") {
t.Fatalf("stderr = %q, want tooling harness warning", stderr.String())
}
}
func TestRunVMRunFallsBackToGuestOpencodeWhenHostAttachUnsupported(t *testing.T) {
repoRoot := t.TempDir()
origBegin := vmCreateBeginFunc
origStatus := vmCreateStatusFunc
origCancel := vmCreateCancelFunc
origWaitForSSH := guestWaitForSSHFunc
origGuestDial := guestDialFunc
origPrepareVMRunRepoCopy := prepareVMRunRepoCopyFunc
origOpencodeExec := opencodeExecFunc
origHostOpencodeAttachSupported := hostOpencodeAttachSupportedFunc
origSSHExec := sshExecFunc
t.Cleanup(func() {
vmCreateBeginFunc = origBegin
vmCreateStatusFunc = origStatus
vmCreateCancelFunc = origCancel
guestWaitForSSHFunc = origWaitForSSH
guestDialFunc = origGuestDial
prepareVMRunRepoCopyFunc = origPrepareVMRunRepoCopy
opencodeExecFunc = origOpencodeExec
hostOpencodeAttachSupportedFunc = origHostOpencodeAttachSupported
sshExecFunc = origSSHExec
})
vm := model.VMRecord{
ID: "vm-id",
Name: "devbox",
Runtime: model.VMRuntime{
State: model.VMStateRunning,
GuestIP: "172.16.0.2",
},
}
vmCreateBeginFunc = func(context.Context, string, api.VMCreateParams) (api.VMCreateBeginResult, error) {
return api.VMCreateBeginResult{Operation: api.VMCreateOperation{ID: "op-1", Stage: "ready", Detail: "vm is ready", Done: true, Success: true, VM: &vm}}, nil
}
vmCreateStatusFunc = func(context.Context, string, string) (api.VMCreateStatusResult, error) {
t.Fatal("vmCreateStatusFunc should not be called")
return api.VMCreateStatusResult{}, nil
}
vmCreateCancelFunc = func(context.Context, string, string) error {
t.Fatal("vmCreateCancelFunc should not be called")
return nil
}
guestWaitForSSHFunc = func(ctx context.Context, address, privateKeyPath string, interval time.Duration) error {
return nil
}
guestDialFunc = func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error) {
return &testVMRunGuestClient{}, nil
}
prepareVMRunRepoCopyFunc = func(ctx context.Context, spec vmRunRepoSpec) (string, func(), error) {
return t.TempDir(), func() {}, nil
}
hostOpencodeAttachSupportedFunc = func(context.Context) (bool, error) {
return false, nil
}
opencodeExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
t.Fatalf("opencodeExecFunc should not be called when host attach is unsupported: %v", args)
return nil
}
var sshArgs []string
sshExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
sshArgs = append([]string(nil), args...)
return nil
}
var stderr bytes.Buffer
err := runVMRun(
context.Background(),
"/tmp/bangerd.sock",
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
strings.NewReader(""),
&bytes.Buffer{},
&stderr,
api.VMCreateParams{Name: "devbox"},
vmRunRepoSpec{RepoRoot: repoRoot, RepoName: "repo", HeadCommit: "deadbeef"},
)
if err != nil {
t.Fatalf("runVMRun: %v", err)
}
if len(sshArgs) < 3 {
t.Fatalf("sshArgs = %v, want fallback SSH invocation", sshArgs)
}
if sshArgs[len(sshArgs)-3] != "bash" || sshArgs[len(sshArgs)-2] != "-lc" {
t.Fatalf("sshArgs = %v, want bash -lc fallback command", sshArgs)
}
if sshArgs[len(sshArgs)-1] != "cd '/root/repo' && exec opencode ." {
t.Fatalf("ssh fallback command = %q, want guest opencode launch", sshArgs[len(sshArgs)-1])
}
if !strings.Contains(stderr.String(), "[vm run] host opencode has no attach support; starting guest opencode over ssh") {
t.Fatalf("stderr = %q, want SSH fallback progress", stderr.String())
}
}
func TestOpencodeAttachHelpOutputSupported(t *testing.T) {
if !opencodeAttachHelpOutputSupported([]byte("opencode attach [url]\n\nAttach a terminal")) {
t.Fatal("expected attach help output to be recognized")
}
if opencodeAttachHelpOutputSupported([]byte("opencode [project]\n\nCommands:\n opencode run [message..]")) {
t.Fatal("unexpected attach support for top-level help output")
if !strings.Contains(stdout.String(), "VM ready.") {
t.Fatalf("stdout = %q, want next steps summary", stdout.String())
}
}
func TestVMRunToolingHarnessScriptUsesMiseOnly(t *testing.T) {
script := vmRunToolingHarnessScript(vmRunRepoSpec{RepoName: "repo"}, toolingplan.Plan{
Steps: []toolingplan.InstallStep{{Tool: "go", Version: "1.25.0", Source: "go.mod"}},
Skips: []toolingplan.SkipNote{{Target: "python", Reason: "no .python-version"}},
RepoManagedTools: []string{"node"},
Steps: []toolingplan.InstallStep{{Tool: "go", Version: "1.25.0", Source: "go.mod"}},
Skips: []toolingplan.SkipNote{{Target: "python", Reason: "no .python-version"}},
})
for _, want := range []string{
`if [ -f .mise.toml ] || [ -f .tool-versions ]; then`,
"PROMPT_FILE=" + shellQuote(vmRunToolingHarnessPromptPath("repo")),
fmt.Sprintf("INSTALL_TIMEOUT_SECS=%d", vmRunToolingInstallTimeoutSeconds),
"MODEL=" + shellQuote(vmRunToolingHarnessModel),
fmt.Sprintf("TIMEOUT_SECS=%d", vmRunToolingHarnessTimeoutSeconds),
`repo-managed mise tools: node`,
`run_best_effort "$MISE_BIN" install`,
`deterministic install: go@1.25.0 (go.mod)`,
`run_bounded_best_effort "$INSTALL_TIMEOUT_SECS" "$MISE_BIN" use -g --pin 'go@1.25.0'`,
`deterministic skip: python (no .python-version)`,
`run_best_effort "$MISE_BIN" reshim`,
`run_bounded_best_effort "$TIMEOUT_SECS" bash -lc 'exec "$1" run --format json -m "$2" "$(cat "$3")"' _ "$OPENCODE_BIN" "$MODEL" "$PROMPT_FILE"`,
`command timed out after ${timeout_secs}s: $*`,
`tooling prompt file missing: $PROMPT_FILE`,
} {
if !strings.Contains(script, want) {
t.Fatalf("script = %q, want %q", script, want)
}
}
for _, unwanted := range []string{"git add", "cat > .mise.toml", "cat > .tool-versions"} {
for _, unwanted := range []string{`opencode run`, `PROMPT_FILE=`, `--format json`, `mimo-v2-pro-free`} {
if strings.Contains(script, unwanted) {
t.Fatalf("script = %q, want no %q", script, unwanted)
}
}
}
func TestPrepareVMRunRepoCopyCreatesShallowMetadataCopy(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not installed")
@ -2065,14 +1918,16 @@ func (c *testVMRunGuestClient) StreamTar(ctx context.Context, sourceDir, remoteC
func (c *testVMRunGuestClient) RunScript(ctx context.Context, script string, logWriter io.Writer) error {
c.runScriptCalls++
switch c.runScriptCalls {
case 1:
if c.runScriptCalls == 1 {
c.script = script
return c.checkoutErr
default:
c.launchScript = script
if c.checkoutErr != nil {
return c.checkoutErr
}
return c.launchErr
}
c.launchScript = script
return c.launchErr
}
func (c *testVMRunGuestClient) StreamTarEntries(ctx context.Context, sourceDir string, entries []string, remoteCommand string, logWriter io.Writer) error {