b5d78d1e0f
The systemic complaint, twice in one evening: you press a button and nothing happens. No progress, no ETA, no named result. Three worst offenders, fixed on the two patterns already here (deploy 3-step panel, storage-init status poll). No new framework — that is a ROADMAP item; three targeted cards ship tonight. 4a — a verification restore names its result. The flash said the app had been restored "to a verification folder on the drive"; which folder, on which drive, was invisible, so the customer could not go and look at what they had just asked for. Full path now. The restore page gained a listing of existing verification copies (app, size, date, path) — nothing anywhere showed these, so they piled up and the only way to find them was SSH — each with a double-confirmed delete. That delete is the only one this release adds, so it names a STACK, never a path: the Manager resolves the name inside a backups/offsite-restore root it computed itself and refuses anything landing outside. Red-proofed — neutralise the name guard and stack:"" resolves to the offsite-restore ROOT and takes every copy with it. Refusals are asserted as non-effects. 4b — Megosztás enable shows what it is waiting for. Enabling ran ReconcileSamba synchronously inside the POST handler; on a golden without felhom-samba baked that is compose pulling ~100MB, i.e. minutes of an apparently-hung form post followed by "Beállítás mentve." whether or not anything came up. Detached + polled now, distinguishing "képfájl letöltése" from "indítás" — decided BEFORE the work starts, since afterwards the image is always present. Success is probed, not inferred (compose up -d exits 0 on a crash-loop). The password form starts the same job: with UserSet false reconcile deploys nothing, so on a fresh box that is where the pull actually happens. 4c — "Távoli mentés most" streams real progress. restic was already reporting bytes and percent; the runner seam used CombinedOutput() and discarded them. The manual run now passes --json and scans stdout line-by-line: total bytes, percent, current app. Manual only — the nightly stays silent, pinned by a test that fails if it ever passes --json. The poll now arms unconditionally, closing a race the manual trigger always ran: the redirect rendered before the goroutine wrote LastStatus=running, so the poll never armed and the page sat static during the very run just started. Red-proofed twice. Also closes the golden/controller infra-image drift at the source: infra.Images() derives from the existing pins and --print-infra-images exposes it, so the golden bake can stop carrying its own copy. That copy had already drifted — felhom-samba was never added, so the golden baked 3 of 4, which is why enabling Megosztás pulled at runtime in the first place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
87 lines
3.4 KiB
Go
87 lines
3.4 KiB
Go
package web
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// v0.147.0 slice 4b — the Megosztás bring-up progress card.
|
|
//
|
|
// The card's whole value is that it distinguishes states the old synchronous handler collapsed into
|
|
// one silent form post. So the tests are about the DISTINCTIONS:
|
|
// - "pulling" vs "starting" — decided by local image presence, and decided BEFORE the work starts
|
|
// (afterwards the image is always present and the card could never truthfully say „letöltés").
|
|
// - reconcile-returned-nil is NOT the same as running: it also covers "deliberately deployed
|
|
// nothing because there is no household password yet".
|
|
// - compose up -d exits 0 on a crash-loop, so success must be PROBED, never inferred.
|
|
//
|
|
// SCOPE, STATED HONESTLY: these cover the single-flight slot, snapshot isolation and the phase
|
|
// vocabulary. They do NOT drive runSambaEnsureJob end-to-end — that needs a real *stacks.Manager
|
|
// (the Server field is the concrete type, not an interface), and introducing an interface purely for
|
|
// this card was more churn than the slice warranted. The pulling-vs-starting decision and the
|
|
// probed-liveness terminal state are therefore covered by the LIVE validation on the demo box, not
|
|
// by a unit test. Recorded here rather than left as an implied gap; the ROADMAP's unified
|
|
// async-job feedback item is where that seam belongs.
|
|
|
|
func TestSambaEnsureSingleFlight(t *testing.T) {
|
|
var st sambaEnsureState
|
|
job := &sambaEnsureJob{Phase: sambaPhaseStarting}
|
|
if !st.acquire(job) {
|
|
t.Fatal("first acquire refused")
|
|
}
|
|
// A double-submit must not start a second `compose up -d` against the same stack dir.
|
|
if st.acquire(&sambaEnsureJob{Phase: sambaPhaseStarting}) {
|
|
t.Error("second acquire succeeded while a job was in flight")
|
|
}
|
|
st.release()
|
|
if !st.acquire(&sambaEnsureJob{Phase: sambaPhaseStarting}) {
|
|
t.Error("acquire refused after release")
|
|
}
|
|
}
|
|
|
|
func TestSambaEnsureSnapshotIsACopy(t *testing.T) {
|
|
var st sambaEnsureState
|
|
job := &sambaEnsureJob{Phase: sambaPhasePulling}
|
|
st.acquire(job)
|
|
|
|
snap := st.snapshot()
|
|
snap.Phase = "mutated-by-caller"
|
|
if again := st.snapshot(); again.Phase != sambaPhasePulling {
|
|
t.Errorf("a caller mutated the shared job through its snapshot: %q", again.Phase)
|
|
}
|
|
// And the live job must not leak into an already-taken snapshot either.
|
|
snap2 := st.snapshot()
|
|
job.Phase = sambaPhaseRunning
|
|
st.set(job)
|
|
if snap2.Phase != sambaPhasePulling {
|
|
t.Errorf("an earlier snapshot changed under the caller: %q", snap2.Phase)
|
|
}
|
|
}
|
|
|
|
func TestSambaEnsureStateStartsNil(t *testing.T) {
|
|
var st sambaEnsureState
|
|
// nil = never ran this process. The status handler maps that to `idle` and lets live container
|
|
// state win, so a page loaded after a restart still tells the truth.
|
|
if j := st.snapshot(); j != nil {
|
|
t.Errorf("fresh state reported a job: %+v", j)
|
|
}
|
|
}
|
|
|
|
// TestSambaPhaseConstantsAreDistinct guards the template contract: sharing.html maps these exact
|
|
// strings, and a duplicate would silently render the wrong Hungarian sentence.
|
|
func TestSambaPhaseConstantsAreDistinct(t *testing.T) {
|
|
all := []string{
|
|
sambaPhasePulling, sambaPhaseStarting, sambaPhaseRunning,
|
|
sambaPhaseNeedsPassword, sambaPhaseFailed, sambaPhaseIdle,
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, p := range all {
|
|
if p == "" {
|
|
t.Error("a phase constant is empty — the template would fall through to 'no card'")
|
|
}
|
|
if seen[p] {
|
|
t.Errorf("duplicate phase constant %q", p)
|
|
}
|
|
seen[p] = true
|
|
}
|
|
}
|