1package core
2
3import (
4 "testing"
5
6 "github.com/patriceckhart/zot/packages/provider"
7)
8
9// TestRepairToolUseResultPairsAppendsStub covers the most common
10// corruption shape: assistant tool_use row on disk, no
11// corresponding tool_result row after it. The repair must inject
12// a stub tool_result so the next request passes the provider's
13// validity check.
14func TestRepairToolUseResultPairsAppendsStub(t *testing.T) {
15 msgs := []provider.Message{
16 {Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "hi"}}},
17 {Role: provider.RoleAssistant, Content: []provider.Content{
18 provider.ToolCallBlock{ID: "t1", Name: "read", Arguments: []byte(`{"path":"/tmp/a"}`)},
19 }},
20 }
21 out := repairToolUseResultPairs(msgs)
22 if len(out) != 3 {
23 t.Fatalf("expected 3 messages after repair, got %d", len(out))
24 }
25 if out[2].Role != provider.RoleTool {
26 t.Fatalf("expected tool-role message at index 2, got %v", out[2].Role)
27 }
28 tr, ok := out[2].Content[0].(provider.ToolResultBlock)
29 if !ok {
30 t.Fatalf("expected ToolResultBlock, got %T", out[2].Content[0])
31 }
32 if tr.CallID != "t1" {
33 t.Errorf("CallID: want t1, got %q", tr.CallID)
34 }
35 if !tr.IsError {
36 t.Errorf("expected IsError=true on stub")
37 }
38}
39
40// TestRepairToolUseResultPairsMergesIntoExistingToolMessage covers
41// the case where a tool-role message exists but is missing one of
42// the call ids. The stub must be merged into it (no new message)
43// so the ordering stays tool_use, tool_result, tool_use, tool_result.
44func TestRepairToolUseResultPairsMergesIntoExistingToolMessage(t *testing.T) {
45 msgs := []provider.Message{
46 {Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "hi"}}},
47 {Role: provider.RoleAssistant, Content: []provider.Content{
48 provider.ToolCallBlock{ID: "t1", Name: "read"},
49 provider.ToolCallBlock{ID: "t2", Name: "bash"},
50 }},
51 {Role: provider.RoleTool, Content: []provider.Content{
52 provider.ToolResultBlock{CallID: "t1", Content: []provider.Content{provider.TextBlock{Text: "ok"}}},
53 }},
54 }
55 out := repairToolUseResultPairs(msgs)
56 if len(out) != 3 {
57 t.Fatalf("expected 3 messages (merged), got %d", len(out))
58 }
59 if out[2].Role != provider.RoleTool {
60 t.Fatalf("expected tool-role at index 2")
61 }
62 if len(out[2].Content) != 2 {
63 t.Fatalf("expected 2 tool_result blocks after merge, got %d", len(out[2].Content))
64 }
65 // second block must be the stub for t2
66 tr, ok := out[2].Content[1].(provider.ToolResultBlock)
67 if !ok {
68 t.Fatalf("expected ToolResultBlock at [1], got %T", out[2].Content[1])
69 }
70 if tr.CallID != "t2" {
71 t.Errorf("merged stub CallID: want t2, got %q", tr.CallID)
72 }
73}
74
75// TestRepairToolUseResultPairsLeavesValidTranscriptsAlone verifies
76// the repair is a no-op when everything is already paired.
77func TestRepairToolUseResultPairsLeavesValidTranscriptsAlone(t *testing.T) {
78 msgs := []provider.Message{
79 {Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "hi"}}},
80 {Role: provider.RoleAssistant, Content: []provider.Content{
81 provider.ToolCallBlock{ID: "t1", Name: "read"},
82 }},
83 {Role: provider.RoleTool, Content: []provider.Content{
84 provider.ToolResultBlock{CallID: "t1", Content: []provider.Content{provider.TextBlock{Text: "ok"}}},
85 }},
86 {Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "done"}}},
87 }
88 out := repairToolUseResultPairs(msgs)
89 if len(out) != len(msgs) {
90 t.Fatalf("no-op expected, len %d -> %d", len(msgs), len(out))
91 }
92 for i := range msgs {
93 if msgs[i].Role != out[i].Role {
94 t.Errorf("msg %d role changed", i)
95 }
96 }
97}
98
99// TestRepairToolUseResultPairsEmpty handles the zero-value case.
100func TestRepairToolUseResultPairsEmpty(t *testing.T) {
101 if out := repairToolUseResultPairs(nil); len(out) != 0 {
102 t.Fatalf("expected empty, got %d", len(out))
103 }
104}
105
106func TestRepairToolUseResultPairsSkipsServerTools(t *testing.T) {
107 msgs := []provider.Message{
108 {Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "hi"}}},
109 {Role: provider.RoleAssistant, Content: []provider.Content{
110 provider.ToolCallBlock{ID: "srv", Name: "openrouter:datetime", Server: true},
111 provider.TextBlock{Text: "noon"},
112 }},
113 }
114 out := repairToolUseResultPairs(msgs)
115 if len(out) != len(msgs) {
116 t.Fatalf("server tools should not get stub results, len %d -> %d", len(msgs), len(out))
117 }
118}