Files
zero/internal/agent/command_prefix_test.go
PierrunoYTandClaude Opus 5.5 13e2596b2e chore: move repository references to Twigpine/zero (#1105)
Update the Go module path, imports, install scripts, docs, workflows,
packaging and action defaults from Gitlawb/zero to Twigpine/zero. The npm
scope (already @twigpine/zero on main, with its @gitlawb/zero migration
notes), LICENSE, action author, the OpenGateway provider and historical
CHANGELOG links are unchanged.

The X-AIMLAPI-Integration-Repo header keeps Gitlawb/zero: it is what
AIMLAPI sees for attribution, so changing it needs a maintainer's yes.
docs/INSTALL.md keeps `go install github.com/Gitlawb/zero/...`, because
every tagged release so far declares that module path. The Homebrew
formula comment names the Twigpine/homebrew-tap tap.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 07:13:21 +08:00

234 lines
8.0 KiB
Go

package agent
import (
"runtime"
"testing"
"github.com/Twigpine/zero/internal/sandbox"
"github.com/Twigpine/zero/internal/tools"
)
func TestProposedCommandPrefixUsesSafeSimpleCommands(t *testing.T) {
got := proposedCommandPrefix("bash", map[string]any{"command": "go test ./..."})
want := []string{"go", "test", "./..."}
if !equalStringSlices(got, want) {
t.Fatalf("prefix = %#v, want %#v", got, want)
}
}
func TestProposedCommandPrefixSupportsExecCommand(t *testing.T) {
got := proposedCommandPrefix("exec_command", map[string]any{"cmd": "go test ./..."})
want := []string{"go", "test", "./..."}
if !equalStringSlices(got, want) {
t.Fatalf("prefix = %#v, want %#v", got, want)
}
}
func TestProposedCommandPrefixHonorsValidatedRequestedPrefix(t *testing.T) {
got := proposedCommandPrefix("bash", map[string]any{
"command": "git status --short",
"prefix_rule": []any{"git", "status"},
})
want := []string{"git", "status"}
if !equalStringSlices(got, want) {
t.Fatalf("prefix = %#v, want %#v", got, want)
}
}
func TestProposedCommandPrefixSupportsSegmentedCommands(t *testing.T) {
got := proposedCommandPrefix("bash", map[string]any{"command": "ps aux | head -5"})
if runtime.GOOS == "windows" {
// head is MSYS-prone on Windows (#458), so proposedCommandPrefix must
// not offer "ps aux" as a reusable prefix here: approving it would
// escalate the whole command, including the uncovered head segment,
// to bypass the sandbox unreviewed. See
// TestProposedCommandPrefixRejectsPrefixLeavingUnsafeTailUncovered for
// the platform-independent regression coverage of this behavior.
if got != nil {
t.Fatalf("expected no prefix on Windows because head is MSYS-prone, got %#v", got)
}
return
}
want := []string{"ps", "aux"}
if !equalStringSlices(got, want) {
t.Fatalf("prefix = %#v, want %#v", got, want)
}
}
// TestProposedCommandPrefixRejectsPrefixLeavingUnsafeTailUncovered guards
// against proposedCommandPrefix offering to approve one segment of a
// multi-segment command (e.g. "ps aux") while a different segment (e.g. "npm
// install") is not known-safe. shellExecutionArgsForApproval escalates the
// entire command once any prefix is approved, so an uncovered unsafe segment
// would bypass the sandbox unreviewed. Uses npm, which is never known-safe on
// any platform, so the assertion does not depend on runtime.GOOS.
func TestProposedCommandPrefixRejectsPrefixLeavingUnsafeTailUncovered(t *testing.T) {
if got := proposedCommandPrefix("bash", map[string]any{"command": "ps aux && npm install"}); got != nil {
t.Fatalf("expected no prefix because npm segment is not known-safe, got %#v", got)
}
}
func TestProposedCommandPrefixHonorsRequestedPrefixAcrossSegments(t *testing.T) {
got := proposedCommandPrefix("bash", map[string]any{
"command": "git status --short && git status --branch",
"prefix_rule": []any{"git", "status"},
})
want := []string{"git", "status"}
if !equalStringSlices(got, want) {
t.Fatalf("prefix = %#v, want %#v", got, want)
}
}
func TestProposedCommandPrefixRejectsRequestedPrefixThatDoesNotCoverSegments(t *testing.T) {
got := proposedCommandPrefix("bash", map[string]any{
"command": "ps aux && npm install",
"prefix_rule": []any{"ps", "aux"},
})
if got != nil {
t.Fatalf("partial requested prefix should be rejected, got %#v", got)
}
}
func TestProposedCommandPrefixRejectsUnsafeRequestedPrefix(t *testing.T) {
got := proposedCommandPrefix("bash", map[string]any{
"command": "git status --short",
"prefix_rule": []any{"git"},
})
if got != nil {
t.Fatalf("broad requested prefix should be rejected, got %#v", got)
}
}
// The launcher denylist is only worth anything if it holds on the path a user
// actually sees. A model naming its own prefix_rule is that path: before
// launcher names were normalized, "python3.11" was offered as a one-token
// grant, and hasStringPrefix then matched every later python3.11 invocation.
func TestProposedCommandPrefixRejectsLauncherSpellingsInRequestedPrefix(t *testing.T) {
for _, testCase := range []struct {
command string
rule []any
}{
{"python3.11 script.py", []any{"python3.11"}},
{"python.exe script.py", []any{"python.exe"}},
{"node.exe app.js", []any{"node.exe"}},
{"cmd /c whoami", []any{"cmd"}},
{"busybox sh -c id", []any{"busybox"}},
{"uv run main.py", []any{"uv", "run"}},
{"python3.11 -c import_os", []any{"python3.11", "-c"}},
} {
got := proposedCommandPrefix("bash", map[string]any{
"command": testCase.command,
"prefix_rule": testCase.rule,
})
if got != nil {
t.Errorf("prefix_rule %v for %q was offered as %#v, want rejected", testCase.rule, testCase.command, got)
}
}
}
func TestProposedCommandPrefixStillOffersOrdinaryCommands(t *testing.T) {
got := proposedCommandPrefix("bash", map[string]any{
"command": "cargo build --release",
"prefix_rule": []any{"cargo", "build"},
})
if len(got) != 2 || got[0] != "cargo" || got[1] != "build" {
t.Fatalf("ordinary prefix rule = %#v, want [cargo build]", got)
}
}
func TestProposedCommandPrefixRejectsUnsafeShellForms(t *testing.T) {
cases := []string{
"cat < in > out",
"FOO=bar go test",
"echo $(whoami)",
"cat *.go",
"bash -lc go test",
}
for _, command := range cases {
t.Run(command, func(t *testing.T) {
if got := proposedCommandPrefix("bash", map[string]any{"command": command}); got != nil {
t.Fatalf("unsafe command got prefix %#v", got)
}
})
}
}
func TestProposedCommandPrefixRejectsUnsafeLaunchers(t *testing.T) {
cases := []string{
"find . -type f",
"xargs rm -rf /tmp/x",
"timeout 5 go test ./...",
"nice go test ./...",
"nohup go test ./...",
"watch ls",
"ssh host ls",
"make test",
"npm run dev",
"command git status",
"eval echo hi",
"exec echo hi",
"python script.py",
"node script.js",
"./script.sh --flag",
"/tmp/script.sh --flag",
}
for _, command := range cases {
t.Run(command, func(t *testing.T) {
if got := proposedCommandPrefix("bash", map[string]any{"command": command}); got != nil {
t.Fatalf("unsafe launcher got prefix %#v", got)
}
})
}
}
func TestMatchCommandPrefixCoversSegmentedCommandWithSafeTail(t *testing.T) {
engine := sandbox.NewEngine(sandbox.EngineOptions{WorkspaceRoot: t.TempDir()})
engine.GrantCommandPrefixForSession("bash", []string{"ps", "aux"})
// head is MSYS-prone on Windows (#458) and must not count as a known-safe tail.
command := "ps aux | head -5"
if runtime.GOOS == "windows" {
command = "ps aux | echo ok"
}
grant, ok, session := matchCommandPrefix("bash", map[string]any{"command": command}, Options{Sandbox: engine})
if !ok || !session || !equalStringSlices(grant.Prefix, []string{"ps", "aux"}) {
t.Fatalf("match = %#v ok=%v session=%v, want session ps aux prefix", grant, ok, session)
}
}
func TestKnownSafeCommandSegmentRejectsMsysProneOnWindows(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("windows-only known-safe MSYS guard")
}
for _, command := range [][]string{{"head", "-5"}, {"cat", "file.txt"}, {"grep", "pat"}} {
if knownSafeCommandSegment(command) {
t.Fatalf("expected %q to be unsafe on Windows, got known-safe", command)
}
}
if !knownSafeCommandSegment([]string{"echo", "ok"}) {
t.Fatal("expected echo to remain known-safe on Windows")
}
if !tools.MsysProneCommandName("head") {
t.Fatal("expected head to be MSYS-prone")
}
}
func TestMatchCommandPrefixRejectsUncoveredSegment(t *testing.T) {
engine := sandbox.NewEngine(sandbox.EngineOptions{WorkspaceRoot: t.TempDir()})
engine.GrantCommandPrefixForSession("bash", []string{"ps", "aux"})
if grant, ok, session := matchCommandPrefix("bash", map[string]any{"command": "ps aux && npm install"}, Options{Sandbox: engine}); ok {
t.Fatalf("match = %#v session=%v, want no match because npm segment is uncovered", grant, session)
}
}
func TestProposedCommandPrefixRejectsRequestedUnsafeLauncherPrefix(t *testing.T) {
got := proposedCommandPrefix("bash", map[string]any{
"command": "find . -type f",
"prefix_rule": []any{"find", "."},
})
if got != nil {
t.Fatalf("unsafe requested launcher prefix should be rejected, got %#v", got)
}
}