353 lines
15 KiB
Go
353 lines
15 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestConfigurationCatalogIsComplete(t *testing.T) {
|
|
if err := ValidateCatalog(); err != nil {
|
|
t.Fatalf("ValidateCatalog: %v", err)
|
|
}
|
|
entries := ConfigurationCatalog()
|
|
if len(entries) < 50 {
|
|
t.Fatalf("catalog has %d entries, want the stable startup inventory", len(entries))
|
|
}
|
|
for _, key := range []string{
|
|
"server.trustedProxies",
|
|
"tasks.preparationTimeout",
|
|
"credentials.file",
|
|
"limits.ghMaxConcurrent",
|
|
"limits.lspMaxConnections",
|
|
"messageQueue.maxPerSession",
|
|
"agentctl.notificationQueueCapacity",
|
|
"launcher.noBrowser",
|
|
} {
|
|
if _, ok := CatalogEntryForKey(key); !ok {
|
|
t.Errorf("catalog is missing %q", key)
|
|
}
|
|
}
|
|
entry, ok := CatalogEntryForKey("limits.lspMaxConnections")
|
|
if !ok || !strings.Contains(entry.Description, "including leases detached from a browser") {
|
|
t.Fatalf("LSP capacity description = %q, want detached leases included", entry.Description)
|
|
}
|
|
}
|
|
|
|
func TestConfigurationCatalogMatchesAuditedEnvironmentInventory(t *testing.T) {
|
|
if err := validateCatalogAgainstAuditedInventory(ConfigurationCatalog(), ConfigurationExclusions(), auditedStartupEnvironmentInventory()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestConfigurationCatalogAuditRejectsUncatalogedEnvironmentVariable(t *testing.T) {
|
|
err := validateCatalogAgainstAuditedInventory(
|
|
[]CatalogEntry{{Key: "test.setting", EnvVars: []string{"KANDEV_UNCATALOGED_TEST"}}},
|
|
nil,
|
|
nil,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("catalog audit accepted an environment variable absent from the audited inventory")
|
|
}
|
|
if !strings.Contains(err.Error(), "KANDEV_UNCATALOGED_TEST") {
|
|
t.Fatalf("catalog audit error = %v, want the uncataloged variable", err)
|
|
}
|
|
}
|
|
|
|
type auditedStartupEnvironment struct {
|
|
envVar string
|
|
class string
|
|
}
|
|
|
|
// auditedStartupEnvironmentInventory is intentionally independent of
|
|
// startupCatalog. Update it only after auditing every stable environment
|
|
// consumer and every reviewed exclusion.
|
|
func auditedStartupEnvironmentInventory() []auditedStartupEnvironment {
|
|
return []auditedStartupEnvironment{
|
|
{envVar: "KANDEV_HOME_DIR", class: "catalog"},
|
|
{envVar: "KANDEV_SERVER_HOST", class: "catalog"},
|
|
{envVar: "KANDEV_SERVER_PORT", class: "catalog"},
|
|
{envVar: "KANDEV_BACKEND_PORT", class: "catalog"},
|
|
{envVar: "KANDEV_PORT", class: "catalog"},
|
|
{envVar: "KANDEV_SERVER_READTIMEOUT", class: "catalog"},
|
|
{envVar: "KANDEV_SERVER_WRITETIMEOUT", class: "catalog"},
|
|
{envVar: "KANDEV_WEB_INTERNAL_URL", class: "catalog"},
|
|
{envVar: "KANDEV_WEB_TITLE_PREFIX", class: "catalog"},
|
|
{envVar: "KANDEV_TRUSTED_PROXIES", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_DRIVER", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_PATH", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_HOST", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_PORT", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_USER", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_PASSWORD", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_DBNAME", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_SSLMODE", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_MAXCONNS", class: "catalog"},
|
|
{envVar: "KANDEV_DATABASE_MINCONNS", class: "catalog"},
|
|
{envVar: "KANDEV_NATS_URL", class: "catalog"},
|
|
{envVar: "KANDEV_NATS_CLUSTERID", class: "catalog"},
|
|
{envVar: "KANDEV_NATS_CLIENTID", class: "catalog"},
|
|
{envVar: "KANDEV_NATS_MAXRECONNECTS", class: "catalog"},
|
|
{envVar: "KANDEV_EVENTS_NAMESPACE", class: "catalog"},
|
|
{envVar: "KANDEV_DOCKER_ENABLED", class: "catalog"},
|
|
{envVar: "KANDEV_DOCKER_HOST", class: "catalog"},
|
|
{envVar: "DOCKER_HOST", class: "catalog"},
|
|
{envVar: "KANDEV_DOCKER_APIVERSION", class: "catalog"},
|
|
{envVar: "KANDEV_DOCKER_TLSVERIFY", class: "catalog"},
|
|
{envVar: "KANDEV_DOCKER_DEFAULTNETWORK", class: "catalog"},
|
|
{envVar: "KANDEV_DOCKER_VOLUMEBASEPATH", class: "catalog"},
|
|
{envVar: "KANDEV_AGENT_STANDALONE_HOST", class: "catalog"},
|
|
{envVar: "AGENTCTL_PORT", class: "catalog"},
|
|
{envVar: "KANDEV_AGENT_STANDALONE_PORT", class: "catalog"},
|
|
{envVar: "KANDEV_AUTH_JWTSECRET", class: "catalog"},
|
|
{envVar: "KANDEV_AUTH_TOKENDURATION", class: "catalog"},
|
|
{envVar: "KANDEV_AUTH_SESSIONTTLHOURS", class: "catalog"},
|
|
{envVar: "KANDEV_AUTH_COOKIE_NAME", class: "catalog"},
|
|
{envVar: "KANDEV_LOG_LEVEL", class: "catalog"},
|
|
{envVar: "KANDEV_LOGGING_FORMAT", class: "catalog"},
|
|
{envVar: "KANDEV_REPOSITORYDISCOVERY_ROOTS", class: "catalog"},
|
|
{envVar: "KANDEV_REPOSITORYDISCOVERY_MAXDEPTH", class: "catalog"},
|
|
{envVar: "KANDEV_WORKTREE_ENABLED", class: "catalog"},
|
|
{envVar: "KANDEV_WORKTREE_DEFAULTBRANCH", class: "catalog"},
|
|
{envVar: "KANDEV_WORKTREE_CLEANUPONREMOVE", class: "catalog"},
|
|
{envVar: "KANDEV_WORKTREE_FETCHTIMEOUTSECONDS", class: "catalog"},
|
|
{envVar: "KANDEV_WORKTREE_PULLTIMEOUTSECONDS", class: "catalog"},
|
|
{envVar: "KANDEV_REPOCLONE_BASEPATH", class: "catalog"},
|
|
{envVar: "KANDEV_DEBUG_DEV_MODE", class: "catalog"},
|
|
{envVar: "KANDEV_DEBUG_PPROF_ENABLED", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_JWTSIGNINGKEY", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_SCHEDULER_TICK_MS", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_MAX_CONCURRENT_INSTANCE", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_MAX_CONCURRENT_WORKSPACE", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_WORKSPACE_BUDGET_PER_HOUR", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_ROUTINE_BUDGET_PER_HOUR", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_PROMOTION_AGE_MINUTES", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_MAX_CAUSATION_DEPTH", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_SELF_TRIGGER_ALLOWANCE", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_SELF_TRIGGER_TOTAL_ALLOWANCE", class: "catalog"},
|
|
{envVar: "KANDEV_OFFICE_GATE_FAILURE_THRESHOLD", class: "catalog"},
|
|
{envVar: "KANDEV_GITHUB_CREDENTIAL_BROKER_PUBLIC_BASE_URL", class: "catalog"},
|
|
{envVar: "KANDEV_TASK_PREPARATION_TIMEOUT", class: "catalog"},
|
|
{envVar: "KANDEV_TASK_STALL_DETECTION_THRESHOLD", class: "catalog"},
|
|
{envVar: "KANDEV_CREDENTIALS_FILE", class: "catalog"},
|
|
{envVar: "KANDEV_GH_MAX_CONCURRENT", class: "catalog"},
|
|
{envVar: "KANDEV_GIT_MAX_CONCURRENT", class: "catalog"},
|
|
{envVar: "KANDEV_LSP_MAX_CONNECTIONS", class: "catalog"},
|
|
{envVar: "KANDEV_QUEUE_MAX_PER_SESSION", class: "catalog"},
|
|
{envVar: "KANDEV_ACP_IDLE_TIMEOUT", class: "catalog"},
|
|
{envVar: "KANDEV_ACP_IDLE_REAPER_INTERVAL", class: "catalog"},
|
|
{envVar: "KANDEV_ACP_NOTIF_QUEUE", class: "catalog"},
|
|
{envVar: "KANDEV_ACP_RECOVERY_DEADLINE", class: "catalog"},
|
|
{envVar: "KANDEV_ACP_RECOVERY_READ_TIMEOUT", class: "catalog"},
|
|
{envVar: "KANDEV_ACP_RECOVERY_READ_RETRIES", class: "catalog"},
|
|
{envVar: "KANDEV_ACP_UNOWNED_PERIOD", class: "catalog"},
|
|
{envVar: "KANDEV_ACP_DETACHED_EVENT_LIMIT", class: "catalog"},
|
|
{envVar: "KANDEV_PLAN_COALESCE_WINDOW_MS", class: "catalog"},
|
|
{envVar: "OTEL_EXPORTER_OTLP_ENDPOINT", class: "catalog"},
|
|
{envVar: "KANDEV_WEB_PORT", class: "catalog"},
|
|
{envVar: "KANDEV_HEALTH_TIMEOUT_MS", class: "catalog"},
|
|
{envVar: "KANDEV_NO_BROWSER", class: "catalog"},
|
|
{envVar: "KANDEV_EXECUTORS_SSHREACHABILITYINTERVALSECONDS", class: "catalog"},
|
|
{envVar: InternalConfigFileEnv, class: "exclusion"},
|
|
{envVar: InternalConfigHomeFileEnv, class: "exclusion"},
|
|
{envVar: InternalAgentctlStartupConfigEnv, class: "exclusion"},
|
|
{envVar: "KANDEV_LAUNCHER_PARENT_PID", class: "exclusion"},
|
|
{envVar: "KANDEV_BACKEND_PID_FILE", class: "exclusion"},
|
|
{envVar: "KANDEV_DESKTOP_HEALTH_TOKEN", class: "exclusion"},
|
|
{envVar: "KANDEV_DESKTOP_NATIVE_NOTIFICATIONS", class: "exclusion"},
|
|
{envVar: "KANDEV_DESKTOP_RUNTIME", class: "exclusion"},
|
|
{envVar: "KANDEV_BUNDLE_DIR", class: "exclusion"},
|
|
{envVar: "KANDEV_WEB_DIST_DIR", class: "exclusion"},
|
|
{envVar: "KANDEV_TASK_ID", class: "exclusion"},
|
|
{envVar: "KANDEV_SESSION_ID", class: "exclusion"},
|
|
{envVar: "KANDEV_WORKSPACE_ID", class: "exclusion"},
|
|
{envVar: "KANDEV_E2E_MOCK", class: "exclusion"},
|
|
{envVar: "KANDEV_MOCK_AGENT", class: "exclusion"},
|
|
{envVar: "KANDEV_MOCK_GITHUB", class: "exclusion"},
|
|
{envVar: "KANDEV_MOCK_GITLAB", class: "exclusion"},
|
|
{envVar: "KANDEV_MOCK_JIRA", class: "exclusion"},
|
|
{envVar: "KANDEV_MOCK_LINEAR", class: "exclusion"},
|
|
{envVar: "KANDEV_FEATURES_OFFICE", class: "exclusion"},
|
|
{envVar: "KANDEV_FEATURES_AUTH", class: "exclusion"},
|
|
{envVar: "KANDEV_FEATURES_CANVASES", class: "exclusion"},
|
|
{envVar: "KANDEV_FEATURES_CLAUDE_BACKGROUND_PROMPT_HANDOFF", class: "exclusion"},
|
|
{envVar: "KANDEV_FEATURES_CLAUDE_MID_TURN_STEERING", class: "exclusion"},
|
|
{envVar: "KANDEV_FEATURES_OFFICE_SESSION_IDENTITY", class: "exclusion"},
|
|
{envVar: "KANDEV_FEATURES_AGENT_SURVIVAL", class: "exclusion"},
|
|
{envVar: "KANDEV_DEBUG_AGENT_MESSAGES", class: "exclusion"},
|
|
{envVar: "KANDEV_DEBUG_ACP_MAX_FILES", class: "exclusion"},
|
|
{envVar: "KANDEV_DEBUG_ACP_RETENTION_HOURS", class: "exclusion"},
|
|
{envVar: "KANDEV_DEBUG_ACP_MAX_FILE_BYTES", class: "exclusion"},
|
|
{envVar: "KANDEV_MCP_LOG_FILE", class: "exclusion"},
|
|
{envVar: "KANDEV_DEBUG_LOG_DIR", class: "exclusion"},
|
|
{envVar: "AGENTCTL_AUTO_APPROVE_PERMISSIONS", class: "exclusion"},
|
|
{envVar: "KANDEV_MAX_CONCURRENT_SESSIONS", class: "exclusion"},
|
|
}
|
|
}
|
|
|
|
func validateCatalogAgainstAuditedInventory(entries []CatalogEntry, exclusions []CatalogExclusion, audited []auditedStartupEnvironment) error {
|
|
tracked := make(map[string]string, len(entries)+len(exclusions))
|
|
for _, entry := range entries {
|
|
for _, envVar := range entry.EnvVars {
|
|
if previous, exists := tracked[envVar]; exists {
|
|
return fmt.Errorf("environment variable %q is tracked more than once (%s and catalog)", envVar, previous)
|
|
}
|
|
tracked[envVar] = "catalog"
|
|
}
|
|
}
|
|
for _, exclusion := range exclusions {
|
|
if previous, exists := tracked[exclusion.EnvVar]; exists {
|
|
return fmt.Errorf("environment variable %q is tracked more than once (%s and exclusion)", exclusion.EnvVar, previous)
|
|
}
|
|
tracked[exclusion.EnvVar] = "exclusion"
|
|
}
|
|
|
|
want := make(map[string]string, len(audited))
|
|
for _, item := range audited {
|
|
if previous, exists := want[item.envVar]; exists {
|
|
return fmt.Errorf("audited environment variable %q is listed more than once (%s and %s)", item.envVar, previous, item.class)
|
|
}
|
|
want[item.envVar] = item.class
|
|
}
|
|
for envVar, class := range tracked {
|
|
if auditedClass, exists := want[envVar]; !exists {
|
|
return fmt.Errorf("environment variable %q is %s but absent from the audited inventory", envVar, class)
|
|
} else if auditedClass != class {
|
|
return fmt.Errorf("environment variable %q is %s, audited as %s", envVar, class, auditedClass)
|
|
}
|
|
}
|
|
for envVar, class := range want {
|
|
if trackedClass, exists := tracked[envVar]; !exists {
|
|
return fmt.Errorf("audited environment variable %q (%s) is neither cataloged nor excluded", envVar, class)
|
|
} else if trackedClass != class {
|
|
return fmt.Errorf("audited environment variable %q is %s, tracked as %s", envVar, class, trackedClass)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestConfigurationCatalogMarksSecretsAndExclusions(t *testing.T) {
|
|
for _, key := range []string{"database.password", "auth.jwtSecret", "office.jwtSigningKey"} {
|
|
entry, ok := CatalogEntryForKey(key)
|
|
if !ok || !entry.Sensitive {
|
|
t.Errorf("catalog entry %q is not marked sensitive: %#v", key, entry)
|
|
}
|
|
}
|
|
exclusions := ConfigurationExclusions()
|
|
foundGenerated := false
|
|
for _, exclusion := range exclusions {
|
|
if exclusion.EnvVar == "KANDEV_DESKTOP_HEALTH_TOKEN" && exclusion.Class == "generated" && strings.TrimSpace(exclusion.Reason) != "" {
|
|
foundGenerated = true
|
|
}
|
|
}
|
|
if !foundGenerated {
|
|
t.Fatal("generated health token exclusion is missing")
|
|
}
|
|
}
|
|
|
|
func TestAgentctlStartupConfigRoundTripsAndRejectsInvalidValues(t *testing.T) {
|
|
want := AgentctlStartupConfig{
|
|
Configured: true,
|
|
IdleTimeout: 2 * time.Hour,
|
|
IdleReaperInterval: 3 * time.Minute,
|
|
NotificationQueueCapacity: 4096,
|
|
OTLPEndpoint: "http://collector:4318",
|
|
UnownedPeriod: 10 * time.Minute,
|
|
DetachedEventLimit: 100,
|
|
AgentSurvivalEnabled: true,
|
|
PromptCancelJoinTimeout: 12 * time.Second,
|
|
}
|
|
raw, err := EncodeAgentctlStartupConfig(want)
|
|
if err != nil {
|
|
t.Fatalf("EncodeAgentctlStartupConfig: %v", err)
|
|
}
|
|
got, err := DecodeAgentctlStartupConfig(raw)
|
|
if err != nil {
|
|
t.Fatalf("DecodeAgentctlStartupConfig: %v", err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("decoded contract = %#v, want %#v", got, want)
|
|
}
|
|
|
|
invalid := want
|
|
invalid.NotificationQueueCapacity = 1
|
|
if _, err := EncodeAgentctlStartupConfig(invalid); err == nil {
|
|
t.Fatal("EncodeAgentctlStartupConfig accepted an invalid queue capacity")
|
|
}
|
|
|
|
invalidUnownedPeriod := want
|
|
invalidUnownedPeriod.UnownedPeriod = -time.Minute
|
|
if _, err := EncodeAgentctlStartupConfig(invalidUnownedPeriod); err == nil {
|
|
t.Fatal("EncodeAgentctlStartupConfig accepted a negative unowned period")
|
|
}
|
|
|
|
// Zero is accepted for both fields: an agent-survival-unaware caller (or
|
|
// an older backend build) omits them, and agentctl substitutes its own
|
|
// built-in default rather than treating an absent value as invalid.
|
|
unset := want
|
|
unset.UnownedPeriod = 0
|
|
unset.DetachedEventLimit = 0
|
|
unset.PromptCancelJoinTimeout = 0
|
|
if _, err := EncodeAgentctlStartupConfig(unset); err != nil {
|
|
t.Fatalf("EncodeAgentctlStartupConfig rejected zero-valued optional values: %v", err)
|
|
}
|
|
|
|
invalidCancelJoinTimeout := want
|
|
invalidCancelJoinTimeout.PromptCancelJoinTimeout = -time.Second
|
|
if _, err := EncodeAgentctlStartupConfig(invalidCancelJoinTimeout); err == nil {
|
|
t.Fatal("EncodeAgentctlStartupConfig accepted a negative prompt cancel join timeout")
|
|
}
|
|
|
|
invalidDetachedEventLimit := want
|
|
invalidDetachedEventLimit.DetachedEventLimit = 10001
|
|
if _, err := EncodeAgentctlStartupConfig(invalidDetachedEventLimit); err == nil {
|
|
t.Fatal("EncodeAgentctlStartupConfig accepted an out-of-range detached event limit")
|
|
}
|
|
}
|
|
|
|
// TestManagedAgentctlStartupConfigReflectsAgentSurvivalFlag pins that the
|
|
// resolved child contract carries the current value of the
|
|
// features.agentSurvival runtime flag (Config.Features.AgentSurvival) for
|
|
// this launch, mirroring the always-on UnownedPeriod/DetachedEventLimit
|
|
// passthrough rather than being gated on either of those being non-zero.
|
|
func TestManagedAgentctlStartupConfigReflectsAgentSurvivalFlag(t *testing.T) {
|
|
cfg := &Config{}
|
|
cfg.Agentctl.NotificationQueueCapacity = 4096
|
|
cfg.Agentctl.IdleReaperInterval = time.Minute
|
|
|
|
cfg.Features.AgentSurvival = false
|
|
if got := cfg.ManagedAgentctlStartupConfig(); got.AgentSurvivalEnabled {
|
|
t.Fatal("AgentSurvivalEnabled = true, want false when the runtime flag is off")
|
|
}
|
|
|
|
cfg.Features.AgentSurvival = true
|
|
if got := cfg.ManagedAgentctlStartupConfig(); !got.AgentSurvivalEnabled {
|
|
t.Fatal("AgentSurvivalEnabled = false, want true when the runtime flag is on")
|
|
}
|
|
}
|
|
|
|
func TestManagedAgentctlStartupConfigResolvesTruthyE2ECancelJoinTimeout(t *testing.T) {
|
|
t.Setenv("KANDEV_E2E_MOCK", "yes")
|
|
t.Setenv("KANDEV_E2E_PROMPT_CANCEL_JOIN_TIMEOUT", "12s")
|
|
cfg := &Config{}
|
|
cfg.Agentctl.NotificationQueueCapacity = 4096
|
|
cfg.Agentctl.IdleReaperInterval = time.Minute
|
|
|
|
if got := cfg.ManagedAgentctlStartupConfig().PromptCancelJoinTimeout; got != 12*time.Second {
|
|
t.Fatalf("prompt cancel join timeout = %s, want 12s", got)
|
|
}
|
|
}
|
|
|
|
func TestManagedAgentctlStartupConfigOmitsCancelJoinTimeoutOutsideE2E(t *testing.T) {
|
|
t.Setenv("KANDEV_E2E_MOCK", "false")
|
|
t.Setenv("KANDEV_E2E_PROMPT_CANCEL_JOIN_TIMEOUT", "12s")
|
|
cfg := &Config{}
|
|
cfg.Agentctl.NotificationQueueCapacity = 4096
|
|
cfg.Agentctl.IdleReaperInterval = time.Minute
|
|
|
|
if got := cfg.ManagedAgentctlStartupConfig().PromptCancelJoinTimeout; got != 0 {
|
|
t.Fatalf("prompt cancel join timeout = %s, want zero outside E2E", got)
|
|
}
|
|
}
|