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
@@ -0,0 +1,67 @@
package com.roubao.autopilot.procurement
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
class CandidateHumanReviewDraftPolicyTest {
@Test
fun `accepted review labels selected and rejected observations`() {
val review = CandidateHumanReviewDraftPolicy.accepted(
candidateOrdinals = listOf(1, 2, 3),
selectedOrdinal = 2,
acceptReasonCode = "SKU_MATCH",
acceptNote = "",
rejectReasonCode = "NOT_BEST_MATCH",
rejectNote = "",
hasBudget = false
)
assertEquals("CANDIDATE_ACCEPTED", review.outcome)
assertEquals(2, review.selectedCandidateOrdinal)
assertEquals(
listOf("REJECT", "ACCEPT", "REJECT"),
review.items.map { it.label }
)
}
@Test
fun `no observations produce an explicit no-match review`() {
val review = CandidateHumanReviewDraftPolicy.rejected(
candidateOrdinals = emptyList(),
rejectReasonCode = "",
rejectNote = "",
hasBudget = false
)
assertEquals("NO_MATCH", review.outcome)
assertEquals(emptyList<CandidateHumanReviewItemDraft>(), review.items)
}
@Test
fun `price reasons require a task budget`() {
assertThrows(IllegalArgumentException::class.java) {
CandidateHumanReviewDraftPolicy.rejected(
candidateOrdinals = listOf(1),
rejectReasonCode = "PRICE_TOO_HIGH",
rejectNote = "",
hasBudget = false
)
}
}
@Test
fun `other reasons require a meaningful note`() {
assertThrows(IllegalArgumentException::class.java) {
CandidateHumanReviewDraftPolicy.accepted(
candidateOrdinals = listOf(1),
selectedOrdinal = 1,
acceptReasonCode = "OTHER",
acceptNote = "短",
rejectReasonCode = "",
rejectNote = "",
hasBudget = true
)
}
}
}
@@ -56,6 +56,23 @@ class ExecutionCandidateIdentityPolicyTest {
)
}
@Test
fun `explicit selection keeps the original observation ordinal`() {
val bindings = ExecutionCandidateIdentityPolicy.bind(
identities = listOf(
identity(sourceOrdinal = 1, rankedOrdinal = 1),
identity(sourceOrdinal = 2, rankedOrdinal = 2),
identity(sourceOrdinal = 3, rankedOrdinal = 3)
),
candidates = listOf(candidate(1), candidate(2), candidate(3))
)
val selected = ExecutionCandidateIdentityPolicy.select(bindings, 3)
assertEquals(3, selected.ordinal)
assertEquals("evidence-3", selected.evidenceLocalIDs.single())
}
@Test
fun `missing ranked ordinal is rejected instead of falling back`() {
assertThrows(IllegalArgumentException::class.java) {
@@ -225,6 +225,29 @@ class ProcurementApiClientTest {
assertEquals("/api/v1/tasks/task-id/events", eventRequest.path)
assertEquals("event-idempotency-key", eventRequest.getHeader("Idempotency-Key"))
assertTrue(eventRequest.body.readUtf8().contains("execution-id"))
server.enqueue(jsonResponse("""{"review":{"id":"review-id"},"replayed":false}"""))
api.uploadExecutionOutboxItem(
session = session(),
task = task,
execution = execution,
claimToken = "claim-token-value",
item = ExecutionOutboxItem(
id = "review-id",
type = ExecutionOutboxType.HUMAN_REVIEW,
idempotencyKey = "review-idempotency-key",
payload = """{"execution_id":"execution-id","items":[]}"""
)
)
val reviewRequest = server.takeRequest()
assertEquals(
"/api/v1/tasks/task-id/human-reviews",
reviewRequest.path
)
assertEquals(
"review-idempotency-key",
reviewRequest.getHeader("Idempotency-Key")
)
}
private fun session() = ProcurementSession(