agent (unreleased): fixes after an independent review — one lock for the writers of agent.json, the trust run serialised and bounded, the CA copy keeps its previous root, the profile stanza from the wrapper's answer, a stale give-up ends, both boot tokens from one read
gates / gates (push) Successful in 48s
gates / gates (push) Successful in 48s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
1 parent
61feb95c06
commit
ec7e699fd7
18 files changed
+333
-59
No files matched your search
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Three functions in this process read agent.json whole, change one section and write it whole: the trust move, the
|
||||
// PBS seed and the backup-target move. Without one shared lock two of them interleave and the second puts back the
|
||||
// document it read before the first one's change (found by review, 2026-11-14). Each must take config.FileMu as its
|
||||
// FIRST statement and release it by defer — asserted from the source, because the race itself is not a thing a unit
|
||||
// test can stage reliably.
|
||||
func TestAgentJSONWritersTakeTheSharedLock(t *testing.T) {
|
||||
for _, w := range []struct{ file, fn string }{
|
||||
{"../../internal/pvetrust/pvetrust.go", "writeConfig"},
|
||||
{"../../internal/pbsdr/manager.go", "seedEscrowStorageID"},
|
||||
{"../../internal/localapi/backup_target.go", "setConfiguredBackupTarget"},
|
||||
} {
|
||||
f, err := parser.ParseFile(token.NewFileSet(), w.file, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", w.file, err)
|
||||
}
|
||||
found := false
|
||||
for _, d := range f.Decls {
|
||||
fn, ok := d.(*ast.FuncDecl)
|
||||
if !ok || fn.Name.Name != w.fn || fn.Body == nil {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
isLockCall := func(e ast.Expr, name string) bool {
|
||||
c, ok := e.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
sel, ok := c.Fun.(*ast.SelectorExpr)
|
||||
if !ok || sel.Sel.Name != name {
|
||||
return false
|
||||
}
|
||||
inner, ok := sel.X.(*ast.SelectorExpr)
|
||||
if !ok || inner.Sel.Name != "FileMu" {
|
||||
return false
|
||||
}
|
||||
pkg, ok := inner.X.(*ast.Ident)
|
||||
return ok && pkg.Name == "config"
|
||||
}
|
||||
if len(fn.Body.List) < 2 {
|
||||
t.Errorf("%s %s: too short to hold the lock", w.file, w.fn)
|
||||
continue
|
||||
}
|
||||
first, ok1 := fn.Body.List[0].(*ast.ExprStmt)
|
||||
second, ok2 := fn.Body.List[1].(*ast.DeferStmt)
|
||||
if !ok1 || !isLockCall(first.X, "Lock") || !ok2 || !isLockCall(second.Call, "Unlock") {
|
||||
t.Errorf("%s %s: must start with config.FileMu.Lock() and defer config.FileMu.Unlock()", w.file, w.fn)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("%s: function %s not found — a writer of agent.json was renamed; keep it under the shared lock and update this test", w.file, w.fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user