R-203 Part 2: a run that missed a MANDATORY directory is not a successful run (v0.197.0)
gates / gates (push) Successful in 8s
gates / gates (push) Successful in 8s
The gap was already detected and warned about, in Hungarian, naming the app and the folders -- that warning is what stopped the R-201 drill. The defect was that the run still reported `ok` beside it, and a warning standing beside a success is read as a success. last_status gains "incomplete": minted, because "ok" | "error" | "running" had nothing meaning "it ran, and this app is not fully protected". NOT "error" -- the rest of the run worked and what was captured is real, so SnapshotCount and the LastSuccess anchor still record it. Half a backup is not no backup. The gaps are now recorded STRUCTURALLY (offboxRunResult.mandatoryGaps), not only as prose, so the verdict has something to act on. It reaches the operator through the EXISTING per-run digest (backup_run_failures) rather than a new event type -- a new type is a two-repo change and the hub drops anything outside allowedEventTypes. The stat-filter gains the ClassMandatory check Tier 2 already had. It is a NO-OP today (TierOffsite admits mandatory only), so no customer-visible warning disappears -- demonstrated by widening the tier filter alone and watching the check hold the line. ANTICIPATED: calibre-web on demo-hp has exactly this gap, so its off-site status becomes incomplete the moment this ships. That is correct and is the point. Red-proofs: my first Scenario-C proof PASSED because the test only reached offboxCaptureSet while the mutation lives in runOffboxInternal -- a mutation the test cannot observe is not a red-proof, and the fix was the test. The run-level test now fails under both mutations (unreachable gap recording; unconditional ok).
This commit is contained in:
@@ -54,6 +54,14 @@ func (m *Manager) SetOffboxNotify(fn func(dur time.Duration, snapshots int, err
|
||||
m.offboxNotify = fn
|
||||
}
|
||||
|
||||
// SetOffboxGapNotify wires the R-203 operator signal: a run that completed but could NOT capture a
|
||||
// directory an app declares MANDATORY. Distinct from offboxNotify, which fires only on a hard run
|
||||
// failure — a coverage gap is not a failed run, and until v0.197.0 it reached nobody at all.
|
||||
// gaps is app → the relative paths that were missed. nil → no signal (pre-R-203 behaviour).
|
||||
func (m *Manager) SetOffboxGapNotify(fn func(gaps map[string][]string)) {
|
||||
m.offboxGapNotify = fn
|
||||
}
|
||||
|
||||
// SetOffboxOrphanEvent wires the offsite-repo continuity event push (main.go → notifier).
|
||||
func (m *Manager) SetOffboxOrphanEvent(fn func(eventType, renamedTo string)) {
|
||||
m.offboxOrphanEvent = fn
|
||||
@@ -854,7 +862,25 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
|
||||
o.LastError = runErr.Error()
|
||||
o.LastWarning = ""
|
||||
} else {
|
||||
o.LastStatus = "ok"
|
||||
// R-203 — THE VERDICT. A run that could not capture a directory the app declares MANDATORY
|
||||
// is not a successful run. Until v0.197.0 it reported `ok` with a warning beside it, and a
|
||||
// warning beside a success is read as a success: that is how calibre-web's declared book
|
||||
// directory stayed out of every off-site snapshot on demo-hp while the card, the counters
|
||||
// and the hub all said the backup worked.
|
||||
//
|
||||
// NOT "error": the rest of the run worked and the data that WAS captured is real. The
|
||||
// snapshot count and the LastSuccess anchor are deliberately left to record it — half a
|
||||
// backup is not no backup, and reporting it as none would be its own lie. `incomplete` is
|
||||
// minted here because the existing vocabulary ("ok" | "error" | "running") has nothing that
|
||||
// means "it ran, and this app is not fully protected".
|
||||
if len(runResult.mandatoryGaps) > 0 {
|
||||
o.LastStatus = "incomplete"
|
||||
if m.offboxGapNotify != nil {
|
||||
m.offboxGapNotify(runResult.mandatoryGaps)
|
||||
}
|
||||
} else {
|
||||
o.LastStatus = "ok"
|
||||
}
|
||||
o.LastError = ""
|
||||
o.SnapshotCount = snapshots
|
||||
o.EnlargedBlocked = blockedNames // replace each run (sorted); empty slice clears it
|
||||
@@ -1027,6 +1053,10 @@ type offboxRunResult struct {
|
||||
// the zero-toggle honesty notice honest: a box with no app toggled but shares in the cloud is NOT
|
||||
// "nothing is covered".
|
||||
sharesBackedUp bool
|
||||
// mandatoryGaps (R-203) is app → the relative paths of its declared MANDATORY data directories
|
||||
// that could NOT be captured. It is the STRUCTURED form of the warnings above, and it is what
|
||||
// decides the run's verdict: a run that dropped a mandatory directory is not a successful run.
|
||||
mandatoryGaps map[string][]string
|
||||
}
|
||||
|
||||
// runOffboxInternal does the repo-ensure + per-app DISCOVER → capture-set → gate → multi-path backup +
|
||||
@@ -1051,8 +1081,17 @@ func (m *Manager) runOffboxInternal(ctx context.Context, apps, base, env []strin
|
||||
}
|
||||
// Task 3-core TierOffsite capture set: mandatory userdata paths added to the unit snapshot,
|
||||
// plus loud warnings for structurally-refused / on-disk-missing mandatory paths (SP-3.4).
|
||||
extra, capWarns := m.offboxCaptureSet(stack)
|
||||
extra, capWarns, capGaps := m.offboxCaptureSet(stack)
|
||||
res.warns = append(res.warns, capWarns...)
|
||||
// R-203: the gaps are recorded STRUCTURALLY, not only as prose, because the run's verdict now
|
||||
// depends on them. A warning standing beside a success is read as a success — which is exactly
|
||||
// how a customer-declared mandatory directory stayed out of the snapshot while the run said ok.
|
||||
if len(capGaps) > 0 {
|
||||
if res.mandatoryGaps == nil {
|
||||
res.mandatoryGaps = map[string][]string{}
|
||||
}
|
||||
res.mandatoryGaps[stack] = append(res.mandatoryGaps[stack], capGaps...)
|
||||
}
|
||||
// Pre-push enlargement gate (§9): if last-known repo raw-data bytes + the mandatory-set estimate
|
||||
// would cross the soft quota, push UNIT-ONLY (protection never regresses) and record the block.
|
||||
if len(extra) > 0 && t != nil && t.QuotaGB > 0 {
|
||||
@@ -1150,7 +1189,7 @@ type OffboxReportStatus struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
EscrowState string `json:"escrow_state"`
|
||||
LastRun string `json:"last_run,omitempty"` // RFC3339
|
||||
LastStatus string `json:"last_status,omitempty"` // "ok" | "error" | "running"
|
||||
LastStatus string `json:"last_status,omitempty"` // "ok" | "incomplete" (R-203) | "error" | "running"
|
||||
// LastSuccess (R-100) is the last run that SUCCEEDED — the hub's staleness anchor. Absent on a
|
||||
// pre-v0.181.0 controller, which the hub must degrade on explicitly rather than by accident:
|
||||
// treating absence as failure alarms every un-upgraded box, treating it as success keeps the bug.
|
||||
|
||||
Reference in New Issue
Block a user