返回 DeepSeek-Reasonix
bash_git_metadata_test.go
根目录 / internal / tool / builtin / bash_git_metadata_test.go
1 //go:build !windows
2
3 package builtin
4
5 import (
6 "context"
7 "errors"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "runtime"
12 "strings"
13 "testing"
14
15 "reasonix/internal/sandbox"
16 )
17
18 func gitMetadataWorkspace(t *testing.T) string {
19 t.Helper()
20 if _, err := exec.LookPath("git"); err != nil {
21 t.Skip("git is unavailable")
22 }
23 root, err := filepath.EvalSymlinks(t.TempDir())
24 if err != nil {
25 t.Fatal(err)
26 }
27 ws := filepath.Join(root, "ws")
28 if out, err := exec.Command("git", "init", "-q", ws).CombinedOutput(); err != nil {
29 t.Fatalf("git init: %v: %s", err, out)
30 }
31 return ws
32 }
33
34 func TestSandboxWriteHintAttributesGitMetadata(t *testing.T) {
35 ws := gitMetadataWorkspace(t)
36 spec := sandbox.Spec{Mode: "enforce", WriteRoots: []string{ws}}
37 exit := errors.New("exit status 255")
38 for _, out := range []string{
39 "error: could not write config file .git/config: Operation not permitted",
40 "cp: " + filepath.Join(ws, ".git", "hooks", "pre-commit") + ": Operation not permitted",
41 "bash: .git/hooks/pre-push: Read-only file system",
42 } {
43 hint := appendSandboxWriteHint(out, exit, bashParams{Command: "x"}, spec, "", ws)
44 if !strings.Contains(hint, sandbox.GitMetadataDeniedCode) || strings.Contains(hint, "Retry the same command with structured additional_write_dirs") {
45 t.Fatalf("git metadata denial not attributed for %q:\n%s", out, hint)
46 }
47 }
48 for _, out := range []string{
49 "touch: /outside/file: Operation not permitted",
50 "fatal: Unable to create '" + filepath.Join(ws, "main", ".git", "index.lock") + "': Operation not permitted",
51 "error: could not write .git/config.lock-other: Operation not permitted",
52 } {
53 hint := appendSandboxWriteHint(out, exit, bashParams{Command: "x"}, spec, "", ws)
54 if strings.Contains(hint, sandbox.GitMetadataDeniedCode) {
55 t.Fatalf("unrelated denial attributed to git metadata for %q:\n%s", out, hint)
56 }
57 }
58 exitZero := "error: could not write config file .git/config: Operation not permitted\nbranch 'topic' set up to track 'main'."
59 if hint := appendSandboxWriteHint(exitZero, nil, bashParams{Command: "x"}, spec, "", ws); !strings.Contains(hint, sandbox.GitMetadataDeniedCode) {
60 t.Fatalf("a refused write git reports with exit 0 is still attributed: %s", hint)
61 }
62 if hint := appendSandboxWriteHint("all good", nil, bashParams{Command: "x"}, spec, "", ws); hint != "all good" {
63 t.Fatalf("an ordinary success carries no note: %s", hint)
64 }
65 }
66
67 // The bash tool itself, under the real Seatbelt profile, tells the model the
68 // refused path is host-protected Git metadata.
69 func TestBashToolAttributesGitConfigDenial(t *testing.T) {
70 if runtime.GOOS != "darwin" || !sandbox.Available() {
71 t.Skip("needs the macOS Seatbelt backend")
72 }
73 ws := gitMetadataWorkspace(t)
74 sh := sandbox.ResolveShell("", "", nil)
75 tool := bash{sb: sandbox.Spec{Mode: "enforce", WriteRoots: []string{ws}}, shell: sh, workDir: ws}
76 out, err := tool.Execute(context.Background(), argsJSON(t, map[string]any{"command": "git config user.name Someone"}))
77 if err == nil {
78 t.Fatalf("git config must be refused, got %q", out)
79 }
80 if !strings.Contains(out, sandbox.GitMetadataDeniedCode) || !strings.Contains(out, ".git/config") {
81 t.Fatalf("denial not attributed to host-protected git metadata:\n%s", out)
82 }
83 data, readErr := os.ReadFile(filepath.Join(ws, ".git", "config"))
84 if readErr != nil || strings.Contains(string(data), "Someone") {
85 t.Fatalf("config changed: %v %s", readErr, data)
86 }
87 }
88
89 func TestBashToolAttributesExitZeroConfigRefusal(t *testing.T) {
90 if runtime.GOOS != "darwin" || !sandbox.Available() {
91 t.Skip("needs the macOS Seatbelt backend")
92 }
93 ws := gitMetadataWorkspace(t)
94 for _, args := range [][]string{{"-c", "user.name=T", "-c", "user.email=t@example.com", "commit", "-q", "--allow-empty", "-m", "i"}, {"branch", "topic"}} {
95 if out, err := exec.Command("git", append([]string{"-C", ws}, args...)...).CombinedOutput(); err != nil {
96 t.Fatalf("git %v: %v %s", args, err, out)
97 }
98 }
99 tool := bash{sb: sandbox.Spec{Mode: "enforce", WriteRoots: []string{ws}}, shell: sandbox.ResolveShell("", "", nil), workDir: ws}
100 out, err := tool.Execute(context.Background(), argsJSON(t, map[string]any{"command": "git branch --set-upstream-to=HEAD topic"}))
101 if err != nil || !strings.Contains(out, sandbox.GitMetadataDeniedCode) {
102 t.Fatalf("exit-0 refusal must be attributed: %v\n%s", err, out)
103 }
104 }
105
106 func TestBashToolRefusesAHardLinkedGitConfig(t *testing.T) {
107 if runtime.GOOS != "darwin" || !sandbox.Available() {
108 t.Skip("needs the macOS Seatbelt backend")
109 }
110 ws := gitMetadataWorkspace(t)
111 if err := os.Link(filepath.Join(ws, ".git", "config"), filepath.Join(ws, "cfg")); err != nil {
112 t.Fatal(err)
113 }
114 tool := bash{sb: sandbox.Spec{Mode: "enforce", WriteRoots: []string{ws}}, shell: sandbox.ResolveShell("", "", nil), workDir: ws}
115 out, err := tool.Execute(context.Background(), argsJSON(t, map[string]any{"command": "echo '[core]' >> cfg"}))
116 if !errors.Is(err, sandbox.ErrGitMetadataLinked) {
117 t.Fatalf("a hard-linked config must refuse the launch: %v %q", err, out)
118 }
119 if data, _ := os.ReadFile(filepath.Join(ws, ".git", "config")); strings.Contains(string(data), "[core]\n[core]") {
120 t.Fatal("the command ran")
121 }
122 }
123
123 lines GO