Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/luci/internal/web/web_test.go

Raw
package web

import (
	"bytes"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"bugabinga.net/luci/internal/ciconfig"
	"bugabinga.net/luci/internal/history"
	"bugabinga.net/luci/internal/logs"
	"bugabinga.net/luci/internal/proquint"
	"bugabinga.net/luci/internal/runstate"
)

func TestDashboardPrioritizesActionableState(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	now := time.Now().UTC()
	store := history.Store{DataDir: dataDir, Now: func() time.Time { return now }}
	_, _ = store.Append(history.Event{RunID: "failed-run", ChildID: "run", Repo: "broken", Job: "push", Rev: "111111111", Ref: "refs/heads/trunk", Trigger: "push", Status: "failed", Time: now.Add(-time.Minute)})
	_, _ = store.Append(history.Event{RunID: "success-run", ChildID: "run", Repo: "smith", Job: "smoke", Rev: "8ae9c890afefe8206a1e9b4e9760e6f40097d9a6", Ref: "refs/heads/trunk", Trigger: "manual", Status: "success", StartedAt: now.Add(-20 * time.Second), Time: now.Add(-2 * time.Second)})
	if err := (runstate.Store{DataDir: dataDir}).Write(runstate.Active{RunID: "active-run", ChildID: "001", Repo: "running", Job: "test", Rev: "abcdef123", Ref: "refs/heads/trunk", Trigger: "push", StartedAt: now.Add(-5 * time.Second)}); err != nil {
		t.Fatal(err)
	}

	res := httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/", nil))
	body := res.Body.String()
	if res.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", res.Code, body)
	}
	for _, want := range []string{"NEEDS ATTENTION", "IN PROGRESS", "HEALTHY", "Needs attention", "In progress", "Recent successful runs", "broken", "running", "smith", "8ae9", "18s"} {
		if !strings.Contains(body, want) {
			t.Fatalf("missing %q: %s", want, body)
		}
	}
	if strings.Contains(body, "<table") {
		t.Fatalf("dashboard table: %s", body)
	}
	if strings.Count(body, "failed-run") != 1 || !strings.Contains(body, `href="/runs/failed-run"`) {
		t.Fatalf("run ID exposed: %s", body)
	}
}

func TestDashboardWarnsAboutMalformedActiveRecord(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	if err := os.WriteFile(filepath.Join(dataDir, "active", "bad.json"), []byte("{"), 0o644); err != nil {
		t.Fatal(err)
	}
	res := httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/", nil))
	if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), "State warning") {
		t.Fatalf("status=%d body=%s", res.Code, res.Body.String())
	}
}

func TestRepoAndRunPreserveStateWarnings(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	_, _ = (history.Store{DataDir: dataDir}).Append(history.Event{RunID: "42", ChildID: "run", Repo: "repo", Job: "check", Status: "success"})
	if err := os.WriteFile(filepath.Join(dataDir, "active", "bad.json"), []byte("{"), 0o644); err != nil {
		t.Fatal(err)
	}
	handler := Handler(dataDir, inboxDir, "", nil)
	for _, path := range []string{"/repos/repo", "/runs/42"} {
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, path, nil))
		if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), "State warning") {
			t.Fatalf("%s status=%d body=%s", path, res.Code, res.Body.String())
		}
	}
}

