Files
backend_fiesta/config/assistant_test.go
2026-09-25 11:55:50 +05:30

262 lines
9.3 KiB
Go

package config
import (
"os"
"strings"
"testing"
)
// Why the assistant is off.
//
// "Off" was the same answer for four different mistakes, and the only symptom
// was a disabled composer. Nobody could tell "we have not switched it on" from
// "somebody misspelled a variable" — which is how it stayed off for days with
// both of us guessing.
func TestAFullyConfiguredAssistantIsOn(t *testing.T) {
cfg := AssistantConfig{
Provider: "openai", BaseURL: "https://api.groq.com/openai/v1",
APIKey: "k", Balanced: "openai/gpt-oss-120b",
}
if !cfg.Enabled() {
t.Fatalf("a complete config was refused: %s", cfg.Why())
}
if cfg.Why() != "" {
t.Fatalf("an enabled assistant gave a reason: %q", cfg.Why())
}
}
func TestEachMissingPieceNamesItself(t *testing.T) {
for name, tc := range map[string]struct {
cfg AssistantConfig
says string
}{
// Nothing set at all names the MODEL, not the provider. The provider is
// derived from the model now, so an empty one is a consequence rather
// than a cause — and sending an operator to set ASSISTANT_PROVIDER, a
// variable they no longer need, while the one they actually missed goes
// unmentioned, is the same "off for four reasons" problem in new words.
"nothing set at all": {AssistantConfig{}, "ASSISTANT_MODEL"},
"no provider": {
AssistantConfig{Balanced: "m", APIKey: "k"}, "ASSISTANT_PROVIDER"},
"unknown provider": {
AssistantConfig{Provider: "anthropik", Balanced: "m", APIKey: "k"}, "not one this server speaks"},
"no model": {AssistantConfig{Provider: "openai", APIKey: "k"}, "ASSISTANT_MODEL"},
"no api key": {AssistantConfig{Provider: "openai", Balanced: "m", BaseURL: "https://api.groq.com/openai/v1"}, "ASSISTANT_API_KEY"},
} {
why := tc.cfg.Why()
if why == "" {
t.Fatalf("%s: reported as working", name)
}
if !strings.Contains(why, tc.says) {
t.Fatalf("%s: does not name the problem: %q", name, why)
}
}
}
func TestALocalModelNeedsNoKey(t *testing.T) {
// Ollama and LM Studio need no credential, and demanding one would refuse
// the setup a developer is most likely to have on their own machine.
for _, base := range []string{
"http://localhost:11434/v1",
"http://127.0.0.1:1234/v1",
"http://host.docker.internal:11434/v1",
} {
cfg := AssistantConfig{Provider: "openai", BaseURL: base, Balanced: "llama3"}
if !cfg.Enabled() {
t.Fatalf("%s was refused without a key: %s", base, cfg.Why())
}
}
}
func TestAHostedModelWithoutAKeyIsRefusedBeforeItFailsAtRuntime(t *testing.T) {
// Otherwise the first question a shopkeeper asks comes back as a 401 from
// the provider, which reads as the assistant being broken rather than as a
// variable nobody set.
cfg := AssistantConfig{Provider: "openai", BaseURL: "https://api.groq.com/openai/v1", Balanced: "m"}
if cfg.Enabled() {
t.Fatal("a hosted provider with no key reported as ready")
}
}
func TestTheTierFallbackDoesNotHideAMissingModel(t *testing.T) {
// `fast` and `deep` fall back to balanced, so a config with only those two
// set has no model at all for the default tier.
cfg := AssistantConfig{Provider: "openai", APIKey: "k", Fast: "small", Deep: "big"}
if cfg.Enabled() {
t.Fatal("an assistant with no balanced model reported as ready")
}
if cfg.ModelFor("fast") != "" && cfg.ModelFor("balanced") != "" {
t.Fatal("balanced resolved to something despite being unset")
}
}
// Where a secret is allowed to live.
//
// `.env`, `.env.local` and `.env.production` are all tracked by git, so a key
// written to any of them is a key published. There was nowhere else, and the
// standing instruction was to export it in the shell on every run — which is
// the kind of instruction people route around by editing a tracked file.
func TestASecretsFileIsReadBeforeAnyTrackedEnvFile(t *testing.T) {
order := envFileOrder("local")
if len(order) == 0 || order[0] != ".env.secrets" {
t.Fatalf(".env.secrets is not read first, so a tracked file wins: %v", order)
}
// godotenv does not overwrite, so being first IS what makes it authoritative.
// Being merely present would let .env.local decide the key instead.
for _, tracked := range []string{".env.local", ".env"} {
for i, name := range order {
if name == tracked && i == 0 {
t.Fatalf("%s is read first; a secret there would be committed", tracked)
}
}
}
}
func TestTheEnvironmentsOwnFileBeatsTheSharedOne(t *testing.T) {
// `.env.production` must be consulted before the shared `.env`, or a
// production deployment silently takes the local defaults.
order := envFileOrder("production")
var production, shared int = -1, -1
for i, name := range order {
switch name {
case ".env.production":
production = i
case ".env":
shared = i
}
}
if production < 0 || shared < 0 || production > shared {
t.Fatalf("the environment's own file does not take precedence: %v", order)
}
}
// One variable, not four.
//
// The assistant sat switched off for days because `ASSISTANT_PROVIDER` had not
// been typed into a hosting platform's environment tab — a variable whose only
// correct value is "openai", because every endpoint this server speaks is
// OpenAI-compatible. The base URL and the model had one right answer too.
//
// So three of the four are constants now. The key is the only one that varies
// between deployments and the only one that cannot live in the repository.
func TestTheKeyAloneSwitchesTheAssistantOn(t *testing.T) {
for _, name := range []string{
"ASSISTANT_PROVIDER", "ASSISTANT_BASE_URL", "ASSISTANT_MODEL",
"ASSISTANT_MODEL_BALANCED", "ASSISTANT_MODEL_FAST", "ASSISTANT_MODEL_DEEP",
} {
t.Setenv(name, "")
}
t.Setenv("ASSISTANT_API_KEY", "gsk_not-a-real-key")
cfg := AssistantConfig{
Provider: assistantProvider(),
BaseURL: env("ASSISTANT_BASE_URL", defaultAssistantBaseURL),
APIKey: env("ASSISTANT_API_KEY", ""),
Balanced: env("ASSISTANT_MODEL_BALANCED", env("ASSISTANT_MODEL", defaultAssistantModel)),
}
if !cfg.Enabled() {
t.Fatalf("the key alone did not switch it on: %s", cfg.Why())
}
if cfg.Provider != "openai" {
t.Fatalf("provider defaulted to %q", cfg.Provider)
}
if cfg.ModelFor("fast") != defaultAssistantModel {
t.Fatalf("the fast tier fell through to %q", cfg.ModelFor("fast"))
}
}
func TestNoKeyIsStillOffAndSaysWhich(t *testing.T) {
// The defaults must not make an unconfigured deployment look ready. Without
// a key every question would reach Groq and come back 401, which reads as
// the assistant being broken rather than as not being set up.
cfg := AssistantConfig{
Provider: defaultAssistantProvider,
BaseURL: defaultAssistantBaseURL,
Balanced: defaultAssistantModel,
}
if cfg.Enabled() {
t.Fatal("reported ready with no key")
}
if !strings.Contains(cfg.Why(), "ASSISTANT_API_KEY") {
t.Fatalf("did not name the one variable left to set: %q", cfg.Why())
}
}
func TestEachDefaultIsStillOverridable(t *testing.T) {
// Running against Ollama on a laptop must not need a code change.
t.Setenv("ASSISTANT_PROVIDER", "ollama")
t.Setenv("ASSISTANT_BASE_URL", "http://localhost:11434/v1")
t.Setenv("ASSISTANT_MODEL", "llama3")
cfg := AssistantConfig{
Provider: assistantProvider(),
BaseURL: env("ASSISTANT_BASE_URL", defaultAssistantBaseURL),
APIKey: env("ASSISTANT_API_KEY", ""),
Balanced: env("ASSISTANT_MODEL_BALANCED", env("ASSISTANT_MODEL", defaultAssistantModel)),
}
if cfg.Provider != "ollama" || cfg.Balanced != "llama3" {
t.Fatalf("an override was ignored: %+v", cfg)
}
// Local endpoints need no key, so this must be on without one.
if !cfg.Enabled() {
t.Fatalf("a local model was refused: %s", cfg.Why())
}
}
// The container reads its own configuration from a file beside the binary.
//
// The Dockerfile copies `.env.production` into the runtime image and sets
// APP_ENV=production, so `loadEnvFiles` reads it on boot. This asserts the
// mechanism rather than the Dockerfile — a COPY line is easy to check by eye
// and easy to believe wrongly, and the failure it produces is a server that
// starts fine with a variable silently unset.
func TestTheEnvironmentFileBesideTheBinaryIsRead(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
if err := os.WriteFile(".env.production",
[]byte("ASSISTANT_API_KEY=from-the-file\n"), 0o600); err != nil {
t.Fatalf("writing the fixture: %v", err)
}
t.Setenv("APP_ENV", "production")
// Registered with t.Setenv first so it is restored on return, then removed:
// godotenv does not overwrite a variable that is PRESENT, and an empty
// string is present. Setting it to "" would have tested nothing.
t.Setenv("ASSISTANT_API_KEY", "placeholder")
os.Unsetenv("ASSISTANT_API_KEY")
loadEnvFiles()
if os.Getenv("ASSISTANT_API_KEY") != "from-the-file" {
t.Fatal("the environment file beside the binary was not read")
}
}
func TestThePlatformStillWinsOverTheFile(t *testing.T) {
// godotenv never overwrites a variable already in the environment, so a
// value set on the hosting platform overrides the committed file without
// the file having to change. Both mechanisms work; neither fights the other.
dir := t.TempDir()
t.Chdir(dir)
if err := os.WriteFile(".env.production",
[]byte("ASSISTANT_API_KEY=from-the-file\n"), 0o600); err != nil {
t.Fatalf("writing the fixture: %v", err)
}
t.Setenv("APP_ENV", "production")
t.Setenv("ASSISTANT_API_KEY", "from-the-platform")
loadEnvFiles()
if got := os.Getenv("ASSISTANT_API_KEY"); got != "from-the-platform" {
t.Fatalf("the file overrode the platform: ASSISTANT_API_KEY=%q", got)
}
}