package app import ( "encoding/json" "errors" "strings" "testing" "time" ) func TestDecideHeartbeatPrimaryLease(t *testing.T) { tests := []struct { name string state heartbeatPrimaryLeaseState want heartbeatPrimaryLeaseAction }{ { name: "current primary fast path", state: heartbeatPrimaryLeaseState{ Found: true, CurrentIsPrimary: true, LeaseLive: true, PrimaryAvailable: true, }, want: heartbeatPrimaryLeaseActionPrimaryHit, }, { name: "current primary renews when due", state: heartbeatPrimaryLeaseState{ Found: true, CurrentIsPrimary: true, LeaseLive: true, RenewDue: true, PrimaryAvailable: true, }, want: heartbeatPrimaryLeaseActionRenew, }, { name: "non primary does not steal live lease", state: heartbeatPrimaryLeaseState{ Found: true, CurrentIsPrimary: false, LeaseLive: true, PrimaryAvailable: true, }, want: heartbeatPrimaryLeaseActionNonPrimaryLive, }, { name: "expired lease can be claimed", state: heartbeatPrimaryLeaseState{ Found: true, CurrentIsPrimary: false, LeaseLive: false, PrimaryAvailable: true, }, want: heartbeatPrimaryLeaseActionClaimExpired, }, { name: "revoked primary can be replaced", state: heartbeatPrimaryLeaseState{ Found: true, CurrentIsPrimary: false, LeaseLive: true, PrimaryAvailable: false, }, want: heartbeatPrimaryLeaseActionClaimUnavailable, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := decideHeartbeatPrimaryLease(tt.state) if got.Action != tt.want { t.Fatalf("decideHeartbeatPrimaryLease() = %v, want %v", got.Action, tt.want) } }) } } func TestMonitoringHeartbeatPrimaryLeaseMetrics(t *testing.T) { resetMonitoringHeartbeatPrimaryLeaseMetricsForTest() t.Cleanup(resetMonitoringHeartbeatPrimaryLeaseMetricsForTest) recordHeartbeatPrimaryLeaseMetric("primary_hit", 20*time.Millisecond) recordHeartbeatPrimaryLeaseMetric("contention", 10*time.Millisecond) recordHeartbeatPrimaryLeaseWriteMetric("renew") snapshot := MonitoringHeartbeatPrimaryLeaseMetricsSnapshotValue() if snapshot.PrimaryHitTotal != 1 { t.Fatalf("PrimaryHitTotal = %d, want 1", snapshot.PrimaryHitTotal) } if snapshot.ContentionTotal != 1 { t.Fatalf("ContentionTotal = %d, want 1", snapshot.ContentionTotal) } if snapshot.DBRenewTotal != 1 { t.Fatalf("DBRenewTotal = %d, want 1", snapshot.DBRenewTotal) } if snapshot.DurationCount != 2 { t.Fatalf("DurationCount = %d, want 2", snapshot.DurationCount) } prometheus := MonitoringHeartbeatPrimaryLeaseMetricsPrometheus() if !strings.Contains(prometheus, `monitoring_heartbeat_primary_lease_resolutions_total{outcome="primary_hit"} 1`) { t.Fatalf("prometheus output missing primary_hit metric: %s", prometheus) } if !strings.Contains(prometheus, `monitoring_heartbeat_primary_lease_db_writes_total{operation="renew"} 1`) { t.Fatalf("prometheus output missing renew write metric: %s", prometheus) } } func TestDecideMonitoringHeartbeatSnapshot(t *testing.T) { now := time.Date(2026, 4, 24, 3, 0, 0, 0, time.UTC) base := monitoringPlatformAccessSnapshotState{ AccessStatus: "accessible", Connected: true, Accessible: true, UpdatedAt: now.Add(-time.Minute), } tests := []struct { name string previous *monitoringPlatformAccessSnapshotState next monitoringPlatformAccessSnapshotState primary bool want monitoringHeartbeatSnapshotDecision }{ { name: "missing snapshot inserts", next: base, primary: true, want: monitoringHeartbeatSnapshotDecision{ WriteSnapshot: true, StateChanged: true, SnapshotOperation: "insert", }, }, { name: "fresh unchanged accessible snapshot skips write", previous: &base, next: base, primary: true, want: monitoringHeartbeatSnapshotDecision{ SnapshotOperation: "skip", }, }, { name: "stale unchanged snapshot refreshes", previous: &monitoringPlatformAccessSnapshotState{ AccessStatus: "accessible", Connected: true, Accessible: true, UpdatedAt: now.Add(-monitoringHeartbeatAccessSnapshotRefreshAfter - time.Second), }, next: base, primary: true, want: monitoringHeartbeatSnapshotDecision{ WriteSnapshot: true, RefreshDue: true, SnapshotOperation: "refresh", }, }, { name: "changed snapshot updates", previous: &base, next: monitoringPlatformAccessSnapshotState{ AccessStatus: "unavailable", Connected: true, Accessible: false, }, primary: true, want: monitoringHeartbeatSnapshotDecision{ WriteSnapshot: true, StateChanged: true, PendingReconcileCandidate: true, SnapshotOperation: "update", }, }, { name: "fresh unchanged primary inaccessible snapshot checks pending tasks without writing snapshot", previous: &monitoringPlatformAccessSnapshotState{ AccessStatus: "not_logged_in", Connected: false, Accessible: false, UpdatedAt: now.Add(-time.Minute), }, next: monitoringPlatformAccessSnapshotState{ AccessStatus: "not_logged_in", Connected: false, Accessible: false, }, primary: true, want: monitoringHeartbeatSnapshotDecision{ PendingReconcileCandidate: true, SnapshotOperation: "skip", }, }, { name: "non-primary inaccessible snapshot does not reconcile", previous: &monitoringPlatformAccessSnapshotState{ AccessStatus: "not_logged_in", Connected: false, Accessible: false, UpdatedAt: now.Add(-time.Minute), }, next: monitoringPlatformAccessSnapshotState{ AccessStatus: "not_logged_in", Connected: false, Accessible: false, }, primary: false, want: monitoringHeartbeatSnapshotDecision{ SnapshotOperation: "skip", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := decideMonitoringHeartbeatSnapshot(tt.previous, tt.next, now, tt.primary) if got.WriteSnapshot != tt.want.WriteSnapshot || got.StateChanged != tt.want.StateChanged || got.RefreshDue != tt.want.RefreshDue || got.PendingReconcileCandidate != tt.want.PendingReconcileCandidate || got.ReconcilePending != tt.want.ReconcilePending || got.SnapshotOperation != tt.want.SnapshotOperation { t.Fatalf("decideMonitoringHeartbeatSnapshot() = %+v, want %+v", got, tt.want) } }) } } func TestMonitoringHeartbeatSnapshotMetrics(t *testing.T) { resetMonitoringHeartbeatSnapshotMetricsForTest() t.Cleanup(resetMonitoringHeartbeatSnapshotMetricsForTest) recordMonitoringHeartbeatSnapshotMetrics(monitoringHeartbeatSnapshotMetricsEvent{ PlatformCount: 3, TransactionOpened: true, SnapshotInsertedCount: 1, SnapshotSkippedCount: 2, ReconciledSkippedTaskCount: 5, ProjectionRebuildCount: 2, Duration: 20 * time.Millisecond, }) recordMonitoringHeartbeatSnapshotMetrics(monitoringHeartbeatSnapshotMetricsEvent{ PlatformCount: 1, Duration: 10 * time.Millisecond, Err: errors.New("snapshot failed"), }) snapshot := MonitoringHeartbeatSnapshotMetricsSnapshotValue() if snapshot.PlatformReportedTotal != 4 { t.Fatalf("PlatformReportedTotal = %d, want 4", snapshot.PlatformReportedTotal) } if snapshot.TransactionOpenedTotal != 1 { t.Fatalf("TransactionOpenedTotal = %d, want 1", snapshot.TransactionOpenedTotal) } if snapshot.SnapshotInsertedTotal != 1 { t.Fatalf("SnapshotInsertedTotal = %d, want 1", snapshot.SnapshotInsertedTotal) } if snapshot.SnapshotSkippedTotal != 2 { t.Fatalf("SnapshotSkippedTotal = %d, want 2", snapshot.SnapshotSkippedTotal) } if snapshot.ReconciledSkippedTaskTotal != 5 { t.Fatalf("ReconciledSkippedTaskTotal = %d, want 5", snapshot.ReconciledSkippedTaskTotal) } if snapshot.ProjectionRebuildTotal != 2 { t.Fatalf("ProjectionRebuildTotal = %d, want 2", snapshot.ProjectionRebuildTotal) } if snapshot.ErrorTotal != 1 { t.Fatalf("ErrorTotal = %d, want 1", snapshot.ErrorTotal) } if snapshot.DurationCount != 2 { t.Fatalf("DurationCount = %d, want 2", snapshot.DurationCount) } prometheus := MonitoringHeartbeatSnapshotMetricsPrometheus() if !strings.Contains(prometheus, `monitoring_heartbeat_snapshot_writes_total{operation="insert"} 1`) { t.Fatalf("prometheus output missing insert write metric: %s", prometheus) } if !strings.Contains(prometheus, "monitoring_heartbeat_snapshot_transactions_total 1") { t.Fatalf("prometheus output missing transaction metric: %s", prometheus) } } func TestBuildMonitoringRawPayloadKimiIncludesSearchResultsInCitationInputs(t *testing.T) { citationTitle := "真实引用文章" searchTitle := "搜索网页结果" req := MonitoringTaskResultRequest{ Status: "succeeded", Citations: []MonitoringSourceItem{ { URL: "https://example.com/citation", Title: &citationTitle, }, }, SearchResults: []MonitoringSourceItem{ { URL: "https://example.com/search", Title: &searchTitle, }, }, } rawPayload, sourceInputs := buildMonitoringRawPayload("kimi", req) if len(sourceInputs) != 2 { t.Fatalf("buildMonitoringRawPayload() sourceInputs len = %d, want 2", len(sourceInputs)) } seen := make(map[string]bool) for _, item := range sourceInputs { seen[item.URL] = true } if !seen["https://example.com/citation"] { t.Fatalf("buildMonitoringRawPayload() missing citation url in source inputs: %#v", sourceInputs) } if !seen["https://example.com/search"] { t.Fatalf("buildMonitoringRawPayload() missing search url in source inputs: %#v", sourceInputs) } var payload map[string]interface{} if err := json.Unmarshal(rawPayload, &payload); err != nil { t.Fatalf("json.Unmarshal(rawPayload) error = %v", err) } if got := len(extractMonitoringSourceItems(payload["search_results"])); got != 1 { t.Fatalf("stored search_results len = %d, want 1", got) } } func TestValidateMonitoringTaskResultSubmissionRequiresAnswerOnSuccess(t *testing.T) { if err := validateMonitoringTaskResultSubmission("kimi", "failed", MonitoringTaskResultRequest{Status: "failed"}); err != nil { t.Fatalf("validateMonitoringTaskResultSubmission() failed status error = %v, want nil", err) } answer := "有效回答" if err := validateMonitoringTaskResultSubmission("kimi", "succeeded", MonitoringTaskResultRequest{Status: "succeeded", Answer: &answer}); err != nil { t.Fatalf("validateMonitoringTaskResultSubmission() succeeded status with answer error = %v, want nil", err) } if err := validateMonitoringTaskResultSubmission("kimi", "succeeded", MonitoringTaskResultRequest{Status: "succeeded"}); err == nil { t.Fatal("validateMonitoringTaskResultSubmission() succeeded status without answer error = nil, want error") } } func TestValidateMonitoringTaskResultSubmissionRequiresSourcesForCitationMarkers(t *testing.T) { answer := "正文 [citation:1]" if err := validateMonitoringTaskResultSubmission("yuanbao", "succeeded", MonitoringTaskResultRequest{ Status: "succeeded", Answer: &answer, }); err == nil { t.Fatal("validateMonitoringTaskResultSubmission() citation marker without sources error = nil, want error") } title := "引用文章" if err := validateMonitoringTaskResultSubmission("yuanbao", "succeeded", MonitoringTaskResultRequest{ Status: "succeeded", Answer: &answer, Citations: []MonitoringSourceItem{ {URL: "https://example.com/source", Title: &title}, }, }); err != nil { t.Fatalf("validateMonitoringTaskResultSubmission() citation marker with sources error = %v, want nil", err) } } func TestBuildMonitoringRawPayloadQwenIncludesSearchResultsInCitationInputs(t *testing.T) { searchTitle := "搜索网页结果" req := MonitoringTaskResultRequest{ Status: "succeeded", SearchResults: []MonitoringSourceItem{ { URL: "https://example.com/search", Title: &searchTitle, }, }, } _, sourceInputs := buildMonitoringRawPayload("qwen", req) if len(sourceInputs) != 1 { t.Fatalf("buildMonitoringRawPayload() sourceInputs len = %d, want 1", len(sourceInputs)) } for _, item := range sourceInputs { if item.URL != "https://example.com/search" { t.Fatalf("buildMonitoringRawPayload() source url = %q, want search url", item.URL) } } } func TestBuildMonitoringCitationFactCanDisableContentResolution(t *testing.T) { aliasArticleID := int64(42) aliasPublishRecordID := int64(84) normalizedURL := "https://example.com/article" input := MonitoringSourceItem{ URL: "https://example.com/article?utm_source=test#section", NormalizedURL: &normalizedURL, } aliasMap := map[string]monitoringAliasResolution{ normalizedURL: { ArticleID: &aliasArticleID, PublishRecordID: &aliasPublishRecordID, }, } resolvedFact, ok := buildMonitoringCitationFact(input, aliasMap, true) if !ok { t.Fatal("buildMonitoringCitationFact() with content resolution returned !ok") } if resolvedFact.ArticleID == nil || *resolvedFact.ArticleID != aliasArticleID { t.Fatalf("resolved ArticleID = %v, want %d", resolvedFact.ArticleID, aliasArticleID) } if resolvedFact.PublishRecordID == nil || *resolvedFact.PublishRecordID != aliasPublishRecordID { t.Fatalf("resolved PublishRecordID = %v, want %d", resolvedFact.PublishRecordID, aliasPublishRecordID) } sourceOnlyFact, ok := buildMonitoringCitationFact(input, aliasMap, false) if !ok { t.Fatal("buildMonitoringCitationFact() without content resolution returned !ok") } if sourceOnlyFact.ArticleID != nil { t.Fatalf("source-only ArticleID = %v, want nil", sourceOnlyFact.ArticleID) } if sourceOnlyFact.PublishRecordID != nil { t.Fatalf("source-only PublishRecordID = %v, want nil", sourceOnlyFact.PublishRecordID) } } func TestScoreMonitoringAliasCandidateUsesCanonicalArticleKey(t *testing.T) { input := monitoringAliasLookupInput{ NormalizedURL: "https://m.163.com/dy/article/KRPRE8DJ0556MCR0.html", MatchKeys: monitoringCitationCandidateKeys("https://m.163.com/dy/article/KRPRE8DJ0556MCR0.html", ""), } candidate := monitoringAliasCandidate{ NormalizedURL: "https://www.163.com/dy/article/KRPRE8DJ0556MCR0.html", MatchKeys: monitoringCitationCandidateKeys("https://www.163.com/dy/article/KRPRE8DJ0556MCR0.html", ""), } if score := scoreMonitoringAliasCandidate(input, candidate); score != 100 { t.Fatalf("scoreMonitoringAliasCandidate() = %d, want canonical URL match", score) } } func TestScoreMonitoringAliasCandidateRejectsSameDomainDifferentArticle(t *testing.T) { input := monitoringAliasLookupInput{ NormalizedURL: "https://www.163.com/dy/article/KIN5DDMK0556GQZ8.html", MatchKeys: monitoringCitationCandidateKeys("https://www.163.com/dy/article/KIN5DDMK0556GQZ8.html", ""), } candidate := monitoringAliasCandidate{ NormalizedURL: "https://www.163.com/dy/article/KRPRE8DJ0556MCR0.html", MatchKeys: monitoringCitationCandidateKeys("https://www.163.com/dy/article/KRPRE8DJ0556MCR0.html", ""), } if score := scoreMonitoringAliasCandidate(input, candidate); score != -1 { t.Fatalf("scoreMonitoringAliasCandidate() = %d, want no same-domain match", score) } } func TestBuildMonitoringRawPayloadDeepSeekIncludesSearchResultsInCitationInputs(t *testing.T) { searchTitle := "搜索网页结果" req := MonitoringTaskResultRequest{ Status: "succeeded", SearchResults: []MonitoringSourceItem{ { URL: "https://example.com/search", Title: &searchTitle, }, }, } _, sourceInputs := buildMonitoringRawPayload("deepseek", req) if len(sourceInputs) != 1 { t.Fatalf("buildMonitoringRawPayload() sourceInputs len = %d, want 1", len(sourceInputs)) } for _, item := range sourceInputs { if item.URL != "https://example.com/search" { t.Fatalf("buildMonitoringRawPayload() source url = %q, want search url", item.URL) } } } func TestExtractMonitoringInlineCitationsFromRawResponseKimi(t *testing.T) { raw := []byte(`{ "inline_citation_candidates": [ { "url": "https://example.com/a#ref", "title": "文章 A", "site_name": "示例站点" }, { "url": "https://example.com/a#other", "title": "文章 A", "site_name": "示例站点" }, { "url": "https://example.com/b", "title": "文章 B", "site_name": "另一个站点" } ] }`) items := extractMonitoringInlineCitationsFromRawResponse(raw, "kimi") if len(items) != 2 { t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() len = %d, want 2", len(items)) } if items[0].URL != "https://example.com/a" { t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() first url = %q, want https://example.com/a", items[0].URL) } if items[1].URL != "https://example.com/b" { t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() second url = %q, want https://example.com/b", items[1].URL) } } func TestExtractMonitoringInlineCitationsFromRawResponseDeepSeekFallsBackToInlineLinks(t *testing.T) { raw := []byte(`{ "inline_links": [ { "url": "https://example.com/a#cite-1", "text": "-1", "siteName": "示例站点" }, { "url": "https://example.com/b", "title": "文章 B", "siteName": "另一个站点" } ] }`) items := extractMonitoringInlineCitationsFromRawResponse(raw, "deepseek") if len(items) != 2 { t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() len = %d, want 2", len(items)) } if items[0].URL != "https://example.com/a" { t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() first url = %q, want https://example.com/a", items[0].URL) } if items[0].SiteName == nil || *items[0].SiteName != "示例站点" { t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() first site = %v, want 示例站点", items[0].SiteName) } if items[1].Title == nil || *items[1].Title != "文章 B" { t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() second title = %v, want 文章 B", items[1].Title) } }