feat(t208): close candidate decision feedback loop

This commit is contained in:
QiuSW
2026-07-28 11:57:25 +08:00
parent 2a5ded42b5
commit e7f4c3e114
35 changed files with 2854 additions and 203 deletions
@@ -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)
}
}