From c3566ffbd6918b38f3cc036310a8cb271f2af018 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 12:34:11 +0800 Subject: [PATCH] fix(test): fix Tcl quoting in RunInteractive and openboot isolation in FirstTimeUser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RunInteractive was using shellescape() (POSIX single-quote escaping) to quote the spawn command in the expect script. Tcl does not treat ' as a quoting character, so the command was split on whitespace and bash received fragmented args — causing 'unexpected EOF while looking for matching quote'. Fix: use Tcl brace quoting {cmd} so the entire string is one Tcl word. TestVM_Journey_FirstTimeUser/bare_system_has_no_openboot was failing because TestVM_Interactive_InstallScript installs openboot via brew on the same host, and the next test finds it with 'which openboot'. Fix: add openboot to the brew uninstall call at the start of FirstTimeUser setup. --- test/e2e/vm_user_journey_test.go | 6 +++--- testutil/machost.go | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/test/e2e/vm_user_journey_test.go b/test/e2e/vm_user_journey_test.go index c262f46..4b27789 100644 --- a/test/e2e/vm_user_journey_test.go +++ b/test/e2e/vm_user_journey_test.go @@ -41,10 +41,10 @@ func TestVM_Journey_FirstTimeUser(t *testing.T) { vm := testutil.NewMacHost(t) vmInstallHomebrew(t, vm) - // Uninstall the minimal preset packages so openboot actually installs them - // rather than finding them pre-installed on the GHA runner image. + // Remove the brew-installed openboot (left by TestVM_Interactive_InstallScript) + // and the minimal preset packages so openboot actually installs them. vm.Run(fmt.Sprintf( - "export PATH=%q && brew uninstall --ignore-dependencies jq ripgrep fd bat fzf htop tree gh 2>/dev/null || true", + "export PATH=%q && brew uninstall --ignore-dependencies openboot jq ripgrep fd bat fzf htop tree gh 2>/dev/null || true", brewPath, )) bin := vmCopyDevBinary(t, vm) diff --git a/testutil/machost.go b/testutil/machost.go index adcdd72..c40e502 100644 --- a/testutil/machost.go +++ b/testutil/machost.go @@ -73,7 +73,10 @@ func (h *MacHost) RunInteractive(command string, steps []ExpectStep, timeoutSec var script strings.Builder fmt.Fprintf(&script, "set timeout %d\n", timeoutSec) - fmt.Fprintf(&script, "spawn bash -c %s\n", shellescape(command)) + // Use Tcl quoting so the entire command is a single word. + // shellescape produces POSIX single-quote escaping which Tcl's word + // splitter does not honour — it splits on whitespace regardless. + fmt.Fprintf(&script, "spawn bash -c %s\n", tclBrace(command)) for _, step := range steps { fmt.Fprintf(&script, "expect %q\n", step.Expect) fmt.Fprintf(&script, "send %q\n", step.Send) @@ -106,6 +109,16 @@ func shellescape(s string) string { return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" } +// tclBrace quotes s as a single Tcl word. Uses brace quoting {s} when safe +// (no unbalanced braces), otherwise falls back to Tcl double-quote escaping. +func tclBrace(s string) string { + if !strings.ContainsAny(s, "{}") { + return "{" + s + "}" + } + r := strings.NewReplacer(`\`, `\\`, `"`, `\"`, `$`, `\$`, `[`, `\[`) + return `"` + r.Replace(s) + `"` +} + func requireEphemeralHost(t *testing.T) { t.Helper() if os.Getenv("OPENBOOT_IN_VM") == "1" {