package flyto import ( "errors" "testing" ) // TestEventType_AllEvents verifies every Event implementation returns // the correct EventType string. Table-driven to cover all 21 types. func TestEventType_AllEvents(t *testing.T) { cases := []struct { event Event wantType string }{ {&TextDeltaEvent{Text: "hi"}, "text_delta"}, {&TextEvent{Text: "hi"}, "text"}, {&ThinkingDeltaEvent{Text: "hmm"}, "thinking_delta"}, {&ThinkingEvent{Text: "hmm"}, "thinking"}, {&ToolUseEvent{ID: "t1", ToolName: "Bash"}, "tool_use"}, {&ToolResultEvent{ID: "t1"}, "tool_result"}, {&ToolProgressEvent{ID: "t1"}, "tool_progress"}, {&PermissionRequestEvent{ToolName: "Bash"}, "permission_request"}, {&TurnStartEvent{TurnNumber: 1}, "turn_start"}, {&TurnEndEvent{TurnNumber: 1}, "turn_end"}, {&CompactEvent{Summary: "compressed"}, "compact"}, {&ResponseValidatedEvent{Approved: true, ValidatorName: "billcost_reflect"}, "response_validated"}, {&DoneEvent{}, "done"}, {&ErrorEvent{Err: errors.New("x"), Code: "test"}, "error"}, {&SessionInfoEvent{SessionID: "s1"}, "session_info"}, {&PermissionLearnEvent{}, "permission_learn"}, {&WarningEvent{Message: "warn"}, "warning"}, {&ToolSummaryEvent{ToolName: "Bash"}, "tool_summary"}, {&SlashCommandEvent{Name: "/help"}, "slash_command"}, {&CheckpointEvent{ToolCallID: "cp1"}, "checkpoint"}, {&CheckpointSuggestedEvent{}, "checkpoint_suggested"}, {&InboxMessageEvent{}, "inbox_message"}, {&UsageEvent{InputTokens: 100}, "usage"}, } for _, tc := range cases { got := tc.event.EventType() if got != tc.wantType { t.Errorf("%T.EventType() = %q, want %q", tc.event, got, tc.wantType) } } } func TestErrorEvent_Error(t *testing.T) { e := &ErrorEvent{ Err: errors.New("something failed"), Code: "internal", } if got := e.Error(); got != "something failed" { t.Errorf("ErrorEvent.Error() = %q, want %q", got, "something failed") } } // TestCompactEvent_KindField verifies the Kind field accepts the three // documented values ("", "micro", "full") and that Summary is independent // (only "full" populates Summary; "micro" leaves it empty). // // 验证 Kind 三态 ("" / "micro" / "full") + Summary 独立性 — Summary 只在 // "full" 路径填充, "micro" 留空. EventType 字符串 "compact" 不因 Kind 变化. func TestCompactEvent_KindField(t *testing.T) { cases := []struct { name string kind string summary string wantSummary bool }{ {"legacy_empty_kind_with_summary", "", "old summary", true}, {"micro_no_summary", "micro", "", false}, {"full_with_summary", "full", "summarized text", true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { e := &CompactEvent{ Kind: tc.kind, Summary: tc.summary, TokensBefore: 16000, TokensAfter: 8000, } if e.EventType() != "compact" { t.Errorf("EventType() should remain %q regardless of Kind, got %q", "compact", e.EventType()) } if e.Kind != tc.kind { t.Errorf("Kind round-trip: got %q, want %q", e.Kind, tc.kind) } if (e.Summary != "") != tc.wantSummary { t.Errorf("Summary populated=%v, want=%v (kind=%q)", e.Summary != "", tc.wantSummary, tc.kind) } // Token numbers must round-trip independently of Kind so // future Kind variants cannot false-pass a schema test. if e.TokensBefore <= e.TokensAfter { t.Errorf("TokensBefore (%d) must exceed TokensAfter (%d) for any compaction", e.TokensBefore, e.TokensAfter) } }) } } // TestResponseValidatedEvent_Schema verifies the four documented verdict // shapes round-trip cleanly: PASS first try / PASS after self-correction / // Fail mid-turn / Fail at cap. EventType is invariant across all shapes. // // 验证 ResponseValidatedEvent 四种 verdict 形态干净 round-trip: 一次 // 即过 / 自纠后过 / 中途 Fail / 触底 Fail. EventType 在所有形态下不变. func TestResponseValidatedEvent_Schema(t *testing.T) { cases := []struct { name string approved bool blockCount int maxBlocks int reason string wantReason bool }{ {"pass_first_try", true, 0, 3, "", false}, {"pass_after_one_block", true, 1, 3, "", false}, {"fail_mid_turn", false, 1, 3, "field X missing", true}, {"fail_at_cap", false, 3, 3, "still failing schema", true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { e := &ResponseValidatedEvent{ Approved: tc.approved, ValidatorName: "billcost_reflect", Reason: tc.reason, BlockCount: tc.blockCount, MaxBlocks: tc.maxBlocks, Turn: 1, } if e.EventType() != "response_validated" { t.Errorf("EventType() should remain %q regardless of verdict, got %q", "response_validated", e.EventType()) } if e.Approved != tc.approved { t.Errorf("Approved round-trip: got %v, want %v", e.Approved, tc.approved) } if (e.Reason != "") != tc.wantReason { t.Errorf("Reason populated=%v, want=%v (approved=%v)", e.Reason != "", tc.wantReason, tc.approved) } // BlockCount must never exceed MaxBlocks so future emit-site // bugs (e.g. fail loop without break) cannot false-pass a // schema test. MaxBlocks=0 means "no cap configured", any // BlockCount allowed. if e.MaxBlocks > 0 && e.BlockCount > e.MaxBlocks { t.Errorf("BlockCount (%d) exceeded MaxBlocks (%d)", e.BlockCount, e.MaxBlocks) } }) } }