ci / vet-and-test (pull_request) Successful in 41s
Why: the approved plan U3 (KTD3/KTD7/KTD10 plus minimal recovery isolation) requires concurrent submits to not silently overwrite, unknown-side-effect runs to never auto-resume, and the shared rate limiter to degrade conservatively instead of failing open. Evidence: full backend suite green twice-relevant paths against real PostgreSQL 18.6 and Redis (isolated instance) and on SQLite; new tests - card_version_test (concurrent single winner), recovery_hold_test + recovery_holds_test (skip/release/fail-closed CRUD), ratelimit atomic-window and degraded-fallback tests. Impact: AppendMessageWithRequest now allocates seq under the conversation advisory lock in one transaction; activity_cards gain version (v19) with SaveCardVersioned/GetCardVersion/CardVersion and cardsvc user mutations (status/pin/action/resolve-by-ref) return 409-mapped conflicts while system mirrors keep blind writes; recovery_holds (v20) with admin list/release endpoints and fail-closed resume skip; Redis limiter uses one Lua INCR+PEXPIRE script and falls back to a per-instance limit with a Degraded counter. Refs: proposals/2026-10-11-collaboration-storage-and-distribution.md U3 KTD3/KTD7/KTD10; R3/R6/R7/R8.
105 lines
2.9 KiB
Go
105 lines
2.9 KiB
Go
package ratelimit
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMemoryWindow(t *testing.T) {
|
|
m := NewMemory()
|
|
ctx := context.Background()
|
|
for i := 1; i <= 3; i++ {
|
|
if !m.Allow(ctx, "o", 3, time.Minute) {
|
|
t.Fatalf("request %d should pass", i)
|
|
}
|
|
}
|
|
if m.Allow(ctx, "o", 3, time.Minute) {
|
|
t.Fatal("4th request must be limited")
|
|
}
|
|
// 其他租户互不影响。
|
|
if !m.Allow(ctx, "p", 3, time.Minute) {
|
|
t.Fatal("other tenant must pass")
|
|
}
|
|
// 窗口滚动后恢复。
|
|
m.now = func() time.Time { return time.Now().Add(2 * time.Minute) }
|
|
if !m.Allow(ctx, "o", 3, time.Minute) {
|
|
t.Fatal("new window must reset")
|
|
}
|
|
// limit<=0 = 不限。
|
|
if !m.Allow(ctx, "x", 0, time.Minute) {
|
|
t.Fatal("limit 0 means unlimited")
|
|
}
|
|
}
|
|
|
|
// Redis 共享档(环境门控:AGENT_REDIS_TEST_ADDR,本机原生实例)。
|
|
func TestRedisWindow(t *testing.T) {
|
|
addr := os.Getenv("AGENT_REDIS_TEST_ADDR")
|
|
if addr == "" {
|
|
t.Skip("AGENT_REDIS_TEST_ADDR not set")
|
|
}
|
|
r := NewRedis(addr, "", 15) // 独立 db 避免污染
|
|
defer r.Close()
|
|
if err := r.Ping(context.Background()); err != nil {
|
|
t.Fatalf("ping: %v", err)
|
|
}
|
|
ctx := context.Background()
|
|
key := "test-" + time.Now().Format("150405.000000000")
|
|
for i := 1; i <= 2; i++ {
|
|
if !r.Allow(ctx, key, 2, time.Minute) {
|
|
t.Fatalf("request %d should pass", i)
|
|
}
|
|
}
|
|
if r.Allow(ctx, key, 2, time.Minute) {
|
|
t.Fatal("3rd request must be limited")
|
|
}
|
|
}
|
|
|
|
// Lua 原子窗口(U3,KTD10):计数与设窗单脚本完成,键必然带 TTL——
|
|
// 窗口翻转后重新放行(原子性回归:旧两步实现窗口语义相同,此处锚定
|
|
// 「INCR 与 PEXPIRE 不可分」的行为面)。
|
|
func TestRedisWindowAtomicScript(t *testing.T) {
|
|
addr := os.Getenv("AGENT_REDIS_TEST_ADDR")
|
|
if addr == "" {
|
|
t.Skip("AGENT_REDIS_TEST_ADDR not set")
|
|
}
|
|
r := NewRedis(addr, "", 15)
|
|
defer r.Close()
|
|
ctx := context.Background()
|
|
key := "atomic-" + time.Now().Format("150405.000000000")
|
|
window := 300 * time.Millisecond
|
|
for i := 1; i <= 3; i++ {
|
|
if !r.Allow(ctx, key, 3, window) {
|
|
t.Fatalf("request %d should pass", i)
|
|
}
|
|
}
|
|
if r.Allow(ctx, key, 3, window) {
|
|
t.Fatal("4th request must be limited")
|
|
}
|
|
time.Sleep(window + 50*time.Millisecond)
|
|
if !r.Allow(ctx, key, 3, window) {
|
|
t.Fatal("new window must allow again")
|
|
}
|
|
}
|
|
|
|
// Redis 故障降级(KTD10):不可达时不再无界放行——按实例保守限流;
|
|
// Degraded 计数供观测告警。
|
|
func TestRedisDegradedFallback(t *testing.T) {
|
|
r := NewRedis("127.0.0.1:1", "", 15) // 不可达端口
|
|
defer r.Close()
|
|
ctx := context.Background()
|
|
key := "degraded-" + time.Now().Format("150405.000000000")
|
|
for i := 1; i <= 2; i++ {
|
|
if !r.Allow(ctx, key, 2, time.Minute) {
|
|
t.Fatalf("degraded request %d should pass (local limit 2)", i)
|
|
}
|
|
}
|
|
if r.Allow(ctx, key, 2, time.Minute) {
|
|
t.Fatal("degraded 3rd request must be limited locally")
|
|
}
|
|
if r.Degraded() == 0 {
|
|
t.Fatal("degradation must be observable")
|
|
}
|
|
}
|