package state import ( "os" "path/filepath" "testing" "github.com/voocel/ainovel-cli/domain" ) func TestSaveAndLoadWorldRules(t *testing.T) { dir := t.TempDir() store := NewStore(dir) rules := []domain.WorldRule{ {Category: "magic", Rule: "法术消耗精神力", Boundary: "精神力耗尽会昏迷"}, {Category: "magic", Rule: "禁咒需要三人合力", Boundary: "单人强行施放会死亡"}, {Category: "society", Rule: "贵族拥有领地裁判权", Boundary: "不得越权审判其他领地居民"}, } if err := store.SaveWorldRules(rules); err != nil { t.Fatalf("SaveWorldRules: %v", err) } // 验证 JSON 文件存在 if _, err := os.Stat(filepath.Join(dir, "world_rules.json")); err != nil { t.Fatalf("world_rules.json not created: %v", err) } // 验证 Markdown 文件存在 if _, err := os.Stat(filepath.Join(dir, "world_rules.md")); err != nil { t.Fatalf("world_rules.md not created: %v", err) } loaded, err := store.LoadWorldRules() if err != nil { t.Fatalf("LoadWorldRules: %v", err) } if len(loaded) != 3 { t.Fatalf("expected 3 rules, got %d", len(loaded)) } if loaded[0].Category != "magic" || loaded[0].Rule != "法术消耗精神力" { t.Errorf("first rule mismatch: %+v", loaded[0]) } if loaded[2].Category != "society" { t.Errorf("third rule category mismatch: %+v", loaded[2]) } } func TestLoadWorldRules_Empty(t *testing.T) { dir := t.TempDir() store := NewStore(dir) rules, err := store.LoadWorldRules() if err != nil { t.Fatalf("LoadWorldRules on empty dir: %v", err) } if rules != nil { t.Fatalf("expected nil for missing file, got %v", rules) } } func TestSaveWorldRules_Overwrite(t *testing.T) { dir := t.TempDir() store := NewStore(dir) v1 := []domain.WorldRule{{Category: "old", Rule: "旧规则", Boundary: "旧边界"}} if err := store.SaveWorldRules(v1); err != nil { t.Fatalf("SaveWorldRules v1: %v", err) } v2 := []domain.WorldRule{ {Category: "new", Rule: "新规则A", Boundary: "新边界A"}, {Category: "new", Rule: "新规则B", Boundary: "新边界B"}, } if err := store.SaveWorldRules(v2); err != nil { t.Fatalf("SaveWorldRules v2: %v", err) } loaded, err := store.LoadWorldRules() if err != nil { t.Fatalf("LoadWorldRules: %v", err) } if len(loaded) != 2 { t.Fatalf("expected 2 rules after overwrite, got %d", len(loaded)) } if loaded[0].Category != "new" { t.Errorf("expected new category, got %s", loaded[0].Category) } } func TestRenderWorldRules(t *testing.T) { rules := []domain.WorldRule{ {Category: "magic", Rule: "法术消耗精神力", Boundary: "精神力耗尽会昏迷"}, {Category: "society", Rule: "贵族有裁判权", Boundary: ""}, {Category: "magic", Rule: "禁咒需三人", Boundary: "单人施放会死"}, } md := renderWorldRules(rules) // 验证标题 if got := md[:len("# 世界观规则")]; got != "# 世界观规则" { t.Errorf("missing title, got: %s", got) } // 验证分组:magic 应该出现在 society 之前(按输入顺序) magicPos := indexOf(md, "## magic") societyPos := indexOf(md, "## society") if magicPos < 0 || societyPos < 0 { t.Fatalf("missing category headers in:\n%s", md) } if magicPos >= societyPos { t.Errorf("magic should appear before society") } // 验证边界渲染:有 boundary 的条目应包含"边界"字样 if indexOf(md, "边界:精神力耗尽会昏迷") < 0 { t.Errorf("missing boundary in:\n%s", md) } // 验证无 boundary 的条目不输出边界行 if indexOf(md, "边界:\n") >= 0 { t.Errorf("empty boundary should not be rendered") } } func TestRenderWorldRules_EmptyCategoryFallback(t *testing.T) { rules := []domain.WorldRule{ {Category: "", Rule: "无分类规则", Boundary: "边界"}, } md := renderWorldRules(rules) if indexOf(md, "## other") < 0 { t.Errorf("empty category should fall back to 'other', got:\n%s", md) } } func indexOf(s, substr string) int { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return i } } return -1 }