func TestRunPageShowsMetadataInlineTailAndRawLog(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	now := time.Now().UTC()
	store := history.Store{DataDir: dataDir, Now: func() time.Time { return now }}
	_, _ = store.Append(history.Event{RunID: "42", ChildID: "001", Repo: "smith", Job: "smoke", Rev: "8ae9c890", Ref: "refs/heads/trunk", Trigger: "push", Status: "success", StartedAt: now.Add(-time.Second), Time: now})
	_, _ = store.Append(history.Event{RunID: "42", ChildID: "run", Repo: "smith", Job: "push", Rev: "8ae9c890", Ref: "refs/heads/trunk", Trigger: "push", Status: "success", StartedAt: now.Add(-time.Second), Time: now})
	logStore := logs.Store{DataDir: dataDir}
	file, err := logStore.CreateLive("42-001")
	if err != nil {
		t.Fatal(err)
	}
	_, _ = file.WriteString(strings.Repeat("x", 256<<10) + "readable end\n")
	_ = file.Close()
	if _, err := logStore.Finalize("42-001"); err != nil {
		t.Fatal(err)
	}
	handler := Handler(dataDir, inboxDir, "", nil)

	res := httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/42", nil))
	body := res.Body.String()
	if res.Code != http.StatusOK || !strings.Contains(body, "Run 42") || !strings.Contains(body, "8ae9") || !strings.Contains(body, "Started") || !strings.Contains(body, "Finished") || !strings.Contains(body, "readable end") || !strings.Contains(body, "[earlier output truncated]") || !strings.Contains(body, "/runs/42/logs/001") {
		t.Fatalf("status=%d body=%s", res.Code, body)
	}

	res = httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/42/logs/001", nil))
	if res.Code != http.StatusOK || res.Header().Get("Content-Type") != "text/plain; charset=utf-8" || !strings.HasSuffix(res.Body.String(), "readable end\n") {
		t.Fatalf("status=%d type=%q body tail=%q", res.Code, res.Header().Get("Content-Type"), res.Body.String()[max(0, res.Body.Len()-30):])
	}
}

func TestArtifactRouteStreamsImmutableBytesWithSafeHeaders(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	if _, err := (history.Store{DataDir: dataDir}).Append(history.Event{RunID: "42", ChildID: "run", Repo: "repo", Job: "check", Status: "success"}); err != nil {
		t.Fatal(err)
	}
	path := filepath.Join(dataDir, "artifacts", "42", "001", "build", "result.html")
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, []byte("<script>alert(1)</script>"), 0o644); err != nil {
		t.Fatal(err)
	}
	stage := filepath.Join(dataDir, "artifacts", "42", "001", ".stage-copy", "private.txt")
	if err := os.MkdirAll(filepath.Dir(stage), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(stage, []byte("private"), 0o600); err != nil {
		t.Fatal(err)
	}
	handler := Handler(dataDir, inboxDir, "", nil)
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/42/artifacts/001/build/result.html", nil))
	if res.Code != http.StatusOK || res.Body.String() != "<script>alert(1)</script>" || res.Header().Get("X-Content-Type-Options") != "nosniff" || res.Header().Get("Content-Security-Policy") != "sandbox" || res.Header().Get("Content-Disposition") != "attachment" || res.Header().Get("Cache-Control") != "public, max-age=31536000, immutable" {
		t.Fatalf("status=%d headers=%v body=%q", res.Code, res.Header(), res.Body.String())
	}
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/42/artifacts/001/.stage-copy/private.txt", nil))
	if res.Code != http.StatusNotFound {
		t.Fatalf("staged=%d", res.Code)
	}
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/42/artifacts/%2e%2e/secrets", nil))
	if res.Code != http.StatusNotFound {
		t.Fatalf("traversal=%d", res.Code)
	}
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/42/artifacts", nil))
	if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), "001/build/result.html\\t25") || strings.Contains(res.Body.String(), "private") {
		t.Fatalf("list=%d body=%q", res.Code, res.Body.String())
	}
}

