package reporting import ( "reflect" "regexp" "strings" "testing" "time" "gopkg.in/yaml.v3" ) func TestMarkdownFixedLayoutAndCounts(t *testing.T) { one, two, three, four, zero := 1, 2, 3, 4, 0 snapshot := Snapshot{ RunID: "run-123", Status: StatusCompleted, StartedAt: time.Date(2026, 9, 24, 10, 0, 0, 0, time.UTC), TranscriptPath: "in|put.json", GlossaryPath: "glossary`secret`.yaml", OutputPath: "out\nfile.json", StageNames: []string{"grammar_1", "grammar_2"}, WorkUnits: []WorkUnit{ {StageIndex: 0, ModuleInstance: "grammar_1", ChunkIndex: 0, StartSegmentID: 1, EndSegmentID: 2, State: "completed", ErrorCount: 1, RecoveryCount: 1, CorrectionsApplied: 1}, {StageIndex: 1, ModuleInstance: "grammar_2", ChunkIndex: 0, StartSegmentID: 1, EndSegmentID: 2, State: "interrupted", RecoveryCount: 2}, {StageIndex: 0, ModuleInstance: "grammar_1", ChunkIndex: 1, StartSegmentID: 3, EndSegmentID: 4, State: "completed"}, {StageIndex: 1, ModuleInstance: "grammar_2", ChunkIndex: 1, StartSegmentID: 3, EndSegmentID: 4, State: "not_started"}, }, Summary: Summary{WorkUnitsTotal: &four, WorkUnitsCompleted: &two, WorkUnitsInterrupted: &one, WorkUnitsNotStarted: &one, WorkUnitsWithErrors: &one, RunErrors: &zero, CorrectionsApplied: &one, LLMOperations: &four, ProposalResponses: &one, ProposalResponsesWithFiltering: &one, Recoveries: &three}, Errors: []ErrorRecord{{Scope: "work_unit", Failures: []Failure{{Reason: "transport_failed"}, {Reason: "transport_failed"}}}}, Recoveries: []RecoveryRecord{{Reasons: []string{"output_repaired", "proposals_filtered"}}, {Reasons: []string{"output_repaired"}}, {Reasons: []string{"service_recovered"}}}, } got := Markdown(snapshot, "secret") text := string(got) // Compare only complete table rows, preserving order and states while leaving // prose and spacing outside the fixed matrices free to change. wantRows := []string{ "| Chunk | grammar_1 | grammar_2 |", "| --- | ---: | ---: |", "| Chunk 1 (segments 1–2) | 1 | 0* |", "| Chunk 2 (segments 3–4) | 0 | — |", "| Chunk | grammar_1 | grammar_2 |", "| --- | ---: | ---: |", "| Chunk 1 (segments 1–2) | 1 | 2* |", "| Chunk 2 (segments 3–4) | 0 | — |", } if rows := markdownTableRows(text); !reflect.DeepEqual(rows, wantRows) { t.Fatalf("matrices: got %v; want %v", rows, wantRows) } for _, fragment := range []string{"in\\|put.json", "out
file.json", "glossary\\`[REDACTED]\\`.yaml", "`transport_failed`: 1", "`output_repaired`: 2", "`proposals_filtered`: 1", "`service_recovered`: 1"} { if !strings.Contains(text, fragment) { t.Fatalf("missing %q in:\n%s", fragment, text) } } // Assert summary quantities independently of the surrounding explanation. numbers := regexp.MustCompile(`[0-9]+`) for label, want := range map[string][]string{ "Corrections applied": {"1"}, "Errors": {"1", "0"}, "Recoveries": {"3", "4"}, } { var got []string for _, line := range strings.Split(text, "\n") { if value, ok := strings.CutPrefix(strings.TrimSpace(line), label+":"); ok { got = numbers.FindAllString(value, -1) break } } if !reflect.DeepEqual(got, want) { t.Fatalf("%s summary: got %v; want %v", label, got, want) } } if strings.Contains(text, "secret") { t.Fatal("secret leaked") } var decoded struct { Summary Summary `yaml:"summary"` } yamlBytes, err := YAML(snapshot.ErrorsDocument()) if err != nil { t.Fatal(err) } if err := yaml.Unmarshal(yamlBytes, &decoded); err != nil { t.Fatal(err) } if CompletionLine(snapshot) != "Completed: 1 corrections applied; 1 work units with errors; 3 recoveries." || *decoded.Summary.WorkUnitsWithErrors != 1 || *decoded.Summary.Recoveries != 3 { t.Fatalf("CLI/YAML count disagreement: %s %+v", CompletionLine(snapshot), decoded.Summary) } } func TestMarkdownNoWorkAndEscapedStageLabel(t *testing.T) { text := string(Markdown(Snapshot{RunID: "r", StageNames: []string{"bad|label`\nsecret"}}, "secret")) if len(markdownTableRows(text)) != 0 || strings.Contains(text, "secret") { t.Fatal(text) } text = string(Markdown(Snapshot{RunID: "r", StageNames: []string{"bad|label`\nsecret"}, WorkUnits: []WorkUnit{{StageIndex: 0, ChunkIndex: 0, State: "completed"}}}, "secret")) if !strings.Contains(text, "bad\\|label\\`
[REDACTED]") { t.Fatal(text) } } func markdownTableRows(text string) []string { var rows []string for _, line := range strings.Split(text, "\n") { if line = strings.TrimSpace(line); strings.HasPrefix(line, "|") { rows = append(rows, line) } } return rows }