返回 DeepSeek-Reasonix
imageinput_failures_test.go
根目录 / internal / agent / imageinput_failures_test.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strings"
8 "testing"
9
10 "reasonix/internal/event"
11 "reasonix/internal/extension"
12 "reasonix/internal/extension/protocol"
13 "reasonix/internal/imageinput"
14 "reasonix/internal/provider"
15 "reasonix/internal/tool"
16 )
17
18 type emptyImageProvider struct{}
19
20 func (emptyImageProvider) Name() string { return "empty" }
21 func (emptyImageProvider) Stream(context.Context, provider.Request) (<-chan provider.Chunk, error) {
22 ch := make(chan provider.Chunk)
23 close(ch)
24 return ch, nil
25 }
26
27 func TestImageFailureKeepsEachBatchResult(t *testing.T) {
28 for _, failure := range []error{context.DeadlineExceeded, errors.New("network unavailable"), nil} {
29 cfg := &imageinput.Config{Model: "vision/model", Resolve: func(string) (provider.Provider, error) { return emptyImageProvider{}, failure }}
30 reg := tool.NewRegistry()
31 shot := &detailedImageTool{fakeImageTool: fakeImageTool{text: "saved", images: []string{"data:image/png;base64,QUFB"}}}
32 reg.Add(shot)
33 p := &scriptedProvider{name: "text", turns: [][]provider.Chunk{{toolCallChunk("a", "shot", `{}`), toolCallChunk("b", "shot", `{}`), {Type: provider.ChunkDone}}, {{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}}}}
34 a := New(p, reg, NewSession("sys"), Options{ImageInput: cfg, ModelRef: "text/model"}, event.Discard)
35 if err := a.Run(context.Background(), "inspect"); err != nil {
36 t.Fatal(err)
37 }
38 var ids []string
39 for _, m := range a.Session().Snapshot() {
40 if m.Role == provider.RoleTool {
41 ids = append(ids, m.ToolCallID)
42 text := m.Content
43 if m.RawContent != "" {
44 text = m.RawContent
45 }
46 if m.ToolRunState != provider.ToolRunCompleted || !strings.Contains(text, "saved") || !strings.Contains(text, "unavailable") || len(m.Images) != 1 {
47 t.Fatalf("result: %+v", m)
48 }
49 }
50 }
51 if strings.Join(ids, ",") != "a,b" || shot.calls.Load() != 2 {
52 t.Fatalf("ids=%v calls=%d", ids, shot.calls.Load())
53 }
54 }
55 }
56
57 func TestRejectedToolImagesNeverInvokeVision(t *testing.T) {
58 vp := &summaryProvider{}
59 cfg := &imageinput.Config{Model: "vision/model", Resolve: func(string) (provider.Provider, error) { return vp, nil }}
60 client := &fakeDispatchClient{interceptFn: func(ev protocol.InterceptEvent, _ json.RawMessage) (protocol.InterceptResult, error) {
61 if ev == protocol.EventToolAfter {
62 return blockWith("withheld"), nil
63 }
64 return protocol.InterceptResult{Decision: protocol.DecisionContinue}, nil
65 }}
66 reg := tool.NewRegistry()
67 reg.Add(&fakeImageTool{text: "saved", images: []string{"data:image/png;base64,QUFB"}})
68 a := New(nil, reg, NewSession("sys"), Options{ImageInput: cfg, Extensions: newExtDispatcher(client, true, nil, extension.PointToolAfter)}, event.Discard)
69 out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "shot", Arguments: `{}`})
70 if out.errMsg == "" || vp.calls.Load() != 0 {
71 t.Fatalf("error=%q calls=%d", out.errMsg, vp.calls.Load())
72 }
73 }
74
74 lines GO