修复全量审查问题;设置接口PATCH化;回传指纹加固
CI / test (push) Successful in 32s

This commit is contained in:
2026-07-22 16:51:23 +08:00
parent 0614ef22af
commit f51fb6c722
66 changed files with 3997 additions and 687 deletions
+70
View File
@@ -11,6 +11,8 @@ import (
"oci-portal/internal/aiwire"
"oci-portal/internal/model"
"oci-portal/internal/oci"
"gorm.io/gorm"
)
// TestSpeechBodyNormalize 断言 TTS 请求体校验与 language 缺省注入。
@@ -220,3 +222,71 @@ func TestAiModerations(t *testing.T) {
t.Errorf("ghost 分组 err = %v, want ErrAiNoChannel", err)
}
}
// TestCleanupTableBatchesOverflow 锁定超限清理的固定批次行为:
// 溢出量大于单批上限时分多轮删完,且始终删最旧的行。
func TestCleanupTableBatchesOverflow(t *testing.T) {
gw, _ := newTestGateway(t, &fakeClient{})
old := cleanupBatch
cleanupBatch = 3
t.Cleanup(func() { cleanupBatch = old })
for i := 0; i < 10; i++ {
if err := gw.db.Create(&model.AiCallLog{Endpoint: "chat"}).Error; err != nil {
t.Fatalf("seed %d: %v", i, err)
}
}
// maxRows=2:溢出 8 行,需 3 轮批次(3+3+2)
gw.cleanupTable(context.Background(), &model.AiCallLog{}, 24*365*time.Hour, 2, "test")
var rest []model.AiCallLog
if err := gw.db.Order("id").Find(&rest).Error; err != nil {
t.Fatalf("load rest: %v", err)
}
if len(rest) != 2 {
t.Fatalf("remaining = %d, want 2", len(rest))
}
for _, row := range rest {
if row.ID <= 8 {
t.Errorf("row %d survived, want oldest deleted first", row.ID)
}
}
}
// TestLogContentRechecksKeyInsideTx 锁定「立即关闭」语义:写入事务内回读密钥,
// 窗口已关、密钥停用或已删时,鉴权期的旧快照不得再落敏感正文。
func TestLogContentRechecksKeyInsideTx(t *testing.T) {
gw, _ := newTestGateway(t, &fakeClient{})
if err := gw.db.AutoMigrate(&model.AiContentLog{}); err != nil {
t.Fatalf("migrate content log: %v", err)
}
future := time.Now().Add(time.Hour)
key := model.AiKey{Name: "k", KeyHash: "h", Enabled: true, ContentLogUntil: &future}
call := model.AiCallLog{Endpoint: "chat"}
if err := gw.db.Create(&key).Error; err != nil {
t.Fatalf("seed key: %v", err)
}
if err := gw.db.Create(&call).Error; err != nil {
t.Fatalf("seed call: %v", err)
}
countIs := func(want int64, note string) {
t.Helper()
var n int64
if err := gw.db.Model(&model.AiContentLog{}).Count(&n).Error; err != nil || n != want {
t.Fatalf("%s: count=%d (%v), want %d", note, n, err, want)
}
}
gw.LogContent(model.AiContentLog{CallLogID: call.ID, KeyID: key.ID, RequestBody: "prompt"})
countIs(1, "窗口开启时应写入")
// 管理员立即关闭窗口:在途请求携带的旧快照不得再写
if err := gw.db.Model(&model.AiKey{}).Where("id = ?", key.ID).
Update("content_log_until", gorm.Expr("NULL")).Error; err != nil {
t.Fatalf("close window: %v", err)
}
gw.LogContent(model.AiContentLog{CallLogID: call.ID, KeyID: key.ID, RequestBody: "late"})
countIs(1, "窗口关闭后不得写入")
// 密钥删除后同样拒写
if err := gw.db.Delete(&model.AiKey{}, key.ID).Error; err != nil {
t.Fatalf("delete key: %v", err)
}
gw.LogContent(model.AiContentLog{CallLogID: call.ID, KeyID: key.ID, RequestBody: "orphan"})
countIs(1, "密钥已删后不得写入")
}