fluid-telegram/internal/apply/apply_test.go
tegwick 1e9481e108 Finish the provisioner's remaining plane: honest check, adapter config, IEI-2
plan --check treated BotFather profile reassertion as drift, so a scheduled
check could never go quiet. Reassertion is now its own kind; a changed spec
digest is the drift signal. apply reasserts the profile, updates channel copy
when the spec changes, and is tested against a fake Telegram surface.

The adapter reads its chat id from resolved state and its token from OpenBao,
never the operator session. IEI-2 records that an approved provisioning plan
is not autonomous Daimon action.

Assistant: grok
Assistant-Session: 01a09d5a-92c1-7802-915b-92bee57fc9f0
2026-09-14 02:55:58 +02:00

310 lines
9 KiB
Go

package apply
import (
"bytes"
"context"
"fmt"
"path/filepath"
"strings"
"testing"
"time"
"github.com/tegwick/fluid-telegram/internal/plan"
"github.com/tegwick/fluid-telegram/internal/secrets"
"github.com/tegwick/fluid-telegram/internal/spec"
"github.com/tegwick/fluid-telegram/internal/state"
tgc "github.com/tegwick/fluid-telegram/internal/tg"
)
type memStore struct {
data map[string]map[string]string
}
func (s *memStore) CreateIfAbsent(_ context.Context, key string, fields map[string]string) (bool, error) {
if s.data == nil {
s.data = map[string]map[string]string{}
}
if _, ok := s.data[key]; ok {
return false, nil
}
s.data[key] = fields
return true, nil
}
func (s *memStore) Put(_ context.Context, key string, fields map[string]string) error {
if s.data == nil {
s.data = map[string]map[string]string{}
}
s.data[key] = fields
return nil
}
func (s *memStore) Ref(key string) string { return "bao:test/" + key }
type fakeConv struct {
taken map[string]bool
profiles int
}
func (c *fakeConv) RegisterBot(_ context.Context, _, username string) (string, string, error) {
if c.taken[username] {
return "", "", fmt.Errorf("%w: @%s", tgc.ErrUsernameTaken, username)
}
return username, "123:token-for-" + username, nil
}
func (c *fakeConv) SetProfile(context.Context, string, string, string, string) error {
c.profiles++
return nil
}
func (c *fakeConv) SetAvatar(context.Context, string, string) error { return nil }
type fakeSurface struct {
conv *fakeConv
nextChat int64
created []string
claimed []string
promoted []string
titles map[int64]string
abouts map[int64]string
botID int64
}
func (f *fakeSurface) OpenBotFather(context.Context) (BotConversation, error) {
if f.conv == nil {
f.conv = &fakeConv{}
}
return f.conv, nil
}
func (f *fakeSurface) ResolveBot(_ context.Context, _ string) (int64, error) {
if f.botID == 0 {
f.botID = 42
}
return f.botID, nil
}
func (f *fakeSurface) CreateChannel(_ context.Context, title, about string) (int64, error) {
f.nextChat--
id := f.nextChat
f.created = append(f.created, title)
if f.titles == nil {
f.titles = map[int64]string{}
f.abouts = map[int64]string{}
}
f.titles[id] = title
f.abouts[id] = about
return id, nil
}
func (f *fakeSurface) ClaimUsername(_ context.Context, _ int64, username string) (bool, error) {
f.claimed = append(f.claimed, username)
return true, nil
}
func (f *fakeSurface) PromoteBot(_ context.Context, _ int64, botUsername string) error {
f.promoted = append(f.promoted, botUsername)
return nil
}
func (f *fakeSurface) EditChannelTitle(_ context.Context, chatID int64, title string) error {
if f.titles == nil {
f.titles = map[int64]string{}
}
f.titles[chatID] = title
return nil
}
func (f *fakeSurface) EditChannelAbout(_ context.Context, chatID int64, about string) error {
if f.abouts == nil {
f.abouts = map[int64]string{}
}
f.abouts[chatID] = about
return nil
}
func presence() *spec.Presence {
return &spec.Presence{
SchemaVersion: "0.1", Campaign: "hall-of-helix", Interface: "i",
Bot: spec.Bot{Name: "HelixForge", UsernamePreference: []string{"HelixForgeBot"},
About: "about", Description: "desc"},
Channels: map[string]spec.Channel{
spec.Test: {Title: "test-title", Description: "test-about", Visibility: spec.Private},
spec.Live: {Title: "live-title", Description: "live-about", Visibility: spec.Public,
UsernamePreference: []string{"hallofhelix"}},
},
}
}
func opts(t *testing.T, rs *state.Resolved, surf *fakeSurface, store *memStore) Options {
t.Helper()
if rs == nil {
rs = &state.Resolved{}
}
if surf == nil {
surf = &fakeSurface{}
}
if store == nil {
store = &memStore{}
}
return Options{
Spec: presence(), SpecDigest: "sha256:x", State: rs,
StatePath: filepath.Join(t.TempDir(), "resolved.yaml"),
Store: store, Surface: surf, Out: &bytes.Buffer{},
}
}
func TestApplyRefusesBlockedPlan(t *testing.T) {
p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Block, Target: "bot", Detail: "gone"}}}
err := Run(context.Background(), p, opts(t, nil, nil, nil))
if err == nil {
t.Fatal("applied a blocked plan")
}
}
func TestFirstRunCreatesBotAndTestChannelOnly(t *testing.T) {
surf := &fakeSurface{}
store := &memStore{}
o := opts(t, nil, surf, store)
p := &plan.Plan{Actions: []plan.Action{
{Kind: plan.Attempt, Target: "bot"},
{Kind: plan.Create, Target: "channel.test"},
{Kind: plan.Defer, Target: "channel.public"},
}}
if err := Run(context.Background(), p, o); err != nil {
t.Fatal(err)
}
if store.data[secrets.KeyBotToken]["token"] == "" {
t.Fatal("bot token was not stored")
}
if _, ok := store.data[secrets.KeyRedactionSalt]; !ok {
t.Fatal("redaction salt was not created")
}
if o.State.Bot.Username != "HelixForgeBot" || o.State.Bot.ID == 0 {
t.Fatalf("bot state = %+v", o.State.Bot)
}
if _, ok := o.State.Channels[spec.Test]; !ok {
t.Fatal("test channel was not recorded")
}
if _, ok := o.State.Channels[spec.Live]; ok {
t.Fatal("public channel was created before the test channel was verified")
}
if len(surf.promoted) != 1 {
t.Fatalf("promotions = %v", surf.promoted)
}
}
func TestWalksUsernameCandidates(t *testing.T) {
surf := &fakeSurface{conv: &fakeConv{taken: map[string]bool{"HelixForgeBot": true}}}
sp := presence()
sp.Bot.UsernamePreference = []string{"HelixForgeBot", "HelixForgePublishBot"}
o := opts(t, nil, surf, nil)
o.Spec = sp
p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Attempt, Target: "bot"}}}
if err := Run(context.Background(), p, o); err != nil {
t.Fatal(err)
}
if o.State.Bot.Username != "HelixForgePublishBot" {
t.Fatalf("username = %s", o.State.Bot.Username)
}
}
func TestExistingBotReassertsProfile(t *testing.T) {
surf := &fakeSurface{}
now := time.Now()
rs := &state.Resolved{
Campaign: "hall-of-helix", SpecDigest: "sha256:x",
Bot: state.Bot{Username: "HelixForgeBot", ID: 42},
Channels: map[string]state.Channel{
spec.Test: {ChatID: -100, AdminRights: []string{spec.PostMessages}, TestPublicationAt: &now},
spec.Live: {ChatID: -200, Username: "hallofhelix", AdminRights: []string{spec.PostMessages}},
},
}
o := opts(t, rs, surf, &memStore{data: map[string]map[string]string{
secrets.KeyRedactionSalt: {"salt": "already"},
}})
p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Reassert, Target: "bot.profile"}}}
if err := Run(context.Background(), p, o); err != nil {
t.Fatal(err)
}
if surf.conv == nil || surf.conv.profiles == 0 {
t.Fatal("existing bot did not reassert its profile")
}
if len(surf.created) != 0 {
t.Fatalf("recreated channels: %v", surf.created)
}
}
func TestSpecChangeUpdatesChannelCopy(t *testing.T) {
surf := &fakeSurface{}
now := time.Now()
rs := &state.Resolved{
Campaign: "hall-of-helix", SpecDigest: "sha256:old",
Bot: state.Bot{Username: "HelixForgeBot", ID: 42},
Channels: map[string]state.Channel{
spec.Test: {ChatID: -100, AdminRights: []string{spec.PostMessages}, TestPublicationAt: &now},
spec.Live: {ChatID: -200, Username: "hallofhelix", AdminRights: []string{spec.PostMessages}},
},
}
o := opts(t, rs, surf, &memStore{data: map[string]map[string]string{
secrets.KeyRedactionSalt: {"salt": "already"},
}})
p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Update, Target: "spec"}}}
if err := Run(context.Background(), p, o); err != nil {
t.Fatal(err)
}
if surf.titles[-100] != "test-title" || surf.titles[-200] != "live-title" {
t.Fatalf("titles = %v", surf.titles)
}
}
func TestSaltIsCreatedOnce(t *testing.T) {
store := &memStore{}
p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Attempt, Target: "bot"}}}
if err := Run(context.Background(), p, opts(t, nil, nil, store)); err != nil {
t.Fatal(err)
}
first := store.data[secrets.KeyRedactionSalt]["salt"]
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}},
},
}
if err := Run(context.Background(), p, opts(t, rs, nil, store)); err != nil {
t.Fatal(err)
}
if store.data[secrets.KeyRedactionSalt]["salt"] != first {
t.Fatal("redaction salt was rotated")
}
}
func TestPublicReleasedAfterTestPublication(t *testing.T) {
now := time.Now()
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}, TestPublicationAt: &now},
},
}
surf := &fakeSurface{}
o := opts(t, rs, surf, &memStore{data: map[string]map[string]string{
secrets.KeyRedactionSalt: {"salt": "already"},
}})
p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Create, Target: "channel.public"}}}
if err := Run(context.Background(), p, o); err != nil {
t.Fatal(err)
}
pub, ok := o.State.Channels[spec.Live]
if !ok {
t.Fatal("public channel was not created")
}
if pub.Username != "hallofhelix" {
t.Fatalf("username = %s", pub.Username)
}
if !strings.Contains(strings.Join(surf.promoted, ","), "HelixForgeBot") {
t.Fatalf("bot was not promoted on public: %v", surf.promoted)
}
}