- 迁移 0020_chat_message_voice:按 design/24 §10 与契约 §7.2 已登记的 voice 形状建列/索引
- 语音回合进编排并落库;voice 引用与统计如实回填(转码/转写没成功就如实标未成功,不假装)
- 复用既有 internal/capability/transcode:非 wav(微信 AMR 等)先转码再送 ASR;失败可分类且不丢消息
⚠️ 部分完成(如实登记):生产端到端未成立(本片只到落库与转码接入),且**未经验真机**。
97 lines
3.7 KiB
Go
97 lines
3.7 KiB
Go
package db
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
)
|
||
|
||
// voiceMigrationID 是「语音事实」列的迁移 id(design/24 §10.4;切片 CODE-VOICE)。
|
||
const voiceMigrationID = "0020_chat_message_voice"
|
||
|
||
// TestMigrateVoiceColumnReversible 验证 0020(chat_messages.voice,语音来源事实 + 音频
|
||
// 副本引用)可上下翻滚:up 建列、重复 up 是空操作、down 删列、再 up 恢复;并钉住列口径
|
||
// —— 默认空串(**不是 NULL**)与「老行 = 不是语音消息」逐字等价。它用自己的内存库,
|
||
// 绝不碰 data/cyrene.db。
|
||
//
|
||
// 0020 的编号**小于**既有的 0021/0022(那两条先落地),因此本测试刻意按
|
||
// `upMigrationIDs` 的下标算回滚步数(照 0018 的写法),不假设「最后一条就是自己」。
|
||
func TestMigrateVoiceColumnReversible(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", "voice") {
|
||
t.Fatal("chat_messages.voice 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", "voice") {
|
||
t.Fatal("chat_messages.voice should still exist after idempotent re-run")
|
||
}
|
||
|
||
// 恰好回滚到 0020(含它之后新加的迁移),因此日后再加迁移也不会让本测试失效。
|
||
ids := upMigrationIDs(t)
|
||
idx := -1
|
||
for i, id := range ids {
|
||
if id == voiceMigrationID {
|
||
idx = i
|
||
}
|
||
}
|
||
if idx < 0 {
|
||
t.Fatalf("%s not found in %v", voiceMigrationID, 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", "voice") {
|
||
t.Fatal("chat_messages.voice should be dropped by the 0020 down")
|
||
}
|
||
|
||
// 重新 up 恢复端到端可用。
|
||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||
t.Fatalf("re-MigrateUp: %v", err)
|
||
}
|
||
if !columnExists(t, ctx, conn, "chat_messages", "voice") {
|
||
t.Fatal("chat_messages.voice should exist again after re-MigrateUp")
|
||
}
|
||
}
|
||
|
||
// TestMigrateVoiceColumnDefaultIsEmptyString 钉住列口径:**用空串不用 NULL**(与
|
||
// document / turn_id / client_msg_id 同族)。插入一条不带 voice 的消息(显式不写该列)
|
||
// 必须读回空串 —— 「老行」与「这条消息不是语音」因此是同一个值,读侧只有一种空情形。
|
||
//
|
||
// 若有人把默认值改成 NULL,SQL 层 Scan 会失败或读出 NULL 语义,客户端看到的 `voice`
|
||
// 键行为随之漂移(design/24 §10.4:非语音消息**键缺席**,不是 `{}` 也不是 `null`)。
|
||
func TestMigrateVoiceColumnDefaultIsEmptyString(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 _, err := conn.ExecContext(ctx,
|
||
`INSERT INTO chat_messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)`,
|
||
"m-voice-default", "web", "user", "hi", 1000); err != nil {
|
||
t.Fatalf("insert without voice: %v", err)
|
||
}
|
||
// 用 *string 读回:默认值若是 NULL,Scan 会得到 nil(这里就会红)。
|
||
var voice *string
|
||
if err := conn.QueryRowContext(ctx,
|
||
`SELECT voice FROM chat_messages WHERE id = ?`, "m-voice-default").Scan(&voice); err != nil {
|
||
t.Fatalf("select voice: %v", err)
|
||
}
|
||
if voice == nil {
|
||
t.Fatal("voice is NULL, want the empty-string default(绝不使用 NULL)")
|
||
}
|
||
if *voice != "" {
|
||
t.Fatalf("voice = %q, want \"\"", *voice)
|
||
}
|
||
}
|