110 lines
4.5 KiB
Go
110 lines
4.5 KiB
Go
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<br>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\\`<br>[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
|
||
}
|