返回 DeepSeek-Reasonix
extensions_argument_validation_test.go
根目录 / internal / agent / extensions_argument_validation_test.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "strings"
7 "testing"
8
9 "reasonix/internal/event"
10 "reasonix/internal/extension"
11 "reasonix/internal/extension/dispatch"
12 "reasonix/internal/extension/protocol"
13 "reasonix/internal/provider"
14 "reasonix/internal/tool"
15 )
16
17 type strictRecordingTool struct {
18 recordingTool
19 }
20
21 func (s *strictRecordingTool) Schema() json.RawMessage {
22 return json.RawMessage(`{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}`)
23 }
24
25 func TestInvalidArgumentsDoNotReachToolBeforeExtension(t *testing.T) {
26 client := &fakeDispatchClient{}
27 d := newExtDispatcher(client, true, nil, extension.PointToolBefore)
28 rec := &strictRecordingTool{recordingTool: recordingTool{name: "read_file", readOnly: true}}
29 reg := tool.NewRegistry()
30 reg.Add(rec)
31 a := New(nil, reg, NewSession(""), Options{Extensions: d}, event.Discard)
32 out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "read_file", Arguments: `{"unexpected":true}`})
33 if out.errMsg == "" || !strings.Contains(out.output, "argument validation failed") {
34 t.Fatalf("outcome = %+v, want host validation failure", out)
35 }
36 if rec.execs != 0 {
37 t.Fatalf("invalid tool executed %d times", rec.execs)
38 }
39 if n := client.notifyCountFor(protocol.EventToolBefore); n != 0 {
40 t.Fatalf("tool.before sidecar calls = %d, want 0 before valid arguments", n)
41 }
42 }
43
44 func TestInvalidReplacementArgumentsRemainCorrectable(t *testing.T) {
45 client := &fakeDispatchClient{interceptFn: func(ev protocol.InterceptEvent, _ json.RawMessage) (protocol.InterceptResult, error) {
46 if ev == protocol.EventToolBefore {
47 return replaceWith(t, dispatch.ToolBeforePayload{Name: "read_file", Arguments: `{"arguments":{"path":"private-file"}}`}), nil
48 }
49 return protocol.InterceptResult{Decision: protocol.DecisionContinue}, nil
50 }}
51 d := newExtDispatcher(client, true, nil, extension.PointToolBefore)
52 rec := &strictRecordingTool{recordingTool: recordingTool{name: "read_file", readOnly: true}}
53 reg := tool.NewRegistry()
54 reg.Add(rec)
55 gate := &stubGate{}
56 a := New(nil, reg, NewSession(""), Options{Extensions: d, Gate: gate}, event.Discard)
57 for range 3 {
58 out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "read_file", Arguments: `{"path":"original"}`})
59 if out.blocked || !strings.Contains(out.output, `sole "arguments" wrapper`) {
60 t.Fatalf("replacement error: %+v", out)
61 }
62 }
63 if rec.execs != 0 || len(gate.checked) != 0 {
64 t.Fatal("invalid replacement reached execution or permission")
65 }
66 }
67
67 lines GO