package engine import ( "os" "path/filepath" "strings" "testing" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/query" ) // ── 版本常量 ───────────────────────────────────────────────────────────────── func TestTranscriptVersionConstants(t *testing.T) { // 确保常量有意义:current >= 1,max == current(单版本时代) if transcriptCurrentVersion < 1 { t.Errorf("transcriptCurrentVersion = %d, want >= 1", transcriptCurrentVersion) } if transcriptMaxSupportedVersion != transcriptCurrentVersion { t.Errorf("max(%d) != current(%d); update this test when multi-version support is added", transcriptMaxSupportedVersion, transcriptCurrentVersion) } } // ── SaveTranscript 写入 FormatVersion ───────────────────────────────────────── func TestSaveTranscript_WritesFormatVersion(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "t.json") stats := TranscriptStats{TurnCount: 1} if err := SaveTranscript(path, "sid", "model", nil, stats); err != nil { t.Fatalf("SaveTranscript: %v", err) } tr, err := LoadTranscript(path) if err != nil { t.Fatalf("LoadTranscript: %v", err) } if tr.FormatVersion != transcriptCurrentVersion { t.Errorf("FormatVersion = %d, want %d", tr.FormatVersion, transcriptCurrentVersion) } } // ── LoadTranscript 旧文件(无 format_version)规范化为 v1 ────────────────────── func TestLoadTranscript_OldFile_NormalizesToV1(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "old.json") // 写一个旧格式文件(无 format_version 字段,JSON 反序列化为 0) oldJSON := `{ "session_id": "abc", "model": "claude", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "messages": [], "stats": {"turn_count": 0} }` if err := os.WriteFile(path, []byte(oldJSON), 0644); err != nil { t.Fatalf("WriteFile: %v", err) } tr, err := LoadTranscript(path) if err != nil { t.Fatalf("LoadTranscript old file: %v", err) } // 旧文件 format_version 缺失 → JSON 反序列化为 0 → 规范化为 1 if tr.FormatVersion != 1 { t.Errorf("FormatVersion = %d, want 1 (normalized from 0)", tr.FormatVersion) } } // ── LoadTranscript MaxSupportedVersion 保护 ─────────────────────────────────── func TestLoadTranscript_FutureVersion_ReturnsError(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "future.json") // 写一个版本号超出当前支持范围的文件 futureVersion := transcriptMaxSupportedVersion + 1 futureJSON := `{"format_version":` + migrateItoa(futureVersion) + `,"session_id":"x","model":"m",` + `"created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z",` + `"messages":[],"stats":{}}` if err := os.WriteFile(path, []byte(futureJSON), 0644); err != nil { t.Fatalf("WriteFile: %v", err) } _, err := LoadTranscript(path) if err == nil { t.Fatal("LoadTranscript should return error for future format version") } if !strings.Contains(err.Error(), "format version") { t.Errorf("error should mention 'format version', got: %v", err) } } // ── migrateTranscript 空表 no-op ────────────────────────────────────────────── func TestMigrateTranscript_EmptyTable_NoOp(t *testing.T) { tr := &Transcript{FormatVersion: transcriptCurrentVersion} if err := migrateTranscript(tr); err != nil { t.Errorf("migrateTranscript no-op: %v", err) } if tr.FormatVersion != transcriptCurrentVersion { t.Errorf("FormatVersion changed: got %d", tr.FormatVersion) } } // ── migrateTranscript 注册迁移函数(测试用,不影响全局状态)────────────────── func TestMigrateTranscript_RegisteredFunc_Executes(t *testing.T) { // 注册一个测试迁移函数(v1 → v2),测试完毕后清理 // 精妙之处(CLEVER): 测试不能修改全局 transcriptMigrations(影响其他测试). // 这里通过直接测试 migrateTranscript 的内部逻辑来验证, // 而不是通过注册全局函数. // 用一个局部注册表来模拟. called := false localMigrations := map[int]MigrateFunc{ 1: func(t *Transcript) error { called = true t.Model = "migrated" return nil }, } // 直接测试迁移逻辑(复用代码而不污染全局状态) tr := &Transcript{FormatVersion: 1, Model: "original"} targetVersion := 2 for tr.FormatVersion < targetVersion { fn, ok := localMigrations[tr.FormatVersion] if !ok { t.Fatalf("missing migration for v%d", tr.FormatVersion) } if err := fn(tr); err != nil { t.Fatalf("migration error: %v", err) } tr.FormatVersion++ } if !called { t.Error("migration function was not called") } if tr.Model != "migrated" { t.Errorf("Model = %q, want \"migrated\"", tr.Model) } if tr.FormatVersion != 2 { t.Errorf("FormatVersion = %d, want 2", tr.FormatVersion) } } // ── migrateTranscript 缺少迁移函数返回错误 ──────────────────────────────────── func TestMigrateTranscript_MissingFunc_ReturnsError(t *testing.T) { // transcriptMigrations 为空,但让 tr.FormatVersion < current 触发迁移路径 // 需要临时修改 transcriptCurrentVersion,但 const 不能修改. // 改为:直接向空迁移表查询触发错误路径,验证错误格式. tr := &Transcript{FormatVersion: 0} // 低于任何已注册版本 // transcriptCurrentVersion == 1,FormatVersion == 0 < 1 → 触发迁移 // transcriptMigrations[0] 不存在 → 应返回错误 err := migrateTranscript(tr) if err == nil { t.Fatal("migrateTranscript should error when migration func is missing") } if !strings.Contains(err.Error(), "no migration registered") { t.Errorf("error should mention 'no migration registered', got: %v", err) } } // ── UpdateTranscript 保留 CreatedAt ─────────────────────────────────────────── func TestUpdateTranscript_PreservesCreatedAt(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "u.json") msgs := []query.Message{{Role: "user"}} if err := SaveTranscript(path, "s1", "m1", msgs, TranscriptStats{}); err != nil { t.Fatalf("SaveTranscript: %v", err) } tr1, _ := LoadTranscript(path) origCreated := tr1.CreatedAt // 等一小段时间确保 UpdatedAt 不同 time.Sleep(2 * time.Millisecond) if err := UpdateTranscript(path, "s1", "m1", msgs, TranscriptStats{TurnCount: 1}); err != nil { t.Fatalf("UpdateTranscript: %v", err) } tr2, _ := LoadTranscript(path) if !tr2.CreatedAt.Equal(origCreated) { t.Errorf("CreatedAt changed: %v → %v", origCreated, tr2.CreatedAt) } if !tr2.UpdatedAt.After(origCreated) { t.Errorf("UpdatedAt should be after CreatedAt") } } // ── 辅助函数 ────────────────────────────────────────────────────────────────── // migrateItoa 简单的 int → string,避免与 activity.go 中的 itoa 冲突 func migrateItoa(n int) string { if n == 0 { return "0" } neg := n < 0 if neg { n = -n } var buf [20]byte pos := len(buf) for n > 0 { pos-- buf[pos] = byte('0' + n%10) n /= 10 } if neg { pos-- buf[pos] = '-' } return string(buf[pos:]) }