R-270: the local_api drift detector also names a TOKEN-only divergence
A rotation that reached bootstrap.json but not controller.yaml left the agent channel at 401 across restarts while the endpoint-only detector stayed silent. The tokens are now compared (constant time, boolean only); the token case gets its own banner key and operator message. Still detection only - nothing is reconciled (R-78). 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:
@@ -221,3 +221,41 @@ func TestScenarioC_AbsentBlockStillMerges(t *testing.T) {
|
||||
t.Errorf("after a successful merge the two files agree — no drift: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
// R-270 — a token rotation that reached bootstrap.json but not controller.yaml left the channel at
|
||||
// HTTP 401 across restarts, and the endpoint-only detector stayed silent (proved live 2026-08-09).
|
||||
// The consequence pinned: the token-only divergence is DETECTED, NAMED as a token (not an address)
|
||||
// on the banner key, nothing is written, and the token value appears in no output.
|
||||
func TestR270_TokenOnlyDriftIsNamedNotLeaked(t *testing.T) {
|
||||
cfgPath, cfg := writeDriftFixture(t, "169.254.253.1:8443", "169.254.253.1:8443", "aaaa1111", "tok-ROTATED")
|
||||
before := sha(t, cfgPath)
|
||||
var buf bytes.Buffer
|
||||
d := DetectEndpointDrift(cfgPath, cfg, log.New(&buf, "", 0))
|
||||
if d == nil {
|
||||
t.Fatal("a token-only divergence must be DETECTED — this is the R-270 live shape (401 across restarts)")
|
||||
}
|
||||
if !d.EndpointAgrees || d.TokenAgrees {
|
||||
t.Errorf("want EndpointAgrees=true TokenAgrees=false, got %+v", *d)
|
||||
}
|
||||
if d.AlertKey() != "alert.local_api_token_drift" {
|
||||
t.Errorf("the banner must name the TOKEN, not the address: key %q", d.AlertKey())
|
||||
}
|
||||
if !strings.Contains(d.EnglishMessage(), "token drift") || !strings.Contains(buf.String(), "TOKEN drift") {
|
||||
t.Errorf("operator message/log must name the token drift:\nmsg=%q\nlog=%q", d.EnglishMessage(), buf.String())
|
||||
}
|
||||
for _, secret := range []string{"tok-secret", "tok-ROTATED", "aaaa1111"} {
|
||||
if strings.Contains(buf.String(), secret) || strings.Contains(d.EnglishMessage(), secret) || strings.Contains(d.HungarianMessage(), secret) {
|
||||
t.Errorf("a secret value leaked: %q", secret)
|
||||
}
|
||||
}
|
||||
if after := sha(t, cfgPath); !bytes.Equal(before, after) {
|
||||
t.Error("controller.yaml was MODIFIED — detection must never write (R-78)")
|
||||
}
|
||||
|
||||
// The address case keeps its own key (no regression of R-77's banner).
|
||||
cfgPath2, cfg2 := writeDriftFixture(t, "192.168.0.87:8443", "169.254.253.1:8443", "aaaa1111", "tok-secret")
|
||||
d2 := DetectEndpointDrift(cfgPath2, cfg2, log.New(io.Discard, "", 0))
|
||||
if d2 == nil || d2.EndpointAgrees || !d2.TokenAgrees || d2.AlertKey() != "alert.endpoint_drift" {
|
||||
t.Errorf("address drift must keep alert.endpoint_drift: %+v", d2)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user