clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

main
puzzletea / cmd / new_export_test.go
new_export_test.go 7.8K
1package cmd23import (4 "bytes"5 "encoding/json"6 "os"7 "path/filepath"8 "strconv"9 "strings"10 "testing"11 "time"1213 "github.com/FelineStateMachine/puzzletea/export/pdf"1415 "github.com/spf13/cobra"16)1718func TestRunNewExportRejectsUnsupportedGame(t *testing.T) {19 withExportFlagReset(t)20 output := filepath.Join(t.TempDir(), "lights.jsonl")21 flagOutput = output2223 cmd, _ := newExportTestCmd(t, false)24 err := runNewExport(cmd, []string{"lights-out"})25 if err == nil {26 t.Fatal("expected unsupported export error")27 }28 if !strings.Contains(err.Error(), "does not support export") {29 t.Fatalf("unexpected error: %v", err)30 }31 if _, statErr := os.Stat(output); !os.IsNotExist(statErr) {32 t.Fatalf("expected no output file, stat err = %v", statErr)33 }34}3536func TestRunNewExportValidation(t *testing.T) {37 t.Run("writes jsonl to stdout when output omitted", func(t *testing.T) {38 withExportFlagReset(t)39 flagExport = 24041 cmd, out := newExportTestCmd(t, true)42 err := runNewExport(cmd, []string{"nonogram", "mini"})43 if err != nil {44 t.Fatalf("expected stdout export success, got error: %v", err)45 }4647 lines := strings.Split(strings.TrimSpace(out.String()), "\n")48 if got, want := len(lines), 2; got != want {49 t.Fatalf("jsonl lines = %d, want %d", got, want)50 }51 for i, line := range lines {52 var record pdfexport.JSONLRecord53 if err := json.Unmarshal([]byte(line), &record); err != nil {54 t.Fatalf("line %d is not valid jsonl: %v", i+1, err)55 }56 if record.Schema != pdfexport.ExportSchemaV2 {57 t.Fatalf("line %d schema = %q, want %q", i+1, record.Schema, pdfexport.ExportSchemaV2)58 }59 }60 })6162 t.Run("output extension must be jsonl", func(t *testing.T) {63 withExportFlagReset(t)64 flagOutput = filepath.Join(t.TempDir(), "out.txt")6566 cmd, _ := newExportTestCmd(t, false)67 err := runNewExport(cmd, []string{"nonogram", "mini"})68 if err == nil {69 t.Fatal("expected extension validation error")70 }71 if !strings.Contains(err.Error(), ".jsonl extension") {72 t.Fatalf("unexpected error: %v", err)73 }74 })7576 t.Run("set-seed cannot be combined with output", func(t *testing.T) {77 withExportFlagReset(t)78 flagSetSeed = "abc"79 flagOutput = filepath.Join(t.TempDir(), "out.jsonl")8081 cmd, _ := newExportTestCmd(t, false)82 err := runNewExport(cmd, []string{"nonogram", "mini"})83 if err == nil {84 t.Fatal("expected set-seed validation error")85 }86 if !strings.Contains(err.Error(), "--set-seed cannot be combined with export (--export/--output)") {87 t.Fatalf("unexpected error: %v", err)88 }89 })90}9192func TestRunNewExportReproducibleWithSeed(t *testing.T) {93 withExportFlagReset(t)9495 fixedNow := time.Date(2026, 2, 22, 11, 0, 0, 0, time.UTC)96 previousNow := exportNow97 exportNow = func() time.Time { return fixedNow }98 t.Cleanup(func() { exportNow = previousNow })99100 fileA := filepath.Join(t.TempDir(), "a.jsonl")101 fileB := filepath.Join(t.TempDir(), "b.jsonl")102103 flagExport = 3104 flagWithSeed = "zine-seed-01"105 flagOutput = fileA106 cmdA, _ := newExportTestCmd(t, false)107 if err := runNewExport(cmdA, []string{"nonogram", "mini"}); err != nil {108 t.Fatalf("first export failed: %v", err)109 }110111 flagOutput = fileB112 cmdB, _ := newExportTestCmd(t, false)113 if err := runNewExport(cmdB, []string{"nonogram", "mini"}); err != nil {114 t.Fatalf("second export failed: %v", err)115 }116117 a, err := os.ReadFile(fileA)118 if err != nil {119 t.Fatal(err)120 }121 b, err := os.ReadFile(fileB)122 if err != nil {123 t.Fatal(err)124 }125 if string(a) != string(b) {126 t.Fatal("expected deterministic jsonl output for identical seed and args")127 }128}129130func TestRunNewExportOverwritesOutputFile(t *testing.T) {131 withExportFlagReset(t)132133 file := filepath.Join(t.TempDir(), "out.jsonl")134 if err := os.WriteFile(file, []byte("old"), 0o644); err != nil {135 t.Fatal(err)136 }137138 flagExport = 1139 flagWithSeed = "overwrite-seed"140 flagOutput = file141142 cmd, _ := newExportTestCmd(t, false)143 if err := runNewExport(cmd, []string{"nonogram", "mini"}); err != nil {144 t.Fatalf("export failed: %v", err)145 }146147 data, err := os.ReadFile(file)148 if err != nil {149 t.Fatal(err)150 }151 if string(data) == "old" {152 t.Fatal("expected output file to be overwritten")153 }154 if !strings.Contains(string(data), pdfexport.ExportSchemaV2) {155 t.Fatal("expected jsonl export schema marker")156 }157}158159func TestRunNewExportOmitsPrintPayload(t *testing.T) {160 withExportFlagReset(t)161 flagExport = 1162163 cmd, out := newExportTestCmd(t, true)164 if err := runNewExport(cmd, []string{"sudoku", "easy"}); err != nil {165 t.Fatalf("expected sudoku export success, got error: %v", err)166 }167168 lines := strings.Split(strings.TrimSpace(out.String()), "\n")169 if len(lines) != 1 {170 t.Fatalf("jsonl lines = %d, want 1", len(lines))171 }172173 var record map[string]any174 if err := json.Unmarshal([]byte(lines[0]), &record); err != nil {175 t.Fatal(err)176 }177 if _, ok := record["print"]; ok {178 t.Fatal("did not expect print payload in export record")179 }180181 puzzle, ok := record["puzzle"].(map[string]any)182 if !ok {183 t.Fatalf("expected puzzle object, got %T", record["puzzle"])184 }185 if _, ok := puzzle["snippet"]; ok {186 t.Fatal("did not expect markdown snippet in export record")187 }188}189190func TestRunNewExportUsesPresetEloMetadata(t *testing.T) {191 withExportFlagReset(t)192 flagExport = 1193194 cmd, out := newExportTestCmd(t, true)195 if err := runNewExport(cmd, []string{"sudoku", "easy"}); err != nil {196 t.Fatalf("expected sudoku export success, got error: %v", err)197 }198199 record := decodeSingleExportRecord(t, out.String())200 if record.Puzzle.TargetDifficultyElo == nil {201 t.Fatal("expected target_difficulty_elo from preset")202 }203 if got, want := *record.Puzzle.TargetDifficultyElo, 600; got != want {204 t.Fatalf("target_difficulty_elo = %d, want %d", got, want)205 }206 if record.Puzzle.ActualDifficultyElo == nil {207 t.Fatal("expected actual_difficulty_elo from Elo report")208 }209 if record.Puzzle.DifficultyConfidence == "" {210 t.Fatal("expected difficulty_confidence")211 }212}213214func TestRunNewExportExplicitDifficultyOverridesPreset(t *testing.T) {215 withExportFlagReset(t)216 flagExport = 1217 flagDifficulty = 2100218219 cmd, out := newExportTestCmd(t, true)220 cmd.Flags().Int("difficulty", -1, "")221 if err := cmd.Flags().Set("difficulty", strconv.Itoa(flagDifficulty)); err != nil {222 t.Fatal(err)223 }224225 if err := runNewExport(cmd, []string{"sudoku", "easy"}); err != nil {226 t.Fatalf("expected sudoku export success, got error: %v", err)227 }228229 record := decodeSingleExportRecord(t, out.String())230 if record.Puzzle.TargetDifficultyElo == nil {231 t.Fatal("expected target_difficulty_elo from explicit difficulty")232 }233 if got, want := *record.Puzzle.TargetDifficultyElo, flagDifficulty; got != want {234 t.Fatalf("target_difficulty_elo = %d, want %d", got, want)235 }236}237238func withExportFlagReset(t *testing.T) {239 t.Helper()240241 prevSetSeed := flagSetSeed242 prevWithSeed := flagWithSeed243 prevDifficulty := flagDifficulty244 prevDifficultySet := flagDifficultySet245 prevExport := flagExport246 prevOutput := flagOutput247248 flagSetSeed = ""249 flagWithSeed = ""250 flagDifficulty = -1251 flagDifficultySet = false252 flagExport = 1253 flagOutput = ""254255 t.Cleanup(func() {256 flagSetSeed = prevSetSeed257 flagWithSeed = prevWithSeed258 flagDifficulty = prevDifficulty259 flagDifficultySet = prevDifficultySet260 flagExport = prevExport261 flagOutput = prevOutput262 })263}264265func decodeSingleExportRecord(t *testing.T, output string) pdfexport.JSONLRecord {266 t.Helper()267268 lines := strings.Split(strings.TrimSpace(output), "\n")269 if len(lines) != 1 {270 t.Fatalf("jsonl lines = %d, want 1", len(lines))271 }272273 var record pdfexport.JSONLRecord274 if err := json.Unmarshal([]byte(lines[0]), &record); err != nil {275 t.Fatal(err)276 }277 return record278}279280func newExportTestCmd(t *testing.T, exportChanged bool) (*cobra.Command, *bytes.Buffer) {281 t.Helper()282283 cmd := &cobra.Command{}284 var out bytes.Buffer285 cmd.SetOut(&out)286 cmd.Flags().Int("export", 1, "")287 if exportChanged {288 if err := cmd.Flags().Set("export", strconv.Itoa(flagExport)); err != nil {289 t.Fatal(err)290 }291 }292 return cmd, &out293}