v0.317.0: a second copy counts only on a proven different disk; the no-drive install speaks in words (felhom.eu architecture/16, build A)
gates / gates (push) Successful in 46s

Tier-2 and the one-copy notice ask the host agent (POST /disks/same-disk, agent 0.157.0): different +
proven -> made and counted; same disk -> not made (D1); unknown (VM, hidden USB serial) -> made, not
counted, the row says why (D2); a NAS never counts and no longer hides the notice (R-1075). An older
agent keeps the st_dev rule, logged once. D4: a data-folder or media app on a box with no drive is
refused in words with a link to the drives page (R-1068's raw-error half). MinAgent 0.157.0.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-15 21:21:54 +02:00
parent c6a567122e
commit 0626b9fc8a
34 changed files with 665 additions and 200 deletions
@@ -0,0 +1,41 @@
package main
import (
"go/ast"
"go/parser"
"go/token"
"testing"
)
// v0.317.0 (`16` §4): the agent's same-disk verdict reaches the backup side. A seam built and never wired
// has shipped in this repo seven times; this walks main.go for the call AND for the agent call inside it.
// COMPANION RED-PROOF: delete the SetDiskJudge block → "not wired"; replace ac.SameDisk with a constant →
// "never asks the agent".
func TestDiskJudge_IsWiredAtStartup(t *testing.T) {
f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
if err != nil {
t.Fatal(err)
}
var judge *ast.CallExpr
ast.Inspect(f, func(n ast.Node) bool {
if c, ok := n.(*ast.CallExpr); ok {
if s, ok := c.Fun.(*ast.SelectorExpr); ok && s.Sel.Name == "SetDiskJudge" {
judge = c
}
}
return true
})
if judge == nil {
t.Fatal("backupMgr.SetDiskJudge is not wired in main.go — Tier-2 would keep the st_dev rule for ever")
}
asks := false
ast.Inspect(judge, func(n ast.Node) bool {
if s, ok := n.(*ast.SelectorExpr); ok && s.Sel.Name == "SameDisk" {
asks = true
}
return true
})
if !asks {
t.Fatal("SetDiskJudge is wired but never asks the agent (no SameDisk call inside it)")
}
}
+23
View File
@@ -583,6 +583,29 @@ func main() {
// re-rendered or the restored shares exist on paper but are not exported. A seam rather than a
// direct call — the backup package must not depend on the stacks package.
backupMgr.SetSharesReconciler(stackMgr.ReconcileSamba)
// v0.317.0 (felhom.eu architecture/16-storage-physical-identity.md §4): every Tier-2 and one-copy
// decision asks the HOST AGENT whether two paths share a physical disk. An agent older than 0.157.0
// (FeatureSameDisk) or a failed call → supported=false → the backup side keeps the old st_dev rule,
// logged once. Pinned by TestDiskJudge_IsWiredAtStartup.
if cfg.LocalAPI.Endpoint != "" && cfg.LocalAPI.Token != "" {
if ac, err := agentapi.New(cfg.LocalAPI.Endpoint, cfg.LocalAPI.Token, cfg.LocalAPI.Fingerprint); err != nil {
logger.Printf("[WARN] [backup] same-disk: agent client init failed (%v) — the st_dev rule stays", err)
} else {
backupMgr.SetDiskJudge(func(a, b string) (backup.DiskVerdict, bool) {
jctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if ac.Supports(jctx, agentapi.FeatureSameDisk) == agentapi.SupportNo {
return backup.DiskVerdict{}, false
}
r, err := ac.SameDisk(jctx, a, b)
if err != nil {
logger.Printf("[WARN] [backup] same-disk: agent call failed for %s / %s: %v", a, b, err)
return backup.DiskVerdict{}, false
}
return backup.DiskVerdict{Different: r.Different, Proven: r.Proven, Reason: r.Reason}, true
})
}
}
}
// --- Slice 4: the guarded update's backup side ---