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{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.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}
63
64func TestToEventPreservesActivityAndToolStreamFacts(t *testing.T) {
65 retry := toEvent(core.EvRetryScheduled{
66 Scope: core.RetryScopeProvider, Attempt: 2, MaxAttempts: 3, Delay: 250 * time.Millisecond,
67 })
68 if retry.Type != "retry_scheduled" || retry.Scope != "provider" || retry.Attempt != 2 || retry.MaxAttempts != 3 || retry.DelayMS == nil || *retry.DelayMS != 250 {
69 t.Fatalf("retry event = %#v", retry)
70 }
71
72 zeroDelay := toEvent(core.EvRetryScheduled{Scope: core.RetryScopeProvider, Attempt: 2, MaxAttempts: 3})
73 encoded, err := json.Marshal(zeroDelay)
74 if err != nil {
75 t.Fatalf("marshal zero-delay retry: %v", err)
76 }
77 var wire map[string]any
78 if err := json.Unmarshal(encoded, &wire); err != nil {
79 t.Fatalf("decode zero-delay retry: %v", err)
80 }
81 if got, ok := wire["delay_ms"]; !ok || got != float64(0) {
82 t.Fatalf("zero-delay retry JSON delay_ms = %#v, want 0", got)
83 }
84
85 toolStart := toEvent(core.EvToolUseStart{ID: "call-1", Name: "read"})
86 if toolStart.Type != "tool_use_start" || toolStart.ID != "call-1" || toolStart.Name != "read" {
87 t.Fatalf("tool start = %#v", toolStart)
88 }
89 toolArgs := toEvent(core.EvToolUseArgs{ID: "call-1", Delta: `{"path":"a"`})
90 if toolArgs.Type != "tool_use_args" || toolArgs.ID != "call-1" || toolArgs.Delta == "" {
91 t.Fatalf("tool args = %#v", toolArgs)
92 }
93 toolEnd := toEvent(core.EvToolExecutionStarted{ID: "call-1", Name: "read"})
94 if toolEnd.Type != "tool_execution_started" || toolEnd.ID != "call-1" || toolEnd.Name != "read" {
95 t.Fatalf("tool execution = %#v", toolEnd)
96 }
97}