f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
162 lines
4.2 KiB
Go
162 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// fakeClock returns a now func that advances only via the returned
|
|
// step function, keeping throttle tests deterministic.
|
|
func fakeClock(start time.Time) (now func() time.Time, step func(time.Duration)) {
|
|
current := start
|
|
return func() time.Time { return current },
|
|
func(d time.Duration) { current = current.Add(d) }
|
|
}
|
|
|
|
func TestStartupStateRoundTrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
base := time.Date(2026, 7, 2, 22, 0, 0, 0, time.UTC)
|
|
now, _ := fakeClock(base)
|
|
|
|
w := newStartupStateWriter(dir, now)
|
|
w.SetPhase("opening database")
|
|
|
|
st := readStartupState(dir)
|
|
require.NotNil(t, st, "state must be readable after SetPhase")
|
|
assert.Equal(t, os.Getpid(), st.PID)
|
|
assert.Equal(t, "opening database", st.Phase)
|
|
assert.Empty(t, st.Detail)
|
|
assert.True(t, st.StartedAt.Equal(base), "started_at = %v", st.StartedAt)
|
|
assert.True(t, st.UpdatedAt.Equal(base), "updated_at = %v", st.UpdatedAt)
|
|
}
|
|
|
|
func TestStartupStateDetailThrottle(t *testing.T) {
|
|
dir := t.TempDir()
|
|
now, step := fakeClock(time.Date(2026, 7, 2, 22, 0, 0, 0, time.UTC))
|
|
|
|
w := newStartupStateWriter(dir, now)
|
|
w.SetPhase("full resync")
|
|
|
|
// Within the throttle window: not persisted.
|
|
w.SetDetail("1/100 sessions")
|
|
st := readStartupState(dir)
|
|
require.NotNil(t, st)
|
|
assert.Empty(t, st.Detail, "detail must not persist inside the throttle window")
|
|
|
|
// The same stable detail must persist once the window passes
|
|
// (regression: a throttled detail was stored in memory and then
|
|
// deduplicated against itself forever).
|
|
step(startupDetailThrottle)
|
|
w.SetDetail("1/100 sessions")
|
|
st = readStartupState(dir)
|
|
require.NotNil(t, st)
|
|
assert.Equal(t, "1/100 sessions", st.Detail)
|
|
|
|
// Past the window the next detail persists.
|
|
step(startupDetailThrottle)
|
|
w.SetDetail("50/100 sessions")
|
|
st = readStartupState(dir)
|
|
require.NotNil(t, st)
|
|
assert.Equal(t, "50/100 sessions", st.Detail)
|
|
|
|
// A phase change bypasses the throttle and clears the detail.
|
|
w.SetDetail("60/100 sessions")
|
|
w.SetPhase("starting HTTP server")
|
|
st = readStartupState(dir)
|
|
require.NotNil(t, st)
|
|
assert.Equal(t, "starting HTTP server", st.Phase)
|
|
assert.Empty(t, st.Detail)
|
|
}
|
|
|
|
func TestReadStartupStateMissingOrCorrupt(t *testing.T) {
|
|
dir := t.TempDir()
|
|
assert.Nil(t, readStartupState(dir), "missing file must read as nil")
|
|
|
|
require.NoError(t, os.WriteFile(
|
|
startupStatePath(dir), []byte("{not json"), 0o600,
|
|
))
|
|
assert.Nil(t, readStartupState(dir), "corrupt file must read as nil")
|
|
}
|
|
|
|
func TestRemoveStartupState(t *testing.T) {
|
|
dir := t.TempDir()
|
|
now, _ := fakeClock(time.Now())
|
|
w := newStartupStateWriter(dir, now)
|
|
w.SetPhase("opening database")
|
|
require.NotNil(t, readStartupState(dir))
|
|
|
|
removeStartupState(dir)
|
|
assert.Nil(t, readStartupState(dir))
|
|
assert.NoFileExists(t, startupStatePath(dir))
|
|
}
|
|
|
|
func TestStartupStateWriterNilReceiver(t *testing.T) {
|
|
var w *startupStateWriter
|
|
assert.NotPanics(t, func() {
|
|
w.SetPhase("opening database")
|
|
w.SetDetail("1/2 sessions")
|
|
})
|
|
}
|
|
|
|
func TestServeStartingStatusLines(t *testing.T) {
|
|
base := time.Date(2026, 7, 2, 22, 0, 0, 0, time.UTC)
|
|
now := base.Add(72 * time.Second)
|
|
logPath := filepath.Join("data", "serve.log")
|
|
|
|
tests := []struct {
|
|
name string
|
|
st *startupState
|
|
want []string
|
|
}{
|
|
{
|
|
name: "nil state",
|
|
st: nil,
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "full state",
|
|
st: &startupState{
|
|
PID: 48151,
|
|
StartedAt: base,
|
|
Phase: "full resync",
|
|
Detail: "claude: 12/38 sessions (32%)",
|
|
LogPath: logPath,
|
|
},
|
|
want: []string{
|
|
" pid: 48151",
|
|
" elapsed: 1m12s",
|
|
" phase: full resync: claude: 12/38 sessions (32%)",
|
|
" log: " + logPath,
|
|
},
|
|
},
|
|
{
|
|
name: "no detail no log",
|
|
st: &startupState{
|
|
PID: 7,
|
|
StartedAt: base,
|
|
Phase: "opening database",
|
|
},
|
|
want: []string{
|
|
" pid: 7",
|
|
" elapsed: 1m12s",
|
|
" phase: opening database",
|
|
},
|
|
},
|
|
{
|
|
name: "zero fields render nothing",
|
|
st: &startupState{},
|
|
want: nil,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, serveStartingStatusLines(tt.st, now))
|
|
})
|
|
}
|
|
}
|