0afd082d96
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Frontend CI / Frontend (push) Successful in 3m12s
Backend CI / Backend (push) Failing after 10m8s
Desktop Client Build / Build Desktop Client (push) Successful in 23m53s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 26s
- Added classification for `doubao_challenge_required` as a challenge in platform-auth-adapters. - Enhanced account action summaries to include specific messages for Doubao human verification. - Introduced monitoring functions to filter out blocked platforms based on account health. - Updated task leasing to support platform-specific filtering for monitor tasks. - Refined issue aggregation in HomeView to consolidate Doubao challenge failures into a single actionable item. - Improved backend logic to handle platform IDs in task leasing requests and responses. - Added tests for new functionality, ensuring proper handling of Doubao challenges and task leasing logic.
693 lines
22 KiB
Go
693 lines
22 KiB
Go
package app
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
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 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|