feat(android): import private Shopee probe tasks

This commit is contained in:
QiuSW
2026-07-25 19:06:28 +08:00
parent d57c4ae2d4
commit 1c1c158149
25 changed files with 1034 additions and 34 deletions
@@ -0,0 +1,35 @@
package com.roubao.autopilot.task
import android.content.Context
import com.roubao.task.ProbeTask
import com.roubao.task.ProbeTaskJson
import com.roubao.task.TaskSource
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicInteger
class FixtureTaskSource(
context: Context,
private val taskDocumentAsset: String = DEFAULT_TASK_DOCUMENT_ASSET
) : TaskSource {
private val assetManager = context.applicationContext.assets
private val nextIndex = AtomicInteger(0)
private val tasks: List<ProbeTask> by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
val payload = assetManager.open(taskDocumentAsset).bufferedReader(
StandardCharsets.UTF_8
).use { it.readText() }
ProbeTaskJson.decode(payload)
}
override suspend fun nextTask(): ProbeTask? {
val index = nextIndex.getAndIncrement()
return tasks.getOrNull(index)
}
fun referenceImageAsset(task: ProbeTask): String =
"$FIXTURE_ASSET_ROOT/${task.referenceImage.relativePath}"
companion object {
private const val FIXTURE_ASSET_ROOT = "probe-fixtures"
private const val DEFAULT_TASK_DOCUMENT_ASSET = "$FIXTURE_ASSET_ROOT/tasks.json"
}
}
@@ -0,0 +1,27 @@
package com.roubao.autopilot.workflow
import com.roubao.task.ProbeTask
import com.roubao.task.TaskSource
sealed interface ProbeWorkflowResult {
data object NoTask : ProbeWorkflowResult
data class Executed(
val probeId: String,
val report: WorkflowReport
) : ProbeWorkflowResult
}
class ProbeWorkflowCoordinator(
private val taskSource: TaskSource,
private val workflowRunner: WorkflowRunner,
private val stepsForTask: (ProbeTask) -> List<WorkflowStep>
) {
suspend fun runNext(): ProbeWorkflowResult {
val task = taskSource.nextTask() ?: return ProbeWorkflowResult.NoTask
return ProbeWorkflowResult.Executed(
probeId = task.probeId,
report = workflowRunner.run(stepsForTask(task))
)
}
}
@@ -0,0 +1,63 @@
package com.roubao.autopilot.workflow
import com.roubao.task.ProbeReferenceImage
import com.roubao.task.ProbeTask
import com.roubao.task.TaskSource
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
class ProbeWorkflowCoordinatorTest {
@Test
fun `workflow consumes task source without knowing fixture implementation`() = runTest {
val expectedTask = probeTask()
val source = TaskSource { expectedTask }
val runner = WorkflowRunner { AutomationResult.Success }
val coordinator = ProbeWorkflowCoordinator(
taskSource = source,
workflowRunner = runner,
stepsForTask = { task ->
assertEquals(expectedTask.title, task.title)
assertEquals(expectedTask.sku, task.sku)
assertEquals(expectedTask.quantity, task.quantity)
listOf(WorkflowStep("prepare_search", timeoutMillis = 1_000))
}
)
val result = coordinator.runNext() as ProbeWorkflowResult.Executed
assertEquals(expectedTask.probeId, result.probeId)
assertEquals(WorkflowState.SUCCEEDED, result.report.state)
}
@Test
fun `empty task source does not start workflow`() = runTest {
var automationCalls = 0
val coordinator = ProbeWorkflowCoordinator(
taskSource = TaskSource { null },
workflowRunner = WorkflowRunner {
automationCalls += 1
AutomationResult.Success
},
stepsForTask = { listOf(WorkflowStep("unused", timeoutMillis = 1_000)) }
)
assertEquals(ProbeWorkflowResult.NoTask, coordinator.runNext())
assertEquals(0, automationCalls)
}
private fun probeTask() = ProbeTask(
probeId = "probe_0123456789abcdef",
sourceOrderNo = "ORDER_000001",
sourceStoreName = "Test Store",
title = "Test Product",
sku = "SKU-001",
quantity = 1,
referenceImage = ProbeReferenceImage(
relativePath = "assets/0123456789abcdef.jpg",
mediaType = "image/jpeg",
sizeBytes = 123,
sha256 = "a".repeat(64)
)
)
}