Improve portable launch, forge persistence, and operator auth UX.

Persist build extra_files for Build Manager history, print dashboard login on every start, add libp2p for Mesh P2P forge, defer WebSocket until login, and split devrun.bat from LAUNCH.bat with USB deck auto-detection.
This commit is contained in:
AetherForge
2026-05-31 18:56:43 -07:00
parent 2f528229f2
commit feba06e008
80 changed files with 2897 additions and 675 deletions

View File

@@ -44,6 +44,7 @@ func seedHashrateSample(t *testing.T, d *db.Database, agentID string, ts time.Ti
func TestStartRetentionJobs_NoOpWhenDisabled(t *testing.T) {
d := openTestDB(t)
t.Cleanup(StopRetentionJobs)
StartRetentionJobs(d, t.TempDir(), 0, 0)
// Disabled config must not start a goroutine that mutates data.
time.Sleep(20 * time.Millisecond)
@@ -53,6 +54,7 @@ func TestStartRetentionJobs_RunsImmediately(t *testing.T) {
d := openTestDB(t)
seedHashrateSample(t, d, "a1", time.Now().Add(-48*time.Hour), 100)
t.Cleanup(StopRetentionJobs)
StartRetentionJobs(d, t.TempDir(), 24, 0)
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
@@ -81,8 +83,8 @@ func TestStartRetentionJobs_TickerInterval(t *testing.T) {
runRetentionFn = noopRetention
t.Cleanup(func() {
retentionTickInterval = prev
// StartRetentionJobs has no stop handle; leave a noop so the leaked goroutine is harmless.
runRetentionFn = func(database *db.Database, dataDir string, statsHours, buildDays int) {}
runRetentionFn = runRetention
StopRetentionJobs()
})
StartRetentionJobs(d, t.TempDir(), 1, 0)
@@ -97,6 +99,42 @@ func TestStartRetentionJobs_TickerInterval(t *testing.T) {
t.Fatalf("expected at least 2 retention passes (immediate + tick), got %d", passes)
}
func TestStopRetentionJobs_StopsBackgroundLoop(t *testing.T) {
prev := retentionTickInterval
retentionTickInterval = 40 * time.Millisecond
t.Cleanup(func() {
retentionTickInterval = prev
runRetentionFn = runRetention
StopRetentionJobs()
})
d := openTestDB(t)
var passes int32
runRetentionFn = func(database *db.Database, dataDir string, statsHours, buildDays int) {
atomic.AddInt32(&passes, 1)
}
StartRetentionJobs(d, t.TempDir(), 1, 0)
deadline := time.Now().Add(250 * time.Millisecond)
for time.Now().Before(deadline) {
if atomic.LoadInt32(&passes) >= 2 {
break
}
time.Sleep(10 * time.Millisecond)
}
if atomic.LoadInt32(&passes) < 2 {
t.Fatalf("expected at least 2 passes before stop, got %d", passes)
}
before := atomic.LoadInt32(&passes)
StopRetentionJobs()
time.Sleep(120 * time.Millisecond)
if got := atomic.LoadInt32(&passes); got != before {
t.Fatalf("expected no retention passes after stop, before=%d after=%d", before, got)
}
}
func TestRunRetention_PurgesHashrateSamples(t *testing.T) {
d := openTestDB(t)
seedHashrateSample(t, d, "a1", time.Now().Add(-48*time.Hour), 100)