Go, per the toolchain decision. cmd/provision with internal/spec, internal/state and internal/plan; plan computation is pure and takes live observation through an interface, so the refusal logic is testable without a Telegram account. It runs against the real campaign spec today. Separates two refusals the design had treated as one. A deferral is the design working -- the public channel waiting on a checked rendering, normal on every first run. A block is the world disagreeing with the state file: drifted rights, a taken-over username, a bot that is no longer reachable. Collapsed together, a first run could never apply anything, because it always defers the public channel. The rights clamp and the private/public username rule are enforced in Go and asserted against the same cases the JSON schema rejects, since mirroring the schema in code is a drift risk worth a test rather than a comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172sgCZEEDJcnQmr4SGDvKa Assistant: claude-code Assistant-Model: opus Assistant-Process: 1361245@bnt-lap001 Assistant-Session: b3b428ef-f3e6-4688-b091-01f71461d66a
213 lines
6.8 KiB
Go
213 lines
6.8 KiB
Go
package plan
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tegwick/fluid-telegram/internal/spec"
|
|
"github.com/tegwick/fluid-telegram/internal/state"
|
|
)
|
|
|
|
type fake struct {
|
|
botExists bool
|
|
rights map[int64][]string
|
|
usernames map[int64]string
|
|
}
|
|
|
|
func (f fake) BotExists(string) (bool, error) { return f.botExists, nil }
|
|
func (f fake) ChannelAdminRights(id int64) ([]string, error) {
|
|
if r, ok := f.rights[id]; ok {
|
|
return r, nil
|
|
}
|
|
return []string{spec.PostMessages}, nil
|
|
}
|
|
func (f fake) ChannelUsername(id int64) (string, error) { return f.usernames[id], nil }
|
|
|
|
func sp() *spec.Presence {
|
|
return &spec.Presence{
|
|
SchemaVersion: "0.1", Campaign: "hall-of-helix", Interface: "i",
|
|
Bot: spec.Bot{Name: "HelixForge", UsernamePreference: []string{"HelixForgeBot"}},
|
|
Channels: map[string]spec.Channel{
|
|
spec.Test: {Title: "t", Visibility: spec.Private},
|
|
spec.Live: {Title: "p", Visibility: spec.Public, UsernamePreference: []string{"hallofhelix"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
func find(p *Plan, kind Kind, target string) *Action {
|
|
for i := range p.Actions {
|
|
if p.Actions[i].Kind == kind && p.Actions[i].Target == target {
|
|
return &p.Actions[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// First run: everything is created, and the public channel is blocked because
|
|
// no rendering has been checked yet.
|
|
func TestFirstRunCreatesAndBlocksPublic(t *testing.T) {
|
|
p, err := Compute(sp(), "sha256:x", &state.Resolved{}, fake{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if find(p, Attempt, "bot") == nil {
|
|
t.Error("expected a bot registration attempt")
|
|
}
|
|
if find(p, Create, "channel.test") == nil {
|
|
t.Error("expected the test channel to be created")
|
|
}
|
|
if find(p, Defer, "channel.public") == nil {
|
|
t.Error("public channel must be deferred until the test channel is verified")
|
|
}
|
|
// A first run is not an error state. The bot and the test channel must still
|
|
// be creatable while the public channel waits.
|
|
if p.Blocked() {
|
|
t.Errorf("a first run should not block:\n%s", p.Render())
|
|
}
|
|
if p.Empty() {
|
|
t.Error("a first run has work to do")
|
|
}
|
|
}
|
|
|
|
func provisioned(withTestPublication bool) *state.Resolved {
|
|
rs := &state.Resolved{
|
|
Campaign: "hall-of-helix",
|
|
Bot: state.Bot{Username: "HelixForgeBot", ID: 42},
|
|
Channels: map[string]state.Channel{
|
|
spec.Test: {ChatID: -100, AdminRights: []string{spec.PostMessages}},
|
|
spec.Live: {ChatID: -200, Username: "hallofhelix", AdminRights: []string{spec.PostMessages}},
|
|
},
|
|
}
|
|
if withTestPublication {
|
|
now := time.Now()
|
|
c := rs.Channels[spec.Test]
|
|
c.TestPublicationAt = &now
|
|
rs.Channels[spec.Test] = c
|
|
}
|
|
return rs
|
|
}
|
|
|
|
// Converged: a second run over an unchanged spec proposes nothing but the
|
|
// idempotent profile reassertion.
|
|
func TestConvergedRunIsQuiet(t *testing.T) {
|
|
f := fake{botExists: true, usernames: map[int64]string{-200: "hallofhelix"}}
|
|
p, err := Compute(sp(), "sha256:x", provisioned(true), f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Blocked() {
|
|
t.Fatalf("converged plan should not block:\n%s", p.Render())
|
|
}
|
|
for _, a := range p.Actions {
|
|
if a.Kind == Create || a.Kind == Attempt {
|
|
t.Errorf("unexpected %s of %s on a converged presence", a.Kind, a.Target)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Widened rights are a refusal, not a repair.
|
|
func TestWidenedRightsBlock(t *testing.T) {
|
|
f := fake{botExists: true, usernames: map[int64]string{-200: "hallofhelix"},
|
|
rights: map[int64][]string{-200: {spec.PostMessages, "can_delete_messages"}}}
|
|
p, _ := Compute(sp(), "sha256:x", provisioned(true), f)
|
|
a := find(p, Block, "channel.public.admin")
|
|
if a == nil {
|
|
t.Fatalf("widened rights must block:\n%s", p.Render())
|
|
}
|
|
if !strings.Contains(a.Why, "not allowed to exercise") {
|
|
t.Errorf("block should explain why: %q", a.Why)
|
|
}
|
|
}
|
|
|
|
// A demoted bot blocks too -- losing the right is as much a drift as gaining one.
|
|
func TestLostRightsBlock(t *testing.T) {
|
|
f := fake{botExists: true, usernames: map[int64]string{-200: "hallofhelix"},
|
|
rights: map[int64][]string{-100: {}}}
|
|
p, _ := Compute(sp(), "sha256:x", provisioned(true), f)
|
|
if find(p, Block, "channel.test.admin") == nil {
|
|
t.Fatalf("a demoted bot must block:\n%s", p.Render())
|
|
}
|
|
}
|
|
|
|
// A username that changed underneath us is not reconciled.
|
|
func TestUsernameTakeoverBlocks(t *testing.T) {
|
|
f := fake{botExists: true, usernames: map[int64]string{-200: "someoneelse"}}
|
|
p, _ := Compute(sp(), "sha256:x", provisioned(true), f)
|
|
if find(p, Block, "channel.public.username") == nil {
|
|
t.Fatalf("a changed username must block:\n%s", p.Render())
|
|
}
|
|
}
|
|
|
|
// A missing bot blocks rather than being re-created, which would orphan the
|
|
// token already in OpenBao.
|
|
func TestMissingBotBlocks(t *testing.T) {
|
|
f := fake{botExists: false}
|
|
p, _ := Compute(sp(), "sha256:x", provisioned(true), f)
|
|
if find(p, Block, "bot") == nil {
|
|
t.Fatalf("an unreachable bot must block:\n%s", p.Render())
|
|
}
|
|
}
|
|
|
|
// Removing a channel from the spec warns; it never deletes.
|
|
func TestRemovedChannelWarnsNeverDeletes(t *testing.T) {
|
|
s := sp()
|
|
delete(s.Channels, spec.Live)
|
|
f := fake{botExists: true}
|
|
p, _ := Compute(s, "sha256:x", provisioned(true), f)
|
|
a := find(p, Warn, "channel.public")
|
|
if a == nil {
|
|
t.Fatalf("expected a warning:\n%s", p.Render())
|
|
}
|
|
for _, act := range p.Actions {
|
|
if strings.Contains(strings.ToLower(act.Detail), "delete") && act.Kind != Warn {
|
|
t.Errorf("plan proposed a deletion: %+v", act)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pointing a state file at a different campaign is a mistake, not a rename.
|
|
func TestCampaignMismatchBlocks(t *testing.T) {
|
|
rs := provisioned(true)
|
|
rs.Campaign = "some-other-campaign"
|
|
p, _ := Compute(sp(), "sha256:x", rs, fake{botExists: true})
|
|
if find(p, Block, "campaign") == nil {
|
|
t.Fatalf("campaign mismatch must block:\n%s", p.Render())
|
|
}
|
|
}
|
|
|
|
func TestRenderShowsRefusalsFirst(t *testing.T) {
|
|
rs := provisioned(true)
|
|
rs.Campaign = "some-other-campaign"
|
|
p, _ := Compute(sp(), "sha256:x", rs, fake{botExists: true})
|
|
out := p.Render()
|
|
if !strings.Contains(out, "nothing will be applied") {
|
|
t.Errorf("a blocked plan should say so:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "BLOCK") {
|
|
t.Error("expected a BLOCK in the render")
|
|
}
|
|
}
|
|
|
|
// A deferral is not a block: the two must not collapse into each other, or a
|
|
// first run can never apply anything.
|
|
func TestDeferIsNotBlock(t *testing.T) {
|
|
p, _ := Compute(sp(), "sha256:x", &state.Resolved{}, fake{})
|
|
if p.Blocked() {
|
|
t.Fatal("a deferral must not block the plan")
|
|
}
|
|
f := fake{botExists: true, usernames: map[int64]string{-200: "someoneelse"}}
|
|
p2, _ := Compute(sp(), "sha256:x", provisioned(true), f)
|
|
if !p2.Blocked() {
|
|
t.Fatal("real drift must block")
|
|
}
|
|
}
|
|
|
|
// Once the test channel is verified, the public channel stops being deferred.
|
|
func TestVerifiedTestChannelReleasesPublic(t *testing.T) {
|
|
f := fake{botExists: true, usernames: map[int64]string{-200: "hallofhelix"}}
|
|
p, _ := Compute(sp(), "sha256:x", provisioned(true), f)
|
|
if find(p, Defer, "channel.public") != nil {
|
|
t.Errorf("public should no longer be deferred:\n%s", p.Render())
|
|
}
|
|
}
|