func TestRunAndLogRoutesResolveOnlyUniqueProquints(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	now := time.Now().UTC()
	store := history.Store{DataDir: dataDir, Now: func() time.Time { return now }}
	for _, id := range []string{"42", "281474976710656"} {
		if _, err := store.Append(history.Event{RunID: id, ChildID: "run", Repo: "repo", Job: "check", Status: "success", Time: now}); err != nil {
			t.Fatal(err)
		}
	}
	if _, err := store.Append(history.Event{RunID: "42", ChildID: "001", Repo: "repo", Job: "check", Status: "success", Time: now}); err != nil {
		t.Fatal(err)
	}
	logStore := logs.Store{DataDir: dataDir}
	file, err := logStore.CreateLive("42-001")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := file.WriteString("resolved log\n"); err != nil {
		t.Fatal(err)
	}
	if err := file.Close(); err != nil {
		t.Fatal(err)
	}
	if _, err := logStore.Finalize("42-001"); err != nil {
		t.Fatal(err)
	}
	handler := Handler(dataDir, inboxDir, "", nil)
	full, prefix := proquint.Encode(42), proquint.Encode(42)[:11]
	for _, path := range []string{"/runs/" + full, "/runs/" + prefix} {
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, path, nil))
		if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), "Run 42") {
			t.Fatalf("%s status=%d body=%s", path, res.Code, res.Body.String())
		}
	}
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/"+prefix+"/logs/001", nil))
	if res.Code != http.StatusOK || res.Body.String() != "resolved log\n" {
		t.Fatalf("resolved log status=%d body=%q", res.Code, res.Body.String())
	}
	if _, err := store.Append(history.Event{RunID: "43", ChildID: "run", Repo: "repo", Job: "check", Status: "success", Time: now}); err != nil {
		t.Fatal(err)
	}
	for _, path := range []string{
		"/runs/" + proquint.Encode(99),
		"/runs/" + proquint.Encode(99) + "/logs/001",
		"/runs/" + full[:5],
		"/runs/" + full[:5] + "/logs/001",
	} {
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, path, nil))
		if res.Code != http.StatusNotFound {
			t.Fatalf("%s status=%d body=%s", path, res.Code, res.Body.String())
		}
	}
}

func TestRunPageShowsDirectKDLFilesOnly(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	repoRoot := t.TempDir()
	work := filepath.Join(repoRoot, "work")
	repos := filepath.Join(repoRoot, "repos")
	webGit(t, repoRoot, "init", work)
	webGit(t, work, "config", "user.email", "test@example.invalid")
	webGit(t, work, "config", "user.name", "Test")
	for path, content := range map[string]string{
		".ci/ci.kdl":        "direct-kdl",
		".ci/nested/ci.kdl": "nested-kdl",
		".ci/readme.txt":    "non-kdl",
	} {
		fullPath := filepath.Join(work, path)
		if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(fullPath, []byte(content), 0o644); err != nil {
			t.Fatal(err)
		}
	}
	webGit(t, work, "add", ".")
	webGit(t, work, "commit", "-m", "configuration")
	rev := strings.TrimSpace(webGit(t, work, "rev-parse", "HEAD"))
	if err := os.MkdirAll(repos, 0o755); err != nil {
		t.Fatal(err)
	}
	webGit(t, repoRoot, "clone", "--bare", work, filepath.Join(repos, "repo.git"))
	if _, err := (history.Store{DataDir: dataDir}).Append(history.Event{RunID: "42", ChildID: "run", Repo: "repo", Job: "check", Rev: rev, Status: "success"}); err != nil {
		t.Fatal(err)
	}
	res := httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", []string{repos}).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/42", nil))
	body := res.Body.String()
	if res.Code != http.StatusOK || !strings.Contains(body, "direct-kdl") || !strings.Contains(body, "Configuration invalid") || strings.Contains(body, "nested-kdl") || strings.Contains(body, "non-kdl") {
		t.Fatalf("status=%d body=%s", res.Code, body)
	}
}

