134 lines
4.8 KiB
Go
134 lines
4.8 KiB
Go
package config
|
|
|
|
import (
|
|
"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)
|
|
}
|
|
}
|