fix(memory): 定宽时间戳存储 + rowid 确定性排序 + 遗留数据归一化

- formatTime 改用固定 30 字节布局(9 位小数秒),让 TEXT 列的字典序与时间序一致(RFC3339Nano 会去掉尾随零产生变宽字符串,秒内会反序)
- memory_items / project / project_chapter 的 ORDER BY 全部补 rowid 兜底,消除同 tick 打平时的顺序不确定
- 开库时幂等归一化变宽的历史值;无法解析的行原样保留,不阻塞开库
- 回归测试:变宽排序反例、同一时间戳插入序、归一化幂等与跳过
This commit is contained in:
AskaEth committed 2026-10-03 16:08:50 +08:00
1 parent 1ac126fc9e
commit 31086ed1ab
10 files changed
+745 -61

No files matched your search

+91
View File
@@ -5,6 +5,7 @@ import (
"errors"
"sync"
"testing"
"time"
)
func TestNewStore_NilDB_异常(t *testing.T) {
@@ -210,6 +211,96 @@ func TestStore_List_正常(t *testing.T) {
}
}
// TestStore_List_变宽时间戳_排序 is the regression for the variable-width
// timestamp bug: time.RFC3339Nano drops trailing fractional zeros, so the older
// item was once stored as ":05.83" and the newer as ":05.8309789", and the text
// comparison SQLite applies to these columns ranked the older one as newer.
func TestStore_List_变宽时间戳_排序(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
sec := time.Date(2026, 9, 7, 3, 20, 5, 0, time.UTC)
older := sec.Add(830 * time.Millisecond) // ":05.83" under RFC3339Nano
newer := older.Add(978900 * time.Nanosecond) // ":05.8309789" under RFC3339Nano
// The newer item is inserted first, so a rowid-only tiebreak would surface
// the older one: only correctly ordered created_at text yields 较晚 first.
for _, in := range []struct {
content string
at time.Time
}{{"较晚", newer}, {"较早", older}} {
item := MemoryItem{PersonaID: "persona.a", Content: in.content, CreatedAt: in.at, UpdatedAt: in.at}
if _, err := s.Add(ctx, item); err != nil {
t.Fatalf("Add(%s): %v", in.content, err)
}
}
var raw string
if err := s.db.QueryRowContext(ctx,
`SELECT created_at FROM memory_items WHERE content = ?`, "较早").Scan(&raw); err != nil {
t.Fatalf("read stored created_at: %v", err)
}
if len(raw) != storedTimeLen {
t.Errorf("stored created_at = %q (%d bytes), want fixed width %d", raw, len(raw), storedTimeLen)
}
got, err := s.List(ctx)
if err != nil {
t.Fatalf("List: %v", err)
}
if len(got) != 2 {
t.Fatalf("List = %d items, want 2", len(got))
}
if got[0].Content != "较晚" {
t.Errorf("got[0].Content = %q, want the later timestamp (较晚)", got[0].Content)
}
if got[1].Content != "较早" {
t.Errorf("got[1].Content = %q, want the earlier timestamp (较早)", got[1].Content)
}
}
// TestStore_List_同一时间戳_插入序 covers the tie case: the host clock is coarse
// enough that three consecutive Adds can share one timestamp, so ordering must
// fall back to rowid rather than an arbitrary row order.
func TestStore_List_同一时间戳_插入序(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
at := time.Date(2026, 9, 7, 3, 20, 5, 830978900, time.UTC)
for _, content := range []string{"第一条", "第二条", "第三条"} {
item := MemoryItem{PersonaID: "persona.a", Content: content, CreatedAt: at, UpdatedAt: at}
if _, err := s.Add(ctx, item); err != nil {
t.Fatalf("Add(%s): %v", content, err)
}
}
want := []string{"第三条", "第二条", "第一条"}
for _, tc := range []struct {
name string
list func(context.Context) ([]MemoryItem, error)
}{
{"List", s.List},
{"ListByPersona", func(ctx context.Context) ([]MemoryItem, error) {
return s.ListByPersona(ctx, "persona.a")
}},
} {
got, err := tc.list(ctx)
if err != nil {
t.Fatalf("%s: %v", tc.name, err)
}
if len(got) != len(want) {
t.Fatalf("%s = %d items, want %d", tc.name, len(got), len(want))
}
for i, w := range want {
if got[i].Content != w {
t.Errorf("%s got[%d].Content = %q, want %q (rowid DESC)", tc.name, i, got[i].Content, w)
}
}
}
}
func TestStore_Delete_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()