feat(history): 消息搜索/编辑/删除 + 会话置顶(D2/D3/D5)
- history/message_ops.go(新): Search(会话内, q/after互斥, LIKE参数化+%/_转义+大小写不敏感, 严格限定会话doc) / UpdateMessage(返回更新后Message) / DeleteMessage / messageByID / escapeLike
- history/session_pin.go(新): SetPinned; 迁移0009 chat_sessions加pinned INTEGER DEFAULT 0; store.go SessionMeta.Pinned + createSessionsTable加列
- session_preview.go: 查询加s.pinned + ORDER BY pinned DESC,(type=main) DESC,created_at ASC
- gateway history_write.go(新,保持history.go≤500): handlePatchSession(pinned+title) / handlePatchMessage / handleDeleteMessage; server.go加PATCH/DELETE /api/v1/messages/{id} + /sessions/{id}置顶
- history.go: handleListMessages读q分流Search; 契约PATCH title/置顶
- D2会话内搜索(不做跨会话,隔离会话不外泄红线); D3不做重发端点(客户端编辑后再发); D5用pinned bool
- 测试(message_ops/history_messages/migrate_pinned), build/vet/test绿+3平台交叉编译; Docs/api-client v0.30
- 未做: 编辑/删除WS实时广播(message_edited/deleted), 下批
This commit is contained in:
1 parent
c7b2cd8ae0
commit
844b2a74c1
15 files changed
+1091
-73
No files matched your search
@@ -0,0 +1,58 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// columnExists reports whether table has a column named column.
|
||||
func columnExists(t *testing.T, ctx context.Context, conn *sql.DB, table, column string) bool {
|
||||
t.Helper()
|
||||
var n int
|
||||
if err := conn.QueryRowContext(ctx,
|
||||
"SELECT count(*) FROM pragma_table_info(?) WHERE name = ?", table, column).Scan(&n); err != nil {
|
||||
t.Fatalf("pragma_table_info(%s): %v", table, err)
|
||||
}
|
||||
return n == 1
|
||||
}
|
||||
|
||||
// TestMigrate0009PinnedColumnIdempotent verifies the pinned migration (Android
|
||||
// collaboration batch ③a / D5) is reversible and re-appliable: up adds the column,
|
||||
// a second up is a no-op, down drops it, and a re-up restores it.
|
||||
func TestMigrate0009PinnedColumnIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
conn := openMemory(t)
|
||||
|
||||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||||
t.Fatalf("MigrateUp: %v", err)
|
||||
}
|
||||
if !columnExists(t, ctx, conn, "chat_sessions", "pinned") {
|
||||
t.Fatal("chat_sessions.pinned should exist after MigrateUp")
|
||||
}
|
||||
|
||||
// Up twice: the recorded migration is skipped, so the column stays and no error.
|
||||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||||
t.Fatalf("MigrateUp second call: %v", err)
|
||||
}
|
||||
if !columnExists(t, ctx, conn, "chat_sessions", "pinned") {
|
||||
t.Fatal("chat_sessions.pinned should still exist after idempotent re-run")
|
||||
}
|
||||
|
||||
// Down one step rolls back only 0009, dropping the column.
|
||||
if err := MigrateDown(ctx, conn, 1, testLogger()); err != nil {
|
||||
t.Fatalf("MigrateDown(1): %v", err)
|
||||
}
|
||||
if columnExists(t, ctx, conn, "chat_sessions", "pinned") {
|
||||
t.Fatal("chat_sessions.pinned should be dropped by the 0009 down")
|
||||
}
|
||||
|
||||
// Re-applying restores it end to end.
|
||||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||||
t.Fatalf("re-MigrateUp: %v", err)
|
||||
}
|
||||
if !columnExists(t, ctx, conn, "chat_sessions", "pinned") {
|
||||
t.Fatal("chat_sessions.pinned should exist again after re-MigrateUp")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 0009_chat_session_pinned down: drop the pinned ordering column added by the up
|
||||
-- migration. SQLite DROP COLUMN (3.35+) is already relied on by 0006/0007, and the
|
||||
-- column is not referenced by any index, so a plain drop is safe and lossless (the
|
||||
-- pin is order metadata, not data the session needs to remain valid).
|
||||
|
||||
ALTER TABLE chat_sessions DROP COLUMN pinned;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 0009_chat_session_pinned: pin a persistent chat session to the top of the session
|
||||
-- list (DevDocs/design/16 §2.2, Android 协同批次 ③a / D5). `pinned` is display/order
|
||||
-- metadata only: it never affects identity or permissions (project red line), and any
|
||||
-- session — the fixed main session or a sub session — may be pinned.
|
||||
--
|
||||
-- The 0003 chat_sessions table already exists at this point, so ALTER ADD COLUMN is
|
||||
-- the correct, non-destructive step. The canonical schema (including this column) is
|
||||
-- also re-issued idempotently by internal/runtime/history.NewStore so a fresh
|
||||
-- in-memory store used by tests stays self-contained; the migration is the
|
||||
-- authoritative path for an existing database.
|
||||
|
||||
ALTER TABLE chat_sessions ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
|
||||
Reference in new issue
Block a user