迁移 0018:ALTER TABLE chat_messages ADD COLUMN turn_id TEXT NOT NULL DEFAULT ''
(与 persona_id / client_msg_id / document 同族:不用 NULL,空串 = 不属于任何已知回合 ⇒ 旧行零迁移、键省略)。
存储层全链路带 turn_id(列清单唯一一份 ⇒ List/ListAfter/Recent/Search/FindByClientMsgID 自动同源);
新增 history/turn_id.go:ensureTurnIDColumn(既有库幂等前滚)+ SetMessagesTurnID
(第二段补写:单事务参数化、先到先得 WHERE turn_id = ''、幂等、未知 id 忽略)。
DTO:historyMessage.TurnID(json turnId,omitempty)⇒ REST 列表 / admin 渠道日志 / WS 握手回放 /
message_persisted / channel_persisted / message_edited 一次全覆盖。
写入面:persistUserMessages 读 ctx 回合标识;persistAssistantReply 增可选尾参(既有调用点零改动);
withTurnID / turnIDFrom / assignTurnID / turnIDOfLeg。
⚠️ 编排器把 turnId 交给落库的 4 处(新起一路 / 续跑路 / 被 steer 吃进 / 助手回复)尚未接线
(那些文件当时被另一路占用);当前生产行为 turn_id 仍为 ''。清单已在切片报告里精确到文件:行。
13 新用例 + 5 条变异(INSERT 不带列 / DTO 不回填 / 去掉 omitempty / 去掉先到先得 / 不回滚前滚)。
为守 session_persist.go ≤500 行,turnId 一节拆到新文件 session_turn_id.go。
72 lines
2.5 KiB
Go
72 lines
2.5 KiB
Go
package db
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
)
|
||
|
||
// turnIDMigrationID 是回合标识列的迁移 id(design/15 §2.5.9)。
|
||
const turnIDMigrationID = "0018_chat_message_turn_id"
|
||
|
||
// TestMigrateTurnIDColumnReversible 验证 0018(chat_messages.turn_id,回合标识列)
|
||
// 可上下翻滚:up 建列、重复 up 是空操作、down 删列、再 up 恢复。它用自己的内存库,
|
||
// 绝不碰 data/cyrene.db。
|
||
func TestMigrateTurnIDColumnReversible(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_messages", "turn_id") {
|
||
t.Fatal("chat_messages.turn_id should exist after MigrateUp")
|
||
}
|
||
|
||
// 重复 up:已记录的迁移被跳过,列仍在且不报错(幂等)。
|
||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||
t.Fatalf("MigrateUp second call: %v", err)
|
||
}
|
||
if !columnExists(t, ctx, conn, "chat_messages", "turn_id") {
|
||
t.Fatal("chat_messages.turn_id should still exist after idempotent re-run")
|
||
}
|
||
|
||
// 恰好回滚到 0018(含它之后新加的迁移),因此日后再加迁移也不会让本测试失效。
|
||
ids := upMigrationIDs(t)
|
||
idx := -1
|
||
for i, id := range ids {
|
||
if id == turnIDMigrationID {
|
||
idx = i
|
||
}
|
||
}
|
||
if idx < 0 {
|
||
t.Fatalf("%s not found in %v", turnIDMigrationID, ids)
|
||
}
|
||
if err := MigrateDown(ctx, conn, len(ids)-idx, testLogger()); err != nil {
|
||
t.Fatalf("MigrateDown(%d): %v", len(ids)-idx, err)
|
||
}
|
||
if columnExists(t, ctx, conn, "chat_messages", "turn_id") {
|
||
t.Fatal("chat_messages.turn_id should be dropped by the 0018 down")
|
||
}
|
||
|
||
// 重新 up 恢复端到端可用:默认空串意味着「这一行不属于任何已知回合」。
|
||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||
t.Fatalf("re-MigrateUp: %v", err)
|
||
}
|
||
if !columnExists(t, ctx, conn, "chat_messages", "turn_id") {
|
||
t.Fatal("chat_messages.turn_id should exist again after re-MigrateUp")
|
||
}
|
||
if _, err := conn.ExecContext(ctx,
|
||
`INSERT INTO chat_messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)`,
|
||
"m1", "web", "user", "hi", 1000); err != nil {
|
||
t.Fatalf("insert without turn id: %v", err)
|
||
}
|
||
var turnID string
|
||
if err := conn.QueryRowContext(ctx, `SELECT turn_id FROM chat_messages WHERE id = ?`, "m1").Scan(&turnID); err != nil {
|
||
t.Fatalf("select turn_id: %v", err)
|
||
}
|
||
if turnID != "" {
|
||
t.Fatalf("turn_id = %q, want the empty default", turnID)
|
||
}
|
||
}
|