package service import ( "context" "database/sql" "encoding/json" "errors" "os" "path/filepath" "strconv" "sync/atomic" "testing" "cmautobuy/admin/model" ) type purchaseSpecContractFixture struct { Contract string `json:"contract"` SchemaVersion int `json:"schema_version"` Defaults purchaseSpecContractDefaults `json:"defaults"` Cases []purchaseSpecContractCase `json:"cases"` ErrorCases []purchaseSpecContractErrorCase `json:"error_cases"` } type purchaseSpecContractDefaults struct { ClientID string `json:"client_id"` TaskType string `json:"task_type"` TaskVersion int64 `json:"task_version"` PDDGoodsID string `json:"pdd_goods_id"` SelectedColor string `json:"selected_color"` AttemptID string `json:"attempt_id"` ObservedAt string `json:"observed_at"` AIConfidenceThresholdBPS int `json:"ai_confidence_threshold_bps"` } type purchaseSpecContractCase struct { Name string `json:"name"` TaskID string `json:"task_id"` TargetSize string `json:"target_size"` Candidates []string `json:"candidates"` CandidateSnapshotHash string `json:"candidate_snapshot_hash"` IdempotencyKey string `json:"idempotency_key"` Mode string `json:"mode"` Model *purchaseSpecContractModel `json:"model"` Expected purchaseSpecContractExpected `json:"expected"` } type purchaseSpecContractModel struct { Conclusion string `json:"conclusion"` CandidateID string `json:"candidate_id"` ConfidenceBPS int `json:"confidence_bps"` Reason string `json:"reason"` MissingDimensions []string `json:"missing_dimensions"` Error string `json:"error"` } type purchaseSpecContractExpected struct { Outcome model.PurchaseSpecResolutionOutcome `json:"outcome"` Source model.PurchaseSpecResolutionSource `json:"source"` CandidateID *string `json:"candidate_id"` ConfidenceBPS *int `json:"confidence_bps"` ModelCalls int `json:"model_calls"` } type purchaseSpecContractErrorCase struct { Name string `json:"name"` BaseCase string `json:"base_case"` Mutation string `json:"mutation"` ExpectedHTTPStatus int `json:"expected_http_status"` ExpectedErrorCode string `json:"expected_error_code"` } type contractAIModelClient struct { response AIModelMatchResponse err error calls atomic.Int32 } func (client *contractAIModelClient) Match(context.Context, model.AIProviderConfig, string, AIModelMatchRequest) (AIModelMatchResponse, error) { client.calls.Add(1) return client.response, client.err } func TestPurchaseSpecResolutionContractVectors_AdminDecisionAndReplay(t *testing.T) { fixture := loadPurchaseSpecContractFixture(t) for _, testCase := range fixture.Cases { t.Run(testCase.Name, func(t *testing.T) { db := newRuntimeSpecTestDB(t) preparePurchaseSpecContractTask(t, db, fixture.Defaults, testCase) req, body := buildPurchaseSpecContractRequest(fixture.Defaults, testCase) if actual := purchaseSpecCandidateSnapshotHash(req); actual != testCase.CandidateSnapshotHash { t.Fatalf("候选哈希=%s,向量=%s", actual, testCase.CandidateSnapshotHash) } if actual := purchaseSpecIdempotencyKey(testCase.TaskID, req); actual != testCase.IdempotencyKey { t.Fatalf("幂等键=%s,向量=%s", actual, testCase.IdempotencyKey) } client := contractAIClientForCase(testCase) var loaderCalls atomic.Int32 loader := func(context.Context, *sql.DB, AISecretStore, AIEndpointPolicy) (AIMatchSnapshot, error) { loaderCalls.Add(1) if testCase.Mode != "ai" { return AIMatchSnapshot{}, errors.New("规则向量不应加载 AI") } return AIMatchSnapshot{ Provider: model.AIProviderConfig{ ProviderID: "contract-provider", Model: "contract-model", ConfidenceThresholdBPS: fixture.Defaults.AIConfidenceThresholdBPS, }, ConfigFingerprint: "contract-fingerprint", Secret: "not-a-real-secret", Client: client, }, nil } first, err := resolvePurchaseSpecWithSnapshotLoader(context.Background(), db, nil, AIEndpointPolicy{}, testCase.TaskID, fixture.Defaults.ClientID, testCase.IdempotencyKey, body, loader) if err != nil { t.Fatal(err) } second, err := resolvePurchaseSpecWithSnapshotLoader(context.Background(), db, nil, AIEndpointPolicy{}, testCase.TaskID, fixture.Defaults.ClientID, testCase.IdempotencyKey, body, loader) if err != nil || second != first { t.Fatalf("幂等重放不一致 first=%s second=%s err=%v", first, second, err) } var response PurchaseSpecResolutionResponse if err := json.Unmarshal([]byte(first), &response); err != nil { t.Fatal(err) } assertPurchaseSpecContractResponse(t, response, testCase) if int(client.calls.Load()) != testCase.Expected.ModelCalls { t.Fatalf("模型调用=%d,期望=%d", client.calls.Load(), testCase.Expected.ModelCalls) } if testCase.Mode == "rule" && loaderCalls.Load() != 0 { t.Fatalf("规则唯一/冲突向量不应加载 AI,实际=%d", loaderCalls.Load()) } var records, decisions, idempotency int if err := db.QueryRow(`SELECT COUNT(*),SUM(outcome<>'pending') FROM purchase_spec_resolutions WHERE task_id=?`, testCase.TaskID).Scan(&records, &decisions); err != nil { t.Fatal(err) } if err := db.QueryRow(`SELECT COUNT(*) FROM idempotency_keys WHERE `+"`key`"+`=?`, testCase.IdempotencyKey).Scan(&idempotency); err != nil { t.Fatal(err) } if records != 1 || decisions != 1 || idempotency != 1 { t.Fatalf("records=%d decisions=%d idempotency=%d", records, decisions, idempotency) } }) } } func loadPurchaseSpecContractFixture(t *testing.T) purchaseSpecContractFixture { t.Helper() path := filepath.Join("..", "..", "testdata", "contracts", "purchase_spec_resolution_v1.json") data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } var fixture purchaseSpecContractFixture if err := json.Unmarshal(data, &fixture); err != nil { t.Fatal(err) } if fixture.Contract != "purchase-spec-resolution-v1" || fixture.SchemaVersion != 1 || len(fixture.Cases) == 0 { t.Fatalf("共享契约向量头无效: %+v", fixture) } return fixture } func buildPurchaseSpecContractRequest(defaults purchaseSpecContractDefaults, testCase purchaseSpecContractCase) (PurchaseSpecResolutionRequest, []byte) { req := PurchaseSpecResolutionRequest{ SchemaVersion: 1, TaskVersion: defaults.TaskVersion, AttemptID: defaults.AttemptID, PddGoodsID: defaults.PDDGoodsID, OriginalOptions: map[string]string{"color": defaults.SelectedColor, "size": testCase.TargetSize}, SelectedColor: defaults.SelectedColor, TargetSize: testCase.TargetSize, ObservedAt: defaults.ObservedAt, } for index, value := range testCase.Candidates { req.Candidates = append(req.Candidates, PurchaseSpecCandidate{ CandidateID: "c" + strconv.Itoa(index+1), RawText: value, Options: map[string]string{"color": defaults.SelectedColor, "size": value}, }) } req.CandidateSnapshotHash = testCase.CandidateSnapshotHash body, _ := json.Marshal(req) return req, body } func preparePurchaseSpecContractTask(t *testing.T, db *sql.DB, defaults purchaseSpecContractDefaults, testCase purchaseSpecContractCase) { t.Helper() now := model.NowISO() if _, err := db.Exec(`INSERT INTO clients (client_id,name,device_address,platform,pdd_package,capabilities,last_seen_at,created_at,updated_at) VALUES(?,?,'','android','','{}',?,?,?)`, defaults.ClientID, "契约测试客户端", now, now, now); err != nil { t.Fatal(err) } options, _ := json.Marshal(map[string]string{"color": defaults.SelectedColor, "size": testCase.TargetSize}) if _, err := db.Exec(`INSERT INTO tasks (task_id,task_type,status,version,assigned_client,goods_id,pdd_goods_url,pdd_goods_id,pdd_options, quantity,max_price_cent,created_at,updated_at) VALUES(?,?,'claimed',?,?,?, ?,?,?,1,2000,?,?)`, testCase.TaskID, model.TaskPurchase, defaults.TaskVersion, defaults.ClientID, "SHOPEE-CONTRACT", "https://example.invalid/contract-goods", defaults.PDDGoodsID, string(options), now, now); err != nil { t.Fatal(err) } if _, err := db.Exec(`INSERT INTO task_claims(task_id,client_id,claimed_at) VALUES(?,?,?)`, testCase.TaskID, defaults.ClientID, now); err != nil { t.Fatal(err) } } func contractAIClientForCase(testCase purchaseSpecContractCase) *contractAIModelClient { client := &contractAIModelClient{} if testCase.Model == nil { return client } if testCase.Model.Error == "timeout" { client.err = context.DeadlineExceeded return client } client.response = AIModelMatchResponse{ Conclusion: testCase.Model.Conclusion, CandidateID: testCase.Model.CandidateID, ConfidenceBPS: testCase.Model.ConfidenceBPS, Reason: testCase.Model.Reason, MissingDimensions: append([]string(nil), testCase.Model.MissingDimensions...), } return client } func assertPurchaseSpecContractResponse(t *testing.T, response PurchaseSpecResolutionResponse, testCase purchaseSpecContractCase) { t.Helper() if response.Outcome != testCase.Expected.Outcome || response.CandidateSnapshotHash != testCase.CandidateSnapshotHash { t.Fatalf("响应结论=%s hash=%s", response.Outcome, response.CandidateSnapshotHash) } if response.Source == nil || *response.Source != testCase.Expected.Source { t.Fatalf("响应来源=%v,期望=%s", response.Source, testCase.Expected.Source) } if testCase.Expected.CandidateID == nil { if response.Match != nil { t.Fatalf("非 matched 不应返回候选: %+v", response.Match) } } else if response.Match == nil || response.Match.CandidateID != *testCase.Expected.CandidateID { t.Fatalf("候选=%+v,期望=%s", response.Match, *testCase.Expected.CandidateID) } if testCase.Expected.ConfidenceBPS == nil { if response.ConfidenceBPS != nil { t.Fatalf("置信度=%v,期望 null", response.ConfidenceBPS) } } else if response.ConfidenceBPS == nil || *response.ConfidenceBPS != *testCase.Expected.ConfidenceBPS { t.Fatalf("置信度=%v,期望=%d", response.ConfidenceBPS, *testCase.Expected.ConfidenceBPS) } }