Files
Cyrene_v2/internal/db/migrate_voice_test.go
T
AskaEth ede00d9928 feat(voice): 语音消息落库 + 转码接入(切片 0020)
- 迁移 0020_chat_message_voice:按 design/24 §10 与契约 §7.2 已登记的 voice 形状建列/索引
- 语音回合进编排并落库;voice 引用与统计如实回填(转码/转写没成功就如实标未成功,不假装)
- 复用既有 internal/capability/transcode:非 wav(微信 AMR 等)先转码再送 ASR;失败可分类且不丢消息

⚠️ 部分完成(如实登记):生产端到端未成立(本片只到落库与转码接入),且**未经验真机**。
2026-10-06 09:04:06 +08:00

97 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}