Files
sub2api-cn-relay-manager/internal/batch/status_projection_test.go
2026-05-22 15:31:33 +08:00

173 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}