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
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package spec
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The real specs must load. If either stops validating, the tool has drifted
|
|
// away from the artifacts it exists to read.
|
|
func TestRealSpecsLoad(t *testing.T) {
|
|
for _, p := range []string{
|
|
"../../presence/telegram.example.yaml",
|
|
"../../../pr-hall-of-helix/presence/telegram.yaml",
|
|
} {
|
|
if _, err := os.Stat(p); err != nil {
|
|
t.Skipf("not present in this checkout: %s", p)
|
|
}
|
|
got, digest, err := Load(p)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", p, err)
|
|
}
|
|
if got.Campaign == "" || !strings.HasPrefix(digest, "sha256:") {
|
|
t.Fatalf("%s: campaign=%q digest=%q", p, got.Campaign, digest)
|
|
}
|
|
}
|
|
}
|
|
|
|
func base() Presence {
|
|
return Presence{
|
|
SchemaVersion: "0.1",
|
|
Campaign: "hall-of-helix",
|
|
Interface: "helix-forge-telegram-publishing",
|
|
Bot: Bot{
|
|
Name: "HelixForge",
|
|
UsernamePreference: []string{"HelixForgeBot"},
|
|
About: "about",
|
|
Description: "description",
|
|
},
|
|
Channels: map[string]Channel{
|
|
Test: {Title: "t", Visibility: Private, AdminRights: []string{PostMessages}},
|
|
Live: {Title: "p", Visibility: Public, UsernamePreference: []string{"hallofhelix"},
|
|
AdminRights: []string{PostMessages}},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Every rule the schema states, asserted here too. These are the cases the
|
|
// schema file rejects; if this table and presence/telegram.schema.yaml ever
|
|
// disagree, one of them is wrong and a spec will pass one gate and fail another.
|
|
func TestValidateRejects(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*Presence)
|
|
want string
|
|
}{
|
|
{"extra admin right", func(p *Presence) {
|
|
c := p.Channels[Live]
|
|
c.AdminRights = []string{PostMessages, "can_delete_messages"}
|
|
p.Channels[Live] = c
|
|
}, "can_delete_messages"},
|
|
{"wrong admin right", func(p *Presence) {
|
|
c := p.Channels[Live]
|
|
c.AdminRights = []string{"can_delete_messages"}
|
|
p.Channels[Live] = c
|
|
}, "refused"},
|
|
{"private channel with username", func(p *Presence) {
|
|
c := p.Channels[Test]
|
|
c.UsernamePreference = []string{"secretchan"}
|
|
p.Channels[Test] = c
|
|
}, "must not declare a username"},
|
|
{"public channel without username", func(p *Presence) {
|
|
c := p.Channels[Live]
|
|
c.UsernamePreference = nil
|
|
p.Channels[Live] = c
|
|
}, "must declare at least one"},
|
|
{"bot username not ending in bot", func(p *Presence) {
|
|
p.Bot.UsernamePreference = []string{"HelixForge"}
|
|
}, `end in "bot"`},
|
|
{"no bot username candidates", func(p *Presence) {
|
|
p.Bot.UsernamePreference = nil
|
|
}, "at least one candidate"},
|
|
{"missing test channel", func(p *Presence) {
|
|
delete(p.Channels, Test)
|
|
}, "channels.test is required"},
|
|
{"unknown channel key", func(p *Presence) {
|
|
p.Channels["archive"] = Channel{Title: "a", Visibility: Private}
|
|
}, "not a known channel"},
|
|
{"bad visibility", func(p *Presence) {
|
|
c := p.Channels[Test]
|
|
c.Visibility = "unlisted"
|
|
p.Channels[Test] = c
|
|
}, "visibility must be"},
|
|
{"wrong schema version", func(p *Presence) {
|
|
p.SchemaVersion = "0.2"
|
|
}, "schema_version"},
|
|
{"campaign not a slug", func(p *Presence) {
|
|
p.Campaign = "Hall Of Helix"
|
|
}, "lowercase slug"},
|
|
{"about too long", func(p *Presence) {
|
|
p.Bot.About = strings.Repeat("x", 121)
|
|
}, "limit is 120"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
p := base()
|
|
tc.mutate(&p)
|
|
err := p.Validate()
|
|
if err == nil {
|
|
t.Fatalf("accepted a spec that must be rejected")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("error did not mention %q:\n%v", tc.want, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateAcceptsBase(t *testing.T) {
|
|
p := base()
|
|
if err := p.Validate(); err != nil {
|
|
t.Fatalf("rejected a valid spec: %v", err)
|
|
}
|
|
}
|
|
|
|
// An unknown field is a mistake about what is being declared, not a comment.
|
|
func TestUnknownFieldRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "s.yaml")
|
|
os.WriteFile(f, []byte(`
|
|
presence:
|
|
schema_version: "0.1"
|
|
campaign: "c"
|
|
interface: "i"
|
|
pinned: true
|
|
bot: {name: "n", username_preference: ["aBot"], about: "a", description: "d"}
|
|
channels:
|
|
test: {title: "t", visibility: private}
|
|
public: {title: "p", visibility: public, username_preference: ["abcdef"]}
|
|
`), 0o600)
|
|
if _, _, err := Load(f); err == nil {
|
|
t.Fatal("accepted an unknown field")
|
|
}
|
|
}
|
|
|
|
// Rights are the tool's decision, not the spec's.
|
|
func TestRightsAlwaysClamped(t *testing.T) {
|
|
c := Channel{AdminRights: nil}
|
|
got := c.Rights()
|
|
if len(got) != 1 || got[0] != PostMessages {
|
|
t.Fatalf("Rights() = %v, want [%s]", got, PostMessages)
|
|
}
|
|
}
|