func TestRunPageShowsSubjectQueueWaitAndSteps(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	now := time.Now().UTC()
	store := history.Store{DataDir: dataDir, Now: func() time.Time { return now }}
	_, _ = store.Append(history.Event{RunID: "7", ChildID: "run", Repo: "smith", Job: "push", Rev: "8ae9c890afefe8206a1e9b4e9760e6f40097d9a6", Ref: "refs/heads/trunk", Trigger: "push", Status: "success", Subject: "fix: rebuild cache keys", StartedAt: now.Add(-40 * time.Minute), Time: now.Add(-time.Minute)})
	_, _ = store.Append(history.Event{RunID: "7", ChildID: "001", Repo: "smith", Job: "check", Rev: "8ae9c890afefe8206a1e9b4e9760e6f40097d9a6", Ref: "refs/heads/trunk", Trigger: "push", Status: "success", Subject: "fix: rebuild cache keys", StartedAt: now.Add(-31 * time.Minute), Time: now.Add(-2 * time.Minute)})
	logStore := logs.Store{DataDir: dataDir}
	file, err := logStore.CreateLive("7-001")
	if err != nil {
		t.Fatal(err)
	}
	_, _ = file.WriteString("[luci] cache target: fallback\n[luci] step 1/2: cargo test\n[luci] step 1/2 done in 41s\n[luci] step 2/2: cargo clippy\n")
	_ = file.Close()
	if _, err := logStore.Finalize("7-001"); err != nil {
		t.Fatal(err)
	}
	res := httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/7", nil))
	body := res.Body.String()
	for _, want := range []string{"fix: rebuild cache keys", "queued 9m", "step timing", "cargo clippy", "41s", "running…"} {
		if !strings.Contains(body, want) {
			t.Fatalf("missing %q in body", want)
		}
	}
}

func TestRawLogMissingAndCorruptResponses(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	now := time.Now().UTC()
	_, _ = (history.Store{DataDir: dataDir}).Append(history.Event{RunID: "bad", ChildID: "001", Repo: "repo", Job: "check", Status: "failed", Time: now})
	if err := os.WriteFile(filepath.Join(dataDir, "logs", "bad-001.log.zst"), []byte("bad zstd"), 0o644); err != nil {
		t.Fatal(err)
	}
	handler := Handler(dataDir, inboxDir, "", nil)
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/missing/logs/001", nil))
	if res.Code != http.StatusNotFound {
		t.Fatalf("missing=%d", res.Code)
	}
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/bad/logs/001", nil))
	if res.Code != http.StatusInternalServerError || res.Body.String() != "internal server error\n" {
		t.Fatalf("corrupt=%d body=%s", res.Code, res.Body.String())
	}
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/bad", nil))
	if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), "Log unavailable") || strings.Contains(res.Body.String(), "invalid input") || !strings.Contains(res.Body.String(), "repo") {
		t.Fatalf("run=%d body=%s", res.Code, res.Body.String())
	}
}

func TestPageCSSUsesAdaptiveNuguPalette(t *testing.T) {
	for _, pattern := range []string{
		`--raised\s*:\s*var\s*\(\s*--nugu-ui-backdrop\s*,\s*#dbdbdb\s*\)`,
		`--cyan\s*:\s*var\s*\(\s*--nugu-info\s*,\s*#116477\s*\)`,
		`@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)`,
		`--bg\s*:\s*var\s*\(\s*--nugu-content-backdrop\s*,\s*#121212\s*\)`,
		`--raised\s*:\s*var\s*\(\s*--nugu-ui-backdrop\s*,\s*#242424\s*\)`,
		`--cyan\s*:\s*var\s*\(\s*--nugu-info\s*,\s*#3ec5e3\s*\)`,
	} {
		if !cssMatch(pageCSS, pattern) {
			t.Fatalf("page CSS missing pattern %q", pattern)
		}
	}
	if strings.Contains(pageCSS, "--nugu-content-link") || cssMatch(pageCSS, `--raised\s*:\s*var\s*\(\s*--nugu-ui-focus`) {
		t.Fatal("page CSS contains invalid palette role")
	}
}

