| index afa428bba..dfa7a62d6 100644 |
| --- a/internal/host/caps_pass_test.go |
| +++ b/internal/host/caps_pass_test.go |
| @@ -474,6 +474,105 @@ func TestBuildPlansAdvisesUngrantedDeclaredCapabilities(t *testing.T) { |
| } |
| } |
| |
| +// The mirror of the advisory above (2026-08-03 review, M5): a token this box |
| +// HAS granted that the live release does not declare. It materializes nothing, |
| +// so it is not a hole — it is a registry row that reads like a hole, which is |
| +// what an operator is looking at during an incident. Two ways in: a grant |
| +// staged for a release nobody shipped, and a release that DROPPED a capability |
| +// it used to declare (converge removes the drop-in; the grant stays behind). |
| +func TestBuildPlansAdvisesGrantedButUndeclaredCapabilities(t *testing.T) { |
| + cases := []struct { |
| + name string |
| + set *caps.Set |
| + grants string |
| + want []string |
| + wantNot []string |
| + }{ |
| + { |
| + name: "granted port the live release never declares", |
| + set: &caps.Set{V: 1, Daemon: true}, |
| + grants: "port:25", |
| + want: []string{"is granted port:25", "the live release does not declare it", |
| + "carlos route -host keymail.dev -revoke port:25", "ship a release that declares it"}, |
| + }, |
| + { |
| + name: "granted host-role the live release never declares", |
| + set: &caps.Set{V: 1, Daemon: true}, |
| + grants: "host-role", |
| + want: []string{"is granted host-role", "keeps DynamicUser", |
| + "carlos route -host keymail.dev -revoke host-role"}, |
| + }, |
| + { |
| + name: "declared and granted says nothing", |
| + set: &caps.Set{V: 1, Ports: []caps.Port{{Port: 25}}, HostRole: true}, |
| + grants: "host-role,port:25", |
| + want: nil, |
| + }, |
| + { |
| + name: "one line per token, in port order, host-role last", |
| + set: &caps.Set{V: 1}, |
| + grants: "host-role,port:25,port:465", |
| + want: []string{"-revoke port:25", "-revoke port:465", "-revoke host-role"}, |
| + }, |
| + { |
| + name: "a declared-but-ungranted capability is the OTHER advisory, not this one", |
| + set: &caps.Set{V: 1, Ports: []caps.Port{{Port: 25}}}, |
| + want: []string{"has not granted it"}, |
| + // The two lines are opposites and must never both fire for one |
| + // token: "granted but not declared" about a token nobody granted |
| + // would be nonsense an operator would chase. |
| + wantNot: []string{"is granted port:25"}, |
| + }, |
| + { |
| + name: "a partly-declared grant advises only the undeclared half", |
| + set: &caps.Set{V: 1, Ports: []caps.Port{{Port: 25}}}, |
| + grants: "host-role,port:25", |
| + want: []string{"is granted host-role"}, |
| + // port:25 is declared AND granted — the healthy case, silent. |
| + wantNot: []string{"port:25 but", "is granted port:25"}, |
| + }, |
| + } |
| + for _, c := range cases { |
| + t.Run(c.name, func(t *testing.T) { |
| + rt := unitRoute("keymail.dev", "keymail") |
| + rt.Caps = c.grants |
| + _, advice := buildPlans([]registry.Route{rt}, loader(map[string]*release.Manifest{"keymail.dev": manifest("keymail", c.set)}, nil)) |
| + joined := strings.Join(advice, "\n") |
| + for _, w := range c.want { |
| + if !strings.Contains(joined, w) { |
| + t.Errorf("advice =\n%s\nwant a line containing %q", joined, w) |
| + } |
| + } |
| + for _, w := range c.wantNot { |
| + if strings.Contains(joined, w) { |
| + t.Errorf("advice =\n%s\nwant NOTHING containing %q", joined, w) |
| + } |
| + } |
| + if len(c.want) == 0 && len(c.wantNot) == 0 && len(advice) != 0 { |
| + t.Errorf("advice = %v, want none", advice) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// A route whose live manifest could not be read gets NO grant advisory at |
| +// all — in either direction. The leave-alone state means this pass does not |
| +// know what the release declares, and "granted but not declared" printed from |
| +// an absent manifest would send an operator to revoke a grant the running |
| +// release is using right now. |
| +func TestBuildPlansSaysNothingAboutGrantsWithNoManifest(t *testing.T) { |
| + rt := unitRoute("keymail.dev", "keymail") |
| + rt.Caps = "host-role,port:25" |
| + plans, advice := buildPlans([]registry.Route{rt}, |
| + loader(map[string]*release.Manifest{}, nil)) // (nil, nil): no live version |
| + if len(plans) != 1 || plans[0].Set != nil { |
| + t.Fatalf("plans = %+v, want one leave-alone plan with a nil Set", plans) |
| + } |
| + if len(advice) != 0 { |
| + t.Fatalf("advice = %v, want none — nothing is known about what the release declares", advice) |
| + } |
| +} |
| + |
| // An exec-backed route whose release declares capabilities materializes |
| // nothing (release.CapabilityRoute excludes it) and is told so — the guided |
| // path through the §5 migration, and the line an operator sees during the |