feat: 定义运行时规格解析契约与审计模型 (#254)
This commit is contained in:
@@ -1166,6 +1166,100 @@ func TestMySQLMigrate_V24升级V25且后台队列状态有效(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLMigrate_V25升级V26且规格解析审计幂等(t *testing.T) {
|
||||
db := openMySQLMigrationTestDB(t)
|
||||
defer db.Close()
|
||||
cleanMySQLTestSchema(t, db)
|
||||
defer cleanMySQLTestSchema(t, db)
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustExec(t, db, `DROP TABLE purchase_spec_resolutions`)
|
||||
mustExec(t, db, `DELETE FROM schema_migrations WHERE version>=26`)
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatalf("v25 升级 v26 失败: %v", err)
|
||||
}
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatalf("v26 重复迁移失败: %v", err)
|
||||
}
|
||||
if err := checkMySQLV26Shape(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
createdAt := "2026-08-17T08:00:00Z"
|
||||
resolution := model.PurchaseSpecResolution{
|
||||
ResolutionID: "PSR-1",
|
||||
TaskID: "cg254",
|
||||
AttemptID: "attempt-254",
|
||||
ClientID: "client-254",
|
||||
TaskVersion: 3,
|
||||
PddGoodsID: "937122477375",
|
||||
OriginalOptionsJSON: `{"color":"黑色","size":"60公斤"}`,
|
||||
SelectedColor: "黑色",
|
||||
TargetSize: "60公斤",
|
||||
CandidatesJSON: `[{"candidate_id":"c1","raw_text":"120斤","options":{"color":"黑色","size":"120斤"}}]`,
|
||||
CandidateSnapshotHash: strings.Repeat("a", 64),
|
||||
RequestHash: strings.Repeat("b", 64),
|
||||
ObservedAt: createdAt,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
if err := InsertPurchaseSpecResolution(db, resolution); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
replayed, err := GetPurchaseSpecResolutionForReplay(db, resolution.TaskID, resolution.AttemptID,
|
||||
resolution.CandidateSnapshotHash, resolution.RequestHash)
|
||||
if err != nil || replayed == nil || replayed.ResolutionID != resolution.ResolutionID {
|
||||
t.Fatalf("相同请求未命中解析记录: resolution=%+v err=%v", replayed, err)
|
||||
}
|
||||
if _, err := GetPurchaseSpecResolutionForReplay(db, resolution.TaskID, resolution.AttemptID,
|
||||
resolution.CandidateSnapshotHash, strings.Repeat("c", 64)); !errors.Is(err, ErrPurchaseSpecResolutionConflict) {
|
||||
t.Fatalf("相同业务身份的不同请求应冲突,实际 %v", err)
|
||||
}
|
||||
duplicate := resolution
|
||||
duplicate.ResolutionID = "PSR-2"
|
||||
if err := InsertPurchaseSpecResolution(db, duplicate); !errors.Is(err, ErrPurchaseSpecResolutionExists) {
|
||||
t.Fatalf("业务身份唯一键未生效,实际 %v", err)
|
||||
}
|
||||
|
||||
decision := model.PurchaseSpecResolutionDecision{
|
||||
ResolutionID: resolution.ResolutionID,
|
||||
Outcome: model.PurchaseSpecResolutionMatched,
|
||||
DecisionSource: model.PurchaseSpecResolutionAI,
|
||||
ChosenCandidateID: "c1",
|
||||
ResolvedOptionsJSON: `{"color":"黑色","size":"120斤"}`,
|
||||
ConfidenceBPS: 9300,
|
||||
ConfidenceSet: true,
|
||||
Reason: "候选唯一且通过门禁",
|
||||
ProviderID: "AI-1",
|
||||
SourceModel: "model-1",
|
||||
ConfigFingerprint: strings.Repeat("d", 64),
|
||||
RulesVersion: "runtime-v1",
|
||||
PromptVersion: "runtime-v1",
|
||||
DecidedAt: "2026-08-17T08:00:01Z",
|
||||
}
|
||||
if err := CompletePurchaseSpecResolution(db, decision); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
completed, err := GetPurchaseSpecResolutionByID(db, resolution.ResolutionID)
|
||||
if err != nil || completed == nil || completed.Outcome != model.PurchaseSpecResolutionMatched ||
|
||||
completed.DecisionSource != model.PurchaseSpecResolutionAI || !completed.ConfidenceSet || completed.ConfidenceBPS != 9300 {
|
||||
t.Fatalf("解析决策读取不完整: resolution=%+v err=%v", completed, err)
|
||||
}
|
||||
if err := CompletePurchaseSpecResolution(db, decision); !errors.Is(err, ErrPurchaseSpecResolutionCompleted) {
|
||||
t.Fatalf("已经完成的决策不应被覆盖,实际 %v", err)
|
||||
}
|
||||
|
||||
tooManyCandidates := resolution
|
||||
tooManyCandidates.ResolutionID = "PSR-TOO-MANY"
|
||||
tooManyCandidates.AttemptID = "attempt-too-many"
|
||||
tooManyCandidates.CandidateSnapshotHash = strings.Repeat("e", 64)
|
||||
tooManyCandidates.RequestHash = strings.Repeat("f", 64)
|
||||
tooManyCandidates.CandidatesJSON = `[` + strings.TrimSuffix(strings.Repeat(`{"candidate_id":"c1"},`, 101), ",") + `]`
|
||||
if err := InsertPurchaseSpecResolution(db, tooManyCandidates); err == nil {
|
||||
t.Fatal("数据库必须拒绝超过 100 个候选的解析观察")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertInnerCodeImportRow_软删除记录按状态安全恢复(t *testing.T) {
|
||||
db := openMySQLMigrationTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
Reference in New Issue
Block a user