intelligent_test_assessment_integration_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. package repository
  2. import (
  3. "context"
  4. "encoding/json"
  5. "testing"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/google/uuid"
  9. "github.com/stretchr/testify/require"
  10. )
  11. func TestIntelligentAssessmentQueueActionsAndHistory(t *testing.T) {
  12. db := intelligentTestDB(t)
  13. repo := &intelligentTestRepository{db: db}
  14. svc := service.NewIntelligentTestService(repo, nil) // no runner: re-evaluation cannot use upstream
  15. ctx := context.Background()
  16. enqueue := func(model string) (*service.IntelligentTestEnqueued, error) {
  17. return svc.Enqueue(ctx, 1, service.IntelligentTestEnqueue{AccountIDs: []int64{10}, TestTypes: []string{"candy"}, Models: map[string]string{"candy": model}, IdempotencyKey: uuid.NewString()})
  18. }
  19. first, err := enqueue("gpt-5.4")
  20. require.NoError(t, err)
  21. require.Equal(t, 1, first.CreatedCount)
  22. again, err := enqueue("gpt-5.4")
  23. require.NoError(t, err)
  24. require.Equal(t, 1, again.ReusedCount)
  25. require.Zero(t, again.CreatedCount)
  26. _, err = enqueue("gpt-5.4-mini")
  27. require.ErrorContains(t, err, "不同模型或配置")
  28. _, err = svc.Cancel(ctx, 2, first.Records[0].ID)
  29. require.ErrorIs(t, err, service.ErrIntelligentTestForbidden)
  30. running, err := repo.Claim(ctx)
  31. require.NoError(t, err)
  32. require.NotNil(t, running)
  33. _, err = svc.Cancel(ctx, 1, running.ID)
  34. require.ErrorContains(t, err, "只能取消")
  35. require.NoError(t, repo.DeferForCapacity(ctx, running))
  36. waiting, err := repo.Get(ctx, running.ID)
  37. require.NoError(t, err)
  38. require.Equal(t, "queued", waiting.Status)
  39. require.Contains(t, waiting.QueueReason, "等待账号空闲")
  40. none, err := repo.Claim(ctx)
  41. require.NoError(t, err)
  42. require.Nil(t, none, "busy tasks do not monopolize claim cycles")
  43. cancelled, err := svc.Cancel(ctx, 1, running.ID)
  44. require.NoError(t, err)
  45. require.Equal(t, "cancelled", cancelled.Status)
  46. _, err = svc.Cancel(ctx, 1, running.ID)
  47. require.NoError(t, err)
  48. var audits int
  49. require.NoError(t, db.QueryRow(`SELECT COUNT(*) FROM audit_logs WHERE action='admin.intelligent_tests.cancel'`).Scan(&audits))
  50. require.Equal(t, 1, audits)
  51. // Construct a complete historical judgment with its original private snapshot.
  52. cfg := service.IntelligentTestConfig{Prompt: "original question", Model: "original-model", Evaluator: "exact_answer", ExpectedAnswer: "12", TimeoutSeconds: 120}
  53. encoded, err := json.Marshal(cfg)
  54. require.NoError(t, err)
  55. var oldID int64
  56. require.NoError(t, db.QueryRow(`INSERT INTO account_tests(account_id,test_type,status,score,result,input,raw_response,config_snapshot,evaluation,finished_at,anti_degradation,requested_by) VALUES(10,'candy','suspected_degradation',0,'12','original question','original raw',$1,'{"method":"exact_answer","reason":"legacy strict mismatch"}',NOW(),true,1) RETURNING id`, string(encoded)).Scan(&oldID))
  57. // Current settings differ: re-evaluation must use the historical snapshot.
  58. _, err = db.Exec(`UPDATE test_settings SET config=jsonb_set(config,'{expected_answer}','"999"'),user_visible=true WHERE test_type='candy'`)
  59. require.NoError(t, err)
  60. next, err := enqueue("gpt-5.4-mini")
  61. require.NoError(t, err)
  62. f := service.IntelligentTestFilter{AccountID: 10, TestType: "candy", Page: 1, PageSize: 12, OnlyAbnormal: true}
  63. before, err := repo.Accounts(ctx, f)
  64. require.NoError(t, err)
  65. require.EqualValues(t, 1, before.Total)
  66. require.EqualValues(t, 1, before.Overview.ReviewAccounts)
  67. require.Zero(t, before.Overview.AbnormalAccounts)
  68. require.Equal(t, next.Records[0].ID, before.Items[0].Tests[0].Latest.ID)
  69. require.Equal(t, oldID, before.Items[0].Tests[0].LatestCompleted.ID)
  70. require.Empty(t, before.Items[0].Tests[0].LatestCompleted.RawResponse)
  71. require.Empty(t, before.Items[0].Tests[0].Risk)
  72. _, err = svc.Reevaluate(ctx, 2, oldID)
  73. require.ErrorIs(t, err, service.ErrIntelligentTestForbidden)
  74. updated, err := svc.Reevaluate(ctx, 1, oldID)
  75. require.NoError(t, err)
  76. require.Equal(t, "completed", updated.Status)
  77. require.Equal(t, "correct", updated.Evaluation["answer_verdict"])
  78. require.Equal(t, "non_compliant", updated.Evaluation["format_verdict"])
  79. require.Equal(t, 100.0, *updated.Score)
  80. require.Equal(t, "12", updated.Result)
  81. require.Equal(t, "original raw", updated.RawResponse)
  82. require.Equal(t, "12", updated.ConfigSnapshot.ExpectedAnswer)
  83. require.Contains(t, updated.Evaluation, "original_judgment")
  84. _, err = svc.Reevaluate(ctx, 1, oldID)
  85. require.NoError(t, err)
  86. require.NoError(t, db.QueryRow(`SELECT COUNT(*) FROM audit_logs WHERE action='admin.intelligent_tests.reevaluate'`).Scan(&audits))
  87. require.Equal(t, 1, audits)
  88. public, err := repo.PublicGet(ctx, 2, oldID)
  89. require.NoError(t, err)
  90. require.Equal(t, "correct", public.Evaluation["answer_verdict"])
  91. require.NotContains(t, public.Evaluation, "expected_answer")
  92. require.NotContains(t, public.Evaluation, "original_judgment")
  93. after, err := repo.Accounts(ctx, f)
  94. require.NoError(t, err)
  95. require.Zero(t, after.Total)
  96. f.OnlyAbnormal = false
  97. after, err = repo.Accounts(ctx, f)
  98. require.NoError(t, err)
  99. require.EqualValues(t, 1, after.Overview.SuccessAccounts)
  100. require.Zero(t, after.Overview.ReviewAccounts)
  101. // Cancelled attempts also retain the last completed result.
  102. _, err = svc.Cancel(ctx, 1, next.Records[0].ID)
  103. require.NoError(t, err)
  104. after, err = repo.Accounts(ctx, f)
  105. require.NoError(t, err)
  106. require.Equal(t, oldID, after.Items[0].Tests[0].LatestCompleted.ID)
  107. require.EqualValues(t, 1, after.Overview.SuccessAccounts)
  108. }
  109. func TestIntelligentQueueDefersUntilCooldownAndAllowsCancellation(t *testing.T) {
  110. db := intelligentTestDB(t)
  111. repo := &intelligentTestRepository{db: db}
  112. ctx := context.Background()
  113. _, err := repo.Enqueue(ctx, 1, service.IntelligentTestEnqueue{AccountIDs: []int64{10}, TestTypes: []string{"candy"}, IdempotencyKey: uuid.NewString()})
  114. require.NoError(t, err)
  115. r, err := repo.Claim(ctx)
  116. require.NoError(t, err)
  117. require.NotNil(t, r)
  118. until := time.Now().Add(2 * time.Minute).UTC().Truncate(time.Second)
  119. r.AvailableAt = &until
  120. r.QueueReason = "模型冷却,等待后执行"
  121. require.NoError(t, repo.DeferForCapacity(ctx, r))
  122. saved, err := repo.Get(ctx, r.ID)
  123. require.NoError(t, err)
  124. require.True(t, until.Equal(*saved.AvailableAt), "queue timing is an instant, independent of database timezone")
  125. require.Equal(t, r.QueueReason, saved.QueueReason)
  126. next, err := repo.Claim(ctx)
  127. require.NoError(t, err)
  128. require.Nil(t, next)
  129. require.Error(t, repo.DeferForCapacity(ctx, r), "an expired worker cannot alter a queued/cancelled job")
  130. cancelled, err := repo.Cancel(ctx, 1, r.ID)
  131. require.NoError(t, err)
  132. require.Equal(t, "cancelled", cancelled.Status)
  133. }
  134. func TestIntelligentDeferredTaskDoesNotStarveAlreadyWaitingTask(t *testing.T) {
  135. db := intelligentTestDB(t)
  136. repo := &intelligentTestRepository{db: db}
  137. ctx := context.Background()
  138. _, err := repo.Enqueue(ctx, 1, service.IntelligentTestEnqueue{AccountIDs: []int64{10, 11}, TestTypes: []string{"candy"}, IdempotencyKey: uuid.NewString()})
  139. require.NoError(t, err)
  140. first, err := repo.Claim(ctx)
  141. require.NoError(t, err)
  142. require.EqualValues(t, 10, first.AccountID)
  143. require.NoError(t, repo.DeferForCapacity(ctx, first))
  144. _, err = db.Exec(`UPDATE account_tests SET available_at=CASE WHEN id=$1 THEN NOW()-INTERVAL '1 second' ELSE NOW()-INTERVAL '1 minute' END WHERE status='queued'`, first.ID)
  145. require.NoError(t, err)
  146. next, err := repo.Claim(ctx)
  147. require.NoError(t, err)
  148. require.EqualValues(t, 11, next.AccountID, "a retried low-ID task must yield to an older waiting task")
  149. }