Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

1878 lines
51 KiB
Go

package config
import (
"bytes"
"flag"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/BurntSushi/toml"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/parser"
)
const configFileName = "config.toml"
func skipIfNotUnix(t *testing.T) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip(
"skipping: Unix permissions not reliable on Windows",
)
}
if os.Getuid() == 0 {
t.Skip(
"skipping: running as root bypasses permissions",
)
}
}
func writeConfig(t *testing.T, dir string, data any) {
t.Helper()
var buf bytes.Buffer
require.NoError(t, toml.NewEncoder(&buf).Encode(data), "marshal config")
require.NoError(t, os.WriteFile(filepath.Join(dir, configFileName), buf.Bytes(), 0o600), "write config")
}
func setupTestEnv(t *testing.T) string {
t.Helper()
dir := t.TempDir()
t.Setenv("AGENTSVIEW_DATA_DIR", dir)
return dir
}
type configFixture struct {
Dir string
}
func newConfigFixture(t *testing.T) configFixture {
t.Helper()
return configFixture{Dir: setupTestEnv(t)}
}
func (f configFixture) Path(name string) string {
return filepath.Join(f.Dir, name)
}
func (f configFixture) WriteTOML(t *testing.T, data any) {
t.Helper()
writeConfig(t, f.Dir, data)
}
func (f configFixture) WriteConfigText(t *testing.T, text string) {
t.Helper()
require.NoError(t, os.WriteFile(f.Path(configFileName), []byte(text), 0o600),
"write config")
}
func (f configFixture) WriteLegacyJSON(t *testing.T, text string) {
t.Helper()
require.NoError(t, os.WriteFile(f.Path("config.json"), []byte(text), 0o600),
"write legacy config")
}
func (f configFixture) LoadMinimal(t *testing.T) Config {
t.Helper()
cfg, err := LoadMinimal()
require.NoError(t, err)
return cfg
}
func (f configFixture) LoadMinimalErr(t *testing.T) error {
t.Helper()
_, err := LoadMinimal()
return err
}
func (f configFixture) LoadFile(t *testing.T) Config {
t.Helper()
cfg, err := Default()
require.NoError(t, err)
cfg.DataDir = f.Dir
require.NoError(t, cfg.loadFile(), "loadFile")
return cfg
}
func (f configFixture) ReadTOMLMap(t *testing.T) map[string]any {
t.Helper()
got, err := os.ReadFile(f.Path(configFileName))
require.NoError(t, err)
var result map[string]any
_, err = toml.Decode(string(got), &result)
require.NoError(t, err)
return result
}
func loadMinimalWithConfig(t *testing.T, data any) Config {
t.Helper()
f := newConfigFixture(t)
f.WriteTOML(t, data)
return f.LoadMinimal(t)
}
func loadMinimalErrWithConfig(t *testing.T, data any) error {
t.Helper()
f := newConfigFixture(t)
f.WriteTOML(t, data)
return f.LoadMinimalErr(t)
}
func loadPFlagsWithConfig(
t *testing.T,
data any,
args ...string,
) Config {
t.Helper()
f := newConfigFixture(t)
f.WriteTOML(t, data)
cfg, err := loadConfigFromPFlags(t, args...)
require.NoError(t, err, "loading config")
return cfg
}
func runConcurrent(t *testing.T, workers int, fn func(i int) error) {
t.Helper()
start := make(chan struct{})
errs := make(chan error, workers)
var wg sync.WaitGroup
wg.Add(workers)
for i := range workers {
go func() {
defer wg.Done()
<-start
if err := fn(i); err != nil {
errs <- err
}
}()
}
close(start)
wg.Wait()
close(errs)
for err := range errs {
require.NoError(t, err)
}
}
func requireAllSameNonEmpty[T comparable](t *testing.T, values []T) {
t.Helper()
require.NotEmpty(t, values)
for _, value := range values {
require.NotZero(t, value)
assert.Equal(t, values[0], value)
}
}
func requireErrorContains(t *testing.T, err error, substrs ...string) {
t.Helper()
require.Error(t, err)
for _, substr := range substrs {
assert.Contains(t, err.Error(), substr)
}
}
func captureLog(t *testing.T) *bytes.Buffer {
t.Helper()
var buf bytes.Buffer
prev := log.Writer()
log.SetOutput(&buf)
t.Cleanup(func() { log.SetOutput(prev) })
return &buf
}
func assertLogContains(t *testing.T, buf *bytes.Buffer, substrs ...string) {
t.Helper()
logged := buf.String()
for _, substr := range substrs {
assert.Contains(t, logged, substr)
}
}
func loadConfigFromFlags(t *testing.T, args ...string) (Config, error) {
t.Helper()
if os.Getenv("AGENTSVIEW_DATA_DIR") == "" {
t.Setenv("AGENTSVIEW_DATA_DIR", t.TempDir())
}
fs := flag.NewFlagSet("test", flag.ContinueOnError)
RegisterServeFlags(fs)
if err := fs.Parse(args); err != nil {
return Config{}, err
}
return Load(fs)
}
func loadConfigFromPFlags(t *testing.T, args ...string) (Config, error) {
t.Helper()
if os.Getenv("AGENTSVIEW_DATA_DIR") == "" {
t.Setenv("AGENTSVIEW_DATA_DIR", t.TempDir())
}
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
RegisterServePFlags(fs)
if err := fs.Parse(args); err != nil {
return Config{}, err
}
return LoadPFlags(fs)
}
func TestLoadMinimal_LoadsAgentBinaryConfig(t *testing.T) {
f := newConfigFixture(t)
f.WriteConfigText(t, `[agent.claude]
binary = "/opt/agents/claude"
[agent.gemini]
binary = "/usr/local/bin/gemini"
sandbox = "sandbox-exec"
allow_unsafe = true
`)
cfg := f.LoadMinimal(t)
assert.Equal(t, "/opt/agents/claude", cfg.Agent["claude"].Binary)
assert.Equal(t, "/usr/local/bin/gemini", cfg.Agent["gemini"].Binary)
assert.Equal(t, "sandbox-exec", cfg.Agent["gemini"].Sandbox)
assert.True(t, cfg.Agent["gemini"].AllowUnsafe)
}
func TestLoadReadOnlyReadsLegacyJSONWithoutMigrating(t *testing.T) {
f := newConfigFixture(t)
jsonPath := f.Path("config.json")
f.WriteLegacyJSON(t, `{
"codex_sessions_dirs": ["/legacy/codex"],
"result_content_blocked_categories": ["Read", "Search"]
}`)
cfg, err := LoadReadOnly()
require.NoError(t, err)
assert.Equal(t, []string{"/legacy/codex"},
cfg.ResolveDirs(parser.AgentCodex))
assert.Equal(t, []string{"Read", "Search"},
cfg.ResultContentBlockedCategories)
assert.FileExists(t, jsonPath)
assert.NoFileExists(t, f.Path(configFileName))
assert.NoFileExists(t, jsonPath+".bak")
}
func TestDefault_IncludesCodexArchivedSessionsDir(t *testing.T) {
cfg, err := Default()
require.NoError(t, err)
dirs := cfg.ResolveDirs(parser.AgentCodex)
require.Len(t, dirs, 2)
assert.True(t, strings.HasSuffix(dirs[0], filepath.Join(".codex", "sessions")), "dirs[0] = %q", dirs[0])
assert.True(t, strings.HasSuffix(dirs[1], filepath.Join(".codex", "archived_sessions")), "dirs[1] = %q", dirs[1])
}
func TestDefault_SkipsAiderUntilConfigured(t *testing.T) {
t.Setenv("AIDER_DIR", "")
cfg, err := Default()
require.NoError(t, err)
// Aider has no safe default root: a passive viewer must not enumerate
// $HOME (macOS privacy prompts), so it stays unresolved until the user
// opts in via AIDER_DIR or aider_dirs.
assert.Empty(t, cfg.ResolveDirs(parser.AgentAider))
assert.False(t, cfg.IsUserConfigured(parser.AgentAider))
}
func TestDefault_IncludesDevinLocalShareRoots(t *testing.T) {
cfg, err := Default()
require.NoError(t, err)
dirs := cfg.ResolveDirs(parser.AgentDevin)
require.Len(t, dirs, 2)
assert.True(t, strings.HasSuffix(dirs[0], filepath.Join("Library", "Application Support", "devin")), "dirs[0] = %q", dirs[0])
assert.True(t, strings.HasSuffix(dirs[1], filepath.Join(".local", "share", "devin")), "dirs[1] = %q", dirs[1])
assert.False(t, cfg.IsUserConfigured(parser.AgentDevin))
}
func TestLoadEnv_OverridesDataDir(t *testing.T) {
custom := setupTestEnv(t)
cfg, err := Default()
require.NoError(t, err)
cfg.loadEnv()
assert.Equal(t, custom, cfg.DataDir)
}
func TestLoadEnv_UsesPrefixedCursorAdminVarsWithLegacyFallback(t *testing.T) {
setupTestEnv(t)
t.Setenv("AGENTSVIEW_CURSOR_ADMIN_API_KEY", "prefixed-key")
t.Setenv("CURSOR_ADMIN_API_KEY", "legacy-key")
t.Setenv("CURSOR_ADMIN_EMAIL", "legacy@example.com")
t.Setenv("AGENTSVIEW_CURSOR_ADMIN_USER_ID", "prefixed-user")
cfg, err := Default()
require.NoError(t, err)
cfg.loadEnv()
assert.Equal(t, "prefixed-key", cfg.CursorAdminAPIKey)
assert.Equal(t, "legacy@example.com", cfg.CursorAdminEmail)
assert.Equal(t, "prefixed-user", cfg.CursorAdminUserID)
}
func TestLoadMinimal_PreservesCursorAdminEnvOverFile(t *testing.T) {
dir := setupTestEnv(t)
writeConfig(t, dir, map[string]any{
"cursor_admin_api_key": "file-key",
"cursor_admin_email": "file@example.com",
"cursor_admin_user_id": "file-user",
})
t.Setenv("AGENTSVIEW_CURSOR_ADMIN_API_KEY", "env-key")
t.Setenv("AGENTSVIEW_CURSOR_ADMIN_EMAIL", "env@example.com")
t.Setenv("AGENTSVIEW_CURSOR_ADMIN_USER_ID", "env-user")
cfg, err := LoadMinimal()
require.NoError(t, err)
assert.Equal(t, "env-key", cfg.CursorAdminAPIKey)
assert.Equal(t, "env@example.com", cfg.CursorAdminEmail)
assert.Equal(t, "env-user", cfg.CursorAdminUserID)
}
func TestLoadMinimal_PreservesAuthTokenEnvOverFile(t *testing.T) {
dir := setupTestEnv(t)
writeConfig(t, dir, map[string]any{
"auth_token": "file-token",
})
t.Setenv("AGENTSVIEW_AUTH_TOKEN", "env-token")
cfg, err := LoadMinimal()
require.NoError(t, err)
assert.Equal(t, "env-token", cfg.AuthToken)
}
func TestLoad_AppliesExplicitFlags(t *testing.T) {
cfg, err := loadConfigFromFlags(t, "-host", "0.0.0.0", "-port", "9090")
require.NoError(t, err)
assert.Equal(t, "0.0.0.0", cfg.Host)
assert.Equal(t, 9090, cfg.Port)
}
func TestLoad_DefaultsWithoutFlags(t *testing.T) {
cfg, err := loadConfigFromFlags(t)
require.NoError(t, err)
assert.Equal(t, "127.0.0.1", cfg.Host)
assert.Equal(t, 8080, cfg.Port)
assert.Empty(t, cfg.PublicOrigins)
}
func TestLoadPFlags_AppliesExplicitFlags(t *testing.T) {
cfg, err := loadConfigFromPFlags(t, "--host", "0.0.0.0", "--port", "9090")
require.NoError(t, err)
assert.Equal(t, "0.0.0.0", cfg.Host)
assert.Equal(t, 9090, cfg.Port)
}
func TestLoad_NilFlagSet(t *testing.T) {
setupTestEnv(t)
cfg, err := Load(nil)
require.NoError(t, err)
assert.Equal(t, "127.0.0.1", cfg.Host)
}
func TestLoad_PublicOriginFlagOverridesConfigFile(t *testing.T) {
tmp := setupTestEnv(t)
writeConfig(t, tmp, map[string]any{
"public_origins": []string{"https://old.example.test"},
})
cfg, err := loadConfigFromFlags(
t,
"-public-origin", "https://viewer.example.test/",
"-public-origin", "http://viewer.example.test:8004",
)
require.NoError(t, err)
got := strings.Join(cfg.PublicOrigins, ",")
assert.Equal(t, "https://viewer.example.test,http://viewer.example.test:8004", got)
}
func TestLoad_HostFromConfigFile(t *testing.T) {
cfg := loadMinimalWithConfig(t, map[string]any{
"host": "0.0.0.0",
})
assert.Equal(t, "0.0.0.0", cfg.Host)
assert.False(t, cfg.HostExplicit,
"config-file host must not count as an explicit flag")
}
func TestLoad_HostFlagOverridesConfigFile(t *testing.T) {
tmp := setupTestEnv(t)
writeConfig(t, tmp, map[string]any{
"host": "0.0.0.0",
})
cfg, err := loadConfigFromFlags(t, "-host", "192.168.1.5")
require.NoError(t, err)
assert.Equal(t, "192.168.1.5", cfg.Host)
assert.True(t, cfg.HostExplicit)
}
func TestLoad_PortFromConfigFile(t *testing.T) {
cfg := loadMinimalWithConfig(t, map[string]any{
"port": 7357,
})
assert.Equal(t, 7357, cfg.Port)
}
func TestLoad_PortFlagOverridesConfigFile(t *testing.T) {
tmp := setupTestEnv(t)
writeConfig(t, tmp, map[string]any{
"port": 7357,
})
cfg, err := loadConfigFromFlags(t, "-port", "9090")
require.NoError(t, err)
assert.Equal(t, 9090, cfg.Port)
}
func TestLoad_PublicOriginsFromConfigFile(t *testing.T) {
cfg := loadMinimalWithConfig(t, map[string]any{
"public_origins": []string{
"https://Viewer.Example.Test:443/",
"http://viewer.example.test:8004",
},
})
got := strings.Join(cfg.PublicOrigins, ",")
assert.Equal(t, "https://viewer.example.test,http://viewer.example.test:8004", got)
}
func TestLoad_PublicOriginsRejectInvalid(t *testing.T) {
err := loadMinimalErrWithConfig(t, map[string]any{
"public_origins": []string{"ftp://viewer.example.test"},
})
requireErrorContains(t, err, "invalid public origins")
}
func TestLoad_PublicURLMergedIntoOrigins(t *testing.T) {
cfg := loadMinimalWithConfig(t, map[string]any{
"public_url": "https://viewer.example.test/",
})
assert.Equal(t, "https://viewer.example.test", cfg.PublicURL)
assert.Equal(t, "https://viewer.example.test", strings.Join(cfg.PublicOrigins, ","))
}
func TestLoad_ProxyConfigFromFile(t *testing.T) {
cfg := loadMinimalWithConfig(t, map[string]any{
"public_url": "https://viewer.example.test",
"proxy": map[string]any{
"mode": "caddy",
"bind_host": "10.0.60.2",
"public_port": 9443,
"tls_cert": "/tmp/viewer.crt",
"tls_key": "/tmp/viewer.key",
"allowed_subnets": []string{"10.1.2.3/16", "192.168.1.0/24"},
},
})
assert.Equal(t, "caddy", cfg.Proxy.Mode)
assert.Equal(t, "caddy", cfg.Proxy.Bin)
assert.Equal(t, "10.0.60.2", cfg.Proxy.BindHost)
assert.Equal(t, 9443, cfg.Proxy.PublicPort)
assert.Equal(t, "https://viewer.example.test:9443", cfg.PublicURL)
assert.Equal(t, "10.1.0.0/16,192.168.1.0/24", strings.Join(cfg.Proxy.AllowedSubnets, ","))
}
func TestLoad_ProxyFlags(t *testing.T) {
cfg, err := loadConfigFromFlags(
t,
"-public-url", "https://viewer.example.test",
"-proxy", "caddy",
"-proxy-bind-host", "0.0.0.0",
"-public-port", "9443",
"-tls-cert", "/tmp/viewer.crt",
"-tls-key", "/tmp/viewer.key",
"-allowed-subnet", "10.0/16",
"-allowed-subnet", "192.168.0.0/24",
)
require.NoError(t, err)
assert.Equal(t, "https://viewer.example.test:9443", cfg.PublicURL)
assert.Equal(t, "caddy", cfg.Proxy.Mode)
assert.Equal(t, "0.0.0.0", cfg.Proxy.BindHost)
assert.Equal(t, 9443, cfg.Proxy.PublicPort)
assert.Equal(t, "10.0.0.0/16,192.168.0.0/24", strings.Join(cfg.Proxy.AllowedSubnets, ","))
}
func TestLoad_ManagedCaddyDefaultsPublicPortAndBindHost(t *testing.T) {
cfg, err := loadConfigFromFlags(
t,
"-public-url", "https://viewer.example.test",
"-proxy", "caddy",
)
require.NoError(t, err)
assert.Equal(t, "https://viewer.example.test:8443", cfg.PublicURL)
assert.Equal(t, "127.0.0.1", cfg.Proxy.BindHost)
assert.Equal(t, 0, cfg.Proxy.PublicPort)
}
func TestLoad_ManagedCaddyRejectsConflictingPublicPort(t *testing.T) {
_, err := loadConfigFromFlags(
t,
"-public-url", "https://viewer.example.test:9443",
"-proxy", "caddy",
"-public-port", "8443",
)
requireErrorContains(t, err, "conflicts with configured public port")
}
func TestLoad_ManagedCaddyRejectsPublicURLPath(t *testing.T) {
_, err := loadConfigFromFlags(
t,
"-public-url", "https://viewer.example.test/path",
"-proxy", "caddy",
)
requireErrorContains(t, err, "must not include a path")
}
func TestLoad_ManagedCaddyNormalizesExplicitDefaultPorts(t *testing.T) {
cfg, err := loadConfigFromFlags(
t,
"-public-url", "https://viewer.example.test:443",
"-proxy", "caddy",
)
require.NoError(t, err)
assert.Equal(t, "https://viewer.example.test", cfg.PublicURL)
cfg, err = loadConfigFromFlags(
t,
"-public-url", "http://viewer.example.test:80",
"-proxy", "caddy",
)
require.NoError(t, err)
assert.Equal(t, "http://viewer.example.test", cfg.PublicURL)
}
func TestLoad_AllowedSubnetsRejectInvalid(t *testing.T) {
err := loadMinimalErrWithConfig(t, map[string]any{
"proxy": map[string]any{
"mode": "caddy",
"allowed_subnets": []string{"10.0.0.0/not-a-mask"},
},
})
requireErrorContains(t, err, "invalid allowed subnets")
}
func TestSaveGithubToken_RejectsCorruptConfig(t *testing.T) {
tmp := setupTestEnv(t)
cfg := Config{DataDir: tmp}
// Write invalid TOML to config file
path := filepath.Join(tmp, configFileName)
require.NoError(t, os.WriteFile(path, []byte("[invalid toml = ="), 0o600))
err := cfg.SaveGithubToken("tok")
require.Error(t, err, "expected error for corrupt config")
}
func TestSaveGithubToken_ReturnsErrorOnReadFailure(t *testing.T) {
skipIfNotUnix(t)
f := newConfigFixture(t)
cfg := Config{DataDir: f.Dir}
// Create a config file that is not readable
path := f.Path(configFileName)
require.NoError(t, os.WriteFile(path, []byte("k = \"v\"\n"), 0o000))
err := cfg.SaveGithubToken("tok")
requireErrorContains(t, err, "reading config file")
}
func TestSaveGithubToken_PreservesExistingKeys(t *testing.T) {
f := newConfigFixture(t)
cfg := Config{DataDir: f.Dir}
existing := map[string]any{"custom_key": "value"}
f.WriteTOML(t, existing)
require.NoError(t, cfg.SaveGithubToken("new-token"))
result := f.ReadTOMLMap(t)
assert.Equal(t, "value", result["custom_key"])
assert.Equal(t, "new-token", result["github_token"])
}
func TestEnsureAuthTokenConcurrentCallersSharePersistedToken(t *testing.T) {
f := newConfigFixture(t)
const workers = 8
tokens := make([]string, workers)
runConcurrent(t, workers, func(i int) error {
cfg := Config{DataDir: f.Dir}
if err := cfg.EnsureAuthToken(); err != nil {
return err
}
tokens[i] = cfg.AuthToken
return nil
})
requireAllSameNonEmpty(t, tokens)
result := f.ReadTOMLMap(t)
assert.Equal(t, tokens[0], result["auth_token"])
}
func TestEnsureCursorSecretConcurrentCallersSharePersistedSecret(t *testing.T) {
f := newConfigFixture(t)
const workers = 8
secrets := make([]string, workers)
runConcurrent(t, workers, func(i int) error {
cfg := Config{DataDir: f.Dir}
if err := cfg.ensureCursorSecret(); err != nil {
return err
}
secrets[i] = cfg.CursorSecret
return nil
})
requireAllSameNonEmpty(t, secrets)
result := f.ReadTOMLMap(t)
assert.Equal(t, secrets[0], result["cursor_secret"])
}
func TestMigrateJSONToTOMLConcurrentCallersMigrateOnce(t *testing.T) {
f := newConfigFixture(t)
jsonPath := f.Path("config.json")
f.WriteLegacyJSON(t, `{
"github_token": "legacy-token",
"require_auth": true
}`)
const workers = 4
runConcurrent(t, workers, func(int) error {
cfg := Config{DataDir: f.Dir}
return cfg.migrateJSONToTOML()
})
assert.NoFileExists(t, jsonPath)
assert.FileExists(t, jsonPath+".bak")
result := f.ReadTOMLMap(t)
assert.Equal(t, "legacy-token", result["github_token"])
assert.Equal(t, true, result["require_auth"])
}
func TestLoadFile_ReadsDirArrays(t *testing.T) {
cfg := loadMinimalWithConfig(t, map[string]any{
"claude_project_dirs": []string{"/path/one", "/path/two"},
"codex_sessions_dirs": []string{"/codex/a"},
"aider_dirs": []string{"/code"},
})
claudeDirs := cfg.ResolveDirs(parser.AgentClaude)
require.Len(t, claudeDirs, 2)
assert.Equal(t, "/path/one", claudeDirs[0])
assert.Equal(t, "/path/two", claudeDirs[1])
codexDirs := cfg.ResolveDirs(parser.AgentCodex)
require.Len(t, codexDirs, 1)
assert.Equal(t, "/codex/a", codexDirs[0])
assert.Equal(t, []string{"/code"}, cfg.ResolveDirs(parser.AgentAider))
assert.True(t, cfg.IsUserConfigured(parser.AgentAider))
}
func TestResolveDirs(t *testing.T) {
tests := []struct {
name string
config map[string]any
envValue string
expectDefault bool
wantDirs []string
wantUserConfig bool
}{
{
"DefaultOnly",
map[string]any{},
"",
true,
nil,
false,
},
{
"ConfigOverrides",
map[string]any{
"claude_project_dirs": []string{"/a", "/b"},
},
"",
false,
[]string{"/a", "/b"},
true,
},
{
"EnvOverrides",
map[string]any{
"claude_project_dirs": []string{"/a"},
},
"/env/override",
false,
[]string{"/env/override"},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := setupTestEnv(t)
writeConfig(t, dir, tt.config)
t.Setenv("CLAUDE_CONFIG_DIR", "")
if tt.envValue != "" {
t.Setenv("CLAUDE_PROJECTS_DIR", tt.envValue)
}
cfg, err := LoadMinimal()
require.NoError(t, err)
dirs := cfg.ResolveDirs(parser.AgentClaude)
want := tt.wantDirs
if tt.expectDefault {
// Default is the home-dir based path
want = cfg.AgentDirs[parser.AgentClaude]
}
assert.Equal(t, want, dirs)
assert.Equal(t, tt.wantUserConfig, cfg.IsUserConfigured(parser.AgentClaude))
})
}
}
func TestResolveDirs_ClaudeConfigDirRootEnvVar(t *testing.T) {
t.Run("root env re-roots implicit default", func(t *testing.T) {
dir := setupTestEnv(t)
root := t.TempDir()
t.Setenv("CLAUDE_CONFIG_DIR", root)
writeConfig(t, dir, map[string]any{})
cfg, err := LoadMinimal()
require.NoError(t, err)
assert.Equal(t, []string{filepath.Join(root, "projects")},
cfg.ResolveDirs(parser.AgentClaude))
assert.False(t, cfg.IsUserConfigured(parser.AgentClaude))
})
t.Run("projects env beats root env", func(t *testing.T) {
dir := setupTestEnv(t)
root := t.TempDir()
t.Setenv("CLAUDE_CONFIG_DIR", root)
t.Setenv("CLAUDE_PROJECTS_DIR", "/env/override")
writeConfig(t, dir, map[string]any{})
cfg, err := LoadMinimal()
require.NoError(t, err)
assert.Equal(t, []string{"/env/override"},
cfg.ResolveDirs(parser.AgentClaude))
assert.True(t, cfg.IsUserConfigured(parser.AgentClaude))
})
t.Run("config file beats root env", func(t *testing.T) {
dir := setupTestEnv(t)
root := t.TempDir()
t.Setenv("CLAUDE_CONFIG_DIR", root)
writeConfig(t, dir, map[string]any{
"claude_project_dirs": []string{"/from/config"},
})
cfg, err := LoadMinimal()
require.NoError(t, err)
assert.Equal(t, []string{"/from/config"},
cfg.ResolveDirs(parser.AgentClaude))
assert.True(t, cfg.IsUserConfigured(parser.AgentClaude))
})
}
func TestResolveDirs_DevinPrecedenceAndMergeRules(t *testing.T) {
t.Run("config overrides defaults", func(t *testing.T) {
cfg := loadMinimalWithConfig(t, map[string]any{
"devin_dirs": []string{"/from/config/devin"},
})
assert.Equal(t, []string{"/from/config/devin"},
cfg.ResolveDirs(parser.AgentDevin))
assert.True(t, cfg.IsUserConfigured(parser.AgentDevin))
})
t.Run("env overrides config", func(t *testing.T) {
f := newConfigFixture(t)
f.WriteTOML(t, map[string]any{
"devin_dirs": []string{"/from/config/devin"},
})
t.Setenv("DEVIN_DIR", "/from/env/devin")
cfg := f.LoadMinimal(t)
assert.Equal(t, []string{"/from/env/devin"},
cfg.ResolveDirs(parser.AgentDevin))
assert.True(t, cfg.IsUserConfigured(parser.AgentDevin))
})
t.Run("config file still applies when env unset", func(t *testing.T) {
f := newConfigFixture(t)
f.WriteTOML(t, map[string]any{
"devin_dirs": []string{"/from/config/devin", "/second/config/devin"},
})
t.Setenv("DEVIN_DIR", "")
cfg := f.LoadMinimal(t)
assert.Equal(t, []string{"/from/config/devin", "/second/config/devin"},
cfg.ResolveDirs(parser.AgentDevin))
assert.True(t, cfg.IsUserConfigured(parser.AgentDevin))
})
}
func TestResolveDataDir_DefaultAndEnvOverride(t *testing.T) {
// Without env override, should return default
dir, err := ResolveDataDir()
require.NoError(t, err)
assert.NotEmpty(t, dir, "ResolveDataDir returned empty string")
// With env override, should return the override
custom := t.TempDir()
t.Setenv("AGENTSVIEW_DATA_DIR", custom)
dir, err = ResolveDataDir()
require.NoError(t, err)
assert.Equal(t, custom, dir)
}
// TestDataDir_LegacyEnvFallback verifies that the legacy AGENT_VIEWER_DATA_DIR
// env var still takes effect when the canonical AGENTSVIEW_DATA_DIR is unset,
// and that the canonical name wins when both are set.
func TestDataDir_LegacyEnvFallback(t *testing.T) {
t.Run("legacy used when canonical unset", func(t *testing.T) {
legacy := t.TempDir()
t.Setenv("AGENT_VIEWER_DATA_DIR", legacy)
dir, err := ResolveDataDir()
require.NoError(t, err)
assert.Equal(t, legacy, dir)
})
t.Run("canonical wins over legacy", func(t *testing.T) {
legacy := t.TempDir()
canonical := t.TempDir()
t.Setenv("AGENT_VIEWER_DATA_DIR", legacy)
t.Setenv("AGENTSVIEW_DATA_DIR", canonical)
dir, err := ResolveDataDir()
require.NoError(t, err)
assert.Equal(t, canonical, dir, "canonical should win")
})
}
func TestEnvOverridesConfigFile(t *testing.T) {
f := newConfigFixture(t)
f.WriteTOML(t, map[string]any{
"codex_sessions_dirs": []string{"/from/config"},
})
t.Setenv("CODEX_SESSIONS_DIR", "/from/env")
cfg := f.LoadMinimal(t)
dirs := cfg.ResolveDirs(parser.AgentCodex)
assert.Equal(t, []string{"/from/env"}, dirs)
}
func TestLoadFile_MalformedDirValueLogsWarning(t *testing.T) {
f := newConfigFixture(t)
t.Setenv("CLAUDE_CONFIG_DIR", "")
// Write a config where claude_project_dirs is a string
// instead of a string array.
f.WriteTOML(t, map[string]any{
"claude_project_dirs": "/not/an/array",
})
// Capture log output during Load.
buf := captureLog(t)
cfg := f.LoadMinimal(t)
// The malformed key should trigger a warning.
assertLogContains(t, buf, "claude_project_dirs", "expected string array")
// ResolveDirs should return the default (malformed value
// was not applied).
dirs := cfg.ResolveDirs(parser.AgentClaude)
home, _ := os.UserHomeDir()
defaultDir := filepath.Join(home, ".claude", "projects")
assert.Equal(t, []string{defaultDir}, dirs)
}
func TestDefault_ResultContentBlockedCategories(t *testing.T) {
cfg, err := Default()
require.NoError(t, err)
assert.Equal(t, []string{"Read", "Glob"}, cfg.ResultContentBlockedCategories)
}
func TestLoadFile_ResultContentBlockedCategories(t *testing.T) {
tests := []struct {
name string
config map[string]any
want []string
}{
{
"NoConfigFileUsesDefault",
map[string]any{},
[]string{"Read", "Glob"},
},
{
"ConfigFileOverridesWithCustomArray",
map[string]any{
"result_content_blocked_categories": []string{"Bash"},
},
[]string{"Bash"},
},
{
"ConfigFileWithMultipleCategories",
map[string]any{
"result_content_blocked_categories": []string{"Bash", "Write", "Edit"},
},
[]string{"Bash", "Write", "Edit"},
},
{
"ConfigFileWithEmptyArrayClearsBlocklist",
map[string]any{
"result_content_blocked_categories": []string{},
},
[]string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := loadMinimalWithConfig(t, tt.config)
assert.Equal(t, tt.want, cfg.ResultContentBlockedCategories)
})
}
}
func TestLoadFile_EventsCoalesceInterval(t *testing.T) {
tests := []struct {
name string
config map[string]any
want time.Duration
}{
{
"NoConfigFileUsesDefault",
map[string]any{},
10 * time.Second,
},
{
"ConfigFileOverrides",
map[string]any{
"events_coalesce_interval": "5s",
},
5 * time.Second,
},
{
"ConfigFileExplicitZeroDisables",
map[string]any{
"events_coalesce_interval": "0s",
},
0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := loadMinimalWithConfig(t, tt.config)
assert.Equal(t, tt.want, cfg.EventsCoalesceInterval)
})
}
}
func TestLoadFile_DaemonIdleTimeout(t *testing.T) {
tests := []struct {
name string
data map[string]any
want time.Duration
}{
{name: "absent uses default", data: map[string]any{}, want: 20 * time.Minute},
{name: "configured", data: map[string]any{"daemon_idle_timeout": "6h"}, want: 6 * time.Hour},
{name: "explicit zero disables", data: map[string]any{"daemon_idle_timeout": "0s"}, want: 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := loadMinimalWithConfig(t, tt.data)
assert.Equal(t, tt.want, cfg.DaemonIdleTimeout)
})
}
}
func TestLoadFile_PGConfig(t *testing.T) {
tests := []struct {
name string
config map[string]any
envURL string
want PGConfig
}{
{
"NoConfig",
map[string]any{},
"",
PGConfig{},
},
{
"FromConfigFile",
map[string]any{
"pg": map[string]any{
"url": "postgres://localhost/test",
"machine_name": "laptop",
},
},
"",
PGConfig{
URL: "postgres://localhost/test",
MachineName: "laptop",
},
},
{
"EnvOverridesConfig",
map[string]any{
"pg": map[string]any{
"url": "postgres://from-config",
},
},
"postgres://from-env",
PGConfig{
URL: "postgres://from-env",
},
},
{
"EnvURLMergesFileFields",
map[string]any{
"pg": map[string]any{
"url": "postgres://from-config",
"machine_name": "laptop",
},
},
"postgres://from-env",
PGConfig{
URL: "postgres://from-env",
MachineName: "laptop",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newConfigFixture(t)
f.WriteTOML(t, tt.config)
if tt.envURL != "" {
t.Setenv("AGENTSVIEW_PG_URL", tt.envURL)
}
cfg := f.LoadMinimal(t)
resolved, err := cfg.ResolvePG()
require.NoError(t, err)
assert.Equal(t, tt.want.URL, resolved.URL)
if tt.want.MachineName == "" {
assert.NotEmpty(t, resolved.MachineName)
} else {
assert.Equal(t, tt.want.MachineName, resolved.MachineName)
}
})
}
}
func TestResolvePGTarget_NamedTargets(t *testing.T) {
cfg := Config{
DefaultPG: "work",
PGTargets: map[string]PGConfig{
"work": {
URL: "postgres://work",
MachineName: "workbox",
},
"archive": {
URL: "postgres://archive",
MachineName: "archivebox",
},
},
pgEnvOverrides: pgEnvOverrides{
URL: "postgres://env-default",
MachineName: "envbox",
},
}
defaultTarget, err := cfg.ResolvePG()
require.NoError(t, err)
assert.Equal(t, "postgres://env-default", defaultTarget.URL)
assert.Equal(t, "envbox", defaultTarget.MachineName)
archiveTarget, err := cfg.ResolvePGTarget("archive")
require.NoError(t, err)
assert.Equal(t, "postgres://archive", archiveTarget.URL)
assert.Equal(t, "archivebox", archiveTarget.MachineName)
}
func TestResolvePGTargets_DefaultFirst(t *testing.T) {
cfg := Config{
DefaultPG: "work",
PGTargets: map[string]PGConfig{
"archive": {URL: "postgres://archive"},
"work": {URL: "postgres://work"},
},
}
targets, err := cfg.ResolvePGTargets()
require.NoError(t, err)
require.Len(t, targets, 2)
assert.Equal(t, "work", targets[0].Name)
assert.True(t, targets[0].IsDefault)
assert.Equal(t, "archive", targets[1].Name)
assert.False(t, targets[1].IsDefault)
}
func TestResolvePGTargets_OneNamedTargetWithoutDefault(t *testing.T) {
cfg := Config{
PGTargets: map[string]PGConfig{
"work": {URL: "postgres://work"},
},
}
targets, err := cfg.ResolvePGTargets()
require.NoError(t, err)
require.Len(t, targets, 1)
assert.Equal(t, "work", targets[0].Name)
assert.True(t, targets[0].IsDefault)
}
func TestResolvePGTargets_MultipleNamedTargetsRequireDefault(t *testing.T) {
cfg := Config{
PGTargets: map[string]PGConfig{
"work": {URL: "postgres://work"},
"archive": {URL: "postgres://archive"},
},
}
_, err := cfg.ResolvePGTargets()
require.Error(t, err)
assert.Contains(t, err.Error(), "default_pg is required")
}
func TestLoadMinimal_DefersNamedPGValidationForNonPGCommands(t *testing.T) {
dir := setupTestEnv(t)
path := filepath.Join(dir, configFileName)
data := []byte(`
default_pg = "missing"
[pg.work]
url = "postgres://work"
`)
require.NoError(t, os.WriteFile(path, data, 0o600))
cfg, err := LoadMinimal()
require.NoError(t, err)
_, err = cfg.ResolvePG()
require.Error(t, err)
assert.Contains(t, err.Error(), `default_pg "missing" does not match any named [pg.NAME] target`)
}
func TestLoadFile_PGMixedLegacyAndNamedTargetsFails(t *testing.T) {
dir := setupTestEnv(t)
path := filepath.Join(dir, configFileName)
data := []byte(`
[pg]
url = "postgres://legacy"
[pg.archive]
url = "postgres://archive"
`)
require.NoError(t, os.WriteFile(path, data, 0o600))
_, err := LoadMinimal()
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot mix legacy [pg] fields with named [pg.NAME] targets")
}
func TestLoadFile_PGNamedTargetValidationErrors(t *testing.T) {
tests := []struct {
name string
toml string
wantErr string
}{
{
name: "reserved all target",
toml: `
[pg.all]
url = "postgres://all"
`,
wantErr: `named PG target "all" is reserved`,
},
{
name: "reserved local target",
toml: `
[pg.local]
url = "postgres://local"
`,
wantErr: `named PG target "local" is reserved`,
},
{
name: "duplicate normalized target names",
toml: `
[pg.Work]
url = "postgres://work"
[pg.work]
url = "postgres://work2"
`,
wantErr: `normalize to the same name "work"`,
},
{
name: "named target must be table",
toml: `
[pg]
archive = "postgres://archive"
`,
wantErr: `[pg].archive must be a named target table`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := setupTestEnv(t)
path := filepath.Join(dir, configFileName)
require.NoError(t, os.WriteFile(path, []byte(tt.toml), 0o600))
_, err := LoadMinimal()
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
})
}
}
func TestPGConfig_ProjectFilter(t *testing.T) {
f := newConfigFixture(t)
f.WriteConfigText(t, `
[pg]
url = "postgres://localhost/test"
projects = ["alpha", "beta"]
`)
cfg := f.LoadFile(t)
require.Len(t, cfg.PG.Projects, 2)
assert.Equal(t, "alpha", cfg.PG.Projects[0])
assert.Equal(t, "beta", cfg.PG.Projects[1])
}
func TestPGConfig_ExcludeProjectFilter(t *testing.T) {
f := newConfigFixture(t)
f.WriteConfigText(t, `
[pg]
url = "postgres://localhost/test"
exclude_projects = ["gamma"]
`)
cfg := f.LoadFile(t)
require.Len(t, cfg.PG.ExcludeProjects, 1)
assert.Equal(t, "gamma", cfg.PG.ExcludeProjects[0])
}
func TestEnsureAuthTokenAdoptsPersistedToken(t *testing.T) {
f := newConfigFixture(t)
f.WriteConfigText(t, `
auth_token = "persisted-token"
require_auth = true
`)
cfg, err := Default()
require.NoError(t, err)
cfg.DataDir = f.Dir
cfg.RequireAuth = true
require.NoError(t, cfg.EnsureAuthToken())
assert.Equal(t, "persisted-token", cfg.AuthToken)
data, err := os.ReadFile(f.Path(configFileName))
require.NoError(t, err)
assert.Contains(t, string(data), `auth_token = "persisted-token"`)
}
func TestResolvePG_Defaults(t *testing.T) {
cfg := Config{
PG: PGConfig{
URL: "postgres://localhost/test",
},
}
resolved, err := cfg.ResolvePG()
require.NoError(t, err, "ResolvePG")
assert.Equal(t, "agentsview", resolved.Schema)
assert.NotEmpty(t, resolved.MachineName, "MachineName should default to hostname")
}
func TestLoadResolvesLocalMachineNameFromHostname(t *testing.T) {
setupTestEnv(t)
cfg, err := loadConfigFromPFlags(t)
require.NoError(t, err)
hostname, err := os.Hostname()
require.NoError(t, err)
assert.Equal(t, hostname, cfg.LocalMachineName)
}
func TestResolvePG_ExpandsEnvVars(t *testing.T) {
t.Setenv("PGPASS", "env-secret")
t.Setenv("PGURL", "postgres://localhost/test")
cfg := Config{
PG: PGConfig{
URL: "${PGURL}?password=${PGPASS}",
},
}
resolved, err := cfg.ResolvePG()
require.NoError(t, err, "ResolvePG")
assert.Equal(t, "postgres://localhost/test?password=env-secret", resolved.URL)
}
func TestResolvePG_ExpandsBareEnvOnlyForWholeValue(t *testing.T) {
t.Setenv("PGURL", "postgres://localhost/test")
cfg := Config{
PG: PGConfig{
URL: "$PGURL",
},
}
resolved, err := cfg.ResolvePG()
require.NoError(t, err, "ResolvePG")
assert.Equal(t, "postgres://localhost/test", resolved.URL)
}
func TestResolvePG_PreservesLiteralDollarSequencesInURL(t *testing.T) {
t.Setenv("PGPASS", "env-secret")
cfg := Config{
PG: PGConfig{
URL: "postgres://user:pa$word@localhost/db?application_name=$client&password=${PGPASS}",
},
}
resolved, err := cfg.ResolvePG()
require.NoError(t, err, "ResolvePG")
assert.Equal(t, "postgres://user:pa$word@localhost/db?application_name=$client&password=env-secret", resolved.URL)
}
func TestResolvePG_ErrorsOnMissingEnvVar(t *testing.T) {
cfg := Config{
PG: PGConfig{
URL: "${NONEXISTENT_PG_VAR}",
},
}
_, err := cfg.ResolvePG()
requireErrorContains(t, err, "NONEXISTENT_PG_VAR")
}
func TestResolvePG_ErrorsOnMissingBareEnvVar(t *testing.T) {
cfg := Config{
PG: PGConfig{
URL: "$NONEXISTENT_PG_BARE_VAR",
},
}
_, err := cfg.ResolvePG()
requireErrorContains(t, err, "NONEXISTENT_PG_BARE_VAR")
}
// TestIsEnvDependentURL locks the helper to the same expansion semantics
// as expandBracedEnv: any ${VAR}, or a whole-string bare $VAR, is
// env-dependent; an embedded bare $VAR or literal dollar sequence is not.
func TestIsEnvDependentURL(t *testing.T) {
cases := []struct {
name string
in string
want bool
}{
{"braced var", "${PGURL}", true},
{"braced var embedded", "postgres://h/db?password=${PGPASS}", true},
{"whole-string bare var", "$PGURL", true},
{"whole-string bare var with surrounding space", " $PGURL ", true},
{"embedded bare var not expanded", "postgres://$USER@host/db", false},
{"literal dollar sequence", "postgres://user:pa$word@host/db", false},
{"plain literal", "postgres://user:pass@localhost/db?sslmode=disable", false},
{"empty", "", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.want, IsEnvDependentURL(c.in))
})
}
}
// ResolvePG must not reject configs with both filter lists —
// that's a push-specific concern validated in runPGPush after
// CLI flags are merged. status and serve use ResolvePG too and
// shouldn't fail on push-only filter conflicts.
func TestResolvePG_AllowsBothFilterLists(t *testing.T) {
cfg := Config{
PG: PGConfig{
URL: "postgres://localhost/test",
Projects: []string{"alpha"},
ExcludeProjects: []string{"beta"},
},
}
_, err := cfg.ResolvePG()
require.NoError(t, err, "ResolvePG should not reject filter conflicts")
}
func TestDuckDBConfig_LoadsFileAndEnv(t *testing.T) {
f := newConfigFixture(t)
f.WriteTOML(t, map[string]any{
"duckdb": map[string]any{
"path": "/from/config/sessions.duckdb",
"url": "quack:config-host",
"token": "config-token",
"machine_name": "config-machine",
"allow_insecure": true,
"projects": []string{"alpha", "beta"},
"exclude_projects": []string{"gamma"},
},
})
t.Setenv("AGENTSVIEW_DUCKDB_PATH", "/from/env/sessions.duckdb")
t.Setenv("AGENTSVIEW_DUCKDB_URL", "quack:env-host")
t.Setenv("AGENTSVIEW_DUCKDB_TOKEN", "env-token")
t.Setenv("AGENTSVIEW_DUCKDB_MACHINE", "env-machine")
cfg := f.LoadMinimal(t)
assert.Equal(t, "/from/env/sessions.duckdb", cfg.DuckDB.Path)
assert.Equal(t, "quack:env-host", cfg.DuckDB.URL)
assert.Equal(t, "env-token", cfg.DuckDB.Token)
assert.Equal(t, "env-machine", cfg.DuckDB.MachineName)
assert.True(t, cfg.DuckDB.AllowInsecure)
assert.Equal(t, []string{"alpha", "beta"}, cfg.DuckDB.Projects)
assert.Equal(t, []string{"gamma"}, cfg.DuckDB.ExcludeProjects)
}
func TestResolveDuckDB_Defaults(t *testing.T) {
dir := t.TempDir()
cfg := Config{DataDir: dir}
resolved, err := cfg.ResolveDuckDB()
require.NoError(t, err, "ResolveDuckDB")
assert.Equal(t, filepath.Join(dir, "sessions.duckdb"), resolved.Path)
assert.NotEmpty(t, resolved.MachineName, "MachineName should default to hostname")
}
func TestResolveDuckDB_ExpandsEnvVars(t *testing.T) {
t.Setenv("DUCKDB_URL", "quack:localhost")
t.Setenv("DUCKDB_TOKEN", "secret-token")
t.Setenv("DUCKDB_PATH", filepath.Join(t.TempDir(), "remote.duckdb"))
cfg := Config{
DuckDB: DuckDBConfig{
Path: "$DUCKDB_PATH",
URL: "${DUCKDB_URL}",
Token: "${DUCKDB_TOKEN}",
},
}
resolved, err := cfg.ResolveDuckDB()
require.NoError(t, err, "ResolveDuckDB")
assert.Equal(t, os.Getenv("DUCKDB_PATH"), resolved.Path)
assert.Equal(t, "quack:localhost", resolved.URL)
assert.Equal(t, "secret-token", resolved.Token)
}
func TestResolveDuckDB_ErrorsOnMissingEnvVar(t *testing.T) {
cfg := Config{
DuckDB: DuckDBConfig{
URL: "${MISSING_DUCKDB_URL}",
},
}
_, err := cfg.ResolveDuckDB()
requireErrorContains(t, err, "MISSING_DUCKDB_URL")
}
func TestAutomatedConfigRoundTrip(t *testing.T) {
cfg := loadPFlagsWithConfig(t, map[string]any{
"automated": map[string]any{
"prefixes": []string{
"You are analyzing an essay",
"You are grading quotes",
" ", // whitespace preserved here; normalization is db-side
"You are analyzing an essay", // duplicate preserved here too
},
"substrings": []string{
"invoked by roborev to perform this review",
" embedded marker ",
"invoked by roborev to perform this review",
},
"exact_matches": []string{
"Warmup",
" Reply with exactly OK. ",
"Warmup",
},
},
})
wantPrefixes := []string{
"You are analyzing an essay",
"You are grading quotes",
" ",
"You are analyzing an essay",
}
wantSubstrings := []string{
"invoked by roborev to perform this review",
" embedded marker ",
"invoked by roborev to perform this review",
}
wantExactMatches := []string{
"Warmup",
" Reply with exactly OK. ",
"Warmup",
}
assert.Equal(t, wantPrefixes, cfg.Automated.Prefixes)
assert.Equal(t, wantSubstrings, cfg.Automated.Substrings)
assert.Equal(t, wantExactMatches, cfg.Automated.ExactMatches)
}
func TestAutomatedConfigAbsentIsNil(t *testing.T) {
cfg := loadPFlagsWithConfig(t, map[string]any{
"public_url": "http://example.com",
})
assert.Nil(t, cfg.Automated.Prefixes)
assert.Nil(t, cfg.Automated.Substrings)
assert.Nil(t, cfg.Automated.ExactMatches)
}
func TestLoadFile_CustomModelPricing(t *testing.T) {
tests := []struct {
name string
data map[string]any
want map[string]CustomModelRate
}{
{
name: "basic rates",
data: map[string]any{
"custom_model_pricing": map[string]CustomModelRate{
"acme-ultra-2.1": {Input: 2.0, Output: 8.0},
},
},
want: map[string]CustomModelRate{
"acme-ultra-2.1": {Input: 2.0, Output: 8.0},
},
},
{
name: "multiple models with cache rates",
data: map[string]any{
"custom_model_pricing": map[string]CustomModelRate{
"acme-ultra-2.1": {Input: 2.0, Output: 8.0, CacheCreation: 2.5, CacheRead: 0.2},
"acme-fast-2.1": {Input: 0.8, Output: 4.0},
},
},
want: map[string]CustomModelRate{
"acme-ultra-2.1": {Input: 2.0, Output: 8.0, CacheCreation: 2.5, CacheRead: 0.2},
"acme-fast-2.1": {Input: 0.8, Output: 4.0},
},
},
{
name: "empty map omitted",
data: map[string]any{},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := loadMinimalWithConfig(t, tt.data)
if len(tt.want) == 0 {
assert.Empty(t, cfg.CustomModelPricing)
return
}
require.Len(t, cfg.CustomModelPricing, len(tt.want))
for model, wantRate := range tt.want {
got, ok := cfg.CustomModelPricing[model]
if !ok {
t.Errorf("missing model %q", model)
continue
}
assert.Equal(t, wantRate, got, "model %q", model)
}
})
}
}
func TestLoadFile_RemoteHosts(t *testing.T) {
f := newConfigFixture(t)
f.WriteConfigText(t, `[[remote_hosts]]
host = "devbox1"
user = "jesse"
port = 22
interval = "5m"
[[remote_hosts]]
host = " laptop2 "
`)
cfg := f.LoadMinimal(t)
require.Len(t, cfg.RemoteHosts, 2)
assert.Equal(t, RemoteHost{Host: "devbox1", User: "jesse", Port: 22, Interval: 5 * time.Minute}, cfg.RemoteHosts[0])
assert.Equal(t, 5*time.Minute, cfg.RemoteHosts[0].Interval)
assert.Equal(t, time.Duration(0), cfg.RemoteHosts[1].Interval)
// host is trimmed at load so validation and SSH see the same value
assert.Equal(t, RemoteHost{Host: "laptop2"}, cfg.RemoteHosts[1])
}
func TestLoadFile_RemoteHostsHTTP(t *testing.T) {
f := newConfigFixture(t)
f.WriteConfigText(t, `[[remote_hosts]]
host = "devbox1"
transport = "http"
url = "http://devbox1.tailnet.ts.net:8080"
token = "remote-token"
interval = "5m"
`)
cfg := f.LoadMinimal(t)
require.Len(t, cfg.RemoteHosts, 1)
assert.Equal(t, RemoteHost{
Host: "devbox1",
Transport: RemoteTransportHTTP,
URL: "http://devbox1.tailnet.ts.net:8080",
Token: "remote-token",
Interval: 5 * time.Minute,
}, cfg.RemoteHosts[0])
}
func TestLoadFile_InvalidRemoteHostsDoNotBlockConfigLoad(t *testing.T) {
f := newConfigFixture(t)
f.WriteConfigText(t, `[[remote_hosts]]
host = "devbox1"
transport = "http"
`)
cfg := f.LoadMinimal(t)
require.Len(t, cfg.RemoteHosts, 1)
assert.Equal(t, RemoteTransportHTTP, cfg.RemoteHosts[0].Transport)
assert.Equal(t, "devbox1", cfg.RemoteHosts[0].Host)
}
func TestLoadFile_RemoteHostsAbsentIsNil(t *testing.T) {
cfg := loadMinimalWithConfig(t,
map[string]any{"public_url": "http://example.com"})
assert.Nil(t, cfg.RemoteHosts)
}
func TestValidateRemoteHosts(t *testing.T) {
tests := []struct {
name string
hosts []RemoteHost
wantErr []string // substrings expected in error; empty => no error
}{
{"valid", []RemoteHost{{Host: "a"}, {Host: "b", Port: 22}, {Host: "c", Port: 0}}, nil},
{"empty host", []RemoteHost{{Host: ""}}, []string{"host is required"}},
{"negative port", []RemoteHost{{Host: "a", Port: -1}}, []string{"invalid port"}},
{"port too large", []RemoteHost{{Host: "a", Port: 70000}}, []string{"invalid port"}},
{"aggregates both", []RemoteHost{{Host: ""}, {Host: "b", Port: 99999}}, []string{"host is required", "invalid port"}},
{"duplicate host", []RemoteHost{{Host: "a"}, {Host: "a"}}, []string{"duplicate host"}},
{"duplicate host different user or port", []RemoteHost{{Host: "box", User: "alice"}, {Host: "box", User: "bob", Port: 2222}}, []string{"duplicate host"}},
{"option shaped host", []RemoteHost{{Host: "-oProxyCommand=sh"}}, []string{"host must not begin with '-'"}},
{"option shaped user", []RemoteHost{{Host: "box", User: "-lroot"}}, []string{"user must not begin with '-'"}},
{"none configured", nil, nil},
{"negative interval", []RemoteHost{{Host: "a", Interval: -1}}, []string{"invalid interval"}},
{"zero interval ok", []RemoteHost{{Host: "a", Interval: 0}}, nil},
{"http valid", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "https://a.example.test", Token: "token"}}, nil},
{"http requires url", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP}}, []string{"url is required"}},
{"http requires token", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "https://a.example.test"}}, []string{"token is required"}},
{"http rejects user", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "https://a.example.test", User: "alice"}}, []string{"user is only valid for ssh"}},
{"http rejects port", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "https://a.example.test", Port: 443}}, []string{"port is only valid for ssh"}},
{"http rejects bad scheme", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "ftp://a.example.test"}}, []string{"url must use http or https"}},
{"http rejects empty hostname with port", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "http://:8080"}}, []string{"url must include a host"}},
{"http rejects query", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "https://a.example.test?token=x"}}, []string{"url must not include query"}},
{"http rejects empty query delimiter", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "https://a.example.test?", Token: "token"}}, []string{"url must not include query"}},
{"http rejects fragment", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "https://a.example.test/#remote"}}, []string{"url must not include fragment"}},
{"http rejects empty fragment delimiter", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "https://a.example.test#", Token: "token"}}, []string{"url must not include fragment"}},
{"http rejects userinfo", []RemoteHost{{Host: "a", Transport: RemoteTransportHTTP, URL: "https://alice@a.example.test"}}, []string{"url must not include userinfo"}},
{"ssh rejects url", []RemoteHost{{Host: "a", Transport: RemoteTransportSSH, URL: "https://a.example.test"}}, []string{"url is only valid for http"}},
{"ssh rejects token", []RemoteHost{{Host: "a", Transport: RemoteTransportSSH, Token: "token"}}, []string{"token is only valid for http"}},
{"unknown transport", []RemoteHost{{Host: "a", Transport: RemoteTransport("sftp")}}, []string{"invalid transport"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Config{RemoteHosts: tt.hosts}.ValidateRemoteHosts()
if len(tt.wantErr) == 0 {
require.NoError(t, err)
return
}
requireErrorContains(t, err, tt.wantErr...)
})
}
}
func TestLoadFile_SyncIncludeCwdPrefixes(t *testing.T) {
f := newConfigFixture(t)
f.WriteConfigText(t, `sync_include_cwd_prefixes = ["/home/me/work", "/home/me/oss"]
`)
cfg := f.LoadMinimal(t)
assert.Equal(t,
[]string{"/home/me/work", "/home/me/oss"},
cfg.SyncIncludeCwdPrefixes,
)
}
func TestLoadFile_SyncIncludeCwdPrefixesDefaultsEmpty(t *testing.T) {
f := newConfigFixture(t)
f.WriteConfigText(t, `host = "127.0.0.1"
`)
cfg := f.LoadMinimal(t)
assert.Empty(t, cfg.SyncIncludeCwdPrefixes)
}
func TestIsDefaultAgentsviewDBPath(t *testing.T) {
t.Parallel()
// A plain file inside a real ~/.agentsview directory.
defaultDir := filepath.Join(t.TempDir(), ".agentsview")
require.NoError(t, os.MkdirAll(defaultDir, 0o700))
defaultDB := filepath.Join(defaultDir, "sessions.db")
require.NoError(t, os.WriteFile(defaultDB, []byte("db"), 0o600))
// A plain file outside ~/.agentsview.
labDir := filepath.Join(t.TempDir(), "recall-lab-data")
require.NoError(t, os.MkdirAll(labDir, 0o700))
labDB := filepath.Join(labDir, "sessions.db")
require.NoError(t, os.WriteFile(labDB, []byte("db"), 0o600))
// A symlink whose target already exists inside ~/.agentsview.
liveLink := filepath.Join(labDir, "live-link.db")
require.NoError(t, os.Symlink(defaultDB, liveLink))
// An absolute symlink whose target does not exist yet. Opening SQLite
// through it would create the production archive, so it must be guarded
// even though EvalSymlinks cannot resolve the dangling target.
danglingLink := filepath.Join(labDir, "dangling.db")
require.NoError(t, os.Symlink(
filepath.Join(defaultDir, "not-created-yet.db"), danglingLink,
))
// A relative dangling symlink resolves against the link's own directory.
siblingRoot := t.TempDir()
require.NoError(t, os.MkdirAll(
filepath.Join(siblingRoot, ".agentsview"), 0o700,
))
relLinkDir := filepath.Join(siblingRoot, "lab")
require.NoError(t, os.MkdirAll(relLinkDir, 0o700))
relLink := filepath.Join(relLinkDir, "sessions.db")
require.NoError(t, os.Symlink(
filepath.Join("..", ".agentsview", "missing.db"), relLink,
))
// A symlink pointing at a harmless location is not the default archive.
safeLink := filepath.Join(labDir, "safe-link.db")
require.NoError(t, os.Symlink(labDB, safeLink))
tests := []struct {
name string
dbPath string
want bool
}{
{"empty", "", false},
{"whitespace", " ", false},
{"plain file in default dir", defaultDB, true},
{"plain file outside default dir", labDB, false},
{"symlink to existing default db", liveLink, true},
{"absolute dangling symlink into default dir", danglingLink, true},
{"relative dangling symlink into default dir", relLink, true},
{"symlink to non-default db", safeLink, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, IsDefaultAgentsviewDBPath(tc.dbPath))
})
}
}
func TestPGConfigPushVectorsEnabled(t *testing.T) {
boolPtr := func(b bool) *bool { return &b }
tests := []struct {
name string
cfg PGConfig
want bool
}{
{"unset defaults to true", PGConfig{}, true},
{"explicit false", PGConfig{PushVectors: boolPtr(false)}, false},
{"explicit true", PGConfig{PushVectors: boolPtr(true)}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.cfg.PushVectorsEnabled())
})
}
}
func TestPGConfig_LoadsFromTOML(t *testing.T) {
f := newConfigFixture(t)
f.WriteTOML(t, map[string]any{
"pg": map[string]any{
"url": "postgres://localhost/test",
"push_vectors": false,
},
})
cfg := f.LoadMinimal(t)
assert.False(t, cfg.PG.PushVectorsEnabled())
}
func TestPGConfig_PushVectorsDefaultsTrue(t *testing.T) {
f := newConfigFixture(t)
f.WriteTOML(t, map[string]any{
"pg": map[string]any{
"url": "postgres://localhost/test",
},
})
cfg := f.LoadMinimal(t)
assert.True(t, cfg.PG.PushVectorsEnabled())
}