返回 DeepSeek-Reasonix
tool_recovery_records_test.go
根目录 / internal / agent / tool_recovery_records_test.go
1 package agent
2
3 import (
4 "encoding/json"
5 "strings"
6 "testing"
7
8 "reasonix/internal/event"
9 "reasonix/internal/provider"
10 "reasonix/internal/tool"
11 )
12
13 func recoverySessionWithCall(id string, r *provider.ToolCallRecord) *Session {
14 s := NewSession("")
15 s.Messages = []provider.Message{{Role: provider.RoleAssistant, ID: "turn-1", ToolCalls: []provider.ToolCall{{ID: id, Name: "write_file", Arguments: `{"path":"x"}`, Recovery: r}}}}
16 return s
17 }
18
19 func TestPendingToolRecoverySurvivesUserTailAndIsProviderExcluded(t *testing.T) {
20 r := &provider.ToolCallRecord{Identity: provider.ActionIdentity{AttemptID: "attempt-1", CallID: "call-1"}, State: provider.ToolRunUnknown, Arguments: json.RawMessage(`{"path":"secret"}`), ReadOnly: false}
21 s := recoverySessionWithCall("call-1", r)
22 a := New(nil, tool.NewRegistry(), s, Options{}, event.Discard)
23 // A subsequent user message must not clear the durable unresolved record.
24 s.Add(provider.Message{Role: provider.RoleUser, Content: "continue"})
25 pending := a.PendingToolRecovery()
26 if len(pending) != 1 || pending[0].Identity.AttemptID != "attempt-1" {
27 t.Fatalf("pending recovery after user tail = %+v", pending)
28 }
29 model := provider.ModelMessages(s.Snapshot())
30 raw, _ := json.Marshal(model)
31 if strings.Contains(string(raw), "tool_recovery") {
32 t.Fatal("local recovery leaked to provider")
33 }
34 for _, m := range model {
35 if m.ToolCalls != nil && m.ToolCalls[0].Recovery != nil {
36 t.Fatal("Recovery metadata leaked into ModelMessages")
37 }
38 }
39 }
40
41 func TestSetToolRecoveryRecordRejectsStaleAttemptAndDetachesArguments(t *testing.T) {
42 original := provider.ToolCallRecord{Identity: provider.ActionIdentity{AttemptID: "new"}, State: provider.ToolRunStarted, Arguments: json.RawMessage(`{"path":"safe"}`)}
43 s := recoverySessionWithCall("call-1", &original)
44 stale := original
45 stale.Identity.AttemptID = "old"
46 if s.setToolRecoveryRecord("call-1", stale) {
47 t.Fatal("stale attempt replaced current recovery record")
48 }
49 updated := original
50 updated.State = provider.ToolRunCompleted
51 updated.Arguments[0] = 'X'
52 if !s.setToolRecoveryRecord("call-1", updated) {
53 t.Fatal("current attempt update rejected")
54 }
55 updated.Arguments[0] = 'Y'
56 got := s.toolRecoveryRecord("call-1")
57 if got == nil || got.State != provider.ToolRunCompleted || got.Arguments[0] != 'X' {
58 t.Fatalf("record was not detached: %+v", got)
59 }
60 }
61
62 func TestFinishToolRecoveryFailedWriterIsNotCompleted(t *testing.T) {
63 r := provider.ToolCallRecord{Identity: provider.ActionIdentity{AttemptID: "attempt-1"}, State: provider.ToolRunStarted, ReadOnly: false}
64 s := recoverySessionWithCall("call-1", &r)
65 a := New(nil, tool.NewRegistry(), s, Options{}, event.Discard)
66 a.finishToolRecovery(provider.ToolCall{ID: "call-1"}, toolOutcome{executed: true, output: "partial", errMsg: "write failed"})
67 got := s.toolRecoveryRecord("call-1")
68 if got == nil || got.State != provider.ToolRunFailed {
69 t.Fatalf("failed writer recovery = %+v", got)
70 }
71 }
72
72 lines GO