返回 DeepSeek-Reasonix
profile_test.go
根目录 / internal / evidence / profile_test.go
1 package evidence
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "testing"
8 )
9
10 func TestClassifyEffectBashReadersAndWriters(t *testing.T) {
11 tests := []struct {
12 name string
13 command string
14 want EffectProfile
15 }{
16 {name: "status", command: "git status", want: EffectProfile{Known: true, ReadOnly: true, Reason: ReasonReadOnly}},
17 {name: "diff", command: "git diff", want: EffectProfile{Known: true, ReadOnly: true, Reason: ReasonReadOnly}},
18 {name: "push", command: "git push origin main", want: EffectProfile{Known: true, ExternalState: true, UsesNetwork: true, Reason: ReasonExternalState, Targets: []Target{{Kind: TargetExternal}}}},
19 {name: "force push", command: "git push --force origin main", want: EffectProfile{Known: true, ExternalState: true, UsesNetwork: true, Destructive: true, Irreversible: true, Reason: ReasonExternalState, Targets: []Target{{Kind: TargetExternal}}}},
20 {name: "unknown", command: "custom-tool --run", want: EffectProfile{WorkspaceWrite: true, Reason: "command effects are not statically known"}},
21 }
22 for _, tt := range tests {
23 t.Run(tt.name, func(t *testing.T) {
24 args, _ := json.Marshal(map[string]string{"command": tt.command})
25 got := ClassifyEffect(EffectInput{ToolName: "bash", Args: args})
26 if got.Known != tt.want.Known || got.ReadOnly != tt.want.ReadOnly || got.WorkspaceWrite != tt.want.WorkspaceWrite ||
27 got.ExternalState != tt.want.ExternalState || got.Destructive != tt.want.Destructive ||
28 got.Irreversible != tt.want.Irreversible || got.UsesNetwork != tt.want.UsesNetwork {
29 t.Fatalf("profile = %+v, want %+v", got, tt.want)
30 }
31 })
32 }
33 }
34
35 func TestClassifyEffectFileAndMCP(t *testing.T) {
36 edit := ClassifyEffect(EffectInput{
37 ToolName: "edit_file",
38 Args: json.RawMessage(`{"path":"internal/agent/agent.go"}`),
39 })
40 if !edit.Known || !edit.WorkspaceWrite || len(edit.Targets) != 1 || edit.Targets[0].Path != "internal/agent/agent.go" {
41 t.Fatalf("edit profile = %+v", edit)
42 }
43
44 read := ClassifyEffect(EffectInput{
45 ToolName: "read_file",
46 Args: json.RawMessage(`{"path":"auth/session.go"}`),
47 StaticReadOnly: true,
48 })
49 if !read.ReadOnly || read.MutatesState() {
50 t.Fatalf("read-only file must not mutate: %+v", read)
51 }
52
53 mcpRead := ClassifyEffect(EffectInput{
54 ToolName: "mcp__srv__get",
55 Hint: CallHint{Present: true, ReadOnly: true},
56 })
57 if !mcpRead.ReadOnly || mcpRead.MutatesState() {
58 t.Fatalf("MCP read-only annotation = %+v", mcpRead)
59 }
60
61 opaque := ClassifyEffect(EffectInput{ToolName: "mcp__srv__write", Args: json.RawMessage(`{}`)})
62 if opaque.Known || !opaque.OpaqueWriter() {
63 t.Fatalf("unknown MCP writer must fail closed: %+v", opaque)
64 }
65 }
66
67 func TestClassifyEffectKillShellIsHostStateOnly(t *testing.T) {
68 profile := ClassifyEffect(EffectInput{ToolName: "kill_shell", Args: json.RawMessage(`{"job_id":"task-1"}`)})
69 if !profile.Known || profile.ReadOnly || !profile.HostState || profile.WorkspaceWrite || profile.RepoMetadata || profile.ExternalState {
70 t.Fatalf("kill_shell profile = %+v, want host-state-only mutation", profile)
71 }
72 effects := profile.ToolEffects()
73 if !effects.StateMutation || effects.WorkspaceMutation || effects.ContentMutation || effects.RepositoryMutation {
74 t.Fatalf("kill_shell effects = %+v, want state mutation without workspace mutation", effects)
75 }
76 }
77
78 func TestClassifyWriteScopeScratchWriteFile(t *testing.T) {
79 workspace := t.TempDir()
80 scratchPath := filepath.Join(os.TempDir(), "reasonix-scope-probe.py")
81 if got := ClassifyWriteScope(scratchPath, workspace, nil); got != WriteScopeScratch {
82 t.Fatalf("write_file /tmp = %s, want scratch", got)
83 }
84 if got := ClassifyWriteScope("internal/agent/agent.go", workspace, nil); got != WriteScopeWorkspace {
85 t.Fatalf("workspace edit = %s, want workspace", got)
86 }
87 }
88
89 func TestClassifyEffectPrefersReceiptPaths(t *testing.T) {
90 got := ClassifyEffect(EffectInput{
91 ToolName: "edit_file",
92 Args: json.RawMessage(`{"path":"expected.go"}`),
93 ActualPaths: []string{"actual.go"},
94 })
95 if len(got.Targets) != 1 || got.Targets[0].Path != "actual.go" {
96 t.Fatalf("receipt paths must win: %+v", got)
97 }
98 }
99
100 func TestEffectProfileCloneCopiesTargets(t *testing.T) {
101 orig := EffectProfile{Targets: []Target{{Path: "a.go", Kind: TargetFile}}}
102 clone := orig.Clone()
103 clone.Targets[0].Path = "b.go"
104 if orig.Targets[0].Path != "a.go" {
105 t.Fatal("clone must not share the target slice")
106 }
107 }
108
108 lines GO