func TestResponsiveAndAccessibilityContracts(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	res := httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/", nil))
	body := res.Body.String()
	if !strings.Contains(body, `name="viewport"`) {
		t.Fatal("viewport metadata missing")
	}
	css := embeddedPageCSS(t, body)
	for _, pattern := range []string{
		`@media\s*\(\s*max-width\s*:\s*700px\s*\)`,
		`min-height\s*:\s*44px`,
		`min-height\s*:\s*24px`,
		`\.run-row:focus-visible`,
		`white-space\s*:\s*pre-wrap`,
		`overflow-wrap\s*:\s*anywhere`,
		`max-height\s*:\s*60vh`,
		`(?:^|[{}])\s*\.facts\s+span\s*\{\s*min-width\s*:\s*0\s*;\s*overflow-wrap\s*:\s*anywhere\s*;?\s*\}`,
	} {
		if !cssMatch(css, pattern) {
			t.Fatalf("page CSS missing pattern %q", pattern)
		}
	}
}

func TestRepoDocsThemeAndNotFound(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	_, _ = (history.Store{DataDir: dataDir}).Append(history.Event{RunID: "run1", ChildID: "run", Repo: "repo", Job: "check", Status: "success"})
	theme := filepath.Join(dataDir, "theme.css")
	if err := os.WriteFile(theme, []byte("body{color:red}"), 0o644); err != nil {
		t.Fatal(err)
	}
	handler := Handler(dataDir, inboxDir, theme, nil)
	for path, want := range map[string]string{"/repos/repo": "repo", "/repos/missing": "No runs", "/runs/missing": "Unknown run", "/docs": "Security model"} {
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, path, nil))
		if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), want) {
			t.Fatalf("%s status=%d body=%s", path, res.Code, res.Body.String())
		}
	}
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/theme.css", nil))
	if res.Code != http.StatusOK || res.Body.String() != "body{color:red}" {
		t.Fatalf("theme=%d %q", res.Code, res.Body.String())
	}
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/missing", nil))
	if res.Code != http.StatusNotFound {
		t.Fatalf("missing=%d", res.Code)
	}
	res = httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/", nil))
	if strings.Contains(res.Body.String(), `href="/theme.css"`) {
		t.Fatal("disabled theme endpoint was loaded")
	}
	res = httptest.NewRecorder()
	Handler(dataDir, inboxDir, filepath.Join(dataDir, "missing-theme.css"), nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/", nil))
	if strings.Contains(res.Body.String(), `href="/theme.css"`) {
		t.Fatal("unavailable theme endpoint was loaded")
	}
}

func TestLLMsReferenceIsPlainTextAndSetupNeutral(t *testing.T) {
	res := httptest.NewRecorder()
	Handler("[", "[", "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/llms.txt", nil))
	body := res.Body.String()
	if res.Code != http.StatusOK || res.Header().Get("Content-Type") != "text/plain; charset=utf-8" {
		t.Fatalf("status=%d type=%q", res.Code, res.Header().Get("Content-Type"))
	}
	for _, want := range []string{"# Luci CI", "schedule \"0 3 * * *\"", "include \"src/**\"", "manual-only", "15 minutes", "Git metadata"} {
		if !strings.Contains(body, want) {
			t.Fatalf("missing %q", want)
		}
	}
	for _, unwanted := range []string{"ci.bugabinga.net", "root@", "2223", "def ci"} {
		if strings.Contains(body, unwanted) {
			t.Fatalf("contains setup-specific %q", unwanted)
		}
	}
}

func TestLLMsSSHCommandsDistinguishLocalDaemon(t *testing.T) {
	if !strings.Contains(llmsText, "Public SSH commands are restricted") || !strings.Contains(llmsText, "SSH never permits serving") {
		t.Fatal("llms reference must document restricted SSH commands and local-only serving")
	}
	if strings.Contains(llmsText, "Over SSH the same commands") {
		t.Fatal("llms reference claims unrestricted command parity")
	}
}

func TestLLMsKDLExampleParses(t *testing.T) {
	const marker = "~~~kdl\n"
	start := strings.Index(llmsText, marker)
	if start < 0 {
		t.Fatal("KDL block missing")
	}
	start += len(marker)
	end := strings.Index(llmsText[start:], "\n~~~")
	if end < 0 {
		t.Fatal("KDL block not closed")
	}
	repo := t.TempDir()
	path := filepath.Join(repo, ".ci", "ci.kdl")
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, []byte(llmsText[start:start+end]), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := ciconfig.Load(repo); err != nil {
		t.Fatalf("documented KDL does not parse: %v", err)
	}
}

