Skip to content
quarry Artifact [2b0b04d1a1]
Login

Artifact [2b0b04d1a1]

Artifact 2b0b04d1a1611d0759aad8d3c52ff046c47e501bc953caa043a6aad4a0b4171a:


package api

import (
	"slices"
	"testing"
)

// repoAt builds a served store in a given condition.
func repoAt(lastOK string, indexing, stalled bool, lastCount, total int, round float64) *Repo {
	return &Repo{
		Indexing: func() bool { return indexing },
		Health: func() RepoHealth {
			return RepoHealth{LastOK: lastOK, Stalled: stalled,
				LastCount: lastCount, Total: total, RoundSeconds: round}
		},
	}
}

// The phase is the only thing on the ops page that is true while a
// fleet is coming up: the panels below it are steady-state numbers and
// read as an empty forge. A multi-repo re-ingest on this fleet was
// mistaken for a hang because nothing said how far along it was.
func TestFleetPhaseNamesTheMoment(t *testing.T) {
	cases := []struct {
		name  string
		repos map[string]*Repo
		want  string
	}{{
		name: "nothing has finished a round yet",
		repos: map[string]*Repo{
			"a": repoAt("", false, false, 0, 0, 0),
			"b": repoAt("2026-01-01T00:00:00Z", false, false, 0, 5, 1),
		},
		want: "starting",
	}, {
		// Rebuilding outranks everything: it is the longest and the
		// most confusing state to sit in without being told.
		name: "a store is rebuilding",
		repos: map[string]*Repo{
			"a": repoAt("2026-01-01T00:00:00Z", true, false, 0, 5, 1),
			"b": repoAt("2026-01-01T00:00:00Z", false, false, 0, 5, 1),
		},
		want: "indexing",
	}, {
		name: "rounds are moving artifacts",
		repos: map[string]*Repo{
			"a": repoAt("2026-01-01T00:00:00Z", false, false, 12, 90, 2),
			"b": repoAt("2026-01-01T00:00:00Z", false, false, 0, 5, 1),
		},
		want: "syncing",
	}, {
		name: "everything is up and quiet",
		repos: map[string]*Repo{
			"a": repoAt("2026-01-01T00:00:00Z", false, false, 0, 90, 1),
		},
		want: "rest",
	}}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := fleetPhase(Deps{Repos: NewRepos(tc.repos)})
			if got.Name != tc.want {
				t.Errorf("phase = %q, want %q", got.Name, tc.want)
			}
		})
	}
}

// A stalled store has stopped retrying: it is waiting for a person, not
// for time. Counting it as pending would hold the fleet in "starting"
// forever and leave a progress bar that never fills.
func TestStalledStoresDoNotHoldTheFleetInStarting(t *testing.T) {
	p := fleetPhase(Deps{Repos: NewRepos(map[string]*Repo{
		"good": repoAt("2026-01-01T00:00:00Z", false, false, 0, 10, 1),
		"dead": repoAt("", false, true, 0, 0, 0),
	})})
	if p.Name != "rest" {
		t.Errorf("phase = %q, want rest — a stalled store is done waiting, not pending", p.Name)
	}
	if !slices.Contains(p.Stalled, "dead") {
		t.Errorf("stalled = %v, want it to name the store", p.Stalled)
	}
	if p.Ready != 1 || p.Repos != 2 {
		t.Errorf("ready/repos = %d/%d, want 1/2 — a stalled store is not ready either", p.Ready, p.Repos)
	}
}

// The numbers the banner reads: totals summed, worst round taken, and
// the working list named rather than merely counted.
func TestFleetPhaseRollsUpTheNumbers(t *testing.T) {
	p := fleetPhase(Deps{Repos: NewRepos(map[string]*Repo{
		"a": repoAt("2026-01-01T00:00:00Z", true, false, 1, 100, 0.5),
		"b": repoAt("2026-01-01T00:00:00Z", false, false, 0, 250, 9.5),
	})})
	if p.Artifacts != 350 {
		t.Errorf("artifacts = %d, want 350 summed across stores", p.Artifacts)
	}
	if p.SlowestRoundSeconds != 9.5 {
		t.Errorf("slowest round = %v, want the worst not the last", p.SlowestRoundSeconds)
	}
	if !slices.Contains(p.Working, "a") || len(p.Working) != 1 {
		t.Errorf("working = %v, want exactly the rebuilding store", p.Working)
	}
}

// An empty forge must not divide by zero or claim to be starting.
func TestFleetPhaseWithNoStores(t *testing.T) {
	if p := fleetPhase(Deps{}); p.Name != "rest" || p.Repos != 0 {
		t.Errorf("phase = %+v, want a quiet empty fleet", p)
	}
}