fix(#53): parameterize purchase rule actions
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Android-Agent-API-Contract
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Android-Agent-API-Contract.-
|
||||
wiki_revision: 5da7f2250f851caab6d0d1713f869b619ed55eb2
|
||||
synchronized_at: 2026-08-20T08:47:11Z
|
||||
wiki_revision: 8b81c816989f9063a6927939a3391c103ddc08ad
|
||||
synchronized_at: 2026-08-20T09:51:45Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# MVP 共享 API 契约
|
||||
@@ -369,7 +369,7 @@ POST /api/agent/v1/tasks/{taskId}/fail
|
||||
| `DEVICE_BUSY` | 设备已有活动任务 | 否 |
|
||||
| `DEVICE_CAPABILITY_MISMATCH` | 设备缺少任务规则要求的版本化能力 | 否 |
|
||||
|
||||
## 采购任务共享契约(#33、#34)
|
||||
## 采购任务共享契约(#33、#34、#53)
|
||||
|
||||
本节固定采购域的数据和接口边界;`purchase_task`、`purchase_task_attempt`、规则校验及服务端 HTTP 状态机已落地,Admin 页面和 Android 执行动作分别由后续工单实现。任何实现都不得扩展为自动支付。
|
||||
|
||||
@@ -399,7 +399,7 @@ POST /api/agent/v1/tasks/{taskId}/fail
|
||||
|
||||
### 规则快照与能力
|
||||
|
||||
采购规则是 JSON 对象:
|
||||
采购规则是 JSON 对象。#53 在既有 schema v1 中增加受限参数,不引入任意脚本:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -407,8 +407,21 @@ POST /api/agent/v1/tasks/{taskId}/fail
|
||||
"ruleType": "pddPurchase",
|
||||
"requiredCapabilities": ["purchase.rehearsal.v1"],
|
||||
"actions": [
|
||||
{"type": "openProduct"},
|
||||
{"type": "verifyProduct"},
|
||||
{
|
||||
"type": "openProduct",
|
||||
"textAliases": ["打开拼多多APP", "打开"],
|
||||
"waitAfterMs": 1000
|
||||
},
|
||||
{
|
||||
"type": "openSpecPanel",
|
||||
"textAliases": ["选择规格"],
|
||||
"swipeAfter": {
|
||||
"direction": "up",
|
||||
"count": 2,
|
||||
"durationMs": 500,
|
||||
"intervalMs": 1000
|
||||
}
|
||||
},
|
||||
{"type": "selectSpec"},
|
||||
{"type": "setQuantity"},
|
||||
{"type": "verifyUnitPrice"},
|
||||
@@ -417,6 +430,30 @@ POST /api/agent/v1/tasks/{taskId}/fail
|
||||
}
|
||||
```
|
||||
|
||||
安全动作参数矩阵:
|
||||
|
||||
| action | `textAliases` | `waitAfterMs` | `swipeAfter` |
|
||||
|---|---:|---:|---:|
|
||||
| `openProduct` | 是 | 是 | 是 |
|
||||
| `verifyProduct` | 是 | 是 | 否 |
|
||||
| `openSpecPanel` | 是 | 是 | 是 |
|
||||
| `selectSpec` | 是 | 是 | 是 |
|
||||
| `setQuantity` | 是 | 是 | 否 |
|
||||
| `verifyUnitPrice` | 是 | 是 | 否 |
|
||||
| `verifyOrderSummary` | 是 | 是 | 否 |
|
||||
| `probeSpecs` | 是 | 是 | 是 |
|
||||
|
||||
参数规则:
|
||||
|
||||
- `textAliases` 省略时使用 Agent 对该类型化动作的内置语义目标;显式提供时必须包含 1~16 个互不重复、无首尾空白的非空文字,每项最多 64 个字符。
|
||||
- 文字候选按控件文字或内容描述**精确匹配**;候选合并后必须唯一命中。点击动作只允许点击唯一文字节点或其最近的可点击父容器,不允许模糊匹配、猜测相近候选、改点兄弟节点。
|
||||
- `textAliases` 不能包含地址修改、创建/提交订单、订单号或支付相关文字,防止用安全 action 绕过危险动作类型和能力门禁。
|
||||
- `waitAfterMs` 表示动作成功后的等待时间,范围为 0~30000 毫秒;省略时为 0。
|
||||
- `swipeAfter` 表示动作成功后执行一个有限滑动计划。`direction` 只能为 `up` / `down` / `left` / `right`,`count` 为 1~10,`durationMs` 为 100~2000,`intervalMs` 为 0~5000 且省略时为 0。
|
||||
- 未在矩阵中授权的 action/参数组合、未知字段、空候选和越界值一律拒绝。`updateShippingAddress`、`createOrder`、`readOrderResult` 等正式动作在其独立高风险契约完成前不接受上述参数。
|
||||
- 旧的仅含 `actions[].type` 的规则继续有效:候选使用 Agent 内置语义,等待为 0,不执行动作后滑动。
|
||||
- 服务端保存创建任务时收到的完整原始规则快照;规则后来更新为规则 B,不会改变已有任务中的规则 A 快照。
|
||||
|
||||
演练规则必须包含 `purchase.rehearsal.v1`,并且不能包含 `updateShippingAddress`、`createOrder`、`readOrderResult`。正式规则必须包含 `purchase.live.v1`;改地址和创建订单还分别要求 `purchase.address-update.v1`、`purchase.order-create.v1`。`probeSpecs` 要求 `purchase.spec-probe.v1`。任意模式下,`pay`、名称包含 `payment` 的动作以及未知动作一律拒绝;服务端不下发任意脚本。
|
||||
|
||||
### 管理端接口
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Delivery-Issues
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Delivery-Issues.-
|
||||
wiki_revision: e80a0564cf94d2feb3c068d0bf24419d36e21e67
|
||||
synchronized_at: 2026-08-20T08:47:11Z
|
||||
wiki_revision: f1f993a6ed4aa4dc53e1ad7943ac60bca6db790e
|
||||
synchronized_at: 2026-08-20T09:51:48Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 当前 MVP 交付工单索引
|
||||
@@ -62,15 +62,16 @@ synchronized_at: 2026-08-20T08:47:11Z
|
||||
| T37 | [#39](https://git.ilapage.cn/OPC/goauto/issues/39) | 采购闭环真机端到端验收 | #33~#38、#42 |
|
||||
| T38 | [#40](https://git.ilapage.cn/OPC/goauto/issues/40) | 虾皮商品档案、PDD 关联与规格映射(2026-08-20 已验收) | 已完成 |
|
||||
| T39 | [#41](https://git.ilapage.cn/OPC/goauto/issues/41) | SYB 货运单商品导入与虾皮信息提取(2026-08-20 已验收) | 已完成 |
|
||||
| T40 | [#42](https://git.ilapage.cn/OPC/goauto/issues/42) | Android 采购演练规则与持久执行基线 | #33、#34;只演练,不改地址、不创建订单 |
|
||||
| T40 | [#42](https://git.ilapage.cn/OPC/goauto/issues/42) | Android 采购演练规则与持久执行基线 | #33、#34、#53;只演练,不改地址、不创建订单 |
|
||||
| T43 | [#45](https://git.ilapage.cn/OPC/goauto/issues/45) | Admin PDD 商品列表多选与批量采集任务创建(2026-08-18 已验收) | 已完成 |
|
||||
| T46 | [#48](https://git.ilapage.cn/OPC/goauto/issues/48) | SYB ERP 客户端移植与真实导入 | #41;已实施,等待验收 |
|
||||
| T47 | [#49](https://git.ilapage.cn/OPC/goauto/issues/49) | 店铺管理与 SYB 同步店铺过滤(2026-08-20 已验收) | 已完成 |
|
||||
| T48 | [#50](https://git.ilapage.cn/OPC/goauto/issues/50) | SYB 导入后台任务与进度展示(2026-08-20 已验收) | 已完成 |
|
||||
| T49 | [#51](https://git.ilapage.cn/OPC/goauto/issues/51) | 优化 SYB 商品列表工具栏与列宽(2026-08-20 已验收) | 已完成 |
|
||||
| T50 | [#52](https://git.ilapage.cn/OPC/goauto/issues/52) | 修复 SYB 同步预创建失败无法定位(2026-08-20 已验收) | 已完成 |
|
||||
| T51 | [#53](https://git.ilapage.cn/OPC/goauto/issues/53) | 补齐采购规则参数化动作契约 | 已实施,等待验收;通过后解除 #42 阻塞 |
|
||||
|
||||
推荐依赖顺序:#31、#40、#41 完成商品域 → #33、#34 建立采购契约和服务端状态机 → #42 完成不下单演练 → #35 管理端人工处理 → #36 高风险真实订单动作 → #37、#38 物流闭环 → #39 真机总验收。
|
||||
推荐依赖顺序:#31、#40、#41 完成商品域 → #33、#34 建立采购契约和服务端状态机 → #53 补齐参数化动作契约 → #42 完成不下单演练 → #35 管理端人工处理 → #36 高风险真实订单动作 → #37、#38 物流闭环 → #39 真机总验收。
|
||||
|
||||
## 延期
|
||||
|
||||
|
||||
@@ -113,6 +113,19 @@ func TestRehearsalCannotContainOrderActions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRehearsalPreservesParameterizedRuleSnapshot(t *testing.T) {
|
||||
db := testDB(t)
|
||||
f := seed(t, db, []string{purchasecontract.CapabilityPurchaseRehearsalV1}, true)
|
||||
raw := []byte(`{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.rehearsal.v1"],"actions":[{"type":"openProduct","textAliases":["打开拼多多APP","打开"],"waitAfterMs":1000},{"type":"openSpecPanel","swipeAfter":{"direction":"up","count":2,"durationMs":500,"intervalMs":1000}},{"type":"verifyOrderSummary"}]}`)
|
||||
task, _, err := testService(db).Create(context.Background(), CreateRequest{RequestID: uuid.NewString(), ExecutionMode: models.PurchaseExecutionModeRehearsal, PDDProductID: &f.pdd.ID, Quantity: 1, Currency: "CNY", MaxUnitPriceCent: 1000, RuleSnapshot: raw})
|
||||
if err != nil {
|
||||
t.Fatalf("parameterized rehearsal create failed: %v", err)
|
||||
}
|
||||
if task.RuleSnapshot != string(raw) {
|
||||
t.Fatalf("rule snapshot changed:\nwant %s\n got %s", raw, task.RuleSnapshot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAndLifecycleValidateCapabilitiesAndIdempotentResult(t *testing.T) {
|
||||
db := testDB(t)
|
||||
f := seed(t, db, liveCaps(), true)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,6 +21,14 @@ const (
|
||||
CapabilitySpecProbeV1 = "purchase.spec-probe.v1"
|
||||
CapabilityAddressUpdateV1 = "purchase.address-update.v1"
|
||||
CapabilityOrderCreateV1 = "purchase.order-create.v1"
|
||||
|
||||
MaxTextAliases = 16
|
||||
MaxTextAliasRunes = 64
|
||||
MaxWaitAfterMs = 30_000
|
||||
MaxSwipeCount = 10
|
||||
MinSwipeDurationMs = 100
|
||||
MaxSwipeDurationMs = 2_000
|
||||
MaxSwipeIntervalMs = 5_000
|
||||
)
|
||||
|
||||
var capabilityPattern = regexp.MustCompile(`^[a-z][a-z0-9.-]{0,79}$`)
|
||||
@@ -36,8 +45,26 @@ var liveOnlyActions = map[string]string{
|
||||
"readOrderResult": CapabilityPurchaseLiveV1,
|
||||
}
|
||||
|
||||
var swipeAfterActions = map[string]bool{
|
||||
"openProduct": true, "openSpecPanel": true, "selectSpec": true, "probeSpecs": true,
|
||||
}
|
||||
|
||||
var forbiddenTextFragments = []string{
|
||||
"pay", "payment", "支付", "付款", "免密", "修改地址", "收货地址", "创建订单", "提交订单", "订单号",
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
Type string `json:"type"`
|
||||
Type string `json:"type"`
|
||||
TextAliases []string `json:"textAliases,omitempty"`
|
||||
WaitAfterMs *int `json:"waitAfterMs,omitempty"`
|
||||
SwipeAfter *SwipePlan `json:"swipeAfter,omitempty"`
|
||||
}
|
||||
|
||||
type SwipePlan struct {
|
||||
Direction string `json:"direction"`
|
||||
Count int `json:"count"`
|
||||
DurationMs int `json:"durationMs"`
|
||||
IntervalMs int `json:"intervalMs,omitempty"`
|
||||
}
|
||||
|
||||
type RuleSnapshot struct {
|
||||
@@ -88,6 +115,9 @@ func Validate(raw []byte, executionMode string) (RuleSnapshot, error) {
|
||||
if action.Type == "probeSpecs" && !capabilities[CapabilitySpecProbeV1] {
|
||||
return rule, fmt.Errorf("动作 probeSpecs 缺少能力 %s", CapabilitySpecProbeV1)
|
||||
}
|
||||
if err := validateSafeActionParameters(index, action); err != nil {
|
||||
return rule, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
requiredCapability, liveOnly := liveOnlyActions[action.Type]
|
||||
@@ -100,10 +130,64 @@ func Validate(raw []byte, executionMode string) (RuleSnapshot, error) {
|
||||
if !capabilities[requiredCapability] {
|
||||
return rule, fmt.Errorf("动作 %s 缺少能力 %s", action.Type, requiredCapability)
|
||||
}
|
||||
if len(action.TextAliases) > 0 || action.WaitAfterMs != nil || action.SwipeAfter != nil {
|
||||
return rule, fmt.Errorf("actions[%d] 的正式动作 %s 暂不支持参数化", index, action.Type)
|
||||
}
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func validateSafeActionParameters(index int, action Action) error {
|
||||
if action.TextAliases != nil && len(action.TextAliases) == 0 {
|
||||
return fmt.Errorf("actions[%d].textAliases 显式提供时不能为空", index)
|
||||
}
|
||||
if len(action.TextAliases) > MaxTextAliases {
|
||||
return fmt.Errorf("actions[%d].textAliases 最多包含 %d 项", index, MaxTextAliases)
|
||||
}
|
||||
seen := make(map[string]bool, len(action.TextAliases))
|
||||
for aliasIndex, alias := range action.TextAliases {
|
||||
if alias == "" || strings.TrimSpace(alias) != alias {
|
||||
return fmt.Errorf("actions[%d].textAliases[%d] 必须是无首尾空白的非空文字", index, aliasIndex)
|
||||
}
|
||||
if !utf8.ValidString(alias) || utf8.RuneCountInString(alias) > MaxTextAliasRunes {
|
||||
return fmt.Errorf("actions[%d].textAliases[%d] 最多包含 %d 个字符", index, aliasIndex, MaxTextAliasRunes)
|
||||
}
|
||||
if seen[alias] {
|
||||
return fmt.Errorf("actions[%d].textAliases 包含重复文字 %q", index, alias)
|
||||
}
|
||||
seen[alias] = true
|
||||
lower := strings.ToLower(alias)
|
||||
for _, fragment := range forbiddenTextFragments {
|
||||
if strings.Contains(lower, fragment) {
|
||||
return fmt.Errorf("actions[%d].textAliases[%d] 包含禁止的地址、下单或支付文字", index, aliasIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
if action.WaitAfterMs != nil && (*action.WaitAfterMs < 0 || *action.WaitAfterMs > MaxWaitAfterMs) {
|
||||
return fmt.Errorf("actions[%d].waitAfterMs 必须在 0..%d 之间", index, MaxWaitAfterMs)
|
||||
}
|
||||
if action.SwipeAfter == nil {
|
||||
return nil
|
||||
}
|
||||
if !swipeAfterActions[action.Type] {
|
||||
return fmt.Errorf("actions[%d] 的动作 %s 不支持 swipeAfter", index, action.Type)
|
||||
}
|
||||
swipe := action.SwipeAfter
|
||||
if swipe.Direction != "up" && swipe.Direction != "down" && swipe.Direction != "left" && swipe.Direction != "right" {
|
||||
return fmt.Errorf("actions[%d].swipeAfter.direction 只支持 up、down、left、right", index)
|
||||
}
|
||||
if swipe.Count < 1 || swipe.Count > MaxSwipeCount {
|
||||
return fmt.Errorf("actions[%d].swipeAfter.count 必须在 1..%d 之间", index, MaxSwipeCount)
|
||||
}
|
||||
if swipe.DurationMs < MinSwipeDurationMs || swipe.DurationMs > MaxSwipeDurationMs {
|
||||
return fmt.Errorf("actions[%d].swipeAfter.durationMs 必须在 %d..%d 之间", index, MinSwipeDurationMs, MaxSwipeDurationMs)
|
||||
}
|
||||
if swipe.IntervalMs < 0 || swipe.IntervalMs > MaxSwipeIntervalMs {
|
||||
return fmt.Errorf("actions[%d].swipeAfter.intervalMs 必须在 0..%d 之间", index, MaxSwipeIntervalMs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RequiredCapabilities(rule RuleSnapshot) []string {
|
||||
result := append([]string(nil), rule.RequiredCapabilities...)
|
||||
sort.Strings(result)
|
||||
|
||||
@@ -1,10 +1,100 @@
|
||||
package purchasecontract
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRehearsalRuleAcceptsBoundedParameters(t *testing.T) {
|
||||
raw := []byte(`{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.rehearsal.v1","purchase.spec-probe.v1"],"actions":[{"type":"openProduct","textAliases":["打开拼多多APP","打开"],"waitAfterMs":1000},{"type":"openSpecPanel","textAliases":["选择规格"],"swipeAfter":{"direction":"up","count":2,"durationMs":500,"intervalMs":1000}},{"type":"probeSpecs","swipeAfter":{"direction":"left","count":3,"durationMs":300}}]}`)
|
||||
rule, err := Validate(raw, "rehearsal")
|
||||
if err != nil {
|
||||
t.Fatalf("valid parameterized rule rejected: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(rule.Actions[0].TextAliases, []string{"打开拼多多APP", "打开"}) || rule.Actions[0].WaitAfterMs == nil || *rule.Actions[0].WaitAfterMs != 1000 {
|
||||
t.Fatalf("parameters were not preserved: %#v", rule.Actions[0])
|
||||
}
|
||||
if got := rule.Actions[1].SwipeAfter; got == nil || got.Direction != "up" || got.Count != 2 || got.DurationMs != 500 || got.IntervalMs != 1000 {
|
||||
t.Fatalf("swipe plan was not preserved: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeOnlyRuleRemainsCompatible(t *testing.T) {
|
||||
raw := []byte(`{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.rehearsal.v1"],"actions":[{"type":"openProduct"},{"type":"verifyProduct"}]}`)
|
||||
rule, err := Validate(raw, "rehearsal")
|
||||
if err != nil {
|
||||
t.Fatalf("legacy type-only rule rejected: %v", err)
|
||||
}
|
||||
for _, action := range rule.Actions {
|
||||
if len(action.TextAliases) != 0 || action.WaitAfterMs != nil || action.SwipeAfter != nil {
|
||||
t.Fatalf("legacy defaults changed: %#v", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParameterizedRuleRejectsUnknownFields(t *testing.T) {
|
||||
tests := []string{
|
||||
`{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.rehearsal.v1"],"actions":[{"type":"openProduct","selector":{"text":"打开"}}]}`,
|
||||
`{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.rehearsal.v1"],"actions":[{"type":"openProduct","swipeAfter":{"direction":"up","count":2,"durationMs":500,"unknown":1}}]}`,
|
||||
}
|
||||
for _, raw := range tests {
|
||||
if _, err := Validate([]byte(raw), "rehearsal"); err == nil || !strings.Contains(err.Error(), "字段无效") {
|
||||
t.Fatalf("expected unknown field rejection for %s, got %v", raw, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParameterizedRuleRejectsInvalidAliases(t *testing.T) {
|
||||
tests := []string{
|
||||
`"textAliases":[]`,
|
||||
`"textAliases":[""]`,
|
||||
`"textAliases":[" 打开"]`,
|
||||
`"textAliases":["打开","打开"]`,
|
||||
`"textAliases":["立即支付"]`,
|
||||
`"textAliases":["submit payment"]`,
|
||||
}
|
||||
for _, params := range tests {
|
||||
raw := `{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.rehearsal.v1"],"actions":[{"type":"openProduct",` + params + `}]}`
|
||||
if _, err := Validate([]byte(raw), "rehearsal"); err == nil {
|
||||
t.Fatalf("expected alias rejection for %s", params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParameterizedRuleRejectsOutOfBoundsWaitAndSwipe(t *testing.T) {
|
||||
tests := []string{
|
||||
`"waitAfterMs":-1`,
|
||||
`"waitAfterMs":30001`,
|
||||
`"swipeAfter":{"direction":"diagonal","count":1,"durationMs":500}`,
|
||||
`"swipeAfter":{"direction":"up","count":0,"durationMs":500}`,
|
||||
`"swipeAfter":{"direction":"up","count":11,"durationMs":500}`,
|
||||
`"swipeAfter":{"direction":"up","count":1,"durationMs":99}`,
|
||||
`"swipeAfter":{"direction":"up","count":1,"durationMs":2001}`,
|
||||
`"swipeAfter":{"direction":"up","count":1,"durationMs":500,"intervalMs":5001}`,
|
||||
}
|
||||
for _, params := range tests {
|
||||
raw := `{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.rehearsal.v1"],"actions":[{"type":"openProduct",` + params + `}]}`
|
||||
if _, err := Validate([]byte(raw), "rehearsal"); err == nil {
|
||||
t.Fatalf("expected bounded parameter rejection for %s", params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwipeAfterIsLimitedToDeclaredActions(t *testing.T) {
|
||||
raw := []byte(`{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.rehearsal.v1"],"actions":[{"type":"verifyUnitPrice","swipeAfter":{"direction":"up","count":1,"durationMs":500}}]}`)
|
||||
if _, err := Validate(raw, "rehearsal"); err == nil || !strings.Contains(err.Error(), "不支持 swipeAfter") {
|
||||
t.Fatalf("expected action parameter matrix rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveOnlyActionsRejectParametersUntilTheirContractExists(t *testing.T) {
|
||||
raw := []byte(`{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.live.v1","purchase.order-create.v1"],"actions":[{"type":"createOrder","waitAfterMs":1000}]}`)
|
||||
if _, err := Validate(raw, "live"); err == nil || !strings.Contains(err.Error(), "暂不支持参数化") {
|
||||
t.Fatalf("expected live parameter rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRehearsalRuleRejectsIrreversibleActions(t *testing.T) {
|
||||
raw := []byte(`{"schemaVersion":1,"ruleType":"pddPurchase","requiredCapabilities":["purchase.rehearsal.v1"],"actions":[{"type":"openProduct"},{"type":"updateShippingAddress"}]}`)
|
||||
if _, err := Validate(raw, "rehearsal"); err == nil || !strings.Contains(err.Error(), "演练规则不能") {
|
||||
|
||||
Reference in New Issue
Block a user