func TestUnreadableStateReturnsGenericLoggedError(t *testing.T) {
	var output bytes.Buffer
	previous := log.Writer()
	log.SetOutput(&output)
	t.Cleanup(func() { log.SetOutput(previous) })
	res := httptest.NewRecorder()
	Handler("[", t.TempDir(), "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/", nil))
	if res.Code != http.StatusInternalServerError || res.Body.String() != "internal server error\n" {
		t.Fatalf("status=%d body=%s", res.Code, res.Body.String())
	}
	if !strings.Contains(output.String(), "luci web load state:") {
		t.Fatalf("missing server error log: %s", output.String())
	}
}

func TestRunPageConnectsChildrenToRevisionConfiguration(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	repoRoot := t.TempDir()
	work, repos := filepath.Join(repoRoot, "work"), filepath.Join(repoRoot, "repos")
	webGit(t, repoRoot, "init", work)
	webGit(t, work, "config", "user.email", "test@example.invalid")
	webGit(t, work, "config", "user.name", "Test")
	config := `job "other" {
  image "alpine"
  run "echo other"
}
job "test" {
  image "docker.io/library/golang:${go}"
  trigger {
    push {
      branch "trunk"
    }
  }
  matrix {
    go "1.26"
  }
  run "go test ./... ${go}"
}`
	if err := os.MkdirAll(filepath.Join(work, ".ci"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(work, ".ci", "ci.kdl"), []byte(config), 0o644); err != nil {
		t.Fatal(err)
	}
	webGit(t, work, "add", ".")
	webGit(t, work, "commit", "-m", "configuration")
	rev := strings.TrimSpace(webGit(t, work, "rev-parse", "HEAD"))
	if err := os.MkdirAll(repos, 0o755); err != nil {
		t.Fatal(err)
	}
	webGit(t, repoRoot, "clone", "--bare", work, filepath.Join(repos, "repo.git"))
	store := history.Store{DataDir: dataDir}
	for _, event := range []history.Event{
		{RunID: "42", ChildID: "run", Repo: "repo", Job: "push", Rev: rev, Status: "failed", Detail: "parent failed"},
		{RunID: "42", ChildID: "001", Repo: "repo", Job: "test[go=1.26]", Rev: rev, Status: "failed", Matrix: map[string]string{"go": "1.26"}},
		{RunID: "42", ChildID: "002", Repo: "repo", Job: "test[go=1.26]", Rev: rev, Status: "success", Matrix: map[string]string{"go": "1.26"}},
	} {
		if _, err := store.Append(event); err != nil {
			t.Fatal(err)
		}
	}
	snapshot, err := (Server{RepoRoots: []string{repos}}).configAt(runDetailView{Known: true, Parent: runView{Repo: "repo", FullRev: rev}})
	if err != nil || snapshot.Error != "" || snapshot.Jobs["test[go=1.26]"].Image == "" {
		t.Fatalf("config snapshot=%#v err=%v", snapshot, err)
	}
	res := httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", []string{repos}).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/42", nil))
	body := res.Body.String()
	if strings.Contains(body, "Configuration invalid") {
		t.Fatalf("configuration parse failed")
	}
	for _, want := range []string{"Run failed.", "1 failed.", "1 passed.", "Parent failure detail", "Execution configuration", "docker.io/library/golang:1.26", "go test ./... 1.26", "Matrix", "go=1.26", "Trigger", "push", "Job source · .ci/ci.kdl", `job &#34;test&#34;`, "All configuration at this revision"} {
		if !strings.Contains(body, want) {
			t.Fatalf("missing %q in %s", want, body)
		}
	}
	if got := strings.Count(body, "Execution configuration"); got != 2 {
		t.Fatalf("execution configs=%d body=%s", got, body)
	}
}

