1package sdk
2
3import (
4 "encoding/json"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/bnema/zut/packages/core"
10)
11
12func TestToEventPreservesCacheDiagnostics(t *testing.T) {
13 for _, diagnostics := range []core.EvCacheDiagnostics{
14 {Eligible: true, Mode: "automatic", Transport: "http_sse", Continuation: "full"},
15 {Eligible: false, Mode: "below_minimum", Transport: "websocket", Continuation: "incremental"},
16 } {
17 event := toEvent(diagnostics)
18 if event.Type != "cache_diagnostics" || event.CacheDiagnostics == nil {
19 t.Fatalf("event = %#v", event)
20 }
21 got := event.CacheDiagnostics
22 if got.Eligible != diagnostics.Eligible || got.Mode != diagnostics.Mode || got.Transport != diagnostics.Transport || got.Continuation != diagnostics.Continuation {
23 t.Fatalf("cache diagnostics = %#v, want %#v", got, diagnostics)
24 }
25 encoded, err := json.Marshal(event)
26 if err != nil {
27 t.Fatal(err)
28 }
29 if !json.Valid(encoded) {
30 t.Fatalf("encoded diagnostics are invalid JSON: %s", encoded)
31 }
32 if !diagnostics.Eligible && !strings.Contains(string(encoded), `"eligible":false`) {
33 t.Fatalf("encoded ineligible diagnostics = %s, want eligible=false", encoded)
34 }
35 }
36}
37
38func TestToEventPreservesPlanUpdate(t *testing.T) {
39 explanation := "Discovery complete"
40 event := toEvent(core.EvPlanUpdate{CallID: "call-7", Update: core.PlanUpdate{
41 Explanation: &explanation,
42 Plan: []core.PlanStep{
43 {Step: "Implement", Status: core.PlanInProgress},
44 {Step: "Verify", Status: core.PlanPending},
45 },
46 }})
47 if event.Type != "plan_update" || event.ID != "call-7" || event.Explanation == nil || *event.Explanation != explanation {
48 t.Fatalf("plan event = %#v", event)
49 }
50 if event.Plan == nil || len(*event.Plan) != 2 || (*event.Plan)[0].Status != "in_progress" {
51 t.Fatalf("plan payload = %#v", event.Plan)
52 }
53
54 empty := toEvent(core.EvPlanUpdate{Update: core.PlanUpdate{Plan: []core.PlanStep{}}})
55 encoded, err := json.Marshal(empty)
56 if err != nil {
57 t.Fatal(err)
58 }
59 if !strings.Contains(string(encoded), `"plan":[]`) {
60 t.Fatalf("empty plan JSON = %s, want explicit empty list", encoded)
61 }
62 if strings.Contains(string(encoded), `"id"`) {
63 t.Fatalf("call id should be omitted when empty: %s", encoded)
64 }
65}
66
67func TestToEventPreservesActivityAndToolStreamFacts(t *testing.T) {
68 retry := toEvent(core.EvRetryScheduled{
69 Scope: core.RetryScopeProvider, Attempt: 2, MaxAttempts: 3, Delay: 250 * time.Millisecond,
70 })
71 if retry.Type != "retry_scheduled" || retry.Scope != "provider" || retry.Attempt != 2 || retry.MaxAttempts != 3 || retry.DelayMS == nil || *retry.DelayMS != 250 {
72 t.Fatalf("retry event = %#v", retry)
73 }
74
75 zeroDelay := toEvent(core.EvRetryScheduled{Scope: core.RetryScopeProvider, Attempt: 2, MaxAttempts: 3})
76 encoded, err := json.Marshal(zeroDelay)
77 if err != nil {
78 t.Fatalf("marshal zero-delay retry: %v", err)
79 }
80 var wire map[string]any
81 if err := json.Unmarshal(encoded, &wire); err != nil {
82 t.Fatalf("decode zero-delay retry: %v", err)
83 }
84 if got, ok := wire["delay_ms"]; !ok || got != float64(0) {
85 t.Fatalf("zero-delay retry JSON delay_ms = %#v, want 0", got)
86 }
87
88 toolStart := toEvent(core.EvToolUseStart{ID: "call-1", Name: "read"})
89 if toolStart.Type != "tool_use_start" || toolStart.ID != "call-1" || toolStart.Name != "read" {
90 t.Fatalf("tool start = %#v", toolStart)
91 }
92 toolArgs := toEvent(core.EvToolUseArgs{ID: "call-1", Delta: `{"path":"a"`})
93 if toolArgs.Type != "tool_use_args" || toolArgs.ID != "call-1" || toolArgs.Delta == "" {
94 t.Fatalf("tool args = %#v", toolArgs)
95 }
96 toolEnd := toEvent(core.EvToolExecutionStarted{ID: "call-1", Name: "read"})
97 if toolEnd.Type != "tool_execution_started" || toolEnd.ID != "call-1" || toolEnd.Name != "read" {
98 t.Fatalf("tool execution = %#v", toolEnd)
99 }
100}