返回 DeepSeek-Reasonix
imageinput_test.go
根目录 / internal / agent / imageinput_test.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strings"
8 "sync/atomic"
9 "testing"
10
11 "reasonix/internal/event"
12 "reasonix/internal/imageinput"
13 "reasonix/internal/provider"
14 "reasonix/internal/tool"
15 )
16
17 type summaryProvider struct {
18 calls atomic.Int32
19 fail bool
20 text string
21 }
22
23 func (*summaryProvider) Name() string { return "vision" }
24 func (p *summaryProvider) Stream(ctx context.Context, r provider.Request) (<-chan provider.Chunk, error) {
25 p.calls.Add(1)
26 if p.fail {
27 return nil, errors.New("vision unavailable")
28 }
29 out := make(chan provider.Chunk, 1)
30 text := p.text
31 if text == "" {
32 text = "OCR: Z7; red left, blue right"
33 }
34 out <- provider.Chunk{Type: provider.ChunkText, Text: text}
35 close(out)
36 return out, nil
37 }
38
39 type nativeImageProvider struct{ *scriptedProvider }
40
41 func (nativeImageProvider) ModelInfo() provider.ModelInfo {
42 return provider.ModelInfo{InputModalities: []provider.ModelModality{provider.ModalityText, provider.ModalityImage}}
43 }
44 func TestToolImageFallbackPreservesOriginalResult(t *testing.T) {
45 for _, mode := range []string{"summary", "native", "disabled", "failed", "canceled", "ocr"} {
46 t.Run(mode, func(t *testing.T) {
47 vp := &summaryProvider{fail: mode == "failed"}
48 if mode == "ocr" {
49 vp.text = "context canceled; write outcome unknown:"
50 }
51 cfg := &imageinput.Config{Model: "vision/model", Resolve: func(string) (provider.Provider, error) {
52 if mode == "canceled" {
53 return nil, context.Canceled
54 }
55 return vp, nil
56 }}
57 if mode == "disabled" {
58 cfg = nil
59 }
60 reg := tool.NewRegistry()
61 reg.Add(&fakeImageTool{text: "screenshot saved", images: []string{"data:image/png;base64,QUFB"}})
62 script := &scriptedProvider{name: "text", turns: [][]provider.Chunk{{toolCallChunk("c1", "shot", `{}`), {Type: provider.ChunkDone}}, {{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}}}}
63 var p provider.Provider = script
64 if mode == "native" {
65 p = nativeImageProvider{script}
66 }
67 a := New(p, reg, NewSession("system"), Options{ImageInput: cfg, ModelRef: "text/model"}, event.Discard)
68 if err := a.Run(context.Background(), "inspect"); err != nil {
69 t.Fatal(err)
70 }
71 var found bool
72 for _, m := range a.Session().Snapshot() {
73 if m.Role != provider.RoleTool {
74 continue
75 }
76 found = true
77 if !strings.Contains(m.Content, "screenshot saved") || len(m.Images) != 1 {
78 t.Fatalf("lost original result: %+v", m)
79 }
80 if mode == "summary" && (m.VisionSummary == nil || !strings.Contains(m.Content, "OCR: Z7")) {
81 t.Fatalf("missing summary: %+v", m)
82 }
83 if (mode == "failed" || mode == "disabled") && (!strings.Contains(m.Content, "already executed") || m.VisionSummary != nil) {
84 t.Fatalf("failure semantics: %+v", m)
85 }
86 if m.ToolRunState != provider.ToolRunCompleted {
87 t.Fatalf("changed execution state: %s", m.ToolRunState)
88 }
89 }
90 if !found {
91 t.Fatal("no tool result")
92 }
93 want := int32(1)
94 if mode == "native" || mode == "disabled" || mode == "canceled" {
95 want = 0
96 }
97 if vp.calls.Load() != want {
98 t.Fatalf("calls %d want %d", vp.calls.Load(), want)
99 }
100 })
101 }
102 }
103
104 type detailedImageTool struct {
105 fakeImageTool
106 calls atomic.Int32
107 }
108
109 var _ tool.DetailedExecutor = (*detailedImageTool)(nil)
110
111 func (*detailedImageTool) ExecutionDescriptor(json.RawMessage) *tool.ShellExecution { return nil }
112 func (t *detailedImageTool) ExecuteDetailed(context.Context, json.RawMessage) (tool.DetailedResult, error) {
113 t.calls.Add(1)
114 return tool.DetailedResult{Output: t.text, Images: t.images}, nil
115 }
116 func TestDetailedImageFailureDoesNotRepeatTool(t *testing.T) {
117 vp := &summaryProvider{fail: true}
118 cfg := &imageinput.Config{Model: "vision/model", Resolve: func(string) (provider.Provider, error) { return vp, nil }}
119 imageTool := &detailedImageTool{fakeImageTool: fakeImageTool{text: "operation completed", images: []string{"data:image/png;base64,QUFB"}}}
120 reg := tool.NewRegistry()
121 reg.Add(imageTool)
122 p := &scriptedProvider{name: "text", turns: [][]provider.Chunk{{toolCallChunk("c1", "shot", `{}`), {Type: provider.ChunkDone}}, {{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}}}}
123 a := New(p, reg, NewSession("sys"), Options{ImageInput: cfg, ModelRef: "text/model"}, event.Discard)
124 if err := a.Run(context.Background(), "inspect"); err != nil {
125 t.Fatal(err)
126 }
127 if imageTool.calls.Load() != 1 || vp.calls.Load() != 1 {
128 t.Fatalf("tool calls=%d vision calls=%d", imageTool.calls.Load(), vp.calls.Load())
129 }
130 for _, m := range a.Session().Snapshot() {
131 if m.Role == provider.RoleTool && (!strings.Contains(m.Content, "operation completed") || !strings.Contains(m.Content, "already executed") || len(m.Images) != 1) {
132 t.Fatalf("lost detailed result: %+v", m)
133 }
134 }
135 }
136 func TestChildImageServiceIsSessionLocal(t *testing.T) {
137 vp := &summaryProvider{}
138 var refs []string
139 cfg := &imageinput.Config{Model: "auto", Resolve: func(string) (provider.Provider, error) { return vp, nil }, Select: func(ref, mode string) (string, bool) { refs = append(refs, ref); return "vision/model", true }}
140 task := NewTaskToolWithOptions(TaskToolOptions{ImageInput: cfg})
141 opts := task.subagentOptions(context.Background(), 5, nil, 10000, 1, "", nil)
142 opts.ModelRef = "child/model"
143 p := &scriptedProvider{name: "text"}
144 first := NewReadOnlyAgent(p, tool.NewRegistry(), NewSession("one"), opts, event.Discard)
145 second := NewPlannerAgent(p, tool.NewRegistry(), NewSession("two"), opts, event.Discard)
146 for _, a := range []*Agent{first, second} {
147 processed := a.processToolImages(context.Background(), "captured", []string{"data:image/png;base64,QUFB"})
148 if processed.summary == nil {
149 t.Fatal("child fallback missing")
150 }
151 }
152 if vp.calls.Load() != 2 || len(refs) != 2 || refs[0] != "child/model" || refs[1] != "child/model" {
153 t.Fatalf("child isolation calls=%d refs=%v", vp.calls.Load(), refs)
154 }
155 }
156
156 lines GO