Files
geo/server/internal/tenant/app/monitoring_callback_service_test.go
T
root b345ee26e4 feat(monitoring): match citations by canonical link and scope summary by brand/keyword/platform/date
Replace the domain+title fuzzy alias scoring with a canonical
candidate-key index built from published-link aliases — only exact
matches now resolve to a SaaS source, eliminating false positives
across articles sharing a host (e.g. m.163.com vs www.163.com).

Surface the summary scoping that already existed on loadCitationRanking
/ loadCitedArticles by accepting brand_id, keyword_id, ai_platform_id
and business_date on CitationSummary, plumbing them through the handler
and admin tracking view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:46:17 +08:00

545 lines
18 KiB
Go

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)
}
}