feat(t216): deliver recoverable device order commands

This commit is contained in:
QiuSW
2026-07-28 13:23:18 +08:00
parent 75fdc63db2
commit 2372ab2280
32 changed files with 2415 additions and 65 deletions
@@ -0,0 +1,37 @@
package com.roubao.autopilot.procurement
import org.junit.Assert.assertEquals
import org.junit.Test
class OrderCommandIntegrityTest {
@Test
fun hashMatchesGoContractVector() {
val command = PendingOrderCommand(
id = "00000000-0000-4000-8000-000000000009",
schemaVersion = 1,
type = "CREATE_PENDING_ORDER",
authorizationVersion = 3,
taskId = "00000000-0000-4000-8000-000000000001",
executionId = "00000000-0000-4000-8000-000000000002",
taskContentSha256 = "a".repeat(64),
originalSku = "红色 M",
quantity = 2,
candidate = OrderCommandCandidate(
candidateKey = "candidate-key-1",
observedOrdinal = 1,
title = "红色连衣裙",
skuText = "红色 / M",
priceText = "19.90 CNY",
cardSignature = "b".repeat(64),
detailSignature = "c".repeat(64),
detailEvidenceSha256 = "d".repeat(64),
specificationEvidenceSha256 = "e".repeat(64)
),
commandSha256 =
"cf3cd802366d4de49d51940551385f7e5bd3283ef18cbf245a227c679d1058f3",
authorizationStatus = "DELIVERED"
)
assertEquals(command.commandSha256, OrderCommandIntegrity.sha256(command))
}
}
@@ -0,0 +1,134 @@
package com.roubao.autopilot.procurement
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class OrderCommandSynchronizationTest {
@Test
fun commandIsPersistedBeforeAcknowledgementAndReplayUsesSameKey() = runBlocking {
val task = task()
val execution = execution()
val delivered = command(task, execution)
val calls = mutableListOf<String>()
var persisted: PendingOrderCommand? = null
val result = OrderCommandSynchronization.synchronize(
existing = null,
task = task,
execution = execution,
pull = {
calls += "pull"
delivered
},
persist = {
calls += if (it.acknowledged) "persist-ack" else "persist-command"
persisted = it
},
acknowledge = { command, key ->
calls += "ack:$key"
assertEquals(command, persisted)
OrderCommandAcknowledgement(
commandId = command.id,
status = "ACKNOWLEDGED",
replayed = false
)
},
newIdempotencyKey = { "stable-ack-key" }
)
assertEquals(
listOf(
"pull",
"persist-command",
"ack:stable-ack-key",
"persist-ack"
),
calls
)
assertTrue(requireNotNull(result).acknowledged)
assertEquals("stable-ack-key", result.acknowledgementIdempotencyKey)
}
@Test
fun persistenceFailurePreventsAcknowledgement() = runBlocking {
val task = task()
val execution = execution()
var acknowledged = false
val failure = runCatching {
OrderCommandSynchronization.synchronize(
existing = null,
task = task,
execution = execution,
pull = { command(task, execution) },
persist = { error("storage unavailable") },
acknowledge = { _, _ ->
acknowledged = true
error("must not acknowledge")
},
newIdempotencyKey = { "stable-ack-key" }
)
}.exceptionOrNull()
assertEquals("storage unavailable", failure?.message)
assertEquals(false, acknowledged)
}
private fun task() = RemotePurchaseTask(
id = "00000000-0000-4000-8000-000000000001",
status = "WAITING_CONFIRMATION",
version = 4,
claimGeneration = 1,
claimExpiresAt = "2099-01-01T00:00:00Z",
title = "测试商品",
description = "",
sku = "红色 M",
imageAssetId = "00000000-0000-4000-8000-000000000003",
referenceImageUrl = "/api/v1/tasks/task/reference-image",
quantity = 2,
maxBudget = "20.00",
currency = "CNY"
)
private fun execution() = RunningExecution(
id = "00000000-0000-4000-8000-000000000002",
currentStep = "WAITING_ADMIN_CONFIRMATION",
expiresAt = "2099-01-01T00:00:00Z",
serverClockOffsetMillis = 0
)
private fun command(
task: RemotePurchaseTask,
execution: RunningExecution
): PendingOrderCommand {
val unsigned = PendingOrderCommand(
id = "00000000-0000-4000-8000-000000000009",
schemaVersion = 1,
type = "CREATE_PENDING_ORDER",
authorizationVersion = 1,
taskId = task.id,
executionId = execution.id,
taskContentSha256 = ExecutionTaskHash.sha256(task),
originalSku = task.sku,
quantity = task.quantity,
candidate = OrderCommandCandidate(
candidateKey = "f".repeat(64),
observedOrdinal = 1,
title = "测试候选",
skuText = "红色 / M",
priceText = "19.90",
cardSignature = "b".repeat(64),
detailSignature = "c".repeat(64),
detailEvidenceSha256 = "d".repeat(64),
specificationEvidenceSha256 = "e".repeat(64)
),
commandSha256 = "0".repeat(64),
authorizationStatus = "DELIVERED"
)
return unsigned.copy(
commandSha256 = OrderCommandIntegrity.sha256(unsigned)
)
}
}
@@ -250,6 +250,109 @@ class ProcurementApiClientTest {
)
}
@Test
fun orderCommandPullAndAcknowledgementFollowDeviceContract() = runBlocking {
val execution = RunningExecution(
id = "execution-id",
currentStep = "WAITING_ADMIN_CONFIRMATION",
expiresAt = "2099-01-01T00:00:00Z",
serverClockOffsetMillis = 0
)
server.enqueue(
jsonResponse(
"""
{
"id":"command-id",
"schema_version":1,
"type":"CREATE_PENDING_ORDER",
"authorization_version":1,
"task_id":"task-id",
"execution_id":"execution-id",
"task_content_sha256":"${"a".repeat(64)}",
"original_sku":"SKU-01",
"quantity":2,
"candidate":{
"candidate_key":"candidate-key-1",
"observed_ordinal":1,
"title":"候选商品",
"sku_text":"红色 / M",
"price_text":"20.00",
"card_signature":"${"b".repeat(64)}",
"detail_signature":"${"c".repeat(64)}",
"detail_evidence_sha256":"${"d".repeat(64)}",
"specification_evidence_sha256":"${"e".repeat(64)}"
},
"command_sha256":"${"f".repeat(64)}",
"authorization_status":"DELIVERED"
}
""".trimIndent()
)
)
val command = requireNotNull(
api.pullOrderCommand(
session(),
task("/api/v1/tasks/task-id/reference-image?claim_generation=1"),
execution,
"claim-token-value"
)
)
val pullRequest = server.takeRequest()
assertEquals(
"/api/v1/tasks/task-id/commands/next",
pullRequest.path
)
assertEquals(
"claim-token-value",
pullRequest.getHeader("X-Claim-Token")
)
assertTrue(pullRequest.body.readUtf8().contains("\"execution_id\":\"execution-id\""))
assertEquals("candidate-key-1", command.candidate.candidateKey)
server.enqueue(
jsonResponse(
"""{"command_id":"command-id","status":"ACKNOWLEDGED","replayed":false}"""
)
)
val acknowledgement = api.acknowledgeOrderCommand(
session(),
task("/api/v1/tasks/task-id/reference-image?claim_generation=1"),
execution,
"claim-token-value",
command,
"command-ack-idempotency-key"
)
val ackRequest = server.takeRequest()
assertEquals(
"/api/v1/tasks/task-id/commands/command-id/ack",
ackRequest.path
)
assertEquals(
"command-ack-idempotency-key",
ackRequest.getHeader("Idempotency-Key")
)
assertTrue(ackRequest.body.readUtf8().contains(command.commandSha256))
assertEquals("ACKNOWLEDGED", acknowledgement.status)
}
@Test
fun noOrderCommandIsAStableEmptyResult() = runBlocking {
server.enqueue(MockResponse().setResponseCode(204))
val command = api.pullOrderCommand(
session(),
task("/api/v1/tasks/task-id/reference-image?claim_generation=1"),
RunningExecution(
id = "execution-id",
currentStep = "WAITING_ADMIN_CONFIRMATION",
expiresAt = "2099-01-01T00:00:00Z",
serverClockOffsetMillis = 0
),
"claim-token-value"
)
assertEquals(null, command)
}
private fun session() = ProcurementSession(
backendUrl = server.url("/").toString().trimEnd('/'),
username = "buyer01",