Migrations were run by hand and nothing recorded which had run, so re-running the setup script against an existing database failed on the first CREATE TABLE, and shipping a new migration gave an operator no way to know whether an estate had it. A missed migration is not a startup error - it is a query referencing a column that is not there, surfacing later on whichever endpoint touches it first. server/internal/migrate applies pending migrations at boot and refuses to start against a schema it does not match. One transaction per file holding both the DDL and the row that records it; an advisory lock so two servers starting at once cannot both apply 008; checksums so an edited migration is refused by name rather than silently skipped; numeric ordering so 010 does not run before 009. `migrate -baseline N` adopts a database built before any of this existed, because "the clients table exists" does not say whether 007's index does. Verified on the live database: adopted 001-007, applied 008. 008 adds two indexes on `purchases`, found by asking the database which foreign keys had nothing behind them and then checking what queries the table. The conversion report filters client_id + occurred_at, which is exactly the estate-wide case with no site to narrow it. run-local.sh had two bugs, both found by running it rather than reading it: it reused a broker container whose bind mount pointed at a directory that no longer existed, and it discarded stderr on the mosquitto_passwd call, so under `set -e` it exited at step 5 with no output at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
96 lines
3.1 KiB
Go
96 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
|
|
agentcfg "github.com/loyaly/behavision-agent/pkg/config"
|
|
agentpaths "github.com/loyaly/behavision-agent/pkg/paths"
|
|
|
|
"github.com/loyaly/behavision-desktop/internal/cloud"
|
|
)
|
|
|
|
// newTestApp builds an App around a config on disk, without startup(): the
|
|
// supervisor, the bridge and the broker all belong to a running PC and none of
|
|
// them is what these assertions are about.
|
|
func newTestApp(t *testing.T, cfg agentcfg.Config) *App {
|
|
t.Helper()
|
|
t.Setenv("BEHAVISION_DATA_DIR", t.TempDir())
|
|
if err := cfg.Save(agentpaths.AgentConfig()); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
return &App{cfg: cfg, cloud: cloud.New("https://example.invalid")}
|
|
}
|
|
|
|
// A fresh install shows the setup screen. That is the state the whole
|
|
// standalone option exists to offer a way out of.
|
|
func TestAFreshPCIsNeitherClaimedNorStandalone(t *testing.T) {
|
|
a := newTestApp(t, agentcfg.Defaults())
|
|
s := a.Session()
|
|
if s.Claimed || s.Standalone || s.LoggedIn {
|
|
t.Fatalf("a blank install reported %+v", s)
|
|
}
|
|
}
|
|
|
|
// The choice has to reach disk. One that only lives in memory puts the
|
|
// enrolment-code screen back in front of a shop that already answered "we have
|
|
// no head office", which reads as the app forgetting it was ever set up.
|
|
func TestRunStandaloneIsRememberedAcrossARestart(t *testing.T) {
|
|
a := newTestApp(t, agentcfg.Defaults())
|
|
s, err := a.RunStandalone()
|
|
if err != nil {
|
|
t.Fatalf("RunStandalone: %v", err)
|
|
}
|
|
if !s.Standalone {
|
|
t.Fatal("the session did not report standalone")
|
|
}
|
|
|
|
// What the next launch sees.
|
|
back, err := agentcfg.Load(agentpaths.AgentConfig())
|
|
if err != nil {
|
|
t.Fatalf("reload: %v", err)
|
|
}
|
|
if !back.Standalone {
|
|
t.Fatal("standalone was not persisted")
|
|
}
|
|
if (&App{cfg: back, cloud: cloud.New("https://example.invalid")}).
|
|
Session().Standalone != true {
|
|
t.Fatal("a reloaded standalone config did not report standalone")
|
|
}
|
|
}
|
|
|
|
// Claimed wins. A PC that has been linked is not standalone whatever the flag
|
|
// says, or the head-office screens stay hidden on the one machine that just
|
|
// earned them.
|
|
func TestBeingClaimedOverridesTheStandaloneFlag(t *testing.T) {
|
|
cfg := agentcfg.Defaults()
|
|
cfg.Standalone = true
|
|
cfg.ClientID, cfg.SiteID, cfg.BrokerURL = "acme", "chennai", "tls://broker:8883"
|
|
a := newTestApp(t, cfg)
|
|
s := a.Session()
|
|
if !s.Claimed {
|
|
t.Fatal("a configured PC did not report itself claimed")
|
|
}
|
|
if s.Standalone {
|
|
t.Fatal("a claimed PC still reported standalone")
|
|
}
|
|
if a.PipelineStatus().Standalone {
|
|
t.Fatal("the pipeline status still reported standalone")
|
|
}
|
|
}
|
|
|
|
// "Nothing is being sent because this PC is on its own" and "nothing is being
|
|
// sent and something is wrong" look identical from the counters alone, and only
|
|
// one of them is a fault.
|
|
func TestPipelineStatusSeparatesStandaloneFromUnclaimed(t *testing.T) {
|
|
unclaimed := newTestApp(t, agentcfg.Defaults()).PipelineStatus()
|
|
if unclaimed.Claimed || unclaimed.Standalone {
|
|
t.Fatalf("unclaimed PC reported %+v", unclaimed)
|
|
}
|
|
cfg := agentcfg.Defaults()
|
|
cfg.Standalone = true
|
|
alone := newTestApp(t, cfg).PipelineStatus()
|
|
if alone.Claimed || !alone.Standalone {
|
|
t.Fatalf("standalone PC reported %+v", alone)
|
|
}
|
|
}
|