feat: 定义运行时规格解析契约与审计模型 (#254)

This commit is contained in:
chengma
2026-08-17 16:53:11 +08:00
parent b11a586527
commit 12414658d0
8 changed files with 736 additions and 16 deletions
@@ -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()