package config import "testing" func TestLoadA2AEdgeURLPrefersEdgeURL(t *testing.T) { t.Setenv("A2A_EDGE_URL", "http://edge.example/a2a") t.Setenv("A2A_AGENT_URL", "http://legacy.example/a2a") cfg := Load() if cfg.A2AEdgeURL != "http://edge.example/a2a" { t.Fatalf("A2AEdgeURL: got %q", cfg.A2AEdgeURL) } if cfg.A2AAgentURL != "http://legacy.example/a2a" { t.Fatalf("A2AAgentURL: got %q", cfg.A2AAgentURL) } } func TestLoadA2AEdgeURLFallsBackToLegacyAgentURL(t *testing.T) { t.Setenv("A2A_AGENT_URL", "http://legacy.example/a2a") cfg := Load() if cfg.A2AEdgeURL != "http://legacy.example/a2a" { t.Fatalf("A2AEdgeURL fallback: got %q", cfg.A2AEdgeURL) } if cfg.A2AAgentURL != "http://legacy.example/a2a" { t.Fatalf("A2AAgentURL: got %q", cfg.A2AAgentURL) } } func TestLoadModelAndA2ADefaults(t *testing.T) { cfg := Load() if cfg.ModelBaseURL != "http://toki-labs.com:18083/v1" { t.Fatalf("ModelBaseURL: got %q", cfg.ModelBaseURL) } if cfg.ModelAPIKey != "" { t.Fatalf("ModelAPIKey: got %q, want empty", cfg.ModelAPIKey) } if cfg.ModelName != "codex" { t.Fatalf("ModelName: got %q", cfg.ModelName) } if cfg.ModelContextSize != 0 { t.Fatalf("ModelContextSize: got %d", cfg.ModelContextSize) } if cfg.ModelTimeoutSec != 900 { t.Fatalf("ModelTimeoutSec: got %d", cfg.ModelTimeoutSec) } if cfg.ModelResponsesStream != false { t.Fatalf("ModelResponsesStream: got %t, want false", cfg.ModelResponsesStream) } if cfg.A2ATimeoutSec != 300 { t.Fatalf("A2ATimeoutSec: got %d", cfg.A2ATimeoutSec) } } func TestConfigLoadsModelResponsesStreamOverride(t *testing.T) { t.Setenv("MODEL_RESPONSES_STREAM", "true") cfg := Load() if cfg.ModelResponsesStream != true { t.Fatalf("ModelResponsesStream override: got %t, want true", cfg.ModelResponsesStream) } } func TestConfigLoadsAuthoringStaleAfterSecDefault(t *testing.T) { cfg := Load() if cfg.AuthoringStaleAfterSec != 1200 { t.Fatalf("AuthoringStaleAfterSec default: got %d, want 1200", cfg.AuthoringStaleAfterSec) } } func TestConfigLoadsAuthoringStaleAfterSecOverride(t *testing.T) { t.Setenv("AUTHORING_STALE_AFTER_SEC", "3600") cfg := Load() if cfg.AuthoringStaleAfterSec != 3600 { t.Fatalf("AuthoringStaleAfterSec override: got %d, want 3600", cfg.AuthoringStaleAfterSec) } } func TestConfigLoadsModelTimeoutOverride(t *testing.T) { t.Setenv("MODEL_TIMEOUT_SEC", "1800") cfg := Load() if cfg.ModelTimeoutSec != 1800 { t.Fatalf("ModelTimeoutSec override: got %d, want 1800", cfg.ModelTimeoutSec) } } func TestConfigLoadsWorkflowTaskTimeout(t *testing.T) { cfg := Load() if cfg.WorkflowTaskTimeoutSec != 300 { t.Fatalf("WorkflowTaskTimeoutSec default: got %d, want 300", cfg.WorkflowTaskTimeoutSec) } t.Setenv("WORKFLOW_TASK_TIMEOUT_SEC", "120") cfgOverride := Load() if cfgOverride.WorkflowTaskTimeoutSec != 120 { t.Fatalf("WorkflowTaskTimeoutSec override: got %d, want 120", cfgOverride.WorkflowTaskTimeoutSec) } } func TestConfigLoadsProtoSocketDefaultsAndOverrides(t *testing.T) { cfg := Load() if cfg.ProtoSocketPath != "/proto-socket" { t.Fatalf("ProtoSocketPath default: got %q, want %q", cfg.ProtoSocketPath, "/proto-socket") } if cfg.ProtoSocketHeartbeatIntervalSec != 30 { t.Fatalf("ProtoSocketHeartbeatIntervalSec default: got %d, want 30", cfg.ProtoSocketHeartbeatIntervalSec) } if cfg.ProtoSocketHeartbeatWaitSec != 10 { t.Fatalf("ProtoSocketHeartbeatWaitSec default: got %d, want 10", cfg.ProtoSocketHeartbeatWaitSec) } t.Setenv("PROTO_SOCKET_PATH", "/custom-path") t.Setenv("PROTO_SOCKET_HEARTBEAT_INTERVAL_SEC", "15") t.Setenv("PROTO_SOCKET_HEARTBEAT_WAIT_SEC", "5") cfgOverride := Load() if cfgOverride.ProtoSocketPath != "/custom-path" { t.Fatalf("ProtoSocketPath override: got %q, want %q", cfgOverride.ProtoSocketPath, "/custom-path") } if cfgOverride.ProtoSocketHeartbeatIntervalSec != 15 { t.Fatalf("ProtoSocketHeartbeatIntervalSec override: got %d, want 15", cfgOverride.ProtoSocketHeartbeatIntervalSec) } if cfgOverride.ProtoSocketHeartbeatWaitSec != 5 { t.Fatalf("ProtoSocketHeartbeatWaitSec override: got %d, want 5", cfgOverride.ProtoSocketHeartbeatWaitSec) } } func TestConfigLoadsJiraEnvVars(t *testing.T) { t.Setenv("JIRA_BASE_URL", "https://jira.example.com") t.Setenv("JIRA_EMAIL", "user@example.com") t.Setenv("JIRA_API_TOKEN", "api-token-123") cfg := Load() if cfg.JiraBaseURL != "https://jira.example.com" { t.Fatalf("JiraBaseURL: got %q", cfg.JiraBaseURL) } if cfg.JiraEmail != "user@example.com" { t.Fatalf("JiraEmail: got %q", cfg.JiraEmail) } if cfg.JiraAPIToken != "api-token-123" { t.Fatalf("JiraAPIToken: got %q", cfg.JiraAPIToken) } } func TestConfigLoadsMattermostEnvVars(t *testing.T) { t.Setenv("MATTERMOST_BASE_URL", "https://mattermost.example.com") t.Setenv("MATTERMOST_TOKEN", "mattermost-token") t.Setenv("MATTERMOST_CHANNEL_ID", "channel-id") cfg := Load() if cfg.MattermostBaseURL != "https://mattermost.example.com" { t.Fatalf("MattermostBaseURL: got %q", cfg.MattermostBaseURL) } if cfg.MattermostToken != "mattermost-token" { t.Fatalf("MattermostToken: got %q", cfg.MattermostToken) } if cfg.MattermostChannelID != "channel-id" { t.Fatalf("MattermostChannelID: got %q", cfg.MattermostChannelID) } } func TestConfigMattermostFieldsAreEmptyByDefault(t *testing.T) { cfg := Load() if cfg.MattermostBaseURL != "" { t.Fatalf("MattermostBaseURL default: got %q, want empty", cfg.MattermostBaseURL) } if cfg.MattermostToken != "" { t.Fatalf("MattermostToken default: got %q, want empty", cfg.MattermostToken) } if cfg.MattermostChannelID != "" { t.Fatalf("MattermostChannelID default: got %q, want empty", cfg.MattermostChannelID) } } func TestConfigGitoBranchDefaultsAndDisabled(t *testing.T) { cfg := Load() if cfg.GitoProtoSocketURL != "" { t.Fatalf("GitoProtoSocketURL default: got %q, want empty", cfg.GitoProtoSocketURL) } if cfg.GitoRepoID != "" { t.Fatalf("GitoRepoID default: got %q, want empty", cfg.GitoRepoID) } if cfg.GitoBranch != "develop" { t.Fatalf("GitoBranch default: got %q, want develop", cfg.GitoBranch) } if cfg.GitoDevelopRepoPath != "" { t.Fatalf("GitoDevelopRepoPath default: got %q, want empty", cfg.GitoDevelopRepoPath) } if cfg.GitoRemoteName != "origin" { t.Fatalf("GitoRemoteName default: got %q, want origin", cfg.GitoRemoteName) } if cfg.RoadmapCreationTodoStateID != "" { t.Fatalf("RoadmapCreationTodoStateID default: got %q, want empty", cfg.RoadmapCreationTodoStateID) } if cfg.GitoBranchEventsEnabled() { t.Fatal("GitoBranchEventsEnabled: want false when endpoint/repo unset") } } func TestConfigGitoBranchEventsEnabledRequiresAllInputs(t *testing.T) { t.Setenv("GITO_PROTO_SOCKET_URL", "ws://gito.example/proto-socket") if Load().GitoBranchEventsEnabled() { t.Fatal("GitoBranchEventsEnabled: want false when repo id is unset") } t.Setenv("GITO_REPO_ID", "nomadcode") if Load().GitoBranchEventsEnabled() { t.Fatal("GitoBranchEventsEnabled: want false when develop repo path is unset") } t.Setenv("GITO_DEVELOP_REPO_PATH", "/srv/develop") if Load().GitoBranchEventsEnabled() { t.Fatal("GitoBranchEventsEnabled: want false when todo state id is unset") } t.Setenv("ROADMAP_CREATION_TODO_STATE_ID", "state-todo") t.Setenv("GITO_BRANCH", "release") cfg := Load() if cfg.GitoProtoSocketURL != "ws://gito.example/proto-socket" { t.Fatalf("GitoProtoSocketURL: got %q", cfg.GitoProtoSocketURL) } if cfg.GitoRepoID != "nomadcode" { t.Fatalf("GitoRepoID: got %q", cfg.GitoRepoID) } if cfg.GitoBranch != "release" { t.Fatalf("GitoBranch override: got %q", cfg.GitoBranch) } if cfg.GitoDevelopRepoPath != "/srv/develop" { t.Fatalf("GitoDevelopRepoPath: got %q", cfg.GitoDevelopRepoPath) } if cfg.RoadmapCreationTodoStateID != "state-todo" { t.Fatalf("RoadmapCreationTodoStateID: got %q", cfg.RoadmapCreationTodoStateID) } if !cfg.GitoBranchEventsEnabled() { t.Fatal("GitoBranchEventsEnabled: want true when all inputs set") } } func TestConfigRoadmapCreationTodoStateIDFallsBackToPlaneEnv(t *testing.T) { t.Setenv("PLANE_TODO_STATE_ID", "plane-state") cfg := Load() if cfg.RoadmapCreationTodoStateID != "plane-state" { t.Fatalf("RoadmapCreationTodoStateID fallback: got %q, want plane-state", cfg.RoadmapCreationTodoStateID) } t.Setenv("ROADMAP_CREATION_TODO_STATE_ID", "roadmap-state") if Load().RoadmapCreationTodoStateID != "roadmap-state" { t.Fatalf("RoadmapCreationTodoStateID: ROADMAP_CREATION_TODO_STATE_ID should win") } } func TestConfigPlaneWebhookDispatchFieldsAreEmptyByDefault(t *testing.T) { cfg := Load() if cfg.PlaneWorkspaceSlug != "" { t.Fatalf("PlaneWorkspaceSlug default: got %q, want empty", cfg.PlaneWorkspaceSlug) } if cfg.PlaneWorkspaceID != "" { t.Fatalf("PlaneWorkspaceID default: got %q, want empty", cfg.PlaneWorkspaceID) } if cfg.PlaneCreationBacklogStateID != "" { t.Fatalf("PlaneCreationBacklogStateID default: got %q, want empty", cfg.PlaneCreationBacklogStateID) } if cfg.PlaneAgentAssigneeID != "" { t.Fatalf("PlaneAgentAssigneeID default: got %q, want empty", cfg.PlaneAgentAssigneeID) } if cfg.PlaneSelfActorID != "" { t.Fatalf("PlaneSelfActorID default: got %q, want empty", cfg.PlaneSelfActorID) } } func TestConfigLoadsPlaneWebhookDispatchEnv(t *testing.T) { t.Setenv("PLANE_WORKSPACE_SLUG", "general") t.Setenv("PLANE_WORKSPACE_ID", "dadf050e-cd1e-4590-bc33-672511630841") t.Setenv("PLANE_BACKLOG_STATE_ID", "backlog-state") t.Setenv("PLANE_AGENT_ASSIGNEE_ID", "agent-assignee") t.Setenv("PLANE_SELF_ACTOR_ID", "self-actor") cfg := Load() if cfg.PlaneWorkspaceSlug != "general" { t.Fatalf("PlaneWorkspaceSlug: got %q", cfg.PlaneWorkspaceSlug) } if cfg.PlaneWorkspaceID != "dadf050e-cd1e-4590-bc33-672511630841" { t.Fatalf("PlaneWorkspaceID: got %q", cfg.PlaneWorkspaceID) } if cfg.PlaneCreationBacklogStateID != "backlog-state" { t.Fatalf("PlaneCreationBacklogStateID from PLANE_BACKLOG_STATE_ID: got %q", cfg.PlaneCreationBacklogStateID) } if cfg.PlaneAgentAssigneeID != "agent-assignee" { t.Fatalf("PlaneAgentAssigneeID: got %q", cfg.PlaneAgentAssigneeID) } if cfg.PlaneSelfActorID != "self-actor" { t.Fatalf("PlaneSelfActorID: got %q", cfg.PlaneSelfActorID) } // PLANE_CREATION_BACKLOG_STATE_ID takes precedence over the PLANE_BACKLOG_STATE_ID alias. t.Setenv("PLANE_CREATION_BACKLOG_STATE_ID", "creation-backlog-state") if got := Load().PlaneCreationBacklogStateID; got != "creation-backlog-state" { t.Fatalf("PlaneCreationBacklogStateID: PLANE_CREATION_BACKLOG_STATE_ID should win, got %q", got) } } func TestConfigGitoWebhookDefaultsAndDisabled(t *testing.T) { cfg := Load() if cfg.GitoWebhookSecret != "" { t.Fatalf("GitoWebhookSecret default: got %q, want empty", cfg.GitoWebhookSecret) } if cfg.GitoWebhookConsumerEnabled() { t.Fatal("GitoWebhookConsumerEnabled: want false when secret is unset") } } func TestConfigGitoWebhookConsumerEnabledRequiresInputs(t *testing.T) { t.Setenv("GITO_WEBHOOK_SECRET", "webhook-secret") if Load().GitoWebhookConsumerEnabled() { t.Fatal("GitoWebhookConsumerEnabled: want false when repo id is unset") } t.Setenv("GITO_REPO_ID", "nomadcode") if Load().GitoWebhookConsumerEnabled() { t.Fatal("GitoWebhookConsumerEnabled: want false when develop repo path is unset") } t.Setenv("GITO_DEVELOP_REPO_PATH", "/srv/develop") if Load().GitoWebhookConsumerEnabled() { t.Fatal("GitoWebhookConsumerEnabled: want false when todo state id is unset") } t.Setenv("ROADMAP_CREATION_TODO_STATE_ID", "state-todo") cfg := Load() if cfg.GitoWebhookSecret != "webhook-secret" { t.Fatalf("GitoWebhookSecret: got %q", cfg.GitoWebhookSecret) } if !cfg.GitoWebhookConsumerEnabled() { t.Fatal("GitoWebhookConsumerEnabled: want true when all inputs set") } } func TestConfigJiraFieldsAreEmptyByDefault(t *testing.T) { cfg := Load() if cfg.JiraBaseURL != "" { t.Fatalf("JiraBaseURL default: got %q, want empty", cfg.JiraBaseURL) } if cfg.JiraEmail != "" { t.Fatalf("JiraEmail default: got %q, want empty", cfg.JiraEmail) } if cfg.JiraAPIToken != "" { t.Fatalf("JiraAPIToken default: got %q, want empty", cfg.JiraAPIToken) } }