Isolate lock-evidence fixture keys without clearing guest fences

Retained full-suite proof reports an inherited agent-cooldown before the invalid-UTF8 lock case dispatches any command. Give each synthetic case a distinct base path, keeping all clients in that case on its shared key. A same-listener control retains the prior uncertainty fence unchanged. No runtime guest guard, native target or assertion is weakened.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-14 14:47:17 +01:00
parent 5b895ff78e
commit 757acf055e
3 changed files with 66 additions and 5 deletions
+54 -5
View File
@@ -54,7 +54,7 @@ func TestGuestAgentLockEvidenceRejectsAmbiguity(t *testing.T) {
backupAgentPayload(w, r)
}))
defer server.Close()
c := backupTestClient(t, server.URL)
c := backupTestClient(t, lockEvidenceFixtureEndpoint(t, server.URL))
if err := read(context.Background(), c, 105); GuestAgentDeferredReason(err) != "lock-unverified" {
t.Errorf("ambiguous lock admitted/published a command: %v", err)
}
@@ -68,7 +68,7 @@ func TestGuestAgentLockEvidenceRejectsAmbiguity(t *testing.T) {
healthy.Store(true)
// A completed command's failed lock check is not transport
// uncertainty. A new diagnostic must reverify and resume.
diagnostic := backupTestClient(t, server.URL)
diagnostic := backupTestClient(t, lockEvidenceFixtureEndpoint(t, server.URL))
if err := read(context.Background(), diagnostic, 105); err != nil {
t.Fatalf("complete unlocked config did not resume: %v", err)
}
@@ -116,7 +116,7 @@ func TestGuestAgentLockEvidenceRejectsRedirects(t *testing.T) {
}
}))
defer server.Close()
c := backupTestClient(t, server.URL)
c := backupTestClient(t, lockEvidenceFixtureEndpoint(t, server.URL))
if err := read(context.Background(), c, 105); GuestAgentDeferredReason(err) != "lock-unverified" {
t.Errorf("redirected lock admitted/published a command: %v", err)
}
@@ -145,7 +145,7 @@ func TestGuestAgentLockEvidenceRejectsRedirects(t *testing.T) {
t.Errorf("ordinary redirected requests = %d, want %d", redirected.Load(), wantRedirected)
}
healthy.Store(true)
if err := read(context.Background(), backupTestClient(t, server.URL), 105); err != nil {
if err := read(context.Background(), backupTestClient(t, lockEvidenceFixtureEndpoint(t, server.URL)), 105); err != nil {
t.Fatalf("valid same-target config did not resume: %v", err)
}
if commands.Load() != want+1 || redirected.Load() != wantRedirected {
@@ -186,7 +186,7 @@ func TestGuestAgentLockEvidenceHealthyConfigAndSessionRecovery(t *testing.T) {
}
}))
defer server.Close()
c, err := NewClient(ClientConfig{Host: server.URL, User: "fixture@pam", Password: "fixture-password", Timeout: time.Second})
c, err := NewClient(ClientConfig{Host: lockEvidenceFixtureEndpoint(t, server.URL), User: "fixture@pam", Password: "fixture-password", Timeout: time.Second})
if err != nil {
t.Fatal(err)
}
@@ -199,3 +199,52 @@ func TestGuestAgentLockEvidenceHealthyConfigAndSessionRecovery(t *testing.T) {
})
}
}
// Shared guest fences intentionally outlive a closed fixture listener. A
// recycled loopback port must not make an independent lock case inherit an
// earlier case's VM-wide uncertainty. Every client within a case keeps its key.
func lockEvidenceFixtureEndpoint(t *testing.T, serverURL string) string {
t.Helper()
return serverURL + "/lock-evidence-fixture/" + strings.ReplaceAll(t.Name(), "/", "-")
}
func TestGuestAgentLockEvidenceFixtureIsolation(t *testing.T) {
var configs, commands atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/config") {
configs.Add(1)
fmt.Fprint(w, `{"data":{}}`)
return
}
commands.Add(1)
backupAgentPayload(w, r)
}))
defer server.Close()
// Deliberately share the listener; no OS port-reuse timing is needed.
old := backupTestClient(t, server.URL+"/previous-lock-fixture")
release, err := acquireGuestAgent(old.baseURL, 105)
if err != nil {
t.Fatal(err)
}
release(true)
key, valid := guestAgentEndpointKey(old.baseURL)
if !valid {
t.Fatal("invalid old fixture key")
}
guestAgentGuards.Lock()
retained := guestAgentGuards.entries[guestAgentGuardKey{endpoint: key, vmid: 105}]
guestAgentGuards.Unlock()
current := backupTestClient(t, lockEvidenceFixtureEndpoint(t, server.URL))
if _, err := current.GetVMAgentInfo(context.Background(), "node", 105); err != nil {
t.Fatalf("independent lock fixture inherited earlier uncertainty: %v", err)
}
if _, err := old.GetVMAgentInfo(context.Background(), "node", 105); GuestAgentDeferredReason(err) != "agent-cooldown" {
t.Fatalf("isolation weakened the original fixture fence: %v", err)
}
guestAgentGuards.Lock()
unchanged := guestAgentGuards.entries[guestAgentGuardKey{endpoint: key, vmid: 105}] == retained
guestAgentGuards.Unlock()
if !unchanged || commands.Load() != 1 || configs.Load() != 2 {
t.Fatal("isolation changed a retained fence or dispatched extra work")
}
}