package batch import ( "testing" "sub2api-cn-relay-manager/internal/store/sqlite" ) func TestStatusProjection(t *testing.T) { t.Parallel() t.Run("run summary exposes recent warnings and warning badge label", func(t *testing.T) { t.Parallel() run := sqlite.ImportRun{ RunID: "run-1", State: string(RunStateCompletedWithWarnings), Mode: "partial", AccessMode: "subscription", TotalItems: 2, CompletedItems: 2, ActiveItems: 1, DegradedItems: 1, BrokenItems: 0, WarningItems: 1, StartedAt: "2026-05-22T12:20:00+08:00", FinishedAt: "2026-05-22T12:20:07+08:00", } view := ProjectRunSummary(run) if view.StateBadge.Label != "warning" { t.Fatalf("StateBadge.Label = %q, want warning", view.StateBadge.Label) } if len(view.RecentWarnings) != 1 { t.Fatalf("len(RecentWarnings) = %d, want 1", len(view.RecentWarnings)) } if view.RecentWarnings[0] != "该批次包含 1 条 advisory item,建议检查 capability profile 与 retry 轨迹" { t.Fatalf("RecentWarnings[0] = %q, want canonical warning copy", view.RecentWarnings[0]) } }) t.Run("item summary projection maps warning copy and reuse badges", func(t *testing.T) { t.Parallel() item := sqlite.ImportRunItem{ ItemID: "item-1", RunID: "run-1", BaseURL: "https://kimi.example.com/v1", ProviderID: "kimi-a7m-7d7ac291", APIKeyFingerprint: "sha256:8d8c4b5f", RequestedModelsJSON: `["kimi-k2.6"]`, CanonicalFamiliesJSON: `["kimi-k2.6"]`, ResolvedSmokeModel: "kimi-k2.6", CurrentStage: string(ItemStageDone), ConfirmationStatus: string(ConfirmationAdvisory), AccessStatus: string(AccessStatusActive), MatchedAccountState: string(MatchedAccountStateActive), AccountResolution: string(AccountResolutionReused), ProvisionReused: true, RetryCount: 2, LastRetryAt: "2026-05-22T12:20:05+08:00", AdvisoryMessagesJSON: `["responses_unsupported_but_chat_ok","gateway_warmup_retry_succeeded"]`, LastErrorStage: string(ItemStageConfirm), LastError: "API returned 403: Forbidden", } view := ProjectItemSummary(item) if len(view.Badges) < 3 { t.Fatalf("len(Badges) = %d, want at least 3", len(view.Badges)) } if !hasBadge(view.Badges, "reused", "reused") { t.Fatalf("Badges = %#v, want reused badge", view.Badges) } if !hasBadge(view.Badges, "matched_account_state", "已启用") { t.Fatalf("Badges = %#v, want matched account state badge", view.Badges) } if !hasBadge(view.Badges, "account_resolution", "复用") { t.Fatalf("Badges = %#v, want account resolution badge", view.Badges) } if len(view.AdvisoryMessages) != 2 { t.Fatalf("len(AdvisoryMessages) = %d, want 2", len(view.AdvisoryMessages)) } if view.AdvisoryMessages[0] != "该上游不支持 /v1/responses,系统已自动回退到 /v1/chat/completions" { t.Fatalf("AdvisoryMessages[0] = %q, want responses fallback copy", view.AdvisoryMessages[0]) } if view.AdvisoryMessages[1] != "初次调度出现 no available accounts,短暂重试后已恢复" { t.Fatalf("AdvisoryMessages[1] = %q, want warmup retry copy", view.AdvisoryMessages[1]) } }) t.Run("item detail projection exposes capability profile and event trail", func(t *testing.T) { t.Parallel() item := sqlite.ImportRunItem{ ItemID: "item-2", RunID: "run-1", BaseURL: "https://kimi.example.com/v1", ProviderID: "kimi-a7m-7d7ac291", APIKeyFingerprint: "sha256:8d8c4b5f", RequestedModelsJSON: `["kimi-k2.6"]`, RawModelsJSON: `["kimi-k2.6"]`, NormalizedModelsJSON: `["kimi-k2.6"]`, CanonicalFamiliesJSON: `["kimi-k2.6"]`, RecommendedModelsJSON: `[]`, ResolvedSmokeModel: "kimi-k2.6", CurrentStage: string(ItemStageDone), ConfirmationStatus: string(ConfirmationAdvisory), AccessStatus: string(AccessStatusActive), MatchedAccountState: string(MatchedAccountStateDeprecated), AccountResolution: string(AccountResolutionReactivated), ProvisionReused: true, ReusedFromProviderID: "kimi-a7m-7d7ac291", ReusedFromAccountID: int64Ptr(4), RetryCount: 2, LastRetryAt: "2026-05-22T12:20:05+08:00", ChannelID: int64Ptr(12), AccountID: int64Ptr(4), AdvisoryMessagesJSON: `["responses_unsupported_but_chat_ok","initial_probe_race_expected"]`, LastErrorStage: string(ItemStageConfirm), LastError: "API returned 403: Forbidden", CapabilityProfileJSON: `{"transport_profile":{"supports_openai_models":true,"supports_openai_chat_completions":true,"supports_openai_responses":false,"supports_anthropic_messages":false,"auth_style":"bearer","model_id_style":"canonical","known_advisories":["responses_unsupported_but_chat_ok","initial_probe_race_expected"]},"model_profiles":[{"raw_model_id":"kimi-k2.6","normalized_model_id":"kimi-k2.6","canonical_model_family":"kimi-k2.6","supports_stream":"true","supports_tools":"unknown","supports_reasoning_fields":"unknown","smoke_chat_ok":true}]}`, } events := []sqlite.ImportRunItemEvent{ { EventID: "evt-01", RunID: "run-1", ItemID: "item-2", EventType: "retry_scheduled", Stage: string(ItemStageConfirm), Attempt: 1, Message: "initial 503 no available accounts, retry scheduled", PayloadJSON: `{"delay_ms":500}`, CreatedAt: "2026-05-22T12:20:04+08:00", }, } view, err := ProjectItemDetail(item, events) if err != nil { t.Fatalf("ProjectItemDetail() error = %v", err) } if view.ReusedFromProviderID != "kimi-a7m-7d7ac291" { t.Fatalf("ReusedFromProviderID = %q, want kimi-a7m-7d7ac291", view.ReusedFromProviderID) } if view.ReusedFromAccountID == nil || *view.ReusedFromAccountID != 4 { t.Fatalf("ReusedFromAccountID = %#v, want 4", view.ReusedFromAccountID) } if len(view.Events) != 1 || view.Events[0].EventType != "retry_scheduled" { t.Fatalf("Events = %#v, want retry event trail", view.Events) } if !view.CapabilityProfile.TransportProfile.SupportsOpenAIChatCompletions { t.Fatal("CapabilityProfile.TransportProfile.SupportsOpenAIChatCompletions = false, want true") } if len(view.CapabilityProfile.ModelProfiles) != 1 || view.CapabilityProfile.ModelProfiles[0].CanonicalModelFamily != "kimi-k2.6" { t.Fatalf("CapabilityProfile.ModelProfiles = %#v, want canonical model family", view.CapabilityProfile.ModelProfiles) } if !hasBadge(view.Badges, "account_resolution", "已快速启用") { t.Fatalf("Badges = %#v, want reactivated badge", view.Badges) } if !hasBadge(view.Badges, "matched_account_state", "已弃用") { t.Fatalf("Badges = %#v, want deprecated badge", view.Badges) } }) } func hasBadge(badges []ProjectionBadge, kind, label string) bool { for _, badge := range badges { if badge.Kind == kind && badge.Label == label { return true } } return false }