- persist: formatTime 定宽;loadLatestSQL 补 rowid 兜底;New 时归一化历史行(避免 LoadLatest 挑到更旧检查点) - channel/session: 写入定宽;listSessionSQL 补 rowid 兜底;核实无 INSERT 吃 DDL 默认值,故只加注释不改默认值 - 新增回归测试并做变异验证(改回 RFC3339Nano 即 FAIL)
119 lines
4.0 KiB
Go
119 lines
4.0 KiB
Go
package persist
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// insertLegacyCheckpointSQL writes a checkpoints row with caller-supplied raw
|
|
// timestamps, standing in for a row persisted before the fixed-width layout.
|
|
const insertLegacyCheckpointSQL = `INSERT INTO checkpoints
|
|
(id, agent_id, step, state, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`
|
|
|
|
func TestFormatTime_定宽_正常(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
at time.Time
|
|
want string
|
|
}{
|
|
{"零小数秒", time.Date(2026, 9, 7, 3, 20, 5, 0, time.UTC), "2026-09-07T03:20:05.000000000Z"},
|
|
{"毫秒", time.Date(2026, 9, 7, 3, 20, 5, 830000000, time.UTC), "2026-09-07T03:20:05.830000000Z"},
|
|
{"纳秒", time.Date(2026, 9, 7, 3, 20, 5, 830978900, time.UTC), "2026-09-07T03:20:05.830978900Z"},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := formatTime(tt.at)
|
|
if got != tt.want {
|
|
t.Errorf("formatTime = %q, want %q", got, tt.want)
|
|
}
|
|
if len(got) != storedTimeLen {
|
|
t.Errorf("formatTime length = %d, want %d", len(got), storedTimeLen)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatTime_非UTC_正常(t *testing.T) {
|
|
t.Parallel()
|
|
zone := time.FixedZone("UTC+8", 8*60*60)
|
|
// 11:20:05.830+08:00 is 03:20:05.830Z, so the stored text is UTC.
|
|
at := time.Date(2026, 9, 7, 11, 20, 5, 830000000, zone)
|
|
if got, want := formatTime(at), "2026-09-07T03:20:05.830000000Z"; got != want {
|
|
t.Errorf("formatTime = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// TestNew_归一化历史时间戳_正常 writes a legacy variable-width row directly,
|
|
// re-opens the Store to trigger normalization, and checks both the repaired text
|
|
// and its idempotence on a further open.
|
|
func TestNew_归一化历史时间戳_正常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
// RFC3339Nano drops the trailing fractional zeros, so this legacy value is
|
|
// only 23 bytes wide and loses the text-ordering guarantee.
|
|
const legacy = "2026-09-07T03:20:05.83Z"
|
|
if _, err := s.db.ExecContext(ctx, insertLegacyCheckpointSQL,
|
|
"legacy", "agent-a", 1, `{}`, legacy, legacy); err != nil {
|
|
t.Fatalf("insert legacy row: %v", err)
|
|
}
|
|
|
|
if _, err := New(ctx, s.db, testLogger()); err != nil {
|
|
t.Fatalf("New (normalize): %v", err)
|
|
}
|
|
var created, updated string
|
|
if err := s.db.QueryRowContext(ctx,
|
|
`SELECT created_at, updated_at FROM checkpoints WHERE id = ?`, "legacy").
|
|
Scan(&created, &updated); err != nil {
|
|
t.Fatalf("read normalized timestamps: %v", err)
|
|
}
|
|
const want = "2026-09-07T03:20:05.830000000Z"
|
|
if created != want || updated != want {
|
|
t.Errorf("normalized timestamps = %q/%q, want %q", created, updated, want)
|
|
}
|
|
|
|
// A further open must be a no-op: the length condition selects nothing once
|
|
// the row is fixed width.
|
|
if _, err := New(ctx, s.db, testLogger()); err != nil {
|
|
t.Fatalf("New (second pass): %v", err)
|
|
}
|
|
var again string
|
|
if err := s.db.QueryRowContext(ctx,
|
|
`SELECT created_at FROM checkpoints WHERE id = ?`, "legacy").Scan(&again); err != nil {
|
|
t.Fatalf("read created_at after second open: %v", err)
|
|
}
|
|
if again != created {
|
|
t.Errorf("second open changed created_at: %q -> %q", created, again)
|
|
}
|
|
}
|
|
|
|
// TestNew_无法解析的时间戳_跳过 pins the "leave it alone" rule: one bad legacy
|
|
// value must not keep the Store from opening, and must not be rewritten.
|
|
func TestNew_无法解析的时间戳_跳过(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
if _, err := s.db.ExecContext(ctx, insertLegacyCheckpointSQL,
|
|
"broken", "agent-a", 1, `{}`, "not-a-time", "not-a-time"); err != nil {
|
|
t.Fatalf("insert unparseable row: %v", err)
|
|
}
|
|
|
|
if _, err := New(ctx, s.db, testLogger()); err != nil {
|
|
t.Fatalf("New with unparseable timestamp: %v", err)
|
|
}
|
|
var raw string
|
|
if err := s.db.QueryRowContext(ctx,
|
|
`SELECT created_at FROM checkpoints WHERE id = ?`, "broken").Scan(&raw); err != nil {
|
|
t.Fatalf("read created_at: %v", err)
|
|
}
|
|
if raw != "not-a-time" {
|
|
t.Errorf("created_at = %q, want the unparseable value left as is", raw)
|
|
}
|
|
}
|