mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-03 11:37:09 +00:00
feat(trace): add WriteHTML + Step.HTMLAvailable
Per-step HTML lives in <run>/html/step-NNNNN.html so trace.jsonl stays line-greppable on apps with hundreds-of-KB DOMs.
This commit is contained in:
1 parent
fa0c82fb40
commit
b6a6aeff5f
2 files changed
+94
-10
No files matched your search
+35
-10
@@ -13,16 +13,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Step struct {
|
type Step struct {
|
||||||
Index int `json:"step"`
|
Index int `json:"step"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
Screen string `json:"screen,omitempty"`
|
Screen string `json:"screen,omitempty"`
|
||||||
Snapshots map[string]json.RawMessage `json:"snapshots,omitempty"`
|
Snapshots map[string]json.RawMessage `json:"snapshots,omitempty"`
|
||||||
Action *Action `json:"action,omitempty"`
|
Action *Action `json:"action,omitempty"`
|
||||||
Exceptions []Exception `json:"exceptions,omitempty"`
|
Exceptions []Exception `json:"exceptions,omitempty"`
|
||||||
Violations []string `json:"violations,omitempty"`
|
Violations []string `json:"violations,omitempty"`
|
||||||
Hierarchy *hierarchy.Tree `json:"hierarchy,omitempty"`
|
Hierarchy *hierarchy.Tree `json:"hierarchy,omitempty"`
|
||||||
Residuals map[string]json.RawMessage `json:"residuals,omitempty"`
|
Residuals map[string]json.RawMessage `json:"residuals,omitempty"`
|
||||||
Metrics *Metrics `json:"metrics,omitempty"`
|
Metrics *Metrics `json:"metrics,omitempty"`
|
||||||
|
HTMLAvailable bool `json:"html_available,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
@@ -143,6 +144,30 @@ func (w *Writer) writePNG(name string, png []byte) error {
|
|||||||
return os.WriteFile(filepath.Join(directory, name), png, 0o644)
|
return os.WriteFile(filepath.Join(directory, name), png, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteHTML stores the page HTML for `stepIndex` under `<dir>/html/`. Empty
|
||||||
|
// payloads are skipped so the directory only exists when there is real
|
||||||
|
// content to render in the inspect UI.
|
||||||
|
func (w *Writer) WriteHTML(stepIndex int, html []byte) error {
|
||||||
|
return w.writeHTML(fmt.Sprintf("step-%05d.html", stepIndex), html)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHTMLAfter writes the post-action HTML, paired with the post-action
|
||||||
|
// screenshot.
|
||||||
|
func (w *Writer) WriteHTMLAfter(stepIndex int, html []byte) error {
|
||||||
|
return w.writeHTML(fmt.Sprintf("step-%05d-after.html", stepIndex), html)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) writeHTML(name string, html []byte) error {
|
||||||
|
if len(html) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
directory := filepath.Join(w.directory, "html")
|
||||||
|
if err := os.MkdirAll(directory, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("mkdir html: %w", err)
|
||||||
|
}
|
||||||
|
return os.WriteFile(filepath.Join(directory, name), html, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
func (w *Writer) Close() error {
|
func (w *Writer) Close() error {
|
||||||
w.mutex.Lock()
|
w.mutex.Lock()
|
||||||
defer w.mutex.Unlock()
|
defer w.mutex.Unlock()
|
||||||
|
|||||||
@@ -269,6 +269,65 @@ func TestWriteAfterClose_Errors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWriteHTML_WritesToHTMLDir(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
writer, _ := NewWriter(directory)
|
||||||
|
defer writer.Close()
|
||||||
|
|
||||||
|
html := []byte("<!doctype html><html><body>hi</body></html>")
|
||||||
|
if err := writer.WriteHTML(3, html); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := writer.WriteHTMLAfter(3, html); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := os.ReadFile(filepath.Join(directory, "html", "step-00003.html"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("step-00003 missing: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != string(html) {
|
||||||
|
t.Errorf("html bytes wrong: %q", got)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(directory, "html", "step-00003-after.html")); err != nil {
|
||||||
|
t.Errorf("step-00003-after missing: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteHTML_EmptyByteSliceIsNoop(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
writer, _ := NewWriter(directory)
|
||||||
|
defer writer.Close()
|
||||||
|
|
||||||
|
if err := writer.WriteHTML(1, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(directory, "html")); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("html dir should not exist after empty write")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStep_HTMLAvailableRoundTrip(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
writer, _ := NewWriter(directory)
|
||||||
|
defer writer.Close()
|
||||||
|
|
||||||
|
if err := writer.WriteStep(Step{Index: 1, HTMLAvailable: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := writer.WriteStep(Step{Index: 2}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body, _ := os.ReadFile(filepath.Join(directory, "trace.jsonl"))
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(body)), "\n")
|
||||||
|
if !strings.Contains(lines[0], `"html_available":true`) {
|
||||||
|
t.Errorf("html_available missing in step 1: %s", lines[0])
|
||||||
|
}
|
||||||
|
if strings.Contains(lines[1], "html_available") {
|
||||||
|
t.Errorf("html_available should be omitted when false: %s", lines[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewWriter_CreatesNestedDirectory(t *testing.T) {
|
func TestNewWriter_CreatesNestedDirectory(t *testing.T) {
|
||||||
parent := t.TempDir()
|
parent := t.TempDir()
|
||||||
target := filepath.Join(parent, "runs", "2026-04-17T22-30-00")
|
target := filepath.Join(parent, "runs", "2026-04-17T22-30-00")
|
||||||
|
|||||||
Reference in new issue
Block a user