R-263 writer invariant pinned by an AST test; R-368 IsDefault comment exact (no binary change; burn-down)
gates / gates (push) Successful in 32s
gates / gates (push) Successful in 32s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1,3 +1,15 @@
|
|||||||
|
## unreleased — a comment pinned by a test, a comment made exact; no binary change (burn-down 2026-10-05: R-263, R-368)
|
||||||
|
|
||||||
|
The next release carries these two lines into its own entry.
|
||||||
|
|
||||||
|
- **R-263:** `SetBackupTarget` is „the only writer that GRANTS" `StoragePath.BackupTarget` (ClearBackupTarget also writes
|
||||||
|
it, only `false`), now pinned by `internal/settings/r263_backup_target_writers_test.go`: an AST scan of every non-test
|
||||||
|
file under `internal/` and `cmd/` for an assignment or a composite-literal key, refusing anything but `false` outside
|
||||||
|
`SetBackupTarget`. Red-proofs (`ClearBackupTarget` writing `true`; a `BackupTarget: true` literal in `internal/web`)
|
||||||
|
both convict — `felhom.eu/documentation/audits/burndown-2026-10-05/r263-red-proof.txt`.
|
||||||
|
- **R-368:** `StoragePath.IsDefault`'s comment says who applies it — the deploy FORM pre-selects it; the deploy API
|
||||||
|
applies no default. Behaviour unchanged on purpose (it is right on the path households use).
|
||||||
|
|
||||||
## v0.296.0 — a backup cut off by a power cut or a restart is said on the page; the whole-system backup text states today's measurement (R-519, R-518) (2026-10-05)
|
## v0.296.0 — a backup cut off by a power cut or a restart is said on the page; the whole-system backup text states today's measurement (R-519, R-518) (2026-10-05)
|
||||||
|
|
||||||
**MinAgent: 0.131.0** (unchanged). New household string: `backups.interrupted_run` (Hungarian and English); the
|
**MinAgent: 0.131.0** (unchanged). New household string: `backups.interrupted_run` (Hungarian and English); the
|
||||||
|
|||||||
@@ -1,35 +1,8 @@
|
|||||||
# REPORT — controller v0.296.0 (2026-10-05, late afternoon): a cut-off backup run is said; honest "Mentés most" wording
|
# REPORT — 2026-10-05 burn-down: a pinned invariant and an exact comment (no release)
|
||||||
|
|
||||||
Brief: the 2026-10-05 hub-safety brief (operator), Part E (R-518, R-519). Full session report:
|
Full session report: `felhom.eu/REPORT-burndown-2026-10-05.md`. Baseline `e563733` (v0.296.0). No binary change.
|
||||||
`felhom.eu/REPORT-hub-safety-2026-10-05.md`. Architecture: `felhom.eu/documentation/architecture/07-backup-architecture.md`
|
|
||||||
(the restore-record paragraph and the R-518 lane-1 note), `09` decision 123.
|
|
||||||
|
|
||||||
## Baseline and commits
|
- R-263 — `internal/settings/r263_backup_target_writers_test.go` (AST scan of internal/ + cmd/); two red-proofs convict.
|
||||||
|
- R-368 — `internal/settings/settings.go` `IsDefault` comment names the deploy form as the one that applies it.
|
||||||
|
|
||||||
- Baseline `477e2548db` (v0.295.0). Commit `ff69074` → image `gitea.dooplex.hu/admin/felhom-controller:0.296.0`.
|
`go build ./... && go vet ./... && go test ./...` green; `controller_gates.py --fast` green.
|
||||||
- **MinAgent 0.131.0** (unchanged). Golden 0.296.0 baked + vouched (`felhom.eu/documentation/tests/golden-0.296.0-2026-10-05/`).
|
|
||||||
- Delivered by per-customer floors (demo-hp, demo-felhom, tester-1 at 0.296.0): both demo boxes ran 0.296.0 (healthy)
|
|
||||||
~30 min later.
|
|
||||||
|
|
||||||
## What changed
|
|
||||||
|
|
||||||
- **R-519.** `internal/backup/run_record.go`: the app-data run is on record (`appdata-run.json`) while it runs; a start
|
|
||||||
that finds it running turns it into a notice on /backups and /backups/apps, kept until a run ends with every step OK.
|
|
||||||
After a restart the synthesised "last database backup" no longer reads OK over a cut run. Restore points were already
|
|
||||||
dated by their OLDEST part (v0.275.0). Wired in `main()` (`loadRunRecordAtStartup`, AST-pinned).
|
|
||||||
- **R-518.** The whole-system "Mentés most" text and confirm now state two measurements: ≈6 min / 9 apps (demo-hp
|
|
||||||
today: 09:19:08Z → last app back 09:24:55Z, local tier only) and ≈8 min / 12 apps (2026-09-14); "minutes, not seconds";
|
|
||||||
longer when the off-site copy runs in the same run.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
`go build/vet/test ./...` green; controller gates green (golden-notice advisory before the bake). New: `TestRunRecord_*`
|
|
||||||
(3, incl. the production path through `runDBDumpsInternal`), `TestRunRecordAtStartup_FindsACutRun`,
|
|
||||||
`TestMainWiresRunRecord`, `TestR518_*` (both measurements, both pages and confirm), parity cases
|
|
||||||
`backups_interrupted_run`, `backups_apps_interrupted_run`; six parity fixtures re-captured for the changed copy (only the
|
|
||||||
copy lines differ). Red-proofs (4, all convict): `felhom.eu/documentation/audits/hub-safety-2026-10-05/partE/red-proof.txt`.
|
|
||||||
|
|
||||||
## NOT yet live-validated
|
|
||||||
|
|
||||||
- **The live cut on 9202** (restart the controller in the middle of a backup run, then read the page): the permission
|
|
||||||
check refused restarting the controller mid-backup. The operator is asked; R-519 stays narrowed until then.
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-263: "SetBackupTarget is the only writer that GRANTS StoragePath.BackupTarget" — a drive never acquires the
|
||||||
|
// whole-guest backup-target role by appearing (E-2 §3). This scans EVERY non-test Go file under controller/internal and
|
||||||
|
// cmd for a write of a field named BackupTarget — an assignment `x.BackupTarget = v` or a composite-literal key
|
||||||
|
// `BackupTarget: v` — and fails on any that is not the literal `false`, unless it sits inside SetBackupTarget.
|
||||||
|
// RED-PROOF: plant `s.StoragePaths[0].BackupTarget = true` in any other function → this test fails.
|
||||||
|
func TestR263_OnlySetBackupTargetGrantsTheRole(t *testing.T) {
|
||||||
|
var roots []string
|
||||||
|
for _, r := range []string{"..", "../../cmd"} { // internal/ (this package's parent) and cmd/
|
||||||
|
if _, err := os.Stat(r); err == nil {
|
||||||
|
roots = append(roots, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files, writes := 0, 0
|
||||||
|
var bad []string
|
||||||
|
for _, root := range roots {
|
||||||
|
_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
f, perr := parser.ParseFile(fset, path, nil, 0)
|
||||||
|
if perr != nil {
|
||||||
|
t.Fatalf("parse %s: %v", path, perr)
|
||||||
|
}
|
||||||
|
files++
|
||||||
|
for _, decl := range f.Decls {
|
||||||
|
fn, _ := decl.(*ast.FuncDecl)
|
||||||
|
inSetter := fn != nil && fn.Name.Name == "SetBackupTarget"
|
||||||
|
ast.Inspect(decl, func(n ast.Node) bool {
|
||||||
|
check := func(val ast.Expr, pos token.Pos) {
|
||||||
|
writes++
|
||||||
|
if id, ok := val.(*ast.Ident); ok && id.Name == "false" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !inSetter {
|
||||||
|
bad = append(bad, fset.Position(pos).String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch x := n.(type) {
|
||||||
|
case *ast.AssignStmt:
|
||||||
|
for i, lhs := range x.Lhs {
|
||||||
|
if sel, ok := lhs.(*ast.SelectorExpr); ok && sel.Sel.Name == "BackupTarget" && i < len(x.Rhs) {
|
||||||
|
check(x.Rhs[i], x.Pos())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *ast.KeyValueExpr:
|
||||||
|
if k, ok := x.Key.(*ast.Ident); ok && k.Name == "BackupTarget" {
|
||||||
|
check(x.Value, x.Pos())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if files < 100 || writes < 2 {
|
||||||
|
t.Fatalf("scan too small to mean anything: %d files, %d BackupTarget writes (want the two in settings.go)", files, writes)
|
||||||
|
}
|
||||||
|
if len(bad) > 0 {
|
||||||
|
t.Fatalf("BackupTarget may be granted outside SetBackupTarget at: %v", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -569,7 +569,7 @@ const NetworkMountRoot = "/mnt/felhom-drives"
|
|||||||
type StoragePath struct {
|
type StoragePath struct {
|
||||||
Path string `json:"path"` // e.g., "/mnt/hdd_1" (drive) or "/mnt/felhom-drives/<name>" (network)
|
Path string `json:"path"` // e.g., "/mnt/hdd_1" (drive) or "/mnt/felhom-drives/<name>" (network)
|
||||||
Label string `json:"label,omitempty"` // e.g., "Külső HDD 1TB"
|
Label string `json:"label,omitempty"` // e.g., "Külső HDD 1TB"
|
||||||
IsDefault bool `json:"is_default,omitempty"` // new apps use this by default
|
IsDefault bool `json:"is_default,omitempty"` // the deploy FORM pre-selects it (templates/deploy.html); the deploy API applies no default (R-368)
|
||||||
Schedulable bool `json:"schedulable"` // whether new apps can be deployed here
|
Schedulable bool `json:"schedulable"` // whether new apps can be deployed here
|
||||||
AddedAt string `json:"added_at"` // RFC3339
|
AddedAt string `json:"added_at"` // RFC3339
|
||||||
Disconnected bool `json:"disconnected,omitempty"` // true when drive detected as disconnected
|
Disconnected bool `json:"disconnected,omitempty"` // true when drive detected as disconnected
|
||||||
@@ -1652,8 +1652,9 @@ func (s *Settings) SetSchedulable(path string, schedulable bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetBackupTarget assigns the whole-guest backup-target role to exactly one storage path, clearing it
|
// SetBackupTarget assigns the whole-guest backup-target role to exactly one storage path, clearing it
|
||||||
// from every other. This is the ONLY writer of StoragePath.BackupTarget — registration must never set
|
// from every other. This is the ONLY writer that GRANTS StoragePath.BackupTarget (ClearBackupTarget also writes
|
||||||
// it (E-2 §3: a drive never acquires a role by appearing).
|
// the field, only ever false) — registration must never set it (E-2 §3: a drive never acquires a role by
|
||||||
|
// appearing). Pinned by TestR263_OnlySetBackupTargetGrantsTheRole (a scan of every non-test file).
|
||||||
//
|
//
|
||||||
// Refusals, both structural rather than advisory:
|
// Refusals, both structural rather than advisory:
|
||||||
// - a NETWORK share can never be the target. vzdump writes a multi-GB archive through the host, and
|
// - a NETWORK share can never be the target. vzdump writes a multi-GB archive through the host, and
|
||||||
|
|||||||
Reference in New Issue
Block a user