Files
Behavision/agent/pkg/config/credentials_firstrun_test.go
Suriyakumarvijayanayagam 16f0e69cec A fresh shop PC could never authenticate to its own engine
The agent read the engine's generated credential file once, at startup.
On a brand new install that file does not exist yet: the agent starts the
engine, and the engine writes its credential seconds later. So the agent
held an empty credential for the life of the process and every call it
makes - health, stats, camera sync, the embedding for a visit - came back
401, with a tray showing a red engine that was running perfectly.

Measured on a fresh state directory today: three 401s, no camera ever
reconciled, and the engine left running the YAML-seeded main stream
instead of the sub-stream head office holds. The install script hid this
on Windows because setup runs the engine once before the app starts.

config.Creds resolves lazily and re-reads on a rejection; the camera
client, the supervisor and the desktop app's engine client all retry once
when it changes. A configured BEHAVISION_API_USER is never re-read - an
operator who set one means it. Tests pin the actual first-run ordering.

Also adds demo/, a one-screen live console for showing the whole chain:
camera, the six steps with a measured camera-to-cloud latency, the
customer editable in place, and the raw JSON a phone and a dashboard
receive from production side by side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-24 13:40:28 +05:30

78 lines
2.6 KiB
Go

package config
import (
"os"
"path/filepath"
"testing"
)
// The sequence on a brand new shop PC, in order:
//
// agent starts -> file does not exist yet
// agent starts the engine
// engine generates its credential and writes the file
// agent calls the engine -> must now succeed
//
// Read once at startup, the agent holds "" for the life of the process and
// every engine call is 401: health, stats, camera sync, the embedding for a
// visit. The tray shows a red engine that is running perfectly, and nothing
// says why. Measured on a fresh state directory before this existed.
func TestCredentialsArriveAfterTheAgentHasAlreadyLooked(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "api_credentials.txt")
creds := NewCreds(path, "", "") // nothing configured, file not there yet
if u, _ := creds.Get(); u != "" {
t.Fatalf("expected no credential before the engine has written one, got %q", u)
}
// the engine starts and writes its credential
if err := os.WriteFile(path, []byte("username=behavision\npassword=s3cret\n"), 0o600); err != nil {
t.Fatal(err)
}
// a 401 makes the agent look again
if !creds.Refresh() {
t.Fatal("Refresh did not pick up the credential the engine just wrote")
}
u, p := creds.Get()
if u != "behavision" || p != "s3cret" {
t.Fatalf("got %q/%q", u, p)
}
}
// An operator who set BEHAVISION_API_USER means it, and a file must never
// override them.
func TestAConfiguredCredentialIsNeverReplacedByTheFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "api_credentials.txt")
if err := os.WriteFile(path, []byte("username=generated\npassword=nope\n"), 0o600); err != nil {
t.Fatal(err)
}
creds := NewCreds(path, "chosen", "byhand")
if u, p := creds.Get(); u != "chosen" || p != "byhand" {
t.Fatalf("configured credential was replaced: %q/%q", u, p)
}
if creds.Refresh() {
t.Fatal("Refresh overrode a configured credential")
}
}
// A credential the engine regenerates under a running agent is picked up too -
// the same mechanism, and the reason paths.APICredentials says the agent reads
// the file "rather than storing a second copy".
func TestARegeneratedCredentialIsPickedUp(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "api_credentials.txt")
os.WriteFile(path, []byte("username=behavision\npassword=old\n"), 0o600)
creds := NewCreds(path, "", "")
creds.Get()
os.WriteFile(path, []byte("username=behavision\npassword=new\n"), 0o600)
if !creds.Refresh() {
t.Fatal("a regenerated password was not picked up")
}
if _, p := creds.Get(); p != "new" {
t.Fatalf("still holding %q", p)
}
}