fix(persist,channel): 同类定宽时间戳修复 — checkpoint 恢复与渠道会话列表排序
- persist: formatTime 定宽;loadLatestSQL 补 rowid 兜底;New 时归一化历史行(避免 LoadLatest 挑到更旧检查点) - channel/session: 写入定宽;listSessionSQL 补 rowid 兜底;核实无 INSERT 吃 DDL 默认值,故只加注释不改默认值 - 新增回归测试并做变异验证(改回 RFC3339Nano 即 FAIL)
This commit is contained in:
1 parent
31086ed1ab
commit
fbbfcc95ee
7 files changed
+445
-14
No files matched your search
@@ -360,3 +360,67 @@ func TestStore_Save_MissingID_异常(t *testing.T) {
|
||||
t.Fatalf("Save(blank id) err = %v, want wrap of ErrMissingID", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStore_LoadLatest_变宽时间戳_取较晚 is the regression for the variable-width
|
||||
// timestamp bug on the durable-checkpoint recovery path. time.RFC3339Nano drops
|
||||
// trailing fractional zeros, so the older checkpoint was once stored as
|
||||
// ":05.83Z" and the later one as ":05.8309789Z". SQLite compares updated_at as
|
||||
// plain text, and "…83Z" ranks above "…8309789Z", so LoadLatest used to recover
|
||||
// the older checkpoint — a silent wrong resume. The older checkpoint also
|
||||
// carries the larger step, so the step DESC tiebreak cannot mask the bad order.
|
||||
func TestStore_LoadLatest_变宽时间戳_取较晚(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
|
||||
const (
|
||||
olderRaw = "2026-09-07T03:20:05.83Z" // X.8300000 under RFC3339Nano
|
||||
newerRaw = "2026-09-07T03:20:05.8309789Z" // X.8309789 under RFC3339Nano
|
||||
)
|
||||
if _, err := s.db.ExecContext(ctx, insertLegacyCheckpointSQL,
|
||||
"cp-older", "agent-a", 9, `{"step":9}`, olderRaw, olderRaw); err != nil {
|
||||
t.Fatalf("insert older checkpoint: %v", err)
|
||||
}
|
||||
if _, err := s.db.ExecContext(ctx, insertLegacyCheckpointSQL,
|
||||
"cp-newer", "agent-a", 2, `{"step":2}`, newerRaw, newerRaw); err != nil {
|
||||
t.Fatalf("insert newer checkpoint: %v", err)
|
||||
}
|
||||
|
||||
// Re-open so the fixed-width normalization repairs the legacy rows.
|
||||
if _, err := New(ctx, s.db, testLogger()); err != nil {
|
||||
t.Fatalf("New (normalize): %v", err)
|
||||
}
|
||||
|
||||
got, err := s.LoadLatest(ctx, "agent-a")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadLatest error: %v", err)
|
||||
}
|
||||
if got.ID != "cp-newer" {
|
||||
t.Fatalf("LoadLatest = %q (step %d), want cp-newer (the later updated_at)", got.ID, got.Step)
|
||||
}
|
||||
if want := time.Date(2026, 9, 7, 3, 20, 5, 830978900, time.UTC); !got.UpdatedAt.Equal(want) {
|
||||
t.Fatalf("LoadLatest updated_at = %v, want %v", got.UpdatedAt, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStore_Save_定宽时间戳 pins that every write uses the fixed-width layout, so
|
||||
// text ordering equals time ordering for rows created by the current code.
|
||||
func TestStore_Save_定宽时间戳(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
|
||||
if err := s.Save(ctx, &Checkpoint{ID: "cp-1", AgentID: "agent-a", Step: 1, State: []byte(`{}`)}); err != nil {
|
||||
t.Fatalf("Save error: %v", err)
|
||||
}
|
||||
var created, updated string
|
||||
if err := s.db.QueryRowContext(ctx,
|
||||
`SELECT created_at, updated_at FROM checkpoints WHERE id = ?`, "cp-1").
|
||||
Scan(&created, &updated); err != nil {
|
||||
t.Fatalf("read stored timestamps: %v", err)
|
||||
}
|
||||
if len(created) != storedTimeLen || len(updated) != storedTimeLen {
|
||||
t.Errorf("stored timestamps = %q/%q (%d/%d bytes), want fixed width %d",
|
||||
created, updated, len(created), len(updated), storedTimeLen)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user