Compare commits

..
Author SHA1 Message Date
anandh8x 4d9a68b666 fix: tighten command output summary detection 2026-08-03 19:20:34 +05:30
anandh8x ad152e0049 test: strengthen command reducer guarantees 2026-08-01 11:35:26 +05:30
anandh8x b4b0418daf perf: compact common test output 2026-08-01 00:00:11 +05:30
7 changed files with 26 additions and 498 deletions

No files matched your search

-28
View File
@@ -308,34 +308,6 @@ func runMCPCheck(ctx context.Context, args []string, stdout io.Writer, stderr io
}
defer closeMCPRuntime(stderr, mcpRuntime)
// A connect failure does not come back as err. RegisterTools is deliberately
// best-effort so one unreachable server cannot stop Zero launching: it records
// the failure on the runtime and returns nil. That is right for startup and
// wrong to ignore here, because this is the command a user runs precisely when
// a server is misbehaving, and reporting "is reachable" for a server that never
// started sends them looking somewhere else.
for _, skipped := range mcpRuntime.Skipped() {
if skipped.Name != serverName {
continue
}
message := fmt.Sprintf("MCP server %s is not reachable", serverName)
if skipped.Err != nil {
message = fmt.Sprintf("%s: %s", message, redaction.ErrorMessage(skipped.Err, redaction.Options{}))
}
if options.json {
payload := struct {
ServerName string `json:"serverName"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
}{ServerName: serverName, Status: "unreachable", Error: message}
if err := writePrettyJSON(stdout, redaction.RedactValue(payload, redaction.Options{})); err != nil {
return exitCrash
}
return exitCrash
}
return writeAppError(stderr, message, exitCrash)
}
items := mcpToolList(registry)
if options.json {
payload := struct {
-79
View File
@@ -6,7 +6,6 @@ package cli
import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
@@ -220,81 +219,3 @@ func TestRunMCPCheckNormalizeError(t *testing.T) {
t.Fatalf("want normalize error, got %q", got)
}
}
// TestRunMCPCheckReportsUnreachableServer covers the case the command exists
// for: a server that fails to start.
//
// RegisterTools is best-effort by design, so a connect failure is recorded on
// the runtime and registration returns nil. runMCPCheck used to inspect only
// that nil error and then print "is reachable", which told the user their
// broken server was fine. The command is spawned for real here rather than
// stubbed, because the whole failure mode was a mismatch between what
// registration returns and what it records.
func TestRunMCPCheckReportsUnreachableServer(t *testing.T) {
setTrustConfigRoot(t)
deps := appDeps{
getwd: func() (string, error) { return t.TempDir(), nil },
resolveMCPConfig: func(_ string, _ bool) (config.MCPConfig, error) {
return config.MCPConfig{Servers: map[string]config.MCPServerConfig{
"broken": {Type: "stdio", Command: "zero-definitely-not-a-real-binary-xyz"},
}}, nil
},
}
var out, errBuf bytes.Buffer
if code := runWithDeps([]string{"mcp", "check", "broken"}, &out, &errBuf, deps); code == exitSuccess {
t.Fatalf("mcp check must fail for a server that did not start; stdout=%q stderr=%q", out.String(), errBuf.String())
}
if got := out.String(); strings.Contains(got, "is reachable") {
t.Fatalf("mcp check reported a failed server as reachable: %q", got)
}
if got := errBuf.String(); !strings.Contains(got, "not reachable") {
t.Fatalf("want an unreachable error naming the server, got %q", got)
}
}
// The --json contract for an unreachable server, which the text case above does
// not touch.
//
// The JSON branch is a separate return path with its own payload, so a caller
// parsing it would otherwise be relying on a shape nothing pins: reverting the
// check leaves it emitting status "ok" with a tool count, which is valid JSON
// and completely wrong.
func TestRunMCPCheckReportsUnreachableServerJSON(t *testing.T) {
setTrustConfigRoot(t)
deps := appDeps{
getwd: func() (string, error) { return t.TempDir(), nil },
resolveMCPConfig: func(_ string, _ bool) (config.MCPConfig, error) {
return config.MCPConfig{Servers: map[string]config.MCPServerConfig{
"broken": {Type: "stdio", Command: "zero-definitely-not-a-real-binary-xyz"},
}}, nil
},
}
var out, errBuf bytes.Buffer
if code := runWithDeps([]string{"mcp", "check", "broken", "--json"}, &out, &errBuf, deps); code == exitSuccess {
t.Fatalf("mcp check --json must fail for a server that did not start; stdout=%q stderr=%q", out.String(), errBuf.String())
}
var payload struct {
ServerName string `json:"serverName"`
Status string `json:"status"`
Error string `json:"error"`
ToolCount int `json:"toolCount"`
}
if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
t.Fatalf("decode mcp check --json output %q: %v", out.String(), err)
}
if payload.ServerName != "broken" {
t.Fatalf("serverName = %q, want the server that was checked", payload.ServerName)
}
if payload.Status != "unreachable" {
t.Fatalf("status = %q, want %q", payload.Status, "unreachable")
}
if !strings.Contains(payload.Error, "not reachable") {
t.Fatalf("error = %q, want it to say the server is not reachable", payload.Error)
}
// A reachable payload carries a tool count; an unreachable one must not
// imply zero tools were found, which reads as a server that simply exposes
// none.
if payload.ToolCount != 0 {
t.Fatalf("toolCount = %d, want it absent from an unreachable payload", payload.ToolCount)
}
}
@@ -9,7 +9,6 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
@@ -387,127 +386,6 @@ func runWindowsRealSmokeCommand(t *testing.T, runnerExe string, base WindowsSand
}
}
// The write jail must hold on a path whose DACL grants Everyone write access.
//
// A WRITE_RESTRICTED token runs TWO checks for a write and needs both to pass:
// the normal one against its enabled SIDs, and a second against its RESTRICTED
// SID list. The jail is built on the second check only succeeding where Zero has
// explicitly ACL'd a capability SID. Putting the World SID (S-1-1-0) in the
// restricted list breaks that globally — every principal is a member of
// Everyone, so on a DACL that grants Everyone write, the restricted half passes
// for free and confinement collapses back to the ordinary user's own
// permissions, which is precisely the boundary the sandbox exists to be
// stricter than.
//
// The runner already states this rule for the SIDs it refuses to add ("None of
// the granted SIDs can be added to the restricted list without collapsing the
// write jail"); Everyone was in the list anyway, since the original sandbox
// baseline.
//
// Realistic rather than theoretical: administrators open share roots with
// Everyone:F, and third-party installers ship loose ACLs. It needs no privilege,
// no symlink and no race. Checked and ruled out: C:\Users\Public\Documents
// grants BATCH, not Everyone, so the common "public folder" case is not this.
func TestWindowsRestrictedTokenDeniesWritesToEveryoneWritablePaths(t *testing.T) {
if os.Getenv("ZERO_SANDBOX_REAL_SMOKE") != "1" {
t.Skip("set ZERO_SANDBOX_REAL_SMOKE=1 to run real Windows sandbox smoke tests")
}
runnerExe := realSmokeExecutable(t, "ZERO_WINDOWS_COMMAND_RUNNER_EXE", WindowsSandboxCommandRunnerName)
root := t.TempDir()
outside := t.TempDir()
everyoneDir := filepath.Join(outside, "everyone-writable")
if err := os.MkdirAll(everyoneDir, 0o700); err != nil {
t.Fatalf("MkdirAll everyone-writable: %v", err)
}
// Granted through the production ACL applier, so the hostile DACL is built
// the same way a real one is rather than by a test-only shortcut.
snapshot, applied, err := applyWindowsACLPathGroup(windowsACLPathGroup{
Path: everyoneDir,
Entries: []WindowsACLEntry{{
Action: WindowsACLAllowWrite,
Path: everyoneDir,
Capability: "S-1-1-0",
}},
})
if err != nil {
t.Fatalf("grant Everyone write: %v", err)
}
if !applied {
t.Fatal("precondition: the Everyone grant did not apply, so there is no bypass to test for")
}
t.Cleanup(func() { _ = rollbackWindowsACLSnapshots([]windowsACLSnapshot{snapshot}) })
sandboxHome := filepath.Join(root, ".zero-sandbox")
// No DenyRead on purpose: that is what makes the runner choose the
// WRITE_RESTRICTED token, which is the default posture and the one whose
// restricted-SID list this test is about.
profile := PermissionProfile{
FileSystem: FileSystemPolicy{
Kind: FileSystemRestricted,
ReadRoots: []string{root},
WriteRoots: []WritableRoot{{Root: root, ProtectedMetadataNames: []string{".git", ".zero", ".agents"}}},
IncludePlatformRoots: true,
AllowTemp: true,
},
Network: NetworkPolicy{Mode: NetworkDeny},
}
config := WindowsSandboxCommandArgsOptions{
SandboxHome: sandboxHome,
CommandCWD: root,
WorkspaceRoots: []string{root},
PermissionProfile: profile,
SandboxLevel: WindowsSandboxLevelUnelevated,
}
// Granted root first. A jail that denies everything would satisfy the two
// assertions below while being useless, and this separates the two outcomes.
insideMarker := filepath.Join(root, "write-ok.txt")
runWindowsRealSmokeCommand(t, runnerExe, config, []string{
"cmd.exe", "/d", "/s", "/c", "echo ok>" + insideMarker,
}, 0)
if contents, err := os.ReadFile(insideMarker); err != nil || strings.TrimSpace(string(contents)) != "ok" {
t.Fatalf("sandboxed write to a granted root = %q, %v; want ok", contents, err)
}
// Control: an ordinary directory outside every granted root. This is the
// behaviour the Everyone case must match.
controlMarker := filepath.Join(outside, "control-denied.txt")
runWindowsRealSmokeCommand(t, runnerExe, config, deniedWriteCommand(controlMarker), deniedWriteExitCode)
if _, err := os.Stat(controlMarker); err == nil {
t.Fatal("precondition: the sandbox allowed a write to an ordinary path outside every granted root, so this test cannot measure the Everyone case")
}
// The assertion. Same as the control in every respect except the DACL.
everyoneMarker := filepath.Join(everyoneDir, "everyone-denied.txt")
runWindowsRealSmokeCommand(t, runnerExe, config, deniedWriteCommand(everyoneMarker), deniedWriteExitCode)
if _, err := os.Stat(everyoneMarker); err == nil {
t.Error("the sandbox wrote outside every granted root because the path grants Everyone write; the restricted-SID list is satisfied by a SID every principal carries")
} else if !os.IsNotExist(err) {
t.Errorf("stat the Everyone-writable marker: %v", err)
}
}
// deniedWriteExitCode is chosen so a denial cannot be confused with the runner
// failing to start the command at all.
//
// The obvious spelling of these assertions is `echo leaked>path` expecting exit
// 1, but the runner itself exits 1 for its own errors — a failed marker
// validation, a bad argument, a token it could not build. A test written that
// way passes both when the sandbox denies the write and when nothing ever ran,
// and the second case proves nothing while looking identical to success. On a
// test whose whole job is catching a confinement regression, that is a failure
// mode to design out rather than hope about.
//
// 77 is arbitrary beyond being outside the range the runner produces for itself.
const deniedWriteExitCode = 77
// deniedWriteCommand attempts a write and reports deniedWriteExitCode when the
// redirect is refused, so the exit code also proves cmd.exe actually ran.
func deniedWriteCommand(marker string) []string {
return []string{"cmd.exe", "/d", "/s", "/c", "echo leaked>" + marker + " || exit " + strconv.Itoa(deniedWriteExitCode)}
}
func powershellSingleQuote(value string) string {
out := "'"
for _, r := range value {
+9 -45
View File
@@ -88,55 +88,19 @@ func createWindowsRestrictedTokenFromBase(base windows.Token, capabilitySIDs []w
if err != nil {
return 0, err
}
worldSID, err := windows.CreateWellKnownSid(windows.WinWorldSid)
if err != nil {
return 0, fmt.Errorf("create world SID: %w", err)
}
entries := make([]windows.SIDAndAttributes, 0, len(capabilitySIDs)+2)
for _, sid := range capabilitySIDs {
entries = append(entries, windows.SIDAndAttributes{Sid: sid.sid})
}
entries = append(entries, windows.SIDAndAttributes{Sid: sidFromBytes(logonSID)})
// The World SID (S-1-1-0, Everyone) is added ONLY to the token that does not
// carry WRITE_RESTRICTED, and putting it back unconditionally would reopen a
// write-jail bypass.
//
// A restricted SID is a key to every object whose DACL names it. That is why
// the runner refuses to add the user SID, Administrators or SYSTEM — "each
// has write access nearly everywhere". Everyone is the broadest of the lot:
// every principal carries it, so under WRITE_RESTRICTED the second
// (restricted-SID) check passes for free on any path whose DACL grants
// Everyone write, and confinement falls back to the ordinary user's own
// permissions — the exact boundary this token exists to be stricter than. It
// needs no privilege, no symlink and no race: an Everyone-writable directory
// is enough, and share roots opened with Everyone:F and loose installer ACLs
// supply them. It was present from the original sandbox baseline,
// uncommented, and under WRITE_RESTRICTED nothing depends on it, because that
// flag already exempts reads from the restricted-SID check.
//
// Without the flag it IS load-bearing and cannot simply be dropped. The
// restricted-SID check then applies to READS too, and default Windows DACLs
// grant BUILTIN\Users rather than anything in this list, so a token without
// Everyone cannot open cmd.exe — the process dies at launch with
// STATUS_ACCESS_DENIED (0xC0000022) before it runs anything. That path is
// only taken when the profile configures DenyRead, which is already the
// posture that trades capability for read-deny enforcement (#612).
//
// So the bypass survives for DenyRead profiles, deliberately and narrowly,
// rather than being traded for a sandbox that cannot start a command. Closing
// it there needs a different mechanism (a read-side grant that is not a
// universal group), tracked separately.
//
// The logon SID above stays in both modes: it is this token's own rather than
// a broad group, and broadenWindowsRestrictedTokenDefaultDacl depends on it so
// the process can use pipes and events it creates for itself.
//
// Anything added here needs the same scrutiny — Authenticated Users, Users,
// INTERACTIVE and BATCH would each produce this bypass on a DACL naming them.
if !writeRestricted {
worldSID, err := windows.CreateWellKnownSid(windows.WinWorldSid)
if err != nil {
return 0, fmt.Errorf("create world SID: %w", err)
}
entries = append(entries, windows.SIDAndAttributes{Sid: worldSID})
}
entries = append(entries,
windows.SIDAndAttributes{Sid: sidFromBytes(logonSID)},
windows.SIDAndAttributes{Sid: worldSID},
)
// WRITE_RESTRICTED scopes the restricted-SID check to write-type accesses:
// reads use only the normal token identity, so the sandboxed process can
+17 -9
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"os"
"runtime"
"strings"
"time"
"unicode"
@@ -2789,14 +2788,23 @@ func (m model) View() tea.View {
// only when the value changes, so gating this costs nothing (§10).
view.KeyboardEnhancements.ReportEventTypes = m.dictation.voiceModeEnabled
if m.wantsMouseCapture() {
// AllMotion (1003) is what hover highlighting needs: it reports cursor
// movement with no button pressed, where CellMotion (1002) reports motion
// only while a button is held. mouseModeFor decides which is safe here,
// and documents why one is not simply better than the other: a terminal
// that does not implement 1003 sends nothing at all rather than degrading.
// The 15ms throttle (mouseEventThrottleInterval) already bounds the redraw
// rate from AllMotion's extra motion events.
view.MouseMode = mouseModeFor(runtime.GOOS, os.Getenv, isRunningUnderPRoot())
if isRunningUnderPRoot() {
// Under PRoot the AllMotion (1003) sequence doesn't work
// reliably, breaking touch-gesture scrolling. Fall back to
// CellMotion which still delivers wheel events, clicks, and
// drag — the only thing lost is hover-highlighting.
view.MouseMode = tea.MouseModeCellMotion
} else {
// AllMotion (not CellMotion) is required for hover highlighting:
// it reports cursor movement even with no button pressed.
// CellMotion only reports motion while a button is held (drag) —
// see bubbletea's MouseMode docs. AllMotion has marginally worse
// terminal compatibility but is well supported by the terminals
// this app targets; the existing 15ms mouse-event throttle
// (mouseEventThrottleInterval) already bounds the redraw rate
// from the extra motion events.
view.MouseMode = tea.MouseModeAllMotion
}
}
return view
}
-64
View File
@@ -8,70 +8,6 @@ import (
tea "charm.land/bubbletea/v2"
)
// mouseModeEnv forces a mouse mode: "all", "cell", or "off"/"none". It exists
// because the failure it guards against is invisible from here and total for the
// user: a terminal we guessed wrong about delivers no mouse events at all, and
// there is no in-app way to recover. Anything unrecognised is ignored rather
// than treated as "off", so a typo cannot silently kill the mouse.
const mouseModeEnv = "ZERO_MOUSE_MODE"
// mouseModeFor picks the mouse reporting mode, preferring the widest support
// over the nicest behaviour.
//
// The trade is asymmetric. AllMotion (1003) buys exactly one thing, hover
// highlighting, because wheel, click and drag all arrive under CellMotion
// (1002). But when a terminal does not implement 1003, it does not degrade to
// 1002: no mouse event arrives at all. The app holds the alternate screen, so
// the terminal's own scrollback and wheel are gone too, and the user is left
// with no way to scroll by mouse. A long answer then looks like a hang rather
// than like a missing highlight, which is how #870 was reported.
//
// So AllMotion is asked for only where it is known to work.
func mouseModeFor(goos string, env func(string) string, underPRoot bool) tea.MouseMode {
switch strings.ToLower(strings.TrimSpace(env(mouseModeEnv))) {
case "all":
return tea.MouseModeAllMotion
case "cell":
return tea.MouseModeCellMotion
case "off", "none":
return tea.MouseModeNone
}
if underPRoot {
return tea.MouseModeCellMotion
}
if goos == "windows" && !windowsTerminalReportsAllMotion(env) {
return tea.MouseModeCellMotion
}
return tea.MouseModeAllMotion
}
// windowsTerminalReportsAllMotion reports whether the Windows terminal in use is
// one that handles 1003.
//
// Identified terminals are trusted: Windows Terminal sets WT_SESSION, and hosts
// like VS Code and mintty set TERM_PROGRAM. The legacy console host sets
// neither, and it is what a user gets by double-clicking zero.exe or running it
// from an old cmd window, so an unidentified Windows terminal is assumed to be
// that one. Guessing wrong in this direction costs a hover highlight; guessing
// wrong in the other direction costs every mouse event.
// KNOWN LIMIT: these variables are INHERITED, so they answer for the process
// that set them rather than for the console host attached right now. A shell
// launched from Windows Terminal, or from Git Bash, that later runs zero.exe
// against the legacy console still carries them, and this returns true for a
// host that may drop every mouse event.
//
// Left as is on purpose. Deciding it properly means asking the console host
// rather than the environment, and erring the other way costs every Windows
// Terminal user their hover highlighting for a case that needs an unusual
// launch path to reach. ZERO_MOUSE_MODE=cell is the recourse in the meantime,
// which is the main reason that override exists.
// TestInheritedWindowsTerminalEnvStillAsksForAllMotion pins this, so it is a
// documented limit rather than a surprise.
func windowsTerminalReportsAllMotion(env func(string) string) bool {
return strings.TrimSpace(env("WT_SESSION")) != "" ||
strings.TrimSpace(env("TERM_PROGRAM")) != ""
}
// parseTracerPid returns true when /proc/self/status contains a non-zero
// TracerPid. This detects any ptrace tracer (PRoot, gdb, strace, dlv, etc.),
// not PRoot specifically. That's intentional: under any ptrace tracer the
-151
View File
@@ -1,151 +0,0 @@
package tui
import (
"testing"
tea "charm.land/bubbletea/v2"
)
func envFrom(pairs map[string]string) func(string) string {
return func(key string) string { return pairs[key] }
}
// AllMotion (1003) buys hover highlighting and nothing else: wheel, click and
// drag all arrive under CellMotion (1002). When a terminal does not support
// 1003 the cost is not a missing highlight, it is that NO mouse event arrives
// at all, and because the app holds the alternate screen the terminal's own
// scrollback is gone too. The user cannot scroll by any means, and a screen of
// cut-off output reads as a hang (#870).
//
// So the rule is: ask for AllMotion only where it is known to work.
func TestMouseModeFallsBackWhereAllMotionIsUnreliable(t *testing.T) {
tests := []struct {
name string
goos string
env map[string]string
underPRoot bool
want tea.MouseMode
}{
{
name: "linux terminal",
goos: "linux",
want: tea.MouseModeAllMotion,
},
{
name: "macOS terminal",
goos: "darwin",
want: tea.MouseModeAllMotion,
},
{
// Pre-existing carve-out: under PRoot the 1003 sequence is unreliable
// and breaks touch-gesture scrolling.
name: "under PRoot",
goos: "linux",
underPRoot: true,
want: tea.MouseModeCellMotion,
},
{
// #870. The legacy Windows console does not handle 1003 reliably, and
// it is what a user gets by running zero.exe from anywhere that is not
// Windows Terminal.
name: "legacy Windows console",
goos: "windows",
want: tea.MouseModeCellMotion,
},
{
name: "Windows Terminal",
goos: "windows",
env: map[string]string{"WT_SESSION": "d1c2-session"},
want: tea.MouseModeAllMotion,
},
{
name: "VS Code terminal on Windows",
goos: "windows",
env: map[string]string{"TERM_PROGRAM": "vscode"},
want: tea.MouseModeAllMotion,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := mouseModeFor(test.goos, envFrom(test.env), test.underPRoot)
if got != test.want {
t.Fatalf("mouseModeFor(%q) = %v, want %v", test.goos, got, test.want)
}
})
}
}
// A KNOWN LIMIT, pinned deliberately rather than left to be rediscovered.
//
// Windows children inherit the parent environment, so WT_SESSION or
// TERM_PROGRAM can outlive the terminal that set them: a shell started from
// Windows Terminal, or from Git Bash, that later runs zero.exe against a
// different console host still carries them, and mouseModeFor then asks for
// AllMotion on a host that may drop every mouse event. That is the #870 failure
// class arriving by a narrower door.
//
// It is not fixed here, and this test says so out loud. Telling an inherited
// variable from a live one needs the actual console host rather than the
// environment, and guessing wrong in the other direction costs every Windows
// Terminal user their hover highlighting. ZERO_MOUSE_MODE=cell is the recourse
// until that host check exists, which is why the override is not a nicety.
//
// If someone later adds real host detection, this test SHOULD fail. That is the
// point of it.
func TestInheritedWindowsTerminalEnvStillAsksForAllMotion(t *testing.T) {
for _, env := range []map[string]string{
{"WT_SESSION": "inherited-from-an-ancestor"},
{"TERM_PROGRAM": "vscode"},
} {
if got := mouseModeFor("windows", envFrom(env), false); got != tea.MouseModeAllMotion {
t.Fatalf("mouseModeFor(windows, %v) = %v; if this now returns CellMotion the "+
"inherited-environment limit has been fixed, so delete this test and its note", env, got)
}
// The escape hatch has to keep working for exactly this case, since it is
// the only recourse a user has when the guess is wrong.
withOverride := map[string]string{mouseModeEnv: "cell"}
for key, value := range env {
withOverride[key] = value
}
if got := mouseModeFor("windows", envFrom(withOverride), false); got != tea.MouseModeCellMotion {
t.Fatalf("ZERO_MOUSE_MODE=cell did not override inherited %v, so an affected user has no recourse", env)
}
}
}
// The override exists so a user who hits an unsupported terminal can fix it
// themselves without waiting for a release, and so a maintainer can ask a bug
// reporter to test one specific mode.
func TestMouseModeOverrideWins(t *testing.T) {
tests := []struct {
value string
want tea.MouseMode
}{
{"cell", tea.MouseModeCellMotion},
{"CELL", tea.MouseModeCellMotion},
{" cell ", tea.MouseModeCellMotion},
{"all", tea.MouseModeAllMotion},
{"off", tea.MouseModeNone},
{"none", tea.MouseModeNone},
}
for _, test := range tests {
t.Run(test.value, func(t *testing.T) {
// windows + PRoot would otherwise force CellMotion, so "all" proves the
// override beats the fallbacks rather than agreeing with them.
got := mouseModeFor("windows", envFrom(map[string]string{mouseModeEnv: test.value}), true)
if got != test.want {
t.Fatalf("%s=%q gave %v, want %v", mouseModeEnv, test.value, got, test.want)
}
})
}
}
// An unreadable override must not silently disable the mouse: fall through to
// the normal decision rather than guessing.
func TestMouseModeIgnoresAnUnknownOverride(t *testing.T) {
got := mouseModeFor("linux", envFrom(map[string]string{mouseModeEnv: "sideways"}), false)
if got != tea.MouseModeAllMotion {
t.Fatalf("an unknown override changed the mode to %v, want the normal AllMotion", got)
}
}