fix(persist,channel): 同类定宽时间戳修复 — checkpoint 恢复与渠道会话列表排序

- persist: formatTime 定宽;loadLatestSQL 补 rowid 兜底;New 时归一化历史行(避免 LoadLatest 挑到更旧检查点)
- channel/session: 写入定宽;listSessionSQL 补 rowid 兜底;核实无 INSERT 吃 DDL 默认值,故只加注释不改默认值
- 新增回归测试并做变异验证(改回 RFC3339Nano 即 FAIL)
This commit is contained in:
AskaEth committed 2026-10-03 16:08:51 +08:00
1 parent 31086ed1ab
commit fbbfcc95ee
7 files changed
+445 -14

No files matched your search

+95
View File
@@ -318,3 +318,98 @@ func TestNewRejectsNilDB(t *testing.T) {
t.Fatalf("New(nil db) error = %v, want ErrNilDB", err)
}
}
// insertSessionAt writes a channel_sessions row directly with a caller-supplied
// raw created_at/updated_at, standing in for a mapping written by past code.
func insertSessionAt(t *testing.T, handle *sql.DB, cid, at string) {
t.Helper()
if _, err := handle.ExecContext(context.Background(),
`INSERT INTO channel_sessions (platform, channel_id, session_id, actor_role, created_at, updated_at)
VALUES ('qq', ?, ?, 'owner', ?, ?)`,
cid, "sess-"+cid, at, at); err != nil {
t.Fatalf("insert %s: %v", cid, err)
}
}
// TestListNewestFirstDeterministic covers the ordering contract: three mappings
// written within the same coarse clock tick share one created_at, so only the
// rowid tiebreak can define their order. List is newest first, so the row
// inserted last (highest rowid) must come first. The cache is deliberately empty
// so List reads the persisted order instead of cached entries.
func TestListNewestFirstDeterministic(t *testing.T) {
t.Parallel()
m, handle := newTestManager(t)
ctx := context.Background()
const at = "2026-09-07T03:20:05.830000000Z"
for _, cid := range []string{"direct-a", "direct-b", "direct-c"} {
insertSessionAt(t, handle, cid, at)
}
got, err := m.List(ctx)
if err != nil {
t.Fatalf("List = %v", err)
}
want := []string{"direct-c", "direct-b", "direct-a"}
if len(got) != len(want) {
t.Fatalf("List = %d sessions, want %d", len(got), len(want))
}
for i, w := range want {
if got[i].ChannelID != w {
t.Errorf("got[%d].ChannelID = %q, want %q (rowid DESC tiebreak)", i, got[i].ChannelID, w)
}
}
}
// TestListNewestFirstByCreatedAt checks the created_at DESC half: the mapping
// with the latest fixed-width timestamp is listed first even though it was
// inserted first and therefore has the lowest rowid.
func TestListNewestFirstByCreatedAt(t *testing.T) {
t.Parallel()
m, handle := newTestManager(t)
ctx := context.Background()
insertSessionAt(t, handle, "direct-newest", "2026-09-07T03:20:05.830978900Z")
insertSessionAt(t, handle, "direct-oldest", "2026-09-07T03:20:05.830000000Z")
got, err := m.List(ctx)
if err != nil {
t.Fatalf("List = %v", err)
}
if len(got) != 2 {
t.Fatalf("List = %d sessions, want 2", len(got))
}
if got[0].ChannelID != "direct-newest" || got[1].ChannelID != "direct-oldest" {
t.Errorf("List order = [%q %q], want newest first", got[0].ChannelID, got[1].ChannelID)
}
}
// TestPersistWritesFixedWidthTimestamps pins the write path: GetOrCreate must
// store created_at/updated_at with the fixed-width layout so channel_sessions
// text ordering matches time ordering. RFC3339Nano would drop trailing zeros
// and persist a variable-width value instead.
func TestPersistWritesFixedWidthTimestamps(t *testing.T) {
t.Parallel()
m, handle := newTestManager(t)
ctx := context.Background()
if _, err := m.GetOrCreate(ctx, directMsg("o-100"), testOwner()); err != nil {
t.Fatalf("GetOrCreate = %v", err)
}
var created, updated string
if err := handle.QueryRowContext(ctx,
`SELECT created_at, updated_at FROM channel_sessions WHERE channel_id = ?`,
"direct-o-100").Scan(&created, &updated); err != nil {
t.Fatalf("read stored timestamps: %v", err)
}
if len(created) != sessionTimeLen || len(updated) != sessionTimeLen {
t.Errorf("stored timestamps = %q/%q (%d/%d bytes), want fixed width %d",
created, updated, len(created), len(updated), sessionTimeLen)
}
if created != updated {
t.Errorf("created_at %q != updated_at %q, want the same value on insert", created, updated)
}
}