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:
parent
497e6dca3d
commit
37c4c091ec
18 changed files with 3212 additions and 405 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue