diff --git a/admin/handler/api/purchase_spec_resolution_contract_test.go b/admin/handler/api/purchase_spec_resolution_contract_test.go index 3d1953f..c2a157e 100644 --- a/admin/handler/api/purchase_spec_resolution_contract_test.go +++ b/admin/handler/api/purchase_spec_resolution_contract_test.go @@ -220,6 +220,18 @@ func applyAPISpecContractMutation(mutation string, taskID *string, request map[s case "attempt_id_too_long": request["attempt_id"] = strings.Repeat("a", 192) *key = apiSpecIdempotencyKey(*taskID, request) + case "candidate_count_101": + candidates := make([]any, 0, 101) + for index := 1; index <= 101; index++ { + value := "测试尺码" + strconv.Itoa(index) + candidates = append(candidates, map[string]any{ + "candidate_id": "c" + strconv.Itoa(index), + "raw_text": value, + "options": map[string]any{"color": "测试黑", "size": value}, + }) + } + request["candidates"] = candidates + apiSpecRefreshHashes(*taskID, request, key) case "body_too_large": request["future_padding"] = strings.Repeat("x", 64<<10) case "task_not_found": diff --git a/admin/service/purchase_spec_resolution_contract_test.go b/admin/service/purchase_spec_resolution_contract_test.go index 106151c..a71b875 100644 --- a/admin/service/purchase_spec_resolution_contract_test.go +++ b/admin/service/purchase_spec_resolution_contract_test.go @@ -46,12 +46,13 @@ type purchaseSpecContractCase struct { } 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"` + Conclusion string `json:"conclusion"` + CandidateID string `json:"candidate_id"` + ConfidenceBPS int `json:"confidence_bps"` + Reason string `json:"reason"` + ConflictDimensions []string `json:"conflict_dimensions"` + MissingDimensions []string `json:"missing_dimensions"` + Error string `json:"error"` } type purchaseSpecContractExpected struct { @@ -230,11 +231,12 @@ func contractAIClientForCase(testCase purchaseSpecContractCase) *contractAIModel 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...), + Conclusion: testCase.Model.Conclusion, + CandidateID: testCase.Model.CandidateID, + ConfidenceBPS: testCase.Model.ConfidenceBPS, + Reason: testCase.Model.Reason, + ConflictDimensions: append([]string(nil), testCase.Model.ConflictDimensions...), + MissingDimensions: append([]string(nil), testCase.Model.MissingDimensions...), } return client } diff --git a/client/test/test_purchase_spec_resolution_contract_vectors.py b/client/test/test_purchase_spec_resolution_contract_vectors.py index fbc1295..67dce6d 100644 --- a/client/test/test_purchase_spec_resolution_contract_vectors.py +++ b/client/test/test_purchase_spec_resolution_contract_vectors.py @@ -375,6 +375,20 @@ class PurchaseSpecResolutionContractVectorTest(unittest.TestCase): elif mutation == "attempt_id_too_long": request["attempt_id"] = "a" * 192 key = _idempotency_key(task_id, request) + elif mutation == "candidate_count_101": + request["candidates"] = [ + { + "candidate_id": f"c{index}", + "raw_text": f"测试尺码{index}", + "options": { + "color": "测试黑", + "size": f"测试尺码{index}", + }, + } + for index in range(1, 102) + ] + request["candidate_snapshot_hash"] = _snapshot_hash(request) + key = _idempotency_key(task_id, request) elif mutation == "body_too_large": request["future_padding"] = "x" * (64 * 1024) elif mutation == "task_not_found": diff --git a/testdata/contracts/purchase_spec_resolution_v1.json b/testdata/contracts/purchase_spec_resolution_v1.json index 517b67c..68f4e2b 100644 --- a/testdata/contracts/purchase_spec_resolution_v1.json +++ b/testdata/contracts/purchase_spec_resolution_v1.json @@ -30,6 +30,40 @@ "model_calls": 0 } }, + { + "name": "rule_traditional_simplified", + "task_id": "cg-contract-traditional", + "target_size": "藍 色 / XL 碼", + "candidates": ["蓝色/xl码"], + "candidate_snapshot_hash": "5433787fa5bc7cae41939504a221bb59cee26639e683f89506e524b0bf318202", + "idempotency_key": "spec-resolution-v1:f1c239574dcbf5c8bbb12a71f21dadf874d5d0b544e4fa18b957f896295801e4", + "mode": "rule", + "model": null, + "expected": { + "outcome": "matched", + "source": "rule", + "candidate_id": "c1", + "confidence_bps": 10000, + "model_calls": 0 + } + }, + { + "name": "rule_weight_range_unique", + "task_id": "cg-contract-range", + "target_size": "45-55公斤", + "candidates": ["70-90斤", "90-110斤", "110-130斤"], + "candidate_snapshot_hash": "1b81a16f0c1efcd457311e0f229859ad7df03453f9f9ee5946a9e78f353e95b6", + "idempotency_key": "spec-resolution-v1:e70666cb6141d09059c4bf9de0e755185fc1b5bd72e93c3b399e1c46507f63b6", + "mode": "rule", + "model": null, + "expected": { + "outcome": "matched", + "source": "rule", + "candidate_id": "c2", + "confidence_bps": 10000, + "model_calls": 0 + } + }, { "name": "rule_overlap_uncertain", "task_id": "cg-contract-overlap", @@ -136,6 +170,70 @@ "model_calls": 1 } }, + { + "name": "ai_uncertain", + "task_id": "cg-contract-ai-uncertain", + "target_size": "L码", + "candidates": ["M码", "XL码"], + "candidate_snapshot_hash": "f634515c8a7a0ebd688fd926520405051e12c622c87a4783a05936099ecfb2fa", + "idempotency_key": "spec-resolution-v1:7b531267d5ba16ed9d81a7d8d7e9e8e4553776bd35d3401e2b260609c6b022fb", + "mode": "ai", + "model": { + "conclusion": "uncertain", + "confidence_bps": 8500, + "reason": "测试信息不足" + }, + "expected": { + "outcome": "uncertain", + "source": "ai", + "candidate_id": null, + "confidence_bps": 8500, + "model_calls": 1 + } + }, + { + "name": "ai_conflict", + "task_id": "cg-contract-ai-conflict", + "target_size": "L码", + "candidates": ["M码", "XL码"], + "candidate_snapshot_hash": "f634515c8a7a0ebd688fd926520405051e12c622c87a4783a05936099ecfb2fa", + "idempotency_key": "spec-resolution-v1:4d6581611767a1a56a305f1b769bb651be80d0113af1827c2fa8c66e72ea14b0", + "mode": "ai", + "model": { + "conclusion": "conflict", + "confidence_bps": 9500, + "reason": "测试规格维度冲突", + "conflict_dimensions": ["尺码"] + }, + "expected": { + "outcome": "rejected", + "source": "ai", + "candidate_id": null, + "confidence_bps": 9500, + "model_calls": 1 + } + }, + { + "name": "ai_invalid_format", + "task_id": "cg-contract-ai-invalid", + "target_size": "L码", + "candidates": ["M码", "XL码"], + "candidate_snapshot_hash": "f634515c8a7a0ebd688fd926520405051e12c622c87a4783a05936099ecfb2fa", + "idempotency_key": "spec-resolution-v1:5a9dcc28c6dc6f8d877a93932533ca5f64e6e5d10153acf7ddb63cb6e06428de", + "mode": "ai", + "model": { + "conclusion": "other", + "confidence_bps": 9200, + "reason": "测试非法结论" + }, + "expected": { + "outcome": "failed", + "source": "ai", + "candidate_id": null, + "confidence_bps": null, + "model_calls": 1 + } + }, { "name": "ai_timeout", "task_id": "cg-contract-ai-timeout", @@ -162,6 +260,7 @@ {"name": "target_size_not_claimed", "base_case": "rule_weight_unique", "mutation": "target_size_not_claimed", "expected_http_status": 422, "expected_error_code": "INVALID_SPEC_RESOLUTION_REQUEST"}, {"name": "observed_at_without_timezone", "base_case": "rule_weight_unique", "mutation": "observed_at_without_timezone", "expected_http_status": 422, "expected_error_code": "INVALID_SPEC_RESOLUTION_REQUEST"}, {"name": "attempt_id_too_long", "base_case": "rule_weight_unique", "mutation": "attempt_id_too_long", "expected_http_status": 422, "expected_error_code": "INVALID_SPEC_RESOLUTION_REQUEST"}, + {"name": "candidate_count_101", "base_case": "rule_weight_unique", "mutation": "candidate_count_101", "expected_http_status": 422, "expected_error_code": "INVALID_SPEC_RESOLUTION_REQUEST"}, {"name": "body_too_large", "base_case": "rule_weight_unique", "mutation": "body_too_large", "expected_http_status": 400, "expected_error_code": "INVALID_BODY"}, {"name": "task_not_found", "base_case": "rule_weight_unique", "mutation": "task_not_found", "expected_http_status": 404, "expected_error_code": "TASK_NOT_FOUND"}, {"name": "task_not_purchase", "base_case": "rule_weight_unique", "mutation": "task_not_purchase", "expected_http_status": 422, "expected_error_code": "TASK_NOT_PURCHASE"},