func TestRunPageShowsQueuedRunningAndSkippedChildCounts(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	store := history.Store{DataDir: dataDir}
	for _, event := range []history.Event{
		{RunID: "state", ChildID: "run", Repo: "repo", Job: "push", Status: "queued"},
		{RunID: "state", ChildID: "001", Repo: "repo", Job: "queued", Status: "queued"},
		{RunID: "state", ChildID: "002", Repo: "repo", Job: "running", Status: "running"},
		{RunID: "state", ChildID: "003", Repo: "repo", Job: "skipped", Status: "skipped"},
	} {
		if _, err := store.Append(event); err != nil {
			t.Fatal(err)
		}
	}
	res := httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/state", nil))
	body := res.Body.String()
	for _, want := range []string{"QUEUED.", "1 queued.", "1 running.", "1 skipped."} {
		if !strings.Contains(body, want) {
			t.Fatalf("missing %q in %s", want, body)
		}
	}
}

func webGit(t *testing.T, dir string, args ...string) string {
	t.Helper()
	cmd := exec.Command("git", args...)
	cmd.Dir = dir
	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("git %v: %v: %s", args, err, output)
	}
	return string(output)
}

func webDirs(t *testing.T) (string, string) {
	t.Helper()
	root := t.TempDir()
	dataDir, inboxDir := filepath.Join(root, "data"), filepath.Join(root, "inbox")
	for _, dir := range []string{filepath.Join(dataDir, "events"), filepath.Join(dataDir, "active"), filepath.Join(dataDir, "live"), filepath.Join(dataDir, "logs"), inboxDir} {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			t.Fatal(err)
		}
	}
	return dataDir, inboxDir
}

func TestLegacyLogMappingQuotesRunIDAndRequiresHistory(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	runID := "literal[1]"
	_, _ = (history.Store{DataDir: dataDir}).Append(history.Event{RunID: runID, Repo: "repo", Job: "check", Status: "success"})
	store := logs.Store{DataDir: dataDir}
	file, err := store.CreateLive(runID + "-check")
	if err != nil {
		t.Fatal(err)
	}
	_, _ = file.WriteString("legacy text")
	_ = file.Close()
	if _, err := store.Finalize(runID + "-check"); err != nil {
		t.Fatal(err)
	}
	handler := Handler(dataDir, inboxDir, "", nil)
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/"+runID, nil))
	if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), "legacy text") || !strings.Contains(res.Body.String(), "check") {
		t.Fatalf("status=%d body=%s", res.Code, res.Body.String())
	}
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/unknown/logs/legacy-001", nil))
	if res.Code != http.StatusNotFound {
		t.Fatalf("unknown legacy=%d", res.Code)
	}
}

func TestLegacySanitizationCollisionIsNotMisattributed(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	now := time.Now().UTC()
	store := history.Store{DataDir: dataDir}
	_, _ = store.Append(history.Event{RunID: "old", Repo: "repo", Job: "a[b]", Status: "failed", Time: now})
	_, _ = store.Append(history.Event{RunID: "old", Repo: "repo", Job: "a_b_", Status: "success", Time: now.Add(time.Second)})
	logStore := logs.Store{DataDir: dataDir}
	file, err := logStore.CreateLive("old-a_b_")
	if err != nil {
		t.Fatal(err)
	}
	_, _ = file.WriteString("ambiguous")
	_ = file.Close()
	_, _ = logStore.Finalize("old-a_b_")
	handler := Handler(dataDir, inboxDir, "", nil)
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/old", nil))
	if res.Code != http.StatusOK || strings.Count(res.Body.String(), "Ambiguous legacy log identity") != 2 {
		t.Fatalf("status=%d body=%s", res.Code, res.Body.String())
	}
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/old/logs/legacy-001", nil))
	if res.Code != http.StatusNotFound {
		t.Fatalf("collision raw=%d", res.Code)
	}
}

func TestHealthyDashboardDoesNotRenderFailureAlarm(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	_, _ = (history.Store{DataDir: dataDir}).Append(history.Event{RunID: "old", Repo: "smith", Job: "smoke", Status: "success", Time: time.Now().UTC()})
	res := httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/", nil))
	body := res.Body.String()
	if !strings.Contains(body, `class="metrics clear"`) {
		t.Fatal("healthy dashboard markup missing")
	}
	css := embeddedPageCSS(t, body)
	for _, pattern := range []string{`\.metrics\.clear`, `(?:^|[{}])\s*\.top\s+a\s*\{\s*min-height\s*:\s*44px\s*;?`} {
		if !cssMatch(css, pattern) {
			t.Fatalf("page CSS missing pattern %q", pattern)
		}
	}
	if strings.Contains(body, `class="metric bad"`) || strings.Contains(body, "unknown · —") || strings.Contains(body, "— · 0s ago") {
		t.Fatalf("healthy dashboard is noisy: %s", body)
	}
}

func TestRunPagePrioritizesFailureAndCopiesCanonicalValues(t *testing.T) {
	dataDir, inboxDir := webDirs(t)
	now := time.Date(2026, 6, 18, 14, 34, 26, 0, time.UTC)
	fullRev := "0674195565099088373bd53507a1eb55f12a95d0"
	store := history.Store{DataDir: dataDir, Now: func() time.Time { return now }}
	_, _ = store.Append(history.Event{RunID: "42", ChildID: "run", Repo: "repo; unsafe", Job: "push", Rev: fullRev, Ref: "refs/heads/trunk", Status: "failed", Detail: "parent failed", StartedAt: now.Add(-2 * time.Minute), Time: now})
	_, _ = store.Append(history.Event{RunID: "42", ChildID: "001", Repo: "repo; unsafe", Job: "test", Rev: fullRev, Ref: "refs/heads/trunk", Status: "failed", Detail: "child failed", Matrix: map[string]string{"go": "1.26"}, StartedAt: now.Add(-time.Minute), Time: now})
	logStore := logs.Store{DataDir: dataDir}
	file, err := logStore.CreateLive("42-001")
	if err != nil {
		t.Fatal(err)
	}
	_, _ = file.WriteString("error: <actual evidence>\n")
	_ = file.Close()
	if _, err := logStore.Finalize("42-001"); err != nil {
		t.Fatal(err)
	}
	res := httptest.NewRecorder()
	Handler(dataDir, inboxDir, "", nil).ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/runs/42", nil))
	body := res.Body.String()
	id := proquint.Encode(42)
	for _, want := range []string{
		"Parent failure detail", "Affected execution", "Actual evidence", "go=1.26",
		`data-raw="` + id + `"`, `data-raw="` + fullRev + `"`, `data-raw="2026-06-18T14:34:26Z"`, `data-raw="120"`, `data-raw="60"`,
		"18 Jun 2026, 14:34 UTC", "luci log", "luci repo", "&#39;repo; unsafe&#39;", id, "001", "Clipboard unavailable. Exact value selected for manual copy.", "event.key==='Escape'", "details[open]", ".scalar:hover",
		"&lt;actual evidence&gt;", "log-fail",
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("missing %q in %s", want, body)
		}
	}
	parent := strings.Index(body, "Parent failure detail")
	variant := strings.Index(body, "Affected execution")
	evidence := strings.Index(body, "Actual evidence")
	if !(parent >= 0 && parent < variant && variant < evidence) {
		t.Fatalf("failure hierarchy is out of order: parent=%d variant=%d evidence=%d", parent, variant, evidence)
	}
	short := strings.Split(id, "-")[0]
	shortButton := `data-raw="` + id + `" data-kind="full run ID" aria-label="Copy full run ID ` + id + `">` + short + `</button>`
	if got := strings.Count(body, shortButton); got != 2 {
		t.Fatalf("short run ID displays=%d body=%s", got, body)
	}
	if strings.Contains(body, "repo; unsafe\" data-") {
		t.Fatalf("unquoted injected command attribute: %s", body)
	}
}