1package cmd23import (4 "bufio"5 "encoding/json"6 "fmt"7 "io"8 "os"9 "path/filepath"10 "strings"1112 tea "charm.land/bubbletea/v2"13 "github.com/FelineStateMachine/puzzletea/export/pdf"14 "github.com/FelineStateMachine/puzzletea/registry"15 "github.com/spf13/cobra"16)1718const (19 testRenderWidth = 12020 testRenderHeight = 4021)2223var testOutput string2425type testInputRecord struct {26 lineNo int27 record pdfexport.JSONLRecord28}2930var testCmd = &cobra.Command{31 Use: "test <input.jsonl>",32 Short: "Render saved puzzle exports into an ANSI review artifact",33 Long: "Parse a PuzzleTea export JSONL file, import each saved puzzle, and render the full board view in a stable ANSI text format for visual review.",34 Args: cobra.ExactArgs(1),35 RunE: runTest,36}3738func init() {39 testCmd.Flags().StringVarP(&testOutput, "output", "o", "", "write rendered output to a file (defaults to stdout)")40}4142func runTest(cmd *cobra.Command, args []string) error {43 applyTheme(flagTheme)4445 records, err := loadTestRecords(args[0])46 if err != nil {47 return err48 }4950 output, err := renderTestRecords(args[0], records)51 if err != nil {52 return err53 }5455 return writeTestOutput(cmd.OutOrStdout(), testOutput, output)56}5758func loadTestRecords(path string) ([]testInputRecord, error) {59 if !strings.EqualFold(filepath.Ext(path), ".jsonl") {60 return nil, fmt.Errorf("%s: expected .jsonl input", path)61 }6263 f, err := os.Open(path)64 if err != nil {65 return nil, fmt.Errorf("open input jsonl: %w", err)66 }67 defer f.Close()6869 scanner := bufio.NewScanner(f)70 scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)7172 records := make([]testInputRecord, 0, 16)73 lineNo := 074 for scanner.Scan() {75 lineNo++76 line := strings.TrimSpace(scanner.Text())77 if line == "" {78 continue79 }8081 var record pdfexport.JSONLRecord82 if err := json.Unmarshal([]byte(line), &record); err != nil {83 return nil, fmt.Errorf("%s:%d: decode jsonl record: %w", path, lineNo, err)84 }85 if record.Schema != pdfexport.ExportSchemaV1 {86 return nil, fmt.Errorf("%s:%d: unsupported schema %q", path, lineNo, record.Schema)87 }8889 records = append(records, testInputRecord{lineNo: lineNo, record: record})90 }91 if err := scanner.Err(); err != nil {92 return nil, fmt.Errorf("read input jsonl: %w", err)93 }94 if len(records) == 0 {95 return nil, fmt.Errorf("%s: input jsonl is empty", path)96 }9798 return records, nil99}100101func renderTestRecords(path string, records []testInputRecord) (string, error) {102 var b strings.Builder103104 for _, item := range records {105 record := item.record106 gameType := strings.TrimSpace(record.Puzzle.Game)107 if gameType == "" {108 gameType = strings.TrimSpace(record.Pack.Category)109 }110 if gameType == "" {111 return "", fmt.Errorf("%s:%d: missing puzzle game/category", path, item.lineNo)112 }113114 g, err := registry.Import(gameType, record.Puzzle.Save)115 if err != nil {116 return "", fmt.Errorf("%s:%d: import %q: %w", path, item.lineNo, gameType, err)117 }118119 sized, _ := g.Update(tea.WindowSizeMsg{Width: testRenderWidth, Height: testRenderHeight})120 fmt.Fprintf(121 &b,122 "=== %s | %s | %s | #%d ===\n",123 gameType,124 testHeaderValue(record.Puzzle.Mode, record.Pack.ModeSelection),125 testHeaderValue(record.Puzzle.Name, "unnamed"),126 record.Puzzle.Index,127 )128 b.WriteString(sized.View())129 b.WriteString("\n\n")130 }131132 return b.String(), nil133}134135func testHeaderValue(value, fallback string) string {136 value = strings.TrimSpace(value)137 if value != "" {138 return value139 }140 return fallback141}142143func writeTestOutput(stdout io.Writer, path, content string) error {144 if strings.TrimSpace(path) == "" {145 _, err := io.WriteString(stdout, content)146 return err147 }148149 dir := filepath.Dir(path)150 if dir != "" && dir != "." {151 if err := os.MkdirAll(dir, 0o755); err != nil {152 return fmt.Errorf("create output directory: %w", err)153 }154 }155 if err := os.WriteFile(path, []byte(content), 0o644); err != nil {156 return fmt.Errorf("write output file: %w", err)157 }158 return nil159}