feat(monitoring): scope authorization-failure cancellation to platform across accounts
When a monitor task's final account hits human verification or an authorization failure, cancel the remaining queued desktop/monitoring tasks for the whole platform instead of only the same account. Follow-up tasks are now marked skipped/aborted (not failed) with a platform_human_verification / platform_authorization_unavailable reason and a user-facing cancellation message, and emit task_canceled lifecycle events for monitor kinds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config"
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
||||
)
|
||||
|
||||
func TestMonitoringAnswerParseModelUsesConfig(t *testing.T) {
|
||||
@@ -635,6 +637,109 @@ func TestMonitoringAuthorizationFailureTargetClientPrefersExplicitTargetClient(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitorAuthorizationFollowupCancellationIsPlatformScopedAcrossAccounts(t *testing.T) {
|
||||
tx := &captureDailyMonitorTaskTx{}
|
||||
task := &repository.DesktopTask{
|
||||
DesktopID: uuid.New(),
|
||||
TenantID: 77,
|
||||
WorkspaceID: 88,
|
||||
Kind: "monitor",
|
||||
TargetClientID: uuid.New(),
|
||||
TargetAccountID: uuid.New(),
|
||||
Platform: "doubao",
|
||||
}
|
||||
|
||||
rows, err := loadQueuedDesktopAuthorizationFailureCandidates(context.Background(), tx, task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, rows)
|
||||
require.True(t, tx.queryCalled)
|
||||
query := normalizeSQLWhitespace(tx.querySQL)
|
||||
assert.Contains(t, query, "AND kind = $3")
|
||||
assert.Contains(t, query, "AND target_client_id = $4")
|
||||
assert.Contains(t, query, "AND (kind = 'monitor' OR target_account_id = $5)")
|
||||
assert.Contains(t, query, "AND platform_id = $6")
|
||||
assert.Equal(t, "aborted", desktopAuthorizationFollowupTaskStatus("monitor"))
|
||||
assert.Equal(t, "failed", desktopAuthorizationFollowupTaskStatus("publish"))
|
||||
assert.Equal(t, "task_canceled", desktopAuthorizationFollowupEventType(&repository.DesktopTask{
|
||||
Kind: "monitor",
|
||||
Status: "aborted",
|
||||
}))
|
||||
}
|
||||
|
||||
func TestFailRemainingMonitoringTasksAfterFinalAccountChallengeScopesToModelAndClient(t *testing.T) {
|
||||
targetClientID := uuid.New().String()
|
||||
completedAt := time.Date(2026, 7, 14, 9, 30, 0, 0, time.UTC)
|
||||
message := "豆包账号触发人机验证,请完成验证码后再继续采集。"
|
||||
tx := &captureDailyMonitorTaskTx{commandTag: pgconn.NewCommandTag("UPDATE 3")}
|
||||
task := &monitoringCollectTask{
|
||||
ID: 901,
|
||||
TenantID: 77,
|
||||
AIPlatformID: "doubao",
|
||||
CollectorType: monitoringCollectorType,
|
||||
BusinessDate: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC),
|
||||
ExecutionOwner: "desktop_tasks",
|
||||
TargetClientID: sql.NullString{String: targetClientID, Valid: true},
|
||||
}
|
||||
req := MonitoringTaskResultRequest{
|
||||
Status: "failed",
|
||||
ErrorMessage: &message,
|
||||
RawResponseJSON: map[string]interface{}{
|
||||
"runtime_error": map[string]interface{}{
|
||||
"code": "doubao_challenge_required",
|
||||
"message": message,
|
||||
"retryable": false,
|
||||
"non_retryable": true,
|
||||
"failure_category": "ai_platform_authorization",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
affected, err := (&MonitoringCallbackService{}).failRemainingMonitoringTasksForAuthorizationFailure(
|
||||
context.Background(),
|
||||
tx,
|
||||
task,
|
||||
req,
|
||||
completedAt,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), affected)
|
||||
require.True(t, tx.execCalled)
|
||||
query := normalizeSQLWhitespace(tx.execSQL)
|
||||
assert.Contains(t, query, "SET status = 'skipped'")
|
||||
assert.Contains(t, query, "skip_reason = $8")
|
||||
assert.Contains(t, query, "WHERE tenant_id = $1")
|
||||
assert.Contains(t, query, "AND business_date = $3::date")
|
||||
assert.Contains(t, query, "AND ai_platform_id = $4")
|
||||
assert.Contains(t, query, "AND id <> $5")
|
||||
assert.Contains(t, query, "target_client_id::text = $6 OR leased_to_executor = $6")
|
||||
assert.Contains(t, query, "AND status IN ('pending', 'leased', 'expired')")
|
||||
require.Len(t, tx.execArgs, 11)
|
||||
assert.Equal(t, int64(77), tx.execArgs[0])
|
||||
assert.Equal(t, monitoringCollectorType, tx.execArgs[1])
|
||||
assert.Equal(t, "2026-07-14", tx.execArgs[2])
|
||||
assert.Equal(t, "doubao", tx.execArgs[3])
|
||||
assert.Equal(t, int64(901), tx.execArgs[4])
|
||||
assert.Equal(t, targetClientID, tx.execArgs[5])
|
||||
assert.Equal(t, completedAt, tx.execArgs[6])
|
||||
assert.Equal(t, monitoringPlatformHumanVerificationSkipReason, tx.execArgs[7])
|
||||
assert.Equal(t, "该模型没有其他可用账号,因触发人机验证,后续采集任务已自动取消。", tx.execArgs[8])
|
||||
assert.Equal(t, "desktop_tasks", tx.execArgs[10])
|
||||
|
||||
payloadJSON, ok := tx.execArgs[9].([]byte)
|
||||
require.True(t, ok)
|
||||
var payload map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(payloadJSON, &payload))
|
||||
assert.Equal(t, true, payload["blocked_by_authorization_failure"])
|
||||
runtimeError, ok := payload["runtime_error"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "doubao_challenge_required", runtimeError["code"])
|
||||
assert.Equal(t, "doubao", runtimeError["blocked_by_authorization_failure_platform"])
|
||||
assert.Equal(t, true, runtimeError["canceled"])
|
||||
assert.Equal(t, monitoringPlatformHumanVerificationSkipReason, runtimeError["cancellation_reason"])
|
||||
}
|
||||
|
||||
func TestBuildMonitoringRawPayloadQwenIncludesSearchResultsInCitationInputs(t *testing.T) {
|
||||
searchTitle := "搜索网页结果"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user