Files
geo/server/internal/tenant/app/monitoring_callback_service_test.go
T

905 lines
30 KiB
Go
Raw Normal View History

package app
import (
"context"
"database/sql"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
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 TestLoadMonitoringBrandDailyAggregateUsesLatestQuestionPlatformSamples(t *testing.T) {
tx := &captureDailyMonitorTaskTx{}
_, err := (&MonitoringCallbackService{}).loadMonitoringBrandDailyAggregate(
context.Background(),
tx,
3,
3,
time.Date(2026, 6, 29, 0, 0, 0, 0, monitoringBusinessLocation()),
)
require.NoError(t, err)
require.Len(t, tx.queryRowSQLs, 5)
combined := normalizeSQLWhitespace(strings.Join(tx.queryRowSQLs, " "))
assert.Contains(t, combined, "SELECT DISTINCT ON (business_date, question_id, ai_platform_id)")
assert.Contains(t, combined, "p.brand_mentioned IS TRUE AND p.sentiment_label = 'positive'")
}
func TestLoadMonitoringPlatformDailyAggregatesUsesLatestQuestionPlatformSamples(t *testing.T) {
tx := &captureDailyMonitorTaskTx{}
_, err := (&MonitoringCallbackService{}).loadMonitoringPlatformDailyAggregates(
context.Background(),
tx,
3,
3,
time.Date(2026, 6, 29, 0, 0, 0, 0, monitoringBusinessLocation()),
[]monitoringPlatformMetadata{{ID: "doubao", Name: "豆包"}},
)
require.NoError(t, err)
require.GreaterOrEqual(t, len(tx.querySQLs), 2)
combined := normalizeSQLWhitespace(strings.Join(tx.querySQLs, " "))
assert.Contains(t, combined, "SELECT DISTINCT ON (business_date, question_id, ai_platform_id)")
}
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 TestMonitoringTaskReadyForQueuedResultAcceptsDesktopReceivedStatus(t *testing.T) {
if !monitoringTaskReadyForQueuedResult(&monitoringCollectTask{
ExecutionOwner: "desktop_tasks",
Status: "received",
CallbackReceivedAt: sqlNullTimeForTest(),
}) {
t.Fatal("monitoringTaskReadyForQueuedResult() desktop received = false, want true")
}
if !monitoringTaskReadyForQueuedResult(&monitoringCollectTask{
ExecutionOwner: "desktop_tasks",
Status: "pending",
CallbackReceivedAt: sqlNullTimeForTest(),
}) {
t.Fatal("monitoringTaskReadyForQueuedResult() legacy desktop pending callback = false, want true")
}
}
func TestIdempotentDesktopMonitoringResultResponseAcceptsReceivedTerminalStates(t *testing.T) {
for _, status := range []string{"received", "failed", "skipped"} {
t.Run(status, func(t *testing.T) {
got := idempotentDesktopMonitoringResultResponse(&monitoringCollectTask{
ID: 123,
ExecutionOwner: "desktop_tasks",
Status: status,
CallbackReceivedAt: sqlNullTimeForTest(),
})
if got == nil {
t.Fatal("idempotentDesktopMonitoringResultResponse() = nil, want response")
}
if got.TaskID != 123 {
t.Fatalf("TaskID = %d, want 123", got.TaskID)
}
if got.TaskStatus != status {
t.Fatalf("TaskStatus = %q, want %q", got.TaskStatus, status)
}
})
}
}
func TestIdempotentDesktopMonitoringResultResponseRequiresDesktopCallback(t *testing.T) {
tests := []struct {
name string
task monitoringCollectTask
}{
{
name: "legacy received",
task: monitoringCollectTask{
ExecutionOwner: "legacy",
Status: "received",
CallbackReceivedAt: sqlNullTimeForTest(),
},
},
{
name: "desktop callback missing",
task: monitoringCollectTask{
ExecutionOwner: "desktop_tasks",
Status: "received",
},
},
{
name: "desktop pending",
task: monitoringCollectTask{
ExecutionOwner: "desktop_tasks",
Status: "pending",
CallbackReceivedAt: sqlNullTimeForTest(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := idempotentDesktopMonitoringResultResponse(&tt.task); got != nil {
t.Fatalf("idempotentDesktopMonitoringResultResponse() = %#v, want nil", got)
}
})
}
}
func sqlNullTimeForTest() sql.NullTime {
return sql.NullTime{Time: time.Now(), Valid: true}
}
func TestBuildDesktopMonitorTaskErrorValuePreservesNonRetryableAuthorizationFailure(t *testing.T) {
message := "用户登录态已失效,请重新授权后再试。"
value := buildDesktopMonitorTaskErrorValue(&monitoringCollectTask{
ID: 522,
AIPlatformID: "yuanbao",
}, MonitoringTaskResultRequest{
Status: "failed",
ErrorMessage: &message,
RawResponseJSON: map[string]interface{}{
"runtime_error": map[string]interface{}{
"code": "desktop_account_auth_expired",
"message": message,
"retryable": false,
"failure_category": "ai_platform_authorization",
},
},
})
if value["code"] != "desktop_account_auth_expired" {
t.Fatalf("error code = %v, want desktop_account_auth_expired", value["code"])
}
if value["retryable"] != false {
t.Fatalf("retryable = %v, want false", value["retryable"])
}
if value["failure_category"] != "ai_platform_authorization" {
t.Fatalf("failure_category = %v, want ai_platform_authorization", value["failure_category"])
}
}
func TestMonitoringTaskFailureDispositionDetectsAuthorizationFailureFromMessage(t *testing.T) {
message := "登录态已失效,请重新授权后再试。"
got := monitoringTaskFailureDispositionFromRequest(MonitoringTaskResultRequest{
Status: "failed",
ErrorMessage: &message,
RawResponseJSON: map[string]interface{}{
"runtime_error": map[string]interface{}{
"message": message,
},
},
})
if !got.NonRetryable {
t.Fatal("NonRetryable = false, want true")
}
if got.Category != "ai_platform_authorization" {
t.Fatalf("Category = %q, want ai_platform_authorization", got.Category)
}
}
2026-06-27 16:17:02 +08:00
func TestMonitoringFailedResultOutcomeRequeuesAutomaticFirstFailure(t *testing.T) {
message := "模型调用超时"
got := monitoringFailedResultOutcomeForTask(&monitoringCollectTask{
TriggerSource: "automatic",
AutomaticFailureCount: 0,
}, MonitoringTaskResultRequest{
Status: "failed",
ErrorMessage: &message,
})
if got.TaskStatus != "pending" {
t.Fatalf("TaskStatus = %q, want pending", got.TaskStatus)
}
if !got.Requeue {
t.Fatal("Requeue = false, want true")
}
if got.RetryAfter != monitoringAutomaticFailureRetryAfter {
t.Fatalf("RetryAfter = %v, want %v", got.RetryAfter, monitoringAutomaticFailureRetryAfter)
}
}
func TestMonitoringFailedResultOutcomeFailsAutomaticSecondFailure(t *testing.T) {
got := monitoringFailedResultOutcomeForTask(&monitoringCollectTask{
TriggerSource: "automatic",
AutomaticFailureCount: monitoringAutomaticFailureMaxAttempts - 1,
}, MonitoringTaskResultRequest{Status: "failed"})
if got.TaskStatus != "failed" {
t.Fatalf("TaskStatus = %q, want failed", got.TaskStatus)
}
if got.Requeue {
t.Fatal("Requeue = true, want false")
}
}
func TestMonitoringFailedResultOutcomeDoesNotRetryAuthorizationFailure(t *testing.T) {
message := "登录态已失效,请重新授权后再试。"
got := monitoringFailedResultOutcomeForTask(&monitoringCollectTask{
TriggerSource: "automatic",
AutomaticFailureCount: 0,
}, MonitoringTaskResultRequest{
Status: "failed",
ErrorMessage: &message,
})
if got.TaskStatus != "failed" {
t.Fatalf("TaskStatus = %q, want failed", got.TaskStatus)
}
if got.Requeue {
t.Fatal("Requeue = true, want false")
}
}
func TestMonitoringFailedResultOutcomeDoesNotRetryManualTask(t *testing.T) {
got := monitoringFailedResultOutcomeForTask(&monitoringCollectTask{
TriggerSource: "manual",
AutomaticFailureCount: 0,
}, MonitoringTaskResultRequest{Status: "failed"})
if got.TaskStatus != "failed" {
t.Fatalf("TaskStatus = %q, want failed", got.TaskStatus)
}
if got.Requeue {
t.Fatal("Requeue = true, want false")
}
}
func TestMonitoringAuthorizationFailureTargetClientPrefersExplicitTargetClient(t *testing.T) {
targetClientID := uuid.New().String()
task := &monitoringCollectTask{
TargetClientID: sql.NullString{String: targetClientID, Valid: true},
LeasedToExecutor: sql.NullString{String: uuid.New().String(), Valid: true},
}
if got := monitoringAuthorizationFailureTargetClientID(task); got != targetClientID {
t.Fatalf("target client = %q, want %q", got, targetClientID)
}
}
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)
}
}
}
2026-06-25 09:31:18 +08:00
func TestMonitoringPlatformResolvesContentCitationsForAllKnownModels(t *testing.T) {
for _, platformID := range []string{"doubao", "qwen", "kimi", "deepseek", "wenxin", "yuanbao"} {
t.Run(platformID, func(t *testing.T) {
if !monitoringPlatformResolvesContentCitations(platformID) {
t.Fatalf("monitoringPlatformResolvesContentCitations(%q) = false, want true", platformID)
}
})
}
}
func TestBuildMonitoringCitationFactResolvesContentForQwen(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)
}
2026-06-25 09:31:18 +08:00
sourceOnlyFact, ok := buildMonitoringCitationFact(input, aliasMap, monitoringPlatformResolvesContentCitations("qwen"))
if !ok {
2026-06-25 09:31:18 +08:00
t.Fatal("buildMonitoringCitationFact() for qwen returned !ok")
}
2026-06-25 09:31:18 +08:00
if sourceOnlyFact.ArticleID == nil || *sourceOnlyFact.ArticleID != aliasArticleID {
t.Fatalf("qwen ArticleID = %v, want %d", sourceOnlyFact.ArticleID, aliasArticleID)
}
2026-06-25 09:31:18 +08:00
if sourceOnlyFact.PublishRecordID == nil || *sourceOnlyFact.PublishRecordID != aliasPublishRecordID {
t.Fatalf("qwen PublishRecordID = %v, want %d", sourceOnlyFact.PublishRecordID, aliasPublishRecordID)
}
}
2026-06-25 23:53:35 +08:00
func TestBuildMonitoringCitationFactResolvesMediaSupplyContentSource(t *testing.T) {
aliasArticleID := int64(42)
sourceID := int64(9001)
normalizedURL := "https://example.com/submitted/article"
input := MonitoringSourceItem{
URL: "https://example.com/submitted/article?utm_source=chat",
NormalizedURL: &normalizedURL,
}
aliasMap := map[string]monitoringAliasResolution{
normalizedURL: {
ArticleID: &aliasArticleID,
NormalizedURLKey: normalizedURL,
SourceType: monitoringContentSourceMediaSupplyOrderItem,
SourceID: &sourceID,
},
}
fact, ok := buildMonitoringCitationFact(input, aliasMap, true)
if !ok {
t.Fatal("buildMonitoringCitationFact() returned !ok")
}
if fact.ArticleID == nil || *fact.ArticleID != aliasArticleID {
t.Fatalf("ArticleID = %v, want %d", fact.ArticleID, aliasArticleID)
}
if fact.ContentSourceType == nil || *fact.ContentSourceType != monitoringContentSourceMediaSupplyOrderItem {
t.Fatalf("ContentSourceType = %v, want %q", fact.ContentSourceType, monitoringContentSourceMediaSupplyOrderItem)
}
if fact.ContentSourceID == nil || *fact.ContentSourceID != sourceID {
t.Fatalf("ContentSourceID = %v, want %d", fact.ContentSourceID, sourceID)
}
}
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)
}
}
2026-06-25 23:53:35 +08:00
func TestMonitoringPublishedAliasIndexMatchesMediaSupplySubmission(t *testing.T) {
index := make(monitoringPublishedAliasIndex)
sourceID := int64(9001)
mediaAlias := monitoringPublishedAlias{
ArticleID: 42,
ArticleTitle: "投稿文章",
PublishPlatform: monitoringMediaSupplySubmissionCitationLabel,
OriginalURL: "https://m.example.com/submitted/article?id=123&utm_source=chat",
NormalizedURL: "https://www.example.com/submitted/article?id=123",
SourceType: monitoringContentSourceMediaSupplyOrderItem,
SourceID: sourceID,
SourcePriority: 1,
}
addMonitoringPublishedAliasToIndex(index, mediaAlias)
match, ok := index.Match(
"https://www.example.com/submitted/article?id=123&utm_medium=ai",
"https://www.example.com/submitted/article?id=123",
)
if !ok {
t.Fatal("Match() = false, want media supply alias match")
}
if match.SourceType != monitoringContentSourceMediaSupplyOrderItem {
t.Fatalf("SourceType = %q, want %q", match.SourceType, monitoringContentSourceMediaSupplyOrderItem)
}
if match.SourceID != sourceID {
t.Fatalf("SourceID = %d, want %d", match.SourceID, sourceID)
}
}
func TestMonitoringPublishedAliasIndexPrefersMediaSupplySubmission(t *testing.T) {
index := make(monitoringPublishedAliasIndex)
normalizedURL := "https://www.example.com/submitted/article?id=123"
addMonitoringPublishedAliasToIndex(index, monitoringPublishedAlias{
ArticleID: 42,
OriginalURL: normalizedURL,
NormalizedURL: normalizedURL,
SourceType: monitoringContentSourcePublishedArticle,
SourceID: 42,
SourcePriority: 2,
})
addMonitoringPublishedAliasToIndex(index, monitoringPublishedAlias{
ArticleID: 42,
OriginalURL: normalizedURL,
NormalizedURL: normalizedURL,
SourceType: monitoringContentSourceMediaSupplyOrderItem,
SourceID: 9001,
SourcePriority: 1,
})
match, ok := index.Match(normalizedURL, normalizedURL)
if !ok {
t.Fatal("Match() = false, want media supply alias match")
}
if match.SourceType != monitoringContentSourceMediaSupplyOrderItem {
t.Fatalf("SourceType = %q, want %q", match.SourceType, monitoringContentSourceMediaSupplyOrderItem)
}
if match.SourceID != 9001 {
t.Fatalf("SourceID = %d, want 9001", match.SourceID)
}
}
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)
}
}