package sessionmanager import ( "context" "errors" "os" "path/filepath" "testing" "github.com/aoagents/agent-orchestrator/backend/internal/domain" ) func TestSpawnEnvProjectVarsCannotOverrideInternal(t *testing.T) { env := spawnEnv("mer-1", "mer", "issue-9", "/data", map[string]string{ "FOO": "bar", EnvSessionID: "hacked", // a project must not override AO-internal vars EnvProjectID: "hacked", }) if env["FOO"] != "bar" { t.Fatalf("FOO = %q, want bar", env["FOO"]) } if env[EnvSessionID] != "mer-1" { t.Fatalf("AO_SESSION_ID = %q, want mer-1 (internal wins)", env[EnvSessionID]) } if env[EnvProjectID] != "mer" { t.Fatalf("AO_PROJECT_ID = %q, want mer (internal wins)", env[EnvProjectID]) } } func TestHookPATH(t *testing.T) { sep := string(os.PathListSeparator) daemonExe := filepath.Join("/opt", "aod", "ao") daemonDir := filepath.Dir(daemonExe) exeOK := func() (string, error) { return daemonExe, nil } cases := []struct { name string executable func() (string, error) daemonPATH string projectEnv map[string]string want string wantErr bool }{ { name: "prepends daemon dir to inherited PATH", executable: exeOK, daemonPATH: "/usr/bin" + sep + "/bin", want: daemonDir + sep + "/usr/bin" + sep + "/bin", }, { name: "project PATH override is the base", executable: exeOK, daemonPATH: "/usr/bin", projectEnv: map[string]string{"PATH": "/proj/bin"}, want: daemonDir + sep + "/proj/bin", }, { name: "empty base PATH yields the daemon dir alone", executable: exeOK, want: daemonDir, }, { name: "unresolvable executable fails", executable: func() (string, error) { return "", errors.New("no exe") }, daemonPATH: "/usr/bin", wantErr: true, }, { // A daemon binary not named "ao" cannot anchor `ao` resolution by // having its directory prepended, so the pin must be refused. name: "executable not named ao fails", executable: func() (string, error) { return filepath.Join("/opt", "aod", "ao-daemon"), nil }, daemonPATH: "/usr/bin", wantErr: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { getenv := func(key string) string { if key == "PATH" { return tc.daemonPATH } return "" } got, err := HookPATH(tc.executable, getenv, tc.projectEnv) if tc.wantErr { if err == nil { t.Fatalf("HookPATH = %q, want error", got) } return } if err != nil { t.Fatalf("HookPATH: %v", err) } if got != tc.want { t.Fatalf("HookPATH = %q, want %q", got, tc.want) } }) } } func TestEffectiveHarnessAndAgentConfig(t *testing.T) { cfg := domain.ProjectConfig{ AgentConfig: domain.AgentConfig{Model: "base", Permissions: domain.PermissionModeAuto}, Worker: domain.RoleOverride{Harness: domain.HarnessCodex, AgentConfig: domain.AgentConfig{Model: "worker"}}, Orchestrator: domain.RoleOverride{Harness: domain.HarnessClaudeCode}, } // Explicit harness always wins. if h := effectiveHarness(domain.HarnessAider, domain.KindWorker, cfg); h != domain.HarnessAider { t.Fatalf("explicit harness = %q, want aider", h) } // Empty harness falls back to the role override per kind. if h := effectiveHarness("", domain.KindWorker, cfg); h != domain.HarnessCodex { t.Fatalf("worker harness = %q, want codex", h) } if h := effectiveHarness("", domain.KindOrchestrator, cfg); h != domain.HarnessClaudeCode { t.Fatalf("orchestrator harness = %q, want claude-code", h) } // Role override merges over the base agent config (set fields win; unset keep base). got := effectiveAgentConfig(domain.KindWorker, cfg) if got.Model != "worker" || got.Permissions != domain.PermissionModeAuto { t.Fatalf("merged worker config = %#v, want model=worker permissions=auto", got) } // Orchestrator has no agent-config override, so the base config is used as-is. if got := effectiveAgentConfig(domain.KindOrchestrator, cfg); got.Model != "base" { t.Fatalf("orchestrator config = %#v, want base", got) } } func TestApplySymlinks(t *testing.T) { project := t.TempDir() workspace := t.TempDir() if err := os.WriteFile(filepath.Join(project, ".env"), []byte("X=1"), 0o644); err != nil { t.Fatal(err) } // A present source is linked; a missing source is skipped, not an error. if err := applySymlinks(project, workspace, []string{".env", "missing.txt"}); err != nil { t.Fatalf("applySymlinks: %v", err) } target := filepath.Join(workspace, ".env") if data, err := os.ReadFile(target); err != nil || string(data) != "X=1" { t.Fatalf("symlinked .env = %q err=%v", data, err) } if _, err := os.Lstat(filepath.Join(workspace, "missing.txt")); !os.IsNotExist(err) { t.Fatal("missing source should not have been linked") } } func TestApplySymlinksRejectsParentTraversal(t *testing.T) { project := t.TempDir() workspace := t.TempDir() // A "..", "/" or "../" segment escapes the project tree and must be refused // before any stat/link runs, so a project config cannot link in arbitrary // host files. for _, bad := range []string{"../escape", "/etc/passwd", "a/../../b", ".."} { if err := applySymlinks(project, workspace, []string{bad}); err == nil { t.Fatalf("applySymlinks(%q) accepted an unsafe path", bad) } } } func TestRunPostCreate(t *testing.T) { workspace := t.TempDir() if err := runPostCreate(context.Background(), workspace, []string{"echo hi > out.txt"}); err != nil { t.Fatalf("runPostCreate: %v", err) } if _, err := os.Stat(filepath.Join(workspace, "out.txt")); err != nil { t.Fatalf("post-create command did not run in workspace: %v", err) } // A failing command surfaces an error. if err := runPostCreate(context.Background(), workspace, []string{"exit 3"}); err == nil { t.Fatal("expected error from failing post-create command") } }