docs(comments): 注释中文化 · internal/runtime/history(15 文件,纯替换)
- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩 4 条行尾注释的翻译,代码前缀全等) - 交叉引用(design/16 §4.6、D4/B3、0010 迁移文件名)逐字保留 - 保留未译技术词:DDL/rowid/LIKE/LEFT JOIN/DTO/rune/WS/actor 及全部标识符与表名 - 顺带副作用:store.go、session_persona_test.go 由注释文字改变而变得 gofmt 干净(未运行 gofmt -w)
This commit is contained in:
1 parent
b0fbb8bc0d
commit
09fba77f87
15 files changed
+430
-516
No files matched your search
@@ -13,15 +13,14 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||||
)
|
||||
|
||||
// These tests cover the D4 per-session read cursor (design/16 §4.2, Android
|
||||
// collaboration B3): the unread count derived in ListSessions, the forward-only
|
||||
// cursor semantics of MarkSessionRead, and the 0010 migration's upgrade backfill.
|
||||
// 这些测试覆盖 D4 的每会话读游标(design/16 §4.2,Android 协同 B3):
|
||||
// ListSessions 推导出的未读数、MarkSessionRead 只向前推进的游标语义,
|
||||
// 以及 0010 迁移的升级回填。
|
||||
|
||||
// TestListSessionsUnreadCountFromCursor pins the unread derivation: a message counts as
|
||||
// unread only when it is strictly newer than the session's cursor (created_at > read_at),
|
||||
// so the cursor is inclusive of the timestamp it names (a client that has rendered a
|
||||
// message sent at T marks T read). A fresh session starts at cursor 0 with everything
|
||||
// unread, and a cursor at the newest message clears the badge.
|
||||
// TestListSessionsUnreadCountFromCursor 锁定未读推导:一条消息只有在严格新于
|
||||
// 会话游标(created_at > read_at)时才计为未读,因此游标包含它所命名的那个
|
||||
// 时间戳(渲染过 T 时刻消息的客户端把 T 标为已读)。全新会话从游标 0 开始、
|
||||
// 一切皆未读,而游标落在最新消息处会清空角标。
|
||||
func TestListSessionsUnreadCountFromCursor(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
@@ -31,11 +30,11 @@ func TestListSessionsUnreadCountFromCursor(t *testing.T) {
|
||||
t.Fatalf("seed %d: %v", ts, err)
|
||||
}
|
||||
}
|
||||
// Never read: all three messages are unread (a brand-new account/session).
|
||||
// 从未读过:三条消息全未读(全新账号/会话)。
|
||||
if got := sessionOf(t, st, MainSessionID).UnreadCount; got != 3 {
|
||||
t.Fatalf("initial unreadCount = %d, want 3", got)
|
||||
}
|
||||
// Cursor on a message's own timestamp counts that message as read.
|
||||
// 游标压在消息自身的毫秒上就算该消息已读。
|
||||
readAt, err := st.MarkSessionRead(ctx, MainSessionID, time.UnixMilli(2000))
|
||||
if err != nil {
|
||||
t.Fatalf("mark read: %v", err)
|
||||
@@ -46,14 +45,14 @@ func TestListSessionsUnreadCountFromCursor(t *testing.T) {
|
||||
if got := sessionOf(t, st, MainSessionID).UnreadCount; got != 1 {
|
||||
t.Fatalf("unreadCount after cursor 2000 = %d, want 1 (only 3000 newer)", got)
|
||||
}
|
||||
// A cursor between two messages reads the earlier one only.
|
||||
// 游标落在两条消息之间只读到较早那条。
|
||||
if _, err := st.MarkSessionRead(ctx, MainSessionID, time.UnixMilli(2500)); err != nil {
|
||||
t.Fatalf("mark read 2500: %v", err)
|
||||
}
|
||||
if got := sessionOf(t, st, MainSessionID).UnreadCount; got != 1 {
|
||||
t.Fatalf("unreadCount after cursor 2500 = %d, want 1", got)
|
||||
}
|
||||
// A cursor at the newest message clears the badge.
|
||||
// 游标落在最新消息处清空角标。
|
||||
if _, err := st.MarkSessionRead(ctx, MainSessionID, time.UnixMilli(3000)); err != nil {
|
||||
t.Fatalf("mark read 3000: %v", err)
|
||||
}
|
||||
@@ -62,9 +61,8 @@ func TestListSessionsUnreadCountFromCursor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestListSessionsUnreadCountSessionIsolation pins the per-session scope: each session
|
||||
// carries its own cursor, so reading the main session must not clear (or inflate) a sub
|
||||
// session's badge, and vice versa.
|
||||
// TestListSessionsUnreadCountSessionIsolation 锁定每会话作用域:每个会话携带
|
||||
// 自己的游标,因此读取主会话不得清空(或撑大)子会话的角标,反之亦然。
|
||||
func TestListSessionsUnreadCountSessionIsolation(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
@@ -89,7 +87,7 @@ func TestListSessionsUnreadCountSessionIsolation(t *testing.T) {
|
||||
t.Fatalf("sub unreadCount = %d, want 1", got)
|
||||
}
|
||||
|
||||
// Reading the main session must leave the sub untouched.
|
||||
// 读取主会话不得动子会话。
|
||||
if _, err := st.MarkSessionRead(ctx, MainSessionID, time.UnixMilli(2000)); err != nil {
|
||||
t.Fatalf("mark main read: %v", err)
|
||||
}
|
||||
@@ -100,7 +98,7 @@ func TestListSessionsUnreadCountSessionIsolation(t *testing.T) {
|
||||
t.Fatalf("sub unreadCount after main read = %d, want 1 (cursors are per-session)", got)
|
||||
}
|
||||
|
||||
// A message arriving after the read re-opens the main badge.
|
||||
// 读取之后到达的消息会重新点亮主会话角标。
|
||||
if err := st.Append(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: "main-3", CreatedAt: 3000}); err != nil {
|
||||
t.Fatalf("append main-3: %v", err)
|
||||
}
|
||||
@@ -109,9 +107,8 @@ func TestListSessionsUnreadCountSessionIsolation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarkSessionReadForwardOnly covers the racing/malformed-request side of the cursor
|
||||
// contract: the cursor never moves backwards, so a stale or repeated request is
|
||||
// idempotent and can never resurrect already-read messages.
|
||||
// TestMarkSessionReadForwardOnly 覆盖游标契约中竞争/畸形请求的一面:游标绝不后
|
||||
// 退,因此过期或重复的请求是幂等的,绝不能让已读消息复活。
|
||||
func TestMarkSessionReadForwardOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
@@ -121,13 +118,12 @@ func TestMarkSessionReadForwardOnly(t *testing.T) {
|
||||
t.Fatalf("seed %d: %v", ts, err)
|
||||
}
|
||||
}
|
||||
// Advance to the newest message.
|
||||
// 推进到最新消息。
|
||||
readAt, err := st.MarkSessionRead(ctx, MainSessionID, time.UnixMilli(3000))
|
||||
if err != nil || readAt != 3000 {
|
||||
t.Fatalf("mark 3000 = (%d, %v), want (3000, nil)", readAt, err)
|
||||
}
|
||||
// A stale request (an end that was behind, or a retry of an older read) must not
|
||||
// move the cursor back and re-open the badge.
|
||||
// 过期请求(落后的端,或一次更旧读取的重试)不得把游标后退并重新点亮角标。
|
||||
readAt, err = st.MarkSessionRead(ctx, MainSessionID, time.UnixMilli(1000))
|
||||
if err != nil {
|
||||
t.Fatalf("stale mark: %v", err)
|
||||
@@ -138,8 +134,7 @@ func TestMarkSessionReadForwardOnly(t *testing.T) {
|
||||
if got := sessionOf(t, st, MainSessionID).UnreadCount; got != 0 {
|
||||
t.Fatalf("unreadCount after stale mark = %d, want 0 (never un-read)", got)
|
||||
}
|
||||
// Repeating the same cursor is a no-op, and a negative cursor is clamped by the
|
||||
// same MAX (it cannot move the stored value at all).
|
||||
// 重复同一游标是空操作,负游标被同一个 MAX 钳制(它根本无法挪动已存的值)。
|
||||
if readAt, err = st.MarkSessionRead(ctx, MainSessionID, time.UnixMilli(3000)); err != nil || readAt != 3000 {
|
||||
t.Fatalf("repeat mark = (%d, %v), want (3000, nil)", readAt, err)
|
||||
}
|
||||
@@ -148,9 +143,9 @@ func TestMarkSessionReadForwardOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarkSessionReadZeroTimeMeansNow covers the "mark everything I have seen" call the
|
||||
// HTTP layer makes when the body omits before: the zero time resolves to the wall clock,
|
||||
// which reads every message that already exists.
|
||||
// TestMarkSessionReadZeroTimeMeansNow 覆盖 HTTP 层在请求体省略 before 时发出的
|
||||
// 「把我看到的全部标为已读」调用:零时刻解析为墙上时钟,从而读到所有已存在的
|
||||
// 消息。
|
||||
func TestMarkSessionReadZeroTimeMeansNow(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
@@ -174,10 +169,9 @@ func TestMarkSessionReadZeroTimeMeansNow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarkSessionReadUnknownSession pins the error contract: an id with no chat_sessions
|
||||
// row (including an isolated group/stranger session) is ErrSessionNotFound, so the HTTP
|
||||
// layer can answer 404 instead of silently reporting success for a session that does not
|
||||
// exist.
|
||||
// TestMarkSessionReadUnknownSession 锁定错误契约:没有 chat_sessions 行的 id
|
||||
// (包括隔离的群聊/陌生人会话)是 ErrSessionNotFound,因此 HTTP 层可以回答 404,
|
||||
// 而不是为一个不存在的会话悄悄报告成功。
|
||||
func TestMarkSessionReadUnknownSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
@@ -186,8 +180,8 @@ func TestMarkSessionReadUnknownSession(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// migrationFileSQL reads one migration file from the source tree, so the test applies
|
||||
// the same SQL text the embedded migration runs rather than a copy.
|
||||
// migrationFileSQL 从源码树读取一个迁移文件,使测试应用的是嵌入式迁移执行的那份
|
||||
// SQL 文本,而不是一份副本。
|
||||
func migrationFileSQL(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(filepath.Join("..", "..", "db", "migrations", name))
|
||||
@@ -197,7 +191,7 @@ func migrationFileSQL(t *testing.T, name string) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// columnPresent reports whether table has a column named column.
|
||||
// columnPresent 报告 table 是否有名为 column 的列。
|
||||
func columnPresent(t *testing.T, conn *sql.DB, table, column string) bool {
|
||||
t.Helper()
|
||||
var n int
|
||||
@@ -208,10 +202,9 @@ func columnPresent(t *testing.T, conn *sql.DB, table, column string) bool {
|
||||
return n == 1
|
||||
}
|
||||
|
||||
// openLegacyDB opens an in-memory DB carrying the pre-0010 schema (chat_sessions with no
|
||||
// read_at column) plus two legacy sessions with prior history — the real fixed main
|
||||
// session "web" and a sub session — so the 0010 migration can be applied to a database
|
||||
// that really predates this feature.
|
||||
// openLegacyDB 打开一个携带 0010 之前 schema(chat_sessions 没有 read_at 列)的
|
||||
// 内存库,放入两个带既有历史的遗留会话 —— 真实固定主会话 "web" 与一个子会话
|
||||
// —— 使 0010 迁移能应用到一个真正早于此特性的数据库上。
|
||||
func openLegacyDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
@@ -273,12 +266,11 @@ func openLegacyDB(t *testing.T) *sql.DB {
|
||||
return conn
|
||||
}
|
||||
|
||||
// TestMigration0010ReadAtBackfill exercises the real 0010 migration against a legacy
|
||||
// database: up adds the column and backfills each session's cursor to its newest message,
|
||||
// so upgrade day reports 0 unread for existing history — including the fixed main session,
|
||||
// whose existing history is the whole point of the upgrade — and the store (which
|
||||
// re-issues the canonical DDL idempotently) then works on top of it; a message that arrives
|
||||
// after the migration is unread until it is marked read; down drops the column again.
|
||||
// TestMigration0010ReadAtBackfill 拿真实的 0010 迁移跑在一个遗留库上:up 增加该
|
||||
// 列并把每个会话的游标回填到其最新消息,使升级日对既有历史报告 0 未读 —— 包括
|
||||
// 固定主会话,它的既有历史正是这次升级的全部意义 —— 而存储(它幂等地重发规范
|
||||
// DDL)随后能在其上工作;迁移之后到达的消息在标记已读前都是未读;down 再次
|
||||
// 丢弃该列。
|
||||
func TestMigration0010ReadAtBackfill(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := openLegacyDB(t)
|
||||
@@ -289,18 +281,17 @@ func TestMigration0010ReadAtBackfill(t *testing.T) {
|
||||
if _, err := conn.ExecContext(ctx, migrationFileSQL(t, "0010_chat_session_read_at.up.sql")); err != nil {
|
||||
t.Fatalf("apply 0010 up: %v", err)
|
||||
}
|
||||
// Bring the simulated legacy database up to the current head (0011 adds the
|
||||
// session/message persona columns), because the store reads the canonical schema:
|
||||
// a database left one migration behind would lack persona_id. Upgrade ordering in
|
||||
// production is db.MigrateUp, so this mirrors what a real upgrade produces.
|
||||
// 让模拟的遗留库追到当前 head(0011 增加会话/消息 persona 列),因为存储
|
||||
// 读取的是规范 schema:落后一个迁移的库会缺 persona_id。生产中的升级顺序是
|
||||
// db.MigrateUp,因此这复现了真实升级的结果。
|
||||
if _, err := conn.ExecContext(ctx, migrationFileSQL(t, "0011_session_persona.up.sql")); err != nil {
|
||||
t.Fatalf("apply 0011 up: %v", err)
|
||||
}
|
||||
if !columnPresent(t, conn, "chat_sessions", "read_at") {
|
||||
t.Fatal("read_at should exist after the 0010 up migration")
|
||||
}
|
||||
// The backfill is each session's newest message timestamp, not a blanket 0 (which
|
||||
// would mark the whole existing history unread) and not the first message's.
|
||||
// 回填的是每个会话最新消息的时间戳,不是一刀切的 0(那会把既有历史全部标为
|
||||
// 未读),也不是第一条消息的时间戳。
|
||||
for _, tc := range []struct {
|
||||
sessionID string
|
||||
want int64
|
||||
@@ -318,10 +309,9 @@ func TestMigration0010ReadAtBackfill(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The store comes up on the migrated schema and reports no unread for either
|
||||
// pre-existing session (upgrade day must not light up every row, main session
|
||||
// included); the store's INSERT OR IGNORE for the main session must not overwrite
|
||||
// the backfilled cursor.
|
||||
// 存储建立在迁移后的 schema 上,对两个既有会话都报告无未读(升级日不得点亮
|
||||
// 每一行,主会话包括在内);存储为主会话做的 INSERT OR IGNORE 不得覆盖回填
|
||||
// 的游标。
|
||||
st, err := NewStore(ctx, conn, testLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("new store on migrated db: %v", err)
|
||||
@@ -331,7 +321,7 @@ func TestMigration0010ReadAtBackfill(t *testing.T) {
|
||||
t.Fatalf("legacy session %s unreadCount = %d, want 0 after backfill", sessionID, got)
|
||||
}
|
||||
}
|
||||
// A message written after the migration is unread, and marking it read clears it.
|
||||
// 迁移之后写入的消息是未读的,标记已读即清除。
|
||||
if err := st.Append(ctx, Message{SessionID: "web:legacy", Role: RoleAssistant, Content: "new", CreatedAt: 9000}); err != nil {
|
||||
t.Fatalf("append after migration: %v", err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user