feat(t208): close candidate decision feedback loop
This commit is contained in:
@@ -567,6 +567,75 @@ func TestDeviceExecutionResultsAreIdempotentAndAuditable(t *testing.T) {
|
||||
})
|
||||
requireDeviceStatus(t, candidates, http.StatusOK)
|
||||
|
||||
humanReviewPayload := fmt.Sprintf(
|
||||
`{"execution_id":%q,"claim_generation":%d,"task_content_sha256":%q,"reason_schema_version":1,"outcome":"CANDIDATE_ACCEPTED","selected_candidate_ordinal":1,"primary_reason_code":"SELECTED_BEST_MATCH","note":"","items":[{"candidate_ordinal":1,"label":"ACCEPT","primary_reason_code":"SKU_MATCH","reason_codes":["SKU_MATCH"],"note":""}]}`,
|
||||
started.Execution.ID,
|
||||
started.Task.ClaimGeneration,
|
||||
taskHash,
|
||||
)
|
||||
humanReview := performDeviceRequest(t, fixture.router, deviceRequest{
|
||||
method: http.MethodPost,
|
||||
target: "/api/v1/tasks/" + taskID + "/human-reviews",
|
||||
contentType: "application/json",
|
||||
body: strings.NewReader(humanReviewPayload),
|
||||
bearerToken: testOpaqueToken,
|
||||
claimToken: testOpaqueToken,
|
||||
idempotencyKey: "result-human-review-1",
|
||||
})
|
||||
requireDeviceStatus(t, humanReview, http.StatusOK)
|
||||
if !strings.Contains(humanReview.Body.String(), `"version":1`) {
|
||||
t.Fatalf("human review response = %s", humanReview.Body.String())
|
||||
}
|
||||
var storedReview struct {
|
||||
Review struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"review"`
|
||||
}
|
||||
decodeResponse(t, humanReview, &storedReview)
|
||||
if storedReview.Review.ID == "" {
|
||||
t.Fatalf("human review ID missing: %+v", storedReview)
|
||||
}
|
||||
humanReviewReplay := performDeviceRequest(t, fixture.router, deviceRequest{
|
||||
method: http.MethodPost,
|
||||
target: "/api/v1/tasks/" + taskID + "/human-reviews",
|
||||
contentType: "application/json",
|
||||
body: strings.NewReader(humanReviewPayload),
|
||||
bearerToken: testOpaqueToken,
|
||||
claimToken: testOpaqueToken,
|
||||
idempotencyKey: "result-human-review-1",
|
||||
})
|
||||
requireDeviceStatus(t, humanReviewReplay, http.StatusOK)
|
||||
if !strings.Contains(humanReviewReplay.Body.String(), `"replayed":true`) {
|
||||
t.Fatalf("human review replay response = %s", humanReviewReplay.Body.String())
|
||||
}
|
||||
revisedReviewPayload := fmt.Sprintf(
|
||||
`{"execution_id":%q,"claim_generation":%d,"task_content_sha256":%q,"reason_schema_version":1,"outcome":"CANDIDATE_ACCEPTED","selected_candidate_ordinal":1,"primary_reason_code":"SELECTED_BEST_MATCH","note":"","supersedes_review_id":%q,"items":[{"candidate_ordinal":1,"label":"ACCEPT","primary_reason_code":"IMAGE_MATCH","reason_codes":["IMAGE_MATCH"],"note":""}]}`,
|
||||
started.Execution.ID,
|
||||
started.Task.ClaimGeneration,
|
||||
taskHash,
|
||||
storedReview.Review.ID,
|
||||
)
|
||||
revisedReview := performDeviceRequest(t, fixture.router, deviceRequest{
|
||||
method: http.MethodPost,
|
||||
target: "/api/v1/tasks/" + taskID + "/human-reviews",
|
||||
contentType: "application/json",
|
||||
body: strings.NewReader(revisedReviewPayload),
|
||||
bearerToken: testOpaqueToken,
|
||||
claimToken: testOpaqueToken,
|
||||
idempotencyKey: "result-human-review-2",
|
||||
})
|
||||
requireDeviceStatus(t, revisedReview, http.StatusOK)
|
||||
if !strings.Contains(revisedReview.Body.String(), `"version":2`) {
|
||||
t.Fatalf("revised human review response = %s", revisedReview.Body.String())
|
||||
}
|
||||
runner, err := migration.New(fixture.db)
|
||||
if err != nil {
|
||||
t.Fatalf("migration.New() after review error = %v", err)
|
||||
}
|
||||
if err := runner.Down(context.Background()); err == nil {
|
||||
t.Fatal("candidate migration down succeeded with retained review data")
|
||||
}
|
||||
|
||||
completePayload := fmt.Sprintf(
|
||||
`{"execution_id":%q,"claim_generation":%d,"task_content_sha256":%q,"execution_mode":"MANUAL_FIRST","outcome":"CANDIDATE_ACCEPTED","operator_reason":"人工核对标题、SKU和截图后接受","candidate":{"ordinal":1,"title":"手动候选","sku_text":"TEST-SKU","price":"12.00","product_url":"https://example.test/product/1","image_url":"https://example.test/image/1.jpg","evidence_asset_ids":[%q],"evaluation":null},"order_submitted":false}`,
|
||||
started.Execution.ID,
|
||||
@@ -609,7 +678,12 @@ func TestDeviceExecutionResultsAreIdempotentAndAuditable(t *testing.T) {
|
||||
detail.Report.Outcome.OrderSubmitted ||
|
||||
len(detail.Report.Events) != 1 ||
|
||||
len(detail.Report.EvidenceAssets) != 1 ||
|
||||
detail.Report.CandidateBatch == nil {
|
||||
detail.Report.CandidateBatch == nil ||
|
||||
detail.Report.DecisionDataset == nil ||
|
||||
len(detail.Report.DecisionDataset.Observations) != 1 ||
|
||||
len(detail.Report.DecisionDataset.HumanReviews) != 2 ||
|
||||
detail.Report.DecisionDataset.HumanReviews[0].Version != 1 ||
|
||||
detail.Report.DecisionDataset.HumanReviews[1].Version != 2 {
|
||||
t.Fatalf("execution report = %+v", detail.Report)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user