173 lines
7.0 KiB
Go
173 lines
7.0 KiB
Go
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
|
||
}
|