Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions internal/exporter/html.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package exporter

import (
"encoding/json"
"fmt"
"html"
"html/template"
Expand All @@ -18,7 +17,6 @@ type htmlData struct {
Format string
Iterations []htmlIteration
Summary htmlSummary
DataJSON template.JS
}

type htmlIteration struct {
Expand Down Expand Up @@ -117,9 +115,6 @@ func buildHTMLData(t *trace.Trace) htmlData {
}
}

jsonBytes, _ := json.Marshal(d.Iterations)
d.DataJSON = template.JS(string(jsonBytes))

return d
}

Expand Down
123 changes: 123 additions & 0 deletions internal/exporter/html_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package exporter

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/loop-eng/looprelay/internal/trace"
)

func sampleTrace() *trace.Trace {
tr := trace.New([]trace.Iteration{
{
Number: 1,
Phase: trace.PhaseAct,
Action: trace.Action{
Type: "Edit",
Target: "src/main.go",
Diff: "+func main() {}\n-func Main() {}",
},
Tokens: trace.TokenUsage{InputTokens: 100, OutputTokens: 50},
CostUSD: 0.0123,
Context: trace.ContextState{FillPct: 42, Compactions: 1},
DurationMs: 1200,
},
{
Number: 2,
Phase: trace.PhaseVerify,
Verification: &trace.Verification{
Status: "success",
Output: "ok <script>alert(1)</script>",
},
Tokens: trace.TokenUsage{InputTokens: 80, OutputTokens: 20},
CostUSD: 0.0045,
Context: trace.ContextState{FillPct: 55, Compactions: 1},
},
})
tr.SourceFile = "demo.ltf"
tr.SessionID = "sess-42"
tr.Agent = "claude"
tr.SourceFormat = "ltf"
return tr
}

func TestBuildHTMLData_NoDataJSONField(t *testing.T) {
d := buildHTMLData(sampleTrace())
if d.Title == "" {
t.Fatal("expected title")
}
if len(d.Iterations) != 2 {
t.Fatalf("expected 2 iterations, got %d", len(d.Iterations))
}
if d.Summary.TotalIterations != 2 {
t.Fatalf("expected summary total 2, got %d", d.Summary.TotalIterations)
}
// Regression for #14: DataJSON was computed via json.Marshal but never
// referenced in the template — the field must stay removed.
// (Compile-time: htmlData no longer has DataJSON.)
}

func TestExportHTML_RendersReplayWithoutUnusedJSON(t *testing.T) {
dir := t.TempDir()
out := filepath.Join(dir, "replay.html")
if err := ExportHTML(sampleTrace(), out); err != nil {
t.Fatalf("ExportHTML: %v", err)
}

raw, err := os.ReadFile(out)
if err != nil {
t.Fatalf("read output: %v", err)
}
html := string(raw)

for _, want := range []string{
"LoopRelay",
"sess-42",
"claude",
"Iteration 1",
"Iteration 2",
"src/main.go",
"diff-add",
"status-pass",
"ok &lt;script&gt;alert(1)&lt;/script&gt;",
} {
if !strings.Contains(html, want) {
t.Errorf("exported HTML missing %q", want)
}
}

// Ensure we did not reintroduce a dumped iterations JSON blob for JS.
if strings.Contains(html, `"ActionType"`) || strings.Contains(html, `"PhaseClass"`) {
t.Error("exported HTML unexpectedly embeds marshaled iteration JSON (DataJSON dead code)")
}
if strings.Contains(html, "<script>alert(1)</script>") {
t.Error("verification output was not HTML-escaped")
}
}

func TestPhaseAndStatusClass(t *testing.T) {
if got := phaseClass("act"); got != "phase-act" {
t.Errorf("phaseClass(act)=%q", got)
}
if got := phaseClass("unknown"); got != "phase-other" {
t.Errorf("phaseClass(unknown)=%q", got)
}
if got := statusClass("success"); got != "status-pass" {
t.Errorf("statusClass(success)=%q", got)
}
if got := statusClass("fail"); got != "status-fail" {
t.Errorf("statusClass(fail)=%q", got)
}
}

func TestRenderDiffHTML(t *testing.T) {
got := renderDiffHTML("+added\n-removed\n context")
if !strings.Contains(got, `class="diff-add"`) || !strings.Contains(got, "+added") {
t.Fatalf("missing add span: %s", got)
}
if !strings.Contains(got, `class="diff-del"`) || !strings.Contains(got, "-removed") {
t.Fatalf("missing del span: %s", got)
}
}