package web
import (
"bytes"
"context"
"log"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/obs"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/timing"
)
type slowPageIn struct{}
type slowPageOut struct{}
// Op timing moved from the page helper to the registry. A page that
// calls an op must still be filed once, with the op's time inside it,
// and the op must be timed and logged once — not by both layers.
func TestAWebRenderIsRecordedOnceWithItsSurface(t *testing.T) {
reg := ops.NewRegistry()
if err := ops.Register(reg, ops.Op[slowPageIn, slowPageOut]{
Name: "test.slowpage", Doc: "sleeps past the slow-op threshold",
Kind: ops.Query, Auth: ops.Anonymous, Route: "GET /api/v1/test/slowpage",
Run: func(context.Context, slowPageIn) (slowPageOut, error) {
time.Sleep(300 * time.Millisecond)
return slowPageOut{}, nil
},
}); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
prev := log.Writer()
log.SetOutput(&buf)
t.Cleanup(func() { log.SetOutput(prev) })
calls := -1
mux := http.NewServeMux()
mux.HandleFunc("GET /slowpage", func(w http.ResponseWriter, req *http.Request) {
if _, err := invoke(req.Context(), reg, "test.slowpage", nil); err != nil {
t.Error(err)
}
calls = timing.From(req.Context()).Calls(timing.Ops)
})
rec := obs.NewRecorder(16, nil)
withTimerFor(rec, mux, nil, nil, mux).ServeHTTP(httptest.NewRecorder(),
httptest.NewRequest("GET", "/slowpage", nil))
if calls != 1 {
t.Errorf("the op was timed %d times, want 1", calls)
}
rs := rec.Snapshot().Renders
if len(rs) != 1 {
t.Fatalf("%d renders filed, want 1", len(rs))
}
if rs[0].Surface != obs.SurfaceWeb {
t.Errorf("surface = %q, want %q", rs[0].Surface, obs.SurfaceWeb)
}
if rs[0].Ops < 250*time.Millisecond {
t.Errorf("render ops time %s, want the op's own time inside it", rs[0].Ops)
}
var slow []string
for _, l := range strings.Split(buf.String(), "\n") {
if strings.Contains(l, "slow op: test.slowpage ") {
slow = append(slow, l)
}
}
if len(slow) != 1 {
t.Fatalf("%d slow-op lines, want 1:\n%s", len(slow), buf.String())
}
if !strings.Contains(slow[0], "surface=web") {
t.Errorf("slow-op line does not name the web surface: %q", slow[0])
}
}
// Both render tables on the operations surface carry the surface of
// each row, so an API call reads as one beside the pages.
func TestOpsTablesShowEachRowsSurface(t *testing.T) {
var live opsView
remarshal(map[string]any{"renders": []map[string]any{
{"at": "2026-09-26T10:00:00Z", "path": "GET /api/v1/r/{repo}/ticket", "surface": "api", "status": 200},
{"at": "2026-09-26T10:00:01Z", "path": "GET /r/{repo}/timeline", "surface": "web", "status": 200},
}}, &live)
var b bytes.Buffer
if err := noHooks.template(opsPage).ExecuteTemplate(&b, "ops-body", pageData{Ops: live}); err != nil {
t.Fatal(err)
}
checkSurfaceCells(t, "live renders", b.String())
var hist opsHistoryView
remarshal(map[string]any{
"store": map[string]any{"configured": true},
"rows": []map[string]any{
{"at": "2026-09-26 10:00:00", "path": "GET /api/v1/r/{repo}/ticket", "surface": "api", "status": 200},
{"at": "2026-09-26 10:00:01", "path": "GET /r/{repo}/timeline", "surface": "web", "status": 200},
}}, &hist)
b.Reset()
if err := noHooks.template(opsHistoryPage).Execute(&b, pageData{OpsHistory: hist, FleetTab: "ops"}); err != nil {
t.Fatal(err)
}
checkSurfaceCells(t, "history", b.String())
}
func checkSurfaceCells(t *testing.T, table, html string) {
t.Helper()
if n := strings.Count(html, `data-hook="ops-surface"`); n != 2 {
t.Errorf("%s: %d surface cells, want one per row (2)", table, n)
}
for _, s := range []string{"api", "web"} {
if !strings.Contains(html, `data-surface="`+s+`"`) {
t.Errorf("%s: no row tagged surface %s", table, s)
}
}
}
// The recent renders can be narrowed to one surface. The control rides
// the page's own form, and the live refresh asks with the page's query
// so a tick does not reset it.
func TestOpsRecentRendersHaveASurfaceFilter(t *testing.T) {
var b bytes.Buffer
if err := noHooks.template(opsPage).Execute(&b, pageData{Ops: opsView{Surface: "api"}, FleetTab: "ops"}); err != nil {
t.Fatal(err)
}
html := b.String()
i := strings.Index(html, `data-hook="ops-surface-filter"`)
if i < 0 {
t.Fatal("no surface filter control on the ops page")
}
ctl := html[strings.LastIndex(html[:i], "<select"):]
ctl = ctl[:strings.Index(ctl, "</select>")]
for _, want := range []string{`name="surface"`, `form="ops-controls"`, `value=""`,
`value="web"`, `value="api" selected`} {
if !strings.Contains(ctl, want) {
t.Errorf("surface control lacks %s:\n%s", want, ctl)
}
}
if !strings.Contains(html, `id="ops-controls"`) {
t.Error("the control names a form the page does not have")
}
if !strings.Contains(html, `@get('/fleet/ops.sse' + location.search)`) {
t.Error("the live refresh does not carry the page's query")
}
}