package runtimeprep import ( "context" "encoding/json" "errors" "os" "path/filepath" "runtime" "strconv" "strings" "testing" "unicode/utf8" ) func TestDesktopCodexPersonalSkillCreatedInOneSessionIsVisibleInAnother(t *testing.T) { home := t.TempDir() setTestHome(t, home) personalRoot := filepath.Join(home, ".codex", "skills") if err := os.MkdirAll(personalRoot, 0o700); err != nil { t.Fatal(err) } preparer := newTestPreparer(t.TempDir()) preparer.RegisterProvider(CodexPreparer{PersonalSkillRoot: personalRoot}) prepare := func(sessionID string) PreparedRuntime { result, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: sessionID, AgentTargetID: "local:codex", Provider: "codex", Cwd: t.TempDir(), }) if err != nil { t.Fatalf("Prepare(%s) error = %v", sessionID, err) } return result } first := prepare("session-1") firstSkills := filepath.Join(envValue(first.Env, "CODEX_HOME"), "skills") created := filepath.Join(firstSkills, "created-skill", "SKILL.md") writeSidecarTestFile(t, created, "---\nname: created-skill\ndescription: created\n---\n") second := prepare("session-2") secondSkill := filepath.Join(envValue(second.Env, "CODEX_HOME"), "skills", "created-skill", "SKILL.md") firstInfo, err := os.Stat(created) if err != nil { t.Fatal(err) } secondInfo, err := os.Stat(secondSkill) if err != nil { t.Fatalf("personal Skill missing from second Session: %v", err) } if !os.SameFile(firstInfo, secondInfo) { t.Fatal("Sessions do not expose the same personal Skill file") } if _, err := os.Stat(filepath.Join(personalRoot, "tutti-cli", "SKILL.md")); !os.IsNotExist(err) { t.Fatalf("Tutti-managed Skill leaked into personal root, err = %v", err) } var extraRoots []string if err := json.Unmarshal([]byte(envValue(second.Env, tuttiAgentExtraSkillRootsEnv)), &extraRoots); err != nil { t.Fatalf("decode extra Skill roots: %v", err) } if len(extraRoots) != 1 || filepath.Base(extraRoots[0]) != "codex-session-skills" { t.Fatalf("extra Skill roots = %#v, want isolated session root", extraRoots) } } func TestConfiguredPersonalSkillRootMustAlreadyExist(t *testing.T) { for _, tc := range []struct { name string provider string register func(*DefaultPreparer, string) }{ { name: "codex", provider: "codex", register: func(preparer *DefaultPreparer, root string) { preparer.RegisterProvider(CodexPreparer{PersonalSkillRoot: root}) }, }, { name: "claude", provider: "claude-code", register: func(preparer *DefaultPreparer, root string) { preparer.RegisterProvider(ClaudeCodePreparer{PersonalSkillRoot: root}) }, }, } { t.Run(tc.name, func(t *testing.T) { personalRoot := filepath.Join(t.TempDir(), "missing-personal-skills") preparer := newTestPreparer(t.TempDir()) tc.register(preparer, personalRoot) _, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:" + tc.provider, Provider: tc.provider, Cwd: t.TempDir(), }) if err == nil || !strings.Contains(strings.ToLower(err.Error()), "personal skill root") { t.Fatalf("Prepare() error = %v, want missing personal skill root error", err) } if _, statErr := os.Lstat(personalRoot); !os.IsNotExist(statErr) { t.Fatalf("runtimeprep created Host-owned personal skill root: %v", statErr) } }) } } func TestDefaultPreparerCodexWritesInstructionsSkillManifestAndEnv(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "auth.json"), []byte(`{"token":"test"}`), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "models_cache.json"), []byte(`{"models":["cached"]}`), 0o600); err != nil { t.Fatal(err) } userCodexConfig := strings.Join([]string{ `notify = ["say", "done"]`, `model_provider = "proxy"`, `model_catalog_json = "cc-switch-model-catalog.json"`, `service_tier = "default"`, "", "[model_providers.proxy]", `base_url = "https://openai.proxy.test/v1"`, "", }, "\n") if err := os.WriteFile(filepath.Join(userCodexHome, "config.toml"), []byte(userCodexConfig), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "cc-switch-model-catalog.json"), []byte(`{"models":[]}`), 0o600); err != nil { t.Fatal(err) } writeSidecarTestFile(t, filepath.Join(userCodexHome, "plugins", "cache", "sample", "plugin.txt"), "plugin cache") writeSidecarTestFile(t, filepath.Join(userCodexHome, "plugins", "data", "sample", "state.txt"), "plugin state") writeSidecarTestFile(t, filepath.Join(userCodexHome, "plugins", ".plugin-appserver", "codex"), "plugin server") writeSidecarTestFile(t, filepath.Join(userCodexHome, "skills", "caveman", "SKILL.md"), "---\nname: caveman\n---\nCaveman mode\n") writeSidecarTestFile(t, filepath.Join(userCodexHome, "skills", "grill-me", "SKILL.md"), "---\nname: grill-me\n---\nGrill me\n") writeSidecarTestFile(t, filepath.Join(userCodexHome, "skills", "broken-frontmatter", "SKILL.md"), "name: broken-frontmatter\n---\nBroken\n") writeSidecarTestFile(t, filepath.Join(userCodexHome, "skills", ".system", "hidden", "SKILL.md"), "---\nname: hidden\n---\nHidden\n") if err := os.MkdirAll(filepath.Join(userCodexHome, "skills", "invalid"), 0o755); err != nil { t.Fatal(err) } stateDir := t.TempDir() cwd := t.TempDir() agentsPath := filepath.Join(cwd, "AGENTS.md") if err := os.WriteFile(agentsPath, []byte("existing guidance\n"), 0o644); err != nil { t.Fatal(err) } preparer := newTestPreparer(stateDir) prepared, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:codex", Provider: "codex", Cwd: cwd, Connector: &ConnectorAgentContext{RoutingHints: []ConnectorRoutingHint{{ConnectorKey: "lark-cli", DisplayName: "Lark CLI", Aliases: []string{"飞书", "Feishu", "Lark", "Lark Suite"}}}, }, ExtraSkills: []ProviderSkillBundle{ { Name: "app-factory", Files: map[string]string{ "SKILL.md": "---\nname: app-factory\n---\nmention://workspace-app-factory/create\n", "references/manifest-contract.md": "manifest contract", }, }, }, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } content, err := os.ReadFile(agentsPath) if err != nil { t.Fatal(err) } if string(content) != "existing guidance\n" { t.Fatalf("cwd AGENTS.md content = %q, want user guidance unchanged", string(content)) } codexHome := envValue(prepared.Env, "CODEX_HOME") if codexHome == "" { t.Fatalf("prepared env = %#v, want CODEX_HOME", prepared.Env) } codexAgents, err := os.ReadFile(filepath.Join(codexHome, "AGENTS.md")) if err != nil { t.Fatalf("codex AGENTS.md missing: %v", err) } // The active Connector alias index has its own 640-rune cap. Keep the full // provider instructions bounded while reserving room for that routing data // plus the session-sticky enable-set protocol in connector-discovery. const maxCodexAgentsChars = 7600 if count := utf8.RuneCountInString(string(codexAgents)); count > maxCodexAgentsChars { t.Fatalf("codex AGENTS.md chars = %d, want <= %d", count, maxCodexAgentsChars) } if !strings.Contains(string(codexAgents), "`tutti --help`") || !strings.Contains(string(codexAgents), "App id mapping") { t.Fatalf("codex AGENTS.md content = %q", string(codexAgents)) } if !strings.Contains(string(codexAgents), `lark-cli=Lark CLI|飞书|Feishu|Lark|Lark Suite`) { t.Fatalf("codex AGENTS.md content = %q, want active connector routing aliases", string(codexAgents)) } if strings.Contains(string(codexAgents), `Skill(skill="issue-manager", args="")`) { t.Fatalf("codex AGENTS.md content = %q, want provider-neutral mention routing", string(codexAgents)) } if strings.Contains(string(codexAgents), "CODEX_HOME/skills//SKILL.md") || strings.Contains(string(codexAgents), "`workspace-app/SKILL.md`") { t.Fatalf("codex AGENTS.md content = %q, want no guessed materialized skill paths", string(codexAgents)) } if !strings.Contains(string(codexAgents), "# Host App Context") || !strings.Contains(string(codexAgents), "Images/videos: use Markdown") || !strings.Contains(string(codexAgents), "Native image generation results are rendered directly from `imageGeneration` tool output as generated-image artifacts.") || !strings.Contains(string(codexAgents), "do not repeat generated images as Markdown image tags") || strings.Contains(string(codexAgents), "Generated/edited image output: final response must include Markdown image tag.") || !strings.Contains(string(codexAgents), "Prefer `$CODEX_HOME/generated_images/`") || !strings.Contains(string(codexAgents), "never use unverified sandbox path") || !strings.Contains(string(codexAgents), "No inline base64.") || !strings.Contains(string(codexAgents), "use `[filename](/abs/path)` Markdown links") || !strings.Contains(string(codexAgents), "No relative paths, line suffixes") || !strings.Contains(string(codexAgents), "Web URLs: Markdown links") { t.Fatalf("codex AGENTS.md content = %q, want host app rendering guidance", string(codexAgents)) } if _, err := os.Lstat(filepath.Join(codexHome, "auth.json")); err != nil { t.Fatalf("codex auth not exposed: %v", err) } modelsCachePath := filepath.Join(codexHome, "models_cache.json") modelsCacheInfo, err := os.Lstat(modelsCachePath) if err != nil { t.Fatalf("codex models cache not exposed: %v", err) } if runtime.GOOS == "windows" { if modelsCacheInfo.Mode()&os.ModeSymlink != 0 { t.Fatalf("codex models cache should use a Windows-compatible file link, got symlink") } } else { if modelsCacheInfo.Mode()&os.ModeSymlink == 0 { t.Fatalf("codex models cache should be a symlink, got mode %v", modelsCacheInfo.Mode()) } modelsCacheTarget, err := os.Readlink(modelsCachePath) if err != nil { t.Fatal(err) } if want := filepath.Join(userCodexHome, "models_cache.json"); modelsCacheTarget != want { t.Fatalf("codex models cache symlink target = %q, want %q", modelsCacheTarget, want) } } if err := os.WriteFile(modelsCachePath, []byte(`{"models":["refreshed"]}`), 0o600); err != nil { t.Fatalf("refresh run-scoped codex models cache: %v", err) } sharedModelsCache, err := os.ReadFile(filepath.Join(userCodexHome, "models_cache.json")) if err != nil { t.Fatal(err) } if got, want := string(sharedModelsCache), `{"models":["refreshed"]}`; got != want { t.Fatalf("shared codex models cache = %q, want %q", got, want) } secondPrepared, err := newTestPreparer(t.TempDir()).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-2", AgentTargetID: "local:codex", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("prepare second Codex session: %v", err) } secondModelsCachePath := filepath.Join(envValue(secondPrepared.Env, "CODEX_HOME"), "models_cache.json") secondModelsCache, err := os.ReadFile(secondModelsCachePath) if err != nil { t.Fatalf("read second session Codex models cache: %v", err) } if got, want := string(secondModelsCache), `{"models":["refreshed"]}`; got != want { t.Fatalf("second session Codex models cache = %q, want %q", got, want) } catalogLink, err := os.Lstat(filepath.Join(codexHome, "cc-switch-model-catalog.json")) if err != nil { t.Fatalf("codex model catalog not exposed: %v", err) } if runtime.GOOS != "windows" && catalogLink.Mode()&os.ModeSymlink == 0 { t.Fatalf("codex model catalog should be a symlink, got mode %v", catalogLink.Mode()) } for _, rel := range []string{ filepath.Join("plugins", "cache"), filepath.Join("plugins", "data"), filepath.Join("plugins", ".plugin-appserver"), } { info, err := os.Lstat(filepath.Join(codexHome, rel)) if err != nil { t.Fatalf("codex plugin state %s not exposed: %v", rel, err) } if runtime.GOOS != "windows" && info.Mode()&os.ModeSymlink == 0 { t.Fatalf("codex plugin state %s should be exposed as symlink", rel) } } if _, err := os.Stat(filepath.Join(cwd, ".tutti-codex-root")); !os.IsNotExist(err) { t.Fatalf("codex preparer should not create project root marker in cwd, err = %v", err) } codexConfigPath := filepath.Join(codexHome, "config.toml") codexConfigInfo, err := os.Lstat(codexConfigPath) if err != nil { t.Fatalf("codex config missing: %v", err) } if codexConfigInfo.Mode()&os.ModeSymlink != 0 { t.Fatalf("codex config should be a session-scoped file, got symlink") } codexConfig, err := os.ReadFile(codexConfigPath) if err != nil { t.Fatal(err) } if !strings.Contains(string(codexConfig), codexProjectRootMarkersDisabledConfig) || !strings.Contains(string(codexConfig), `notify = ["say", "done"]`) || !strings.Contains(string(codexConfig), `model_provider = "proxy"`) || !strings.Contains(string(codexConfig), "[model_providers.proxy]") { t.Fatalf("codex config = %q, want copied user config with project root markers disabled", string(codexConfig)) } if strings.Contains(string(codexConfig), `service_tier = "default"`) { t.Fatalf("codex config = %q, want unsupported default service_tier removed", string(codexConfig)) } userConfigAfterPrepare, err := os.ReadFile(filepath.Join(userCodexHome, "config.toml")) if err != nil { t.Fatal(err) } if string(userConfigAfterPrepare) != userCodexConfig { t.Fatalf("user codex config was modified: %q", string(userConfigAfterPrepare)) } cavemanPath := filepath.Join(codexHome, "skills", "caveman") cavemanInfo, err := os.Lstat(cavemanPath) if err != nil { t.Fatalf("caveman skill not exposed: %v", err) } if runtime.GOOS != "windows" && cavemanInfo.Mode()&os.ModeSymlink == 0 { t.Fatalf("caveman skill mode = %v, want symlink", cavemanInfo.Mode()) } if runtime.GOOS != "windows" { cavemanTarget, err := os.Readlink(cavemanPath) if err != nil { t.Fatal(err) } if cavemanTarget != filepath.Join(userCodexHome, "skills", "caveman") { t.Fatalf("caveman symlink target = %q", cavemanTarget) } } cavemanSkill, err := os.ReadFile(filepath.Join(cavemanPath, "SKILL.md")) if err != nil { t.Fatal(err) } if !strings.Contains(string(cavemanSkill), "Caveman mode") { t.Fatalf("caveman skill = %q", string(cavemanSkill)) } grillMeInfo, err := os.Lstat(filepath.Join(codexHome, "skills", "grill-me")) if err != nil { t.Fatalf("grill-me skill not exposed: %v", err) } if runtime.GOOS != "windows" && grillMeInfo.Mode()&os.ModeSymlink == 0 { t.Fatalf("grill-me skill mode = %v, want symlink", grillMeInfo.Mode()) } if _, err := os.Lstat(filepath.Join(codexHome, "skills", ".system")); !os.IsNotExist(err) { t.Fatalf("hidden system skill exposed, err = %v", err) } if _, err := os.Lstat(filepath.Join(codexHome, "skills", "invalid")); !os.IsNotExist(err) { t.Fatalf("invalid skill exposed, err = %v", err) } if _, err := os.Lstat(filepath.Join(codexHome, "skills", "broken-frontmatter")); !os.IsNotExist(err) { t.Fatalf("broken-frontmatter skill exposed, err = %v", err) } skill, err := os.ReadFile(filepath.Join(codexHome, "skills", "tutti-cli", "SKILL.md")) if err != nil { t.Fatalf("tutti skill missing: %v", err) } if !strings.Contains(string(skill), "`tutti --help`") || !strings.Contains(string(skill), "this skill's `command-guide.md`") || !strings.Contains(string(skill), "mention://agent-target") || !strings.Contains(string(skill), "handed off, not absorbed") || !strings.Contains(string(skill), "render the session as a descriptive Markdown link") || !strings.Contains(string(skill), "keep the raw `agentSessionId` only as secondary data") { t.Fatalf("skill content = %q", string(skill)) } commandGuideReference, err := os.ReadFile(filepath.Join(codexHome, "skills", "tutti-cli", commandGuideReferencePath)) if err != nil { t.Fatalf("tutti command guide reference missing: %v", err) } if !strings.Contains(string(commandGuideReference), "tutti agent sessions") || !strings.Contains(string(commandGuideReference), "tutti issue get --issue-id --json") { t.Fatalf("tutti command guide reference = %q", string(commandGuideReference)) } if !strings.Contains(string(skill), "local Tutti daemon") || !strings.Contains(string(skill), "localhost/IPC") || !strings.Contains(string(skill), "execution environment") || !strings.Contains(string(skill), "Issue execution sequencing belongs to `$issue-manager`") { t.Fatalf("skill content = %q, want local daemon environment guidance", string(skill)) } if !strings.HasPrefix(strings.ReplaceAll(string(skill), "\r\n", "\n"), "---\nname: tutti-cli\n") { t.Fatalf("skill missing YAML frontmatter: %q", string(skill)) } if strings.Contains(string(skill), "### Mention-driven issue handoff") { t.Fatalf("tutti skill should stay reference-focused: %q", string(skill)) } issueSkill, err := os.ReadFile(filepath.Join(codexHome, "skills", "issue-manager", "SKILL.md")) if err != nil { t.Fatalf("issue-manager skill missing: %v", err) } if !strings.Contains(string(issueSkill), "mention://workspace-issue") || !strings.Contains(string(issueSkill), "mode=breakdown") || !strings.Contains(string(issueSkill), "command-guide.md") || !strings.Contains(string(issueSkill), "## Inspection Mode") || !strings.Contains(string(issueSkill), "Create the run yourself before doing the work") || !strings.Contains(string(issueSkill), "tutti issue run create --issue-id --agent-provider codex --json") || !strings.Contains(string(issueSkill), "with child tasks execute them in order") || !strings.Contains(string(issueSkill), "Complete that same run") || !strings.Contains(string(issueSkill), "Do not edit code, do not execute the task, and do not create or complete runs in breakdown mode") || !strings.Contains(string(issueSkill), "**Done when:**") { t.Fatalf("issue-manager skill content = %q", string(issueSkill)) } if strings.Contains(string(issueSkill), "--agent-session-id session-1") { t.Fatalf("issue-manager skill should not hard-code explicit session ids: %q", string(issueSkill)) } if envValue(prepared.Env, "TUTTI_AGENT_PROVIDER") != "codex" { t.Fatalf("prepared env = %#v, want TUTTI_AGENT_PROVIDER", prepared.Env) } if envValue(prepared.Env, "TUTTI_AGENT_TARGET_ID") != "local:codex" { t.Fatalf("prepared env = %#v, want TUTTI_AGENT_TARGET_ID", prepared.Env) } if envValue(prepared.Env, "TUTTI_AGENT_CWD") != cwd { t.Fatalf("prepared env = %#v, want TUTTI_AGENT_CWD", prepared.Env) } workspaceAppSkill, err := os.ReadFile(filepath.Join(codexHome, "skills", "workspace-app", "SKILL.md")) if err != nil { t.Fatalf("workspace-app skill missing: %v", err) } if !strings.Contains(string(workspaceAppSkill), "mention://workspace-app") || !strings.Contains(string(workspaceAppSkill), "appId") || !strings.Contains(string(workspaceAppSkill), "use injected `$tutti-cli`") || !strings.Contains(string(workspaceAppSkill), "command-guide.md") || !strings.Contains(string(workspaceAppSkill), "Do not derive filesystem paths from the plugin directory, plugin name, or skill slug") || !strings.Contains(string(workspaceAppSkill), "inherit the caller agent session working directory") { t.Fatalf("workspace-app skill content = %q", string(workspaceAppSkill)) } if strings.Contains(string(workspaceAppSkill), "turn-resources") || strings.Contains(string(workspaceAppSkill), "--image ") { t.Fatalf("workspace-app skill owns agent image handoff guidance: %q", string(workspaceAppSkill)) } if strings.Contains(string(workspaceAppSkill), "read the materialized sibling `tutti-cli/SKILL.md`") { t.Fatalf("workspace-app skill should not ask agents to guess sibling skill paths: %q", string(workspaceAppSkill)) } if strings.Contains(string(workspaceAppSkill), "plugin root `tutti-cli/SKILL.md`") { t.Fatalf("workspace-app skill should not anchor agents to plugin-root paths: %q", string(workspaceAppSkill)) } appFactorySkill, err := os.ReadFile(filepath.Join(codexHome, "skills", "app-factory", "SKILL.md")) if err != nil { t.Fatalf("app-factory skill missing: %v", err) } if !strings.Contains(string(appFactorySkill), "mention://workspace-app-factory/create") { t.Fatalf("app-factory skill content = %q", string(appFactorySkill)) } appFactoryReference, err := os.ReadFile(filepath.Join(codexHome, "skills", "app-factory", "references", "manifest-contract.md")) if err != nil { t.Fatalf("app-factory reference missing: %v", err) } if string(appFactoryReference) != "manifest contract" { t.Fatalf("app-factory reference = %q", string(appFactoryReference)) } rules, err := os.ReadFile(filepath.Join(codexHome, "rules", "default.rules")) if err != nil { t.Fatalf("codex approval rules missing: %v", err) } if !strings.Contains(string(rules), `prefix_rule(pattern=["tutti"], decision="allow")`) { t.Fatalf("codex approval rules = %q, want tutti allow rule", string(rules)) } runtimeRoot, err := LocalStore{StateDir: stateDir}.RuntimeRoot("workspace-1", "session-1") if err != nil { t.Fatal(err) } manifestPath := filepath.Join(runtimeRoot, SidecarManifestFileName) if _, err := os.Stat(manifestPath); err != nil { t.Fatalf("manifest missing: %v", err) } manifestContent, err := os.ReadFile(manifestPath) if err != nil { t.Fatal(err) } if strings.Contains(string(manifestContent), `"workspaceId"`) { t.Fatalf("manifest leaks workspace id: %s", manifestContent) } if envValue(prepared.Env, "TUTTI_WORKSPACE_ID") != "workspace-1" { t.Fatalf("prepared env = %#v, want workspace id", prepared.Env) } } func TestDefaultPreparerReturnsAuthoritativeMCPBindings(t *testing.T) { setTestHome(t, t.TempDir()) input := PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "unknown-provider", Cwd: t.TempDir(), MCPServers: []MCPServerBinding{{ Name: "connector", Type: "http", URL: "http://127.0.0.1:1234/mcp/connector", Headers: map[string]string{"Authorization": "Bearer test-token"}, }}, } prepared, err := newTestPreparer(t.TempDir()).Prepare(t.Context(), input) if err != nil { t.Fatalf("Prepare() error = %v", err) } if len(prepared.MCPServers) != 1 || prepared.MCPServers[0].URL != input.MCPServers[0].URL || prepared.MCPServers[0].Headers["Authorization"] != "Bearer test-token" { t.Fatalf("prepared MCP servers = %#v", prepared.MCPServers) } prepared.MCPServers[0].Headers["Authorization"] = "mutated" if input.MCPServers[0].Headers["Authorization"] != "Bearer test-token" { t.Fatal("prepared MCP binding shares mutable headers with input") } } func TestDefaultPreparerExpandsConnectorAgentContext(t *testing.T) { setTestHome(t, t.TempDir()) connectorBinDir := filepath.Join(t.TempDir(), "connector-bin") input := PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "unknown-provider", Cwd: t.TempDir(), Connector: &ConnectorAgentContext{ MCPServers: []MCPServerBinding{{ Name: "connector", Type: "http", URL: "http://127.0.0.1:1234/mcp/connector", Headers: map[string]string{"Authorization": "Bearer gateway-token"}, }}, RoutingHints: []ConnectorRoutingHint{{ConnectorKey: "lark-cli", DisplayName: "Lark CLI"}}, CLIBinDir: connectorBinDir, }, } prepared, err := newTestPreparer(t.TempDir()).Prepare(t.Context(), input) if err != nil { t.Fatalf("Prepare() error = %v", err) } if len(prepared.MCPServers) != 1 || prepared.MCPServers[0].Headers["Authorization"] != "Bearer gateway-token" { t.Fatalf("prepared MCP servers = %#v", prepared.MCPServers) } pathEntries := filepath.SplitList(envValue(prepared.Env, "PATH")) if len(pathEntries) == 0 || filepath.Clean(pathEntries[0]) != filepath.Clean(connectorBinDir) { t.Fatalf("prepared PATH = %q, want Connector bin first", envValue(prepared.Env, "PATH")) } prepared.MCPServers[0].Headers["Authorization"] = "mutated" if input.Connector.MCPServers[0].Headers["Authorization"] != "Bearer gateway-token" { t.Fatal("prepared MCP binding shares mutable headers with ConnectorAgentContext") } } func TestDefaultPreparerCodexSaverModeKeepsLunaWorkerIndependentFromRTK(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) prepared, err := newTestPreparer(t.TempDir()).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-codex-saver", AgentTargetID: "local:codex", Provider: "codex", Cwd: t.TempDir(), CodexSaverMode: true, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") role, err := os.ReadFile(filepath.Join(codexHome, "agents", "luna_worker.toml")) if err != nil { t.Fatalf("read Luna worker role: %v", err) } for _, expected := range []string{`model = "gpt-5.6-luna"`, `model_reasoning_effort = "max"`} { if !strings.Contains(string(role), expected) { t.Fatalf("role = %q, want %q", role, expected) } } instructions, err := os.ReadFile(filepath.Join(codexHome, "AGENTS.md")) if err != nil { t.Fatalf("read Codex instructions: %v", err) } if !strings.Contains(string(instructions), "default subagent as the Luna worker") { t.Fatalf("Codex saver instructions = %q", instructions) } if strings.Contains(string(instructions), "Always prefix supported shell commands with `rtk`.") { t.Fatalf("Codex-only saver mode unexpectedly enabled RTK: %q", instructions) } } func TestDefaultPreparerRTKSaverModeInstallsProviderNeutralSessionRuntime(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("the fake RTK fixture is a POSIX script") } home := t.TempDir() t.Setenv("HOME", home) binDir := t.TempDir() sourceRTK := filepath.Join(binDir, rtkExecutableName()) writeSidecarTestFile(t, sourceRTK, "#!/bin/sh\nexit 0\n") if err := os.Chmod(sourceRTK, 0o700); err != nil { t.Fatal(err) } stateDir := t.TempDir() preparer := newTestPreparer(stateDir) preparer.RTKExecutableResolver = func(context.Context) (string, error) { return sourceRTK, nil } prepared, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:opencode", Provider: "opencode", Cwd: t.TempDir(), RTKSaverMode: true, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } runtimeRoot, err := LocalStore{StateDir: stateDir}.RuntimeRoot("workspace-1", "session-1") if err != nil { t.Fatal(err) } sessionRTK := filepath.Join(runtimeRoot, "rtk", "bin", rtkExecutableName()) if sessionRTK == sourceRTK { t.Fatal("RTK executable was not isolated") } if info, err := os.Stat(sessionRTK); err != nil || info.Mode()&0o100 == 0 { t.Fatalf("Session RTK executable is unavailable: info=%v err=%v", info, err) } pathValue := envValue(prepared.Env, "PATH") pathEntries := filepath.SplitList(pathValue) if len(pathEntries) == 0 || filepath.Clean(pathEntries[0]) != filepath.Dir(sessionRTK) { t.Fatalf("PATH = %q, want Session RTK first", pathValue) } for key, wantPrefix := range map[string]string{ "RTK_DB_PATH": filepath.Join(runtimeRoot, "rtk", "data"), "RTK_TEE_DIR": filepath.Join(runtimeRoot, "rtk", "tee"), } { if got := envValue(prepared.Env, key); !strings.HasPrefix(got, wantPrefix) { t.Fatalf("%s = %q, want Session runtime prefix %q", key, got, wantPrefix) } } if got := envValue(prepared.Env, "RTK_TELEMETRY_DISABLED"); got != "1" { t.Fatalf("RTK_TELEMETRY_DISABLED = %q, want 1", got) } instructions, err := os.ReadFile(filepath.Join(runtimeRoot, "rtk", "RTK.md")) if err != nil { t.Fatalf("read Session RTK.md: %v", err) } if !strings.Contains(string(instructions), "Always prefix supported shell commands with `rtk`.") { t.Fatalf("RTK.md = %q", instructions) } openCodeHome := envValue(prepared.Env, "OPENCODE_CONFIG_DIR") agentInstructions, err := os.ReadFile(filepath.Join(openCodeHome, "AGENTS.md")) if err != nil { t.Fatal(err) } if !strings.Contains(string(agentInstructions), "Always prefix supported shell commands with `rtk`.") { t.Fatalf("AGENTS.md = %q, want inline RTK instruction injection", agentInstructions) } if want := "@" + filepath.Join(runtimeRoot, "rtk", "RTK.md") + "\n\n"; !strings.HasPrefix(string(agentInstructions), want) { t.Fatalf("AGENTS.md = %q, want Session RTK reference %q first", agentInstructions, want) } // Resume reuses the Session copy even when RTK is no longer available from // the daemon's process PATH, preserving the launch-time tool version. t.Setenv("PATH", t.TempDir()) if _, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:opencode", Provider: "opencode", Cwd: t.TempDir(), RTKSaverMode: true, }); err != nil { t.Fatalf("resumed Prepare() error = %v", err) } } func TestDefaultPreparerRTKSaverModeReferencesSessionInstructionsFirstForAgentHomes(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("the fake RTK fixture is a POSIX script") } setTestHome(t, t.TempDir()) sourceRTK := filepath.Join(t.TempDir(), rtkExecutableName()) writeSidecarTestFile(t, sourceRTK, "#!/bin/sh\nexit 0\n") if err := os.Chmod(sourceRTK, 0o700); err != nil { t.Fatal(err) } for _, test := range []struct { provider string homeEnv string }{ {provider: "codex", homeEnv: "CODEX_HOME"}, {provider: "tutti-agent", homeEnv: "TUTTI_AGENT_HOME"}, {provider: "opencode", homeEnv: "OPENCODE_CONFIG_DIR"}, } { t.Run(test.provider, func(t *testing.T) { stateDir := t.TempDir() preparer := newTestPreparer(stateDir) if test.provider == "tutti-agent" { preparer.RegisterProvider(TuttiAgentPreparer{}) } preparer.RTKExecutableResolver = func(context.Context) (string, error) { return sourceRTK, nil } prepared, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:" + test.provider, Provider: test.provider, Cwd: t.TempDir(), RTKSaverMode: true, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } runtimeRoot, err := LocalStore{StateDir: stateDir}.RuntimeRoot("workspace-1", "session-1") if err != nil { t.Fatal(err) } instructions, err := os.ReadFile(filepath.Join(envValue(prepared.Env, test.homeEnv), "AGENTS.md")) if err != nil { t.Fatal(err) } wantReference := "@" + filepath.Join(runtimeRoot, "rtk", "RTK.md") + "\n\n" if !strings.HasPrefix(string(instructions), wantReference) { t.Fatalf("AGENTS.md = %q, want Session RTK reference %q first", instructions, wantReference) } if !strings.Contains(string(instructions), "Always prefix supported shell commands with `rtk`.") { t.Fatalf("AGENTS.md lost inline RTK fallback: %q", instructions) } }) } } func TestDefaultPreparerRTKSaverModeFailsWhenRTKIsUnavailable(t *testing.T) { t.Setenv("HOME", t.TempDir()) t.Setenv("PATH", t.TempDir()) _, err := newTestPreparer(t.TempDir()).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:claude-code", Provider: "claude-code", Cwd: t.TempDir(), RTKSaverMode: true, }) if err == nil || !strings.Contains(err.Error(), "requires a Tutti-managed rtk executable resolver") { t.Fatalf("Prepare() error = %v", err) } if _, err := newTestPreparer(t.TempDir()).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-disabled", AgentTargetID: "local:claude-code", Provider: "claude-code", Cwd: t.TempDir(), RTKSaverMode: false, }); err != nil { t.Fatalf("disabled Prepare() must not require RTK: %v", err) } } func TestDefaultPreparerCodexDedicatedProjectionNarrowsAutomaticCLIApproval(t *testing.T) { setTestHome(t, t.TempDir()) stateDir := t.TempDir() cwd := t.TempDir() catalog := append(testCommandCapabilities(), CommandCapability{ ID: "tutti-goal-review.goal-review.verdict", Path: []string{"goal-review", "verdict"}, Summary: "Submit Goal Review verdict", Visibility: "integration", Output: CommandCapabilityOutput{DefaultMode: "json", JSON: true}, }) preparer := NewDefaultPreparer(stateDir) preparer.CommandCatalog = staticCommandCatalog(catalog) _, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "review-session-1", AgentTargetID: "local:codex", Provider: "codex", Cwd: cwd, CommandCapabilityProjection: &CommandCapabilityProjection{ AllowedIDs: []string{ "issue-manager.issue.get", "tutti-goal-review.goal-review.verdict", }, IncludeIntegrationIDs: []string{ "tutti-goal-review.goal-review.verdict", }, ExcludeIDs: []string{ "issue-manager.issue.update", }, }, }) if err != nil { t.Fatal(err) } runtimeRoot, err := LocalStore{StateDir: stateDir}.RuntimeRoot( "workspace-1", "review-session-1", ) if err != nil { t.Fatal(err) } rulesPath := filepath.Join(runtimeRoot, "codex-home", "rules", "default.rules") rules, err := os.ReadFile(rulesPath) if err != nil { t.Fatal(err) } content := string(rules) for _, want := range []string{ `prefix_rule(pattern=["tutti", "issue", "get"], decision="allow")`, `prefix_rule(pattern=["tutti", "goal-review", "verdict"], decision="allow")`, } { if !strings.Contains(content, want) { t.Fatalf("dedicated Codex approval rules = %q, want %q", content, want) } } for _, denied := range []string{ `prefix_rule(pattern=["tutti"], decision="allow")`, `prefix_rule(pattern=["tutti", "issue", "update"], decision="allow")`, } { if strings.Contains(content, denied) { t.Fatalf("dedicated Codex approval rules = %q, must omit %q", content, denied) } } } func TestExposeUserCodexModelsCacheSharesFirstRefreshAcrossSessions(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") firstCodexHome := t.TempDir() if err := exposeUserCodexModelsCache(firstCodexHome, userCodexHome); err != nil { t.Fatal(err) } firstCachePath := filepath.Join(firstCodexHome, "models_cache.json") info, err := os.Lstat(firstCachePath) if err != nil { t.Fatalf("first session models cache link missing: %v", err) } if runtime.GOOS != "windows" && info.Mode()&os.ModeSymlink == 0 { t.Fatalf("first session models cache mode = %v, want symlink", info.Mode()) } assertCodexModelsCacheCleared(t, filepath.Join(userCodexHome, "models_cache.json"), "shared models cache should be absent before first provider refresh") if err := os.WriteFile(firstCachePath, []byte(`{"models":["first-refresh"]}`), 0o600); err != nil { t.Fatalf("write first session models cache: %v", err) } secondCodexHome := t.TempDir() if err := exposeUserCodexModelsCache(secondCodexHome, userCodexHome); err != nil { t.Fatal(err) } secondCache, err := os.ReadFile(filepath.Join(secondCodexHome, "models_cache.json")) if err != nil { t.Fatalf("read second session models cache: %v", err) } if got, want := string(secondCache), `{"models":["first-refresh"]}`; got != want { t.Fatalf("second session models cache = %q, want %q", got, want) } } func TestDefaultPreparerCodexRefreshesRunConfigFromCurrentUserConfig(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } userConfigPath := filepath.Join(userCodexHome, "config.toml") if err := os.WriteFile(userConfigPath, []byte("model = \"gpt-5.6-sol\"\n"), 0o600); err != nil { t.Fatal(err) } stateDir := t.TempDir() input := PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:codex", Provider: "codex", Cwd: t.TempDir(), } first, err := newTestPreparer(stateDir).Prepare(t.Context(), input) if err != nil { t.Fatalf("first Prepare() error = %v", err) } codexConfigPath := filepath.Join(envValue(first.Env, "CODEX_HOME"), "config.toml") firstConfig, err := os.ReadFile(codexConfigPath) if err != nil { t.Fatal(err) } if !strings.Contains(string(firstConfig), `model = "gpt-5.6-sol"`) { t.Fatalf("first run config = %q, want initial user model", string(firstConfig)) } if err := os.WriteFile(userConfigPath, []byte(strings.Join([]string{ `model_provider = "custom"`, `model = "glm-5"`, "", "[model_providers.custom]", `base_url = "https://proxy.example/v1"`, "", }, "\n")), 0o600); err != nil { t.Fatal(err) } second, err := newTestPreparer(stateDir).Prepare(t.Context(), input) if err != nil { t.Fatalf("second Prepare() error = %v", err) } if got, want := envValue(second.Env, "CODEX_HOME"), envValue(first.Env, "CODEX_HOME"); got != want { t.Fatalf("second CODEX_HOME = %q, want stable run home %q", got, want) } secondConfig, err := os.ReadFile(codexConfigPath) if err != nil { t.Fatal(err) } if !strings.Contains(string(secondConfig), `model_provider = "custom"`) || !strings.Contains(string(secondConfig), `model = "glm-5"`) || strings.Contains(string(secondConfig), `model = "gpt-5.6-sol"`) { t.Fatalf("second run config = %q, want refreshed custom provider config", string(secondConfig)) } if !strings.Contains(string(secondConfig), codexProjectRootMarkersDisabledConfig) { t.Fatalf("second run config = %q, want runtime session overlay reapplied", string(secondConfig)) } } func TestDefaultPreparerCodexRemovesRunConfigWhenUserConfigDisappears(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } userConfigPath := filepath.Join(userCodexHome, "config.toml") if err := os.WriteFile(userConfigPath, []byte("model = \"glm-5\"\n"), 0o600); err != nil { t.Fatal(err) } stateDir := t.TempDir() input := PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:codex", Provider: "codex", Cwd: t.TempDir(), } first, err := newTestPreparer(stateDir).Prepare(t.Context(), input) if err != nil { t.Fatalf("first Prepare() error = %v", err) } codexConfigPath := filepath.Join(envValue(first.Env, "CODEX_HOME"), "config.toml") if err := os.Remove(userConfigPath); err != nil { t.Fatal(err) } if _, err := newTestPreparer(stateDir).Prepare(t.Context(), input); err != nil { t.Fatalf("second Prepare() error = %v", err) } config, err := os.ReadFile(codexConfigPath) if err != nil { t.Fatal(err) } if strings.Contains(string(config), `model = "glm-5"`) { t.Fatalf("second run config retained removed user model: %q", string(config)) } if !strings.Contains(string(config), codexProjectRootMarkersDisabledConfig) { t.Fatalf("second run config = %q, want runtime session config", string(config)) } } func TestExposeUserCodexModelsCacheDropsUnfencedRunCache(t *testing.T) { userCodexHome := filepath.Join(t.TempDir(), ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "config.toml"), []byte(`model_provider = "custom"`), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "models_cache.json"), []byte(`{"models":["shared-stale"]}`), 0o600); err != nil { t.Fatal(err) } codexHome := t.TempDir() target := filepath.Join(codexHome, "models_cache.json") if err := os.WriteFile(target, []byte(`{"models":["session-local"]}`), 0o600); err != nil { t.Fatal(err) } if err := exposeUserCodexModelsCache(codexHome, userCodexHome); err != nil { t.Fatal(err) } info, err := os.Lstat(target) if err != nil { t.Fatal(err) } if runtime.GOOS != "windows" && info.Mode()&os.ModeSymlink == 0 { t.Fatalf("run cache mode = %v, want authority-scoped shared-cache symlink", info.Mode()) } assertCodexModelsCacheCleared(t, filepath.Join(userCodexHome, "models_cache.json"), "unfenced shared cache should be removed") if _, err := os.Stat(filepath.Join(userCodexHome, codexModelsCacheAuthorityFile)); err != nil { t.Fatalf("models cache authority fence missing: %v", err) } } func TestExposeUserCodexModelsCacheInvalidatesWhenGlobalConfigChanges(t *testing.T) { userCodexHome := filepath.Join(t.TempDir(), ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } configPath := filepath.Join(userCodexHome, "config.toml") if err := os.WriteFile(configPath, []byte("model_provider = \"first\"\nmodel = \"first-model\"\n"), 0o600); err != nil { t.Fatal(err) } firstCodexHome := t.TempDir() if err := exposeUserCodexModelsCache(firstCodexHome, userCodexHome); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(firstCodexHome, "models_cache.json"), []byte(`{"models":["first"]}`), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(configPath, []byte("model_provider = \"second\"\nmodel = \"second-model\"\n"), 0o600); err != nil { t.Fatal(err) } secondCodexHome := t.TempDir() if err := exposeUserCodexModelsCache(secondCodexHome, userCodexHome); err != nil { t.Fatal(err) } assertCodexModelsCacheCleared(t, filepath.Join(userCodexHome, "models_cache.json"), "cache from previous global config should be removed") if info, err := os.Lstat(filepath.Join(secondCodexHome, "models_cache.json")); err != nil || runtime.GOOS != "windows" && info.Mode()&os.ModeSymlink == 0 { t.Fatalf("second run cache link missing after config invalidation: info=%#v err=%v", info, err) } } func TestExposeUserCodexModelsCacheInvalidatesWhenGlobalCatalogChanges(t *testing.T) { home := t.TempDir() userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } configPath := filepath.Join(userCodexHome, "config.toml") catalogPath := filepath.Join(userCodexHome, "catalog.json") if err := os.WriteFile(configPath, []byte("model_provider = \"first\"\nmodel_catalog_json = \"catalog.json\"\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(catalogPath, []byte(`{"models":[{"slug":"first"}]}`), 0o600); err != nil { t.Fatal(err) } firstCodexHome := t.TempDir() if err := exposeUserCodexModelsCache(firstCodexHome, userCodexHome); err != nil { t.Fatal(err) } firstCache := filepath.Join(firstCodexHome, "models_cache.json") if err := os.WriteFile(firstCache, []byte(`{"models":["first"]}`), 0o600); err != nil { t.Fatal(err) } firstFence, err := os.ReadFile(filepath.Join(userCodexHome, codexModelsCacheAuthorityFile)) if err != nil { t.Fatal(err) } if err := os.WriteFile(catalogPath, []byte(`{"models":[{"slug":"second"}]}`), 0o600); err != nil { t.Fatal(err) } secondCodexHome := t.TempDir() if err := exposeUserCodexModelsCache(secondCodexHome, userCodexHome); err != nil { t.Fatal(err) } assertCodexModelsCacheCleared(t, filepath.Join(userCodexHome, "models_cache.json"), "cache from previous global catalog should be removed") secondFence, err := os.ReadFile(filepath.Join(userCodexHome, codexModelsCacheAuthorityFile)) if err != nil { t.Fatal(err) } if string(firstFence) == string(secondFence) { t.Fatalf("models cache authority fence did not change after global catalog update") } if info, err := os.Lstat(filepath.Join(secondCodexHome, "models_cache.json")); err != nil || runtime.GOOS != "windows" && info.Mode()&os.ModeSymlink == 0 { t.Fatalf("second run cache link missing after invalidation: info=%#v err=%v", info, err) } } func TestDefaultPreparerCodexExposesRelativeModelCatalogJSON(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "auth.json"), []byte(`{"token":"test"}`), 0o600); err != nil { t.Fatal(err) } catalogName := "cc-switch-model-catalog.json" catalogBody := `{"models":[{"slug":"gpt-5.5","display_name":"gpt-5.5"}]}` if err := os.WriteFile(filepath.Join(userCodexHome, catalogName), []byte(catalogBody), 0o600); err != nil { t.Fatal(err) } userCodexConfig := strings.Join([]string{ `model_catalog_json = "cc-switch-model-catalog.json"`, `model_provider = "proxy"`, "", "[model_providers.proxy]", `base_url = "https://openai.proxy.test/v1"`, "", }, "\n") if err := os.WriteFile(filepath.Join(userCodexHome, "config.toml"), []byte(userCodexConfig), 0o600); err != nil { t.Fatal(err) } stateDir := t.TempDir() cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-catalog", AgentTargetID: "local:codex", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") if codexHome == "" { t.Fatalf("prepared env = %#v, want CODEX_HOME", prepared.Env) } sandboxCatalog := filepath.Join(codexHome, catalogName) info, err := os.Lstat(sandboxCatalog) if err != nil { t.Fatalf("relative model catalog not exposed into sandbox: %v", err) } if runtime.GOOS != "windows" && info.Mode()&os.ModeSymlink == 0 { t.Fatalf("sandbox model catalog mode = %v, want symlink", info.Mode()) } if runtime.GOOS != "windows" { linkTarget, err := os.Readlink(sandboxCatalog) if err != nil { t.Fatal(err) } if linkTarget != filepath.Join(userCodexHome, catalogName) { t.Fatalf("sandbox catalog symlink target = %q, want user catalog", linkTarget) } } got, err := os.ReadFile(sandboxCatalog) if err != nil { t.Fatal(err) } if string(got) != catalogBody { t.Fatalf("sandbox catalog body = %q, want %q", string(got), catalogBody) } } func TestDefaultPreparerCodexReconcilesNativeSkillsAcrossRepeatedPrepare(t *testing.T) { home := t.TempDir() setTestHome(t, home) preparer := newTestPreparer(t.TempDir()) input := PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:codex", Provider: "codex", Cwd: t.TempDir(), } first, err := preparer.Prepare(t.Context(), input) if err != nil { t.Fatalf("first Prepare() error = %v", err) } second, err := preparer.Prepare(t.Context(), input) if err != nil { t.Fatalf("second Prepare() error = %v", err) } firstCodexHome := envValue(first.Env, "CODEX_HOME") secondCodexHome := envValue(second.Env, "CODEX_HOME") if firstCodexHome != secondCodexHome { t.Fatalf("repeated Prepare() CODEX_HOME = %q and %q, want the same durable home", firstCodexHome, secondCodexHome) } if _, err := os.Stat(filepath.Join(secondCodexHome, "skills", "tutti-cli", "SKILL.md")); err != nil { t.Fatalf("stable tutti-cli skill missing after repeated Prepare(): %v", err) } if _, err := os.Stat(filepath.Join(secondCodexHome, "skills", "tutti-cli-tutti")); !os.IsNotExist(err) { t.Fatalf("repeated Prepare() created a suffixed tutti-cli skill, err = %v", err) } } func TestDefaultPreparerCodexSkipsSkillsForModelProbe(t *testing.T) { setTestHome(t, t.TempDir()) preparer := newTestPreparer(t.TempDir()) prepared, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "model-probe-1", AgentTargetID: "local:codex", Provider: "codex", Cwd: t.TempDir(), SkipSkills: true, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") if codexHome == "" { t.Fatalf("prepared env = %#v, want CODEX_HOME", prepared.Env) } if _, err := os.Stat(filepath.Join(codexHome, "skills")); !os.IsNotExist(err) { t.Fatalf("model-only Prepare() created a Skill root, err = %v", err) } } func TestDefaultPreparerCodexUserSkillNameWinsBeforeTuttiInjection(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") writeSidecarTestFile(t, filepath.Join(userCodexHome, "skills", "tutti-cli", "SKILL.md"), "---\nname: tutti-cli\n---\nUser tutti skill\n") stateDir := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: t.TempDir(), }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") userSkillPath := filepath.Join(codexHome, "skills", "tutti-cli") userSkillInfo, err := os.Lstat(userSkillPath) if err != nil { t.Fatalf("user tutti-cli skill not exposed: %v", err) } if runtime.GOOS != "windows" && userSkillInfo.Mode()&os.ModeSymlink == 0 { t.Fatalf("user tutti-cli skill mode = %v, want symlink", userSkillInfo.Mode()) } userSkill, err := os.ReadFile(filepath.Join(userSkillPath, "SKILL.md")) if err != nil { t.Fatal(err) } if !strings.Contains(string(userSkill), "User tutti skill") { t.Fatalf("user tutti-cli skill = %q", string(userSkill)) } tuttiSkill, err := os.ReadFile(filepath.Join(codexHome, "skills", "tutti-cli-tutti", "SKILL.md")) if err != nil { t.Fatalf("tutti fallback skill missing: %v", err) } if !strings.Contains(string(tuttiSkill), "`tutti --help`") || !strings.Contains(string(tuttiSkill), "this skill's `command-guide.md`") { t.Fatalf("tutti fallback skill = %q", string(tuttiSkill)) } tuttiReference, err := os.ReadFile(filepath.Join(codexHome, "skills", "tutti-cli-tutti", commandGuideReferencePath)) if err != nil { t.Fatalf("tutti fallback command guide reference missing: %v", err) } if !strings.Contains(string(tuttiReference), "tutti agent sessions") { t.Fatalf("tutti fallback command guide reference = %q", string(tuttiReference)) } } func TestDefaultPreparerCodexWritesProjectRootMarkersDisabledConfigWithoutUserConfig(t *testing.T) { home := t.TempDir() setTestHome(t, home) stateDir := t.TempDir() cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } if _, err := os.Stat(filepath.Join(cwd, ".tutti-codex-root")); !os.IsNotExist(err) { t.Fatalf("codex preparer should not create project root marker in cwd, err = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") codexConfig, err := os.ReadFile(filepath.Join(codexHome, "config.toml")) if err != nil { t.Fatalf("codex config missing: %v", err) } config := string(codexConfig) if !strings.Contains(config, codexProjectRootMarkersDisabledConfig) || !strings.Contains(config, "[tutti]") || !strings.Contains(config, `conversationDetailMode = "coding"`) || strings.Contains(config, "### Non-technical UI") { t.Fatalf("codex config = %q, want project root markers disabled and Tutti coding marker only", config) } } func TestDefaultPreparerCodexWritesGeneralConversationDetailModeToSessionConfig(t *testing.T) { home := t.TempDir() setTestHome(t, home) stateDir := t.TempDir() cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: cwd, ConversationDetailMode: "general", }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") codexConfig, err := os.ReadFile(filepath.Join(codexHome, "config.toml")) if err != nil { t.Fatalf("codex config missing: %v", err) } config := string(codexConfig) if !strings.Contains(config, "[tutti]") || !strings.Contains(config, `conversationDetailMode = "general"`) || !strings.Contains(config, `developer_instructions =`) || !strings.Contains(config, "### Non-technical UI") || !strings.Contains(config, "don't name bash commands you're running") || !strings.Contains(config, "focus on outputs") { t.Fatalf("codex config = %q, want Tutti general marker and non-technical UI developer instructions", config) } } func TestCodexConfigWithTuttiConversationDetailModeUpdatesExistingMarker(t *testing.T) { input := strings.Join([]string{ `project_root_markers = []`, "", "[tutti]", `conversationDetailMode = "coding"`, "", "[model_providers.proxy]", `base_url = "https://openai.proxy.test/v1"`, }, "\n") next, changed := codexConfigWithTuttiConversationDetailMode(input, "general") if !changed { t.Fatalf("codexConfigWithTuttiConversationDetailMode changed = false, want true") } if !strings.Contains(next, `[tutti]`) || !strings.Contains(next, `conversationDetailMode = "general"`) || strings.Contains(next, `conversationDetailMode = "coding"`) || !strings.Contains(next, "[model_providers.proxy]") { t.Fatalf("merged config = %q, want updated Tutti conversation detail mode marker", next) } } func TestCodexConfigWithConversationDetailModeInstructionsAppendsExistingDeveloperInstructions(t *testing.T) { input := strings.Join([]string{ `developer_instructions = "Existing guidance."`, `model = "gpt-5.5"`, "", "[model_providers.proxy]", `base_url = "https://openai.proxy.test/v1"`, }, "\n") next, changed := codexConfigWithConversationDetailModeInstructions(input, "general") if !changed { t.Fatalf("codexConfigWithConversationDetailModeInstructions changed = false, want true") } if !strings.Contains(next, "Existing guidance.") || !strings.Contains(next, "### Non-technical UI") || !strings.Contains(next, "[model_providers.proxy]") { t.Fatalf("merged config = %q, want existing developer instructions plus non-technical UI", next) } } func TestCodexConfigWithConversationDetailModeInstructionsRemovesManagedInstructionsForCoding(t *testing.T) { input := strings.Join([]string{ `developer_instructions = ` + strconv.Quote("Existing guidance.\n\n"+nonTechnicalUIConversationDetailModeInstructions), `model = "gpt-5.5"`, "", "[tutti]", `conversationDetailMode = "general"`, }, "\n") next, changed := codexConfigWithConversationDetailModeInstructions(input, "coding") if !changed { t.Fatalf("codexConfigWithConversationDetailModeInstructions changed = false, want true") } if !strings.Contains(next, `developer_instructions = "Existing guidance."`) || strings.Contains(next, "### Non-technical UI") || !strings.Contains(next, "[tutti]") { t.Fatalf("merged config = %q, want existing developer instructions without non-technical UI", next) } } func TestCodexConfigWithConversationDetailModeInstructionsRemovesEmptyManagedKeyForCoding(t *testing.T) { input := strings.Join([]string{ `developer_instructions = ` + strconv.Quote(nonTechnicalUIConversationDetailModeInstructions), `model = "gpt-5.5"`, "", "[tutti]", `conversationDetailMode = "general"`, }, "\n") next, changed := codexConfigWithConversationDetailModeInstructions(input, "coding") if !changed { t.Fatalf("codexConfigWithConversationDetailModeInstructions changed = false, want true") } if strings.Contains(next, `developer_instructions =`) || strings.Contains(next, "### Non-technical UI") || !strings.Contains(next, `model = "gpt-5.5"`) || !strings.Contains(next, "[tutti]") { t.Fatalf("merged config = %q, want managed developer_instructions key removed", next) } } func TestCodexConfigWithProjectRootMarkersDisabledReplacesExistingRootMarkers(t *testing.T) { input := strings.Join([]string{ `project_root_markers = [".git"]`, `model = "gpt-5.5"`, "", "[model_providers.proxy]", `base_url = "https://openai.proxy.test/v1"`, }, "\n") next, changed := codexConfigWithProjectRootMarkersDisabled(input) if !changed { t.Fatalf("codexConfigWithProjectRootMarkersDisabled changed = false, want true") } if !strings.Contains(next, codexProjectRootMarkersDisabledConfig) || strings.Contains(next, `project_root_markers = [".git"]`) || !strings.Contains(next, "[model_providers.proxy]") { t.Fatalf("merged config = %q", next) } } func TestCodexConfigWithProjectRootMarkersDisabledReplacesMultilineRootMarkers(t *testing.T) { input := strings.Join([]string{ `project_root_markers = [ # allow project-local config discovery`, ` ".git",`, ` "path]with-bracket",`, `]`, `model = "gpt-5.5"`, "", "[model_providers.proxy]", `base_url = "https://openai.proxy.test/v1"`, }, "\n") next, changed := codexConfigWithProjectRootMarkersDisabled(input) if !changed { t.Fatalf("codexConfigWithProjectRootMarkersDisabled changed = false, want true") } if strings.Count(next, "project_root_markers") != 1 || strings.Contains(next, `".git"`) || strings.Contains(next, `"path]with-bracket"`) || strings.Contains(next, "\n]\n") || !strings.Contains(next, codexProjectRootMarkersDisabledConfig) || !strings.Contains(next, `model = "gpt-5.5"`) || !strings.Contains(next, "[model_providers.proxy]") { t.Fatalf("merged config = %q", next) } } func TestCodexConfigWithProjectRootMarkersDisabledKeepsExistingEmptyMarkers(t *testing.T) { input := codexProjectRootMarkersDisabledConfig + "\n\n" + `[model_providers.proxy]` next, changed := codexConfigWithProjectRootMarkersDisabled(input) if changed { t.Fatalf("codexConfigWithProjectRootMarkersDisabled changed = true, want false") } if next != input { t.Fatalf("merged config = %q, want original", next) } } func TestCodexConfigWithConnectorMCPReplacesReservedServerAndPreservesCustomServers(t *testing.T) { input := "[mcp_servers.connector]\nurl = \"http://old\"\n\n[mcp_servers.custom]\nurl = \"http://custom\"\n" next, changed := codexConfigWithConnectorMCP(input, []MCPServerBinding{{Name: "connector", Type: "http", URL: "http://127.0.0.1:1234/mcp/connector", Headers: map[string]string{"Authorization": "Bearer session-token"}}}) if !changed || strings.Count(next, "[mcp_servers.connector]") != 1 || !strings.Contains(next, `url = "http://127.0.0.1:1234/mcp/connector"`) || !strings.Contains(next, `"Authorization" = "Bearer session-token"`) || !strings.Contains(next, "[mcp_servers.custom]") || strings.Contains(next, "http://old") { t.Fatalf("connector MCP config = %q", next) } } func TestCodexConfigWithSupportedServiceTierSanitizesLegacyValues(t *testing.T) { tests := []struct { name string in string want string }{ { name: "removes default", in: strings.Join([]string{ `model = "gpt-5.5"`, `service_tier = "default"`, `model_reasoning_effort = "high"`, }, "\n"), want: strings.Join([]string{ `model = "gpt-5.5"`, `model_reasoning_effort = "high"`, }, "\n"), }, { name: "normalizes priority", in: strings.Join([]string{ `service_tier = "priority"`, `model = "gpt-5.5"`, }, "\n"), want: strings.Join([]string{ `service_tier = "fast"`, `model = "gpt-5.5"`, }, "\n"), }, { name: "keeps flex", in: strings.Join([]string{ `service_tier = "flex"`, `model = "gpt-5.5"`, }, "\n"), want: strings.Join([]string{ `service_tier = "flex"`, `model = "gpt-5.5"`, }, "\n"), }, { name: "ignores nested config", in: strings.Join([]string{ `model = "gpt-5.5"`, "", `[mcp_servers.example]`, `service_tier = "default"`, }, "\n"), want: strings.Join([]string{ `model = "gpt-5.5"`, "", `[mcp_servers.example]`, `service_tier = "default"`, }, "\n"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, _ := codexConfigWithSupportedServiceTier(tt.in) if got != tt.want { t.Fatalf("codexConfigWithSupportedServiceTier() = %q, want %q", got, tt.want) } }) } } func TestDefaultPreparerUsesStateRootCLIShimName(t *testing.T) { setTestHome(t, t.TempDir()) t.Setenv("PATH", "/usr/bin:/bin") stateDir := t.TempDir() if err := os.MkdirAll(filepath.Join(stateDir, "bin"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(stateDir, "bin", "tutti-dev"), []byte("#!/bin/sh\n"), 0o755); err != nil { t.Fatal(err) } cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") content, err := os.ReadFile(filepath.Join(codexHome, "AGENTS.md")) if err != nil { t.Fatal(err) } if !strings.Contains(string(content), "`tutti-dev --help`") || !strings.Contains(string(content), "App id mapping") { t.Fatalf("codex AGENTS.md content = %q, want tutti-dev command", string(content)) } pathEnv := envValue(prepared.Env, "PATH") wantPrefix := filepath.Join(stateDir, "bin") + string(os.PathListSeparator) if !strings.HasPrefix(pathEnv, wantPrefix) { t.Fatalf("PATH = %q, want prefix %q", pathEnv, wantPrefix) } rules, err := os.ReadFile(filepath.Join(codexHome, "rules", "default.rules")) if err != nil { t.Fatalf("codex approval rules missing: %v", err) } if !strings.Contains(string(rules), `prefix_rule(pattern=["tutti-dev"], decision="allow")`) { t.Fatalf("codex approval rules = %q, want tutti-dev allow rule", string(rules)) } } func TestDefaultPreparerCleanupRemovesManagedBlocksAndRuntimeRoot(t *testing.T) { setTestHome(t, t.TempDir()) stateDir := t.TempDir() cwd := t.TempDir() agentsPath := filepath.Join(cwd, "AGENTS.md") if err := os.WriteFile(agentsPath, []byte("user guidance\n"), 0o644); err != nil { t.Fatal(err) } preparer := newTestPreparer(stateDir) _, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } if err := preparer.Cleanup(t.Context(), CleanupInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", }); err != nil { t.Fatalf("Cleanup() error = %v", err) } content, err := os.ReadFile(agentsPath) if err != nil { t.Fatal(err) } if strings.Contains(string(content), "BEGIN TUTTI-RUNTIME") || !strings.Contains(string(content), "user guidance") { t.Fatalf("cleanup content = %q", string(content)) } runtimeRoot, err := LocalStore{StateDir: stateDir}.RuntimeRoot("workspace-1", "session-1") if err != nil { t.Fatal(err) } if _, err := os.Stat(runtimeRoot); !os.IsNotExist(err) { t.Fatalf("runtime root still exists, err = %v", err) } } func TestDefaultPreparerCleanupCanPreserveRecoverableRuntimeRoot(t *testing.T) { setTestHome(t, t.TempDir()) stateDir := t.TempDir() cwd := t.TempDir() preparer := newTestPreparer(stateDir) prepared, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") rolloutPath := filepath.Join(codexHome, "sessions", "rollout.jsonl") if err := os.MkdirAll(filepath.Dir(rolloutPath), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(rolloutPath, []byte("recoverable"), 0o600); err != nil { t.Fatal(err) } if err := preparer.Cleanup(t.Context(), CleanupInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", PreserveRuntimeRoot: true, }); err != nil { t.Fatalf("Cleanup() error = %v", err) } if content, err := os.ReadFile(rolloutPath); err != nil || string(content) != "recoverable" { t.Fatalf("recoverable rollout = %q, error = %v", content, err) } if err := preparer.Cleanup(t.Context(), CleanupInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", }); err != nil { t.Fatalf("permanent Cleanup() error = %v", err) } if _, err := os.Stat(codexHome); !os.IsNotExist(err) { t.Fatalf("codex home still exists after permanent cleanup, err = %v", err) } } func TestDefaultPreparerCodexUsesSessionScopedInstructionFile(t *testing.T) { setTestHome(t, t.TempDir()) stateDir := t.TempDir() cwd := t.TempDir() preparer := newTestPreparer(stateDir) prepared, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } agentsPath := filepath.Join(cwd, "AGENTS.md") if _, err := os.Stat(agentsPath); !os.IsNotExist(err) { t.Fatalf("cwd AGENTS.md exists after prepare, err = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") if _, err := os.Stat(filepath.Join(codexHome, "AGENTS.md")); err != nil { t.Fatalf("codex AGENTS.md missing after prepare: %v", err) } if err := preparer.Cleanup(t.Context(), CleanupInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", }); err != nil { t.Fatalf("Cleanup() error = %v", err) } if _, err := os.Stat(codexHome); !os.IsNotExist(err) { t.Fatalf("codex home still exists, err = %v", err) } } func TestDefaultPreparerCodexProjectsUserAgentsIntoSessionHome(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } userAgentsPath := filepath.Join(userCodexHome, "AGENTS.md") userAgents := "global Codex guidance\n" if err := os.WriteFile(userAgentsPath, []byte(userAgents), 0o600); err != nil { t.Fatal(err) } stateDir := t.TempDir() cwd := t.TempDir() preparer := newTestPreparer(stateDir) prepared, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") codexAgentsPath := filepath.Join(codexHome, "AGENTS.md") codexAgents, err := os.ReadFile(codexAgentsPath) if err != nil { t.Fatalf("read session AGENTS.md: %v", err) } if !strings.Contains(string(codexAgents), userAgents) { t.Fatalf("session AGENTS.md = %q, want user guidance", string(codexAgents)) } if !strings.Contains(string(codexAgents), managedBlockBegin) { t.Fatalf("session AGENTS.md = %q, want Tutti managed block", string(codexAgents)) } info, err := os.Lstat(codexAgentsPath) if err != nil { t.Fatalf("stat session AGENTS.md: %v", err) } if info.Mode()&os.ModeSymlink != 0 { t.Fatalf("session AGENTS.md is a symlink, want an isolated copy") } userAgentsAfter, err := os.ReadFile(userAgentsPath) if err != nil { t.Fatalf("read user AGENTS.md after prepare: %v", err) } if string(userAgentsAfter) != userAgents { t.Fatalf("user AGENTS.md changed to %q", string(userAgentsAfter)) } if err := preparer.Cleanup(t.Context(), CleanupInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", }); err != nil { t.Fatalf("Cleanup() error = %v", err) } } func TestDefaultPreparerCodexReplacesPreexistingAgentsLinks(t *testing.T) { for _, kind := range []string{"hardlink", "symlink"} { t.Run(kind, func(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } userAgentsPath := filepath.Join(userCodexHome, "AGENTS.md") userAgents := "global Codex guidance\n" if err := os.WriteFile(userAgentsPath, []byte(userAgents), 0o600); err != nil { t.Fatal(err) } stateDir := t.TempDir() runtimeRoot, err := LocalStore{StateDir: stateDir}.RuntimeRoot("workspace-1", "session-1") if err != nil { t.Fatal(err) } codexHome := filepath.Join(runtimeRoot, "codex-home") if err := os.MkdirAll(codexHome, 0o700); err != nil { t.Fatal(err) } target := filepath.Join(codexHome, "AGENTS.md") switch kind { case "hardlink": if err := os.Link(userAgentsPath, target); err != nil { t.Fatal(err) } case "symlink": if err := os.Symlink(userAgentsPath, target); err != nil { t.Skipf("symlinks unavailable: %v", err) } } preparer := newTestPreparer(stateDir) prepared, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: t.TempDir(), }) if err != nil { t.Fatalf("Prepare() error = %v", err) } gotUserAgents, err := os.ReadFile(userAgentsPath) if err != nil { t.Fatal(err) } if string(gotUserAgents) != userAgents { t.Fatalf("user AGENTS.md changed to %q", gotUserAgents) } gotTargetInfo, err := os.Lstat(envValue(prepared.Env, "CODEX_HOME") + string(os.PathSeparator) + "AGENTS.md") if err != nil { t.Fatal(err) } if gotTargetInfo.Mode()&os.ModeSymlink != 0 { t.Fatal("session AGENTS.md is still a symlink") } gotTarget, err := os.ReadFile(target) if err != nil { t.Fatal(err) } if !strings.Contains(string(gotTarget), managedBlockBegin) { t.Fatalf("session AGENTS.md = %q, want managed block", gotTarget) } }) } } func TestDefaultPreparerCodexIgnoresNonRegularUserAgents(t *testing.T) { home := t.TempDir() setTestHome(t, home) if err := os.MkdirAll(filepath.Join(home, ".codex", "AGENTS.md"), 0o700); err != nil { t.Fatal(err) } stateDir := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: t.TempDir(), }) if err != nil { t.Fatalf("Prepare() error = %v", err) } content, err := os.ReadFile(filepath.Join(envValue(prepared.Env, "CODEX_HOME"), "AGENTS.md")) if err != nil { t.Fatal(err) } if !strings.Contains(string(content), managedBlockBegin) { t.Fatalf("session AGENTS.md = %q, want managed block", content) } } func TestDefaultPreparerRejectsMissingCwd(t *testing.T) { setTestHome(t, t.TempDir()) stateDir := t.TempDir() missingCwd := filepath.Join(t.TempDir(), "deleted-project") _, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: missingCwd, }) if !errors.Is(err, ErrCwdNotDirectory) { t.Fatalf("Prepare() error = %v, want ErrCwdNotDirectory", err) } if _, statErr := os.Stat(missingCwd); !os.IsNotExist(statErr) { t.Fatalf("missing cwd was recreated, stat err = %v", statErr) } } func TestDefaultPreparerClaudeCodeUsesSessionScopedSystemPrompt(t *testing.T) { stateDir := t.TempDir() cwd := t.TempDir() claudePath := filepath.Join(cwd, "CLAUDE.md") if err := os.WriteFile(claudePath, []byte("user claude guidance\n"), 0o644); err != nil { t.Fatal(err) } userSkillPath := filepath.Join(cwd, ".claude", "skills", "tutti-cli", "SKILL.md") if err := os.MkdirAll(filepath.Dir(userSkillPath), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(userSkillPath, []byte("user skill\n"), 0o644); err != nil { t.Fatal(err) } prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "claude-code", Cwd: cwd, ConversationDetailMode: "general", }) if err != nil { t.Fatalf("Prepare() error = %v", err) } content, err := os.ReadFile(userSkillPath) if err != nil { t.Fatal(err) } if string(content) != "user skill\n" { t.Fatalf("user skill was overwritten: %q", string(content)) } claudeContent, err := os.ReadFile(claudePath) if err != nil { t.Fatal(err) } if string(claudeContent) != "user claude guidance\n" { t.Fatalf("cwd CLAUDE.md content = %q, want user guidance unchanged", string(claudeContent)) } tuttiSkillPath := filepath.Join(cwd, ".claude", "skills", "tutti-cli-tutti", "SKILL.md") if _, err := os.Stat(tuttiSkillPath); !os.IsNotExist(err) { t.Fatalf("claude cwd tutti provider skill exists after prepare, err = %v", err) } issueSkillPath := filepath.Join(cwd, ".claude", "skills", "issue-manager", "SKILL.md") if _, err := os.Stat(issueSkillPath); !os.IsNotExist(err) { t.Fatalf("claude cwd issue-manager skill exists after prepare, err = %v", err) } workspaceAppSkillPath := filepath.Join(cwd, ".claude", "skills", "workspace-app", "SKILL.md") if _, err := os.Stat(workspaceAppSkillPath); !os.IsNotExist(err) { t.Fatalf("claude cwd workspace-app skill exists after prepare, err = %v", err) } systemPromptPath := envValue(prepared.Env, claudeSystemPromptFileEnv) if systemPromptPath == "" { t.Fatalf("prepared env = %#v, want %s", prepared.Env, claudeSystemPromptFileEnv) } if rel, err := filepath.Rel(cwd, systemPromptPath); err == nil && !strings.HasPrefix(rel, "..") { t.Fatalf("claude system prompt path = %q, want outside cwd %q", systemPromptPath, cwd) } systemPrompt, err := os.ReadFile(systemPromptPath) if err != nil { t.Fatalf("claude system prompt missing: %v", err) } if !strings.Contains(string(systemPrompt), "`tutti --help`") || !strings.Contains(string(systemPrompt), "App id mapping") { t.Fatalf("claude system prompt content = %q", string(systemPrompt)) } if !strings.Contains(string(systemPrompt), "### Non-technical UI") || !strings.Contains(string(systemPrompt), "don't name bash commands you're running") || !strings.Contains(string(systemPrompt), "focus on outputs") { t.Fatalf("claude system prompt content = %q, want non-technical UI guidance", string(systemPrompt)) } if !strings.Contains(string(systemPrompt), "## Mention Routing") || !strings.Contains(string(systemPrompt), "| URI") || !strings.Contains(string(systemPrompt), "Fallback") || !strings.Contains(string(systemPrompt), "`mention://workspace-issue/?workspaceId=...`") || !strings.Contains(string(systemPrompt), "`mention://workspace-app/?workspaceId=...`") || !strings.Contains(string(systemPrompt), "`mention://workspace-reference/?source=...&workspaceId=...`") || !strings.Contains(string(systemPrompt), "`mention://agent-session/?workspaceId=...`") || !strings.Contains(string(systemPrompt), "`mention://agent-target/?workspaceId=...`") || !strings.Contains(string(systemPrompt), "If a provider Skill tool exists, call the exact visible name") || !strings.Contains(string(systemPrompt), "If the Skill is unavailable, read its materialized `SKILL.md`") || !strings.Contains(string(systemPrompt), "Claude Code mention routing") || !strings.Contains(string(systemPrompt), "Claude Code skill names may be namespaced") || !strings.Contains(string(systemPrompt), "`tutti-cli:tutti-handoff`") || !strings.Contains(string(systemPrompt), "`tutti-cli:issue-manager`") || !strings.Contains(string(systemPrompt), "`tutti-cli:workspace-app`") || !strings.Contains(string(systemPrompt), `Skill(skill="tutti-cli:workspace-app")`) || !strings.Contains(string(systemPrompt), `Skill(skill="tutti-cli:tutti-handoff")`) || !strings.Contains(string(systemPrompt), "Do not use `ToolSearch` to select Claude Code's native `SendMessage`") || !strings.Contains(string(systemPrompt), "never pass a Tutti agent target id such as `local:opencode` to native `SendMessage`") || !strings.Contains(string(systemPrompt), "Do not call a plain skill name that is not visible") || !strings.Contains(string(systemPrompt), "Do not pass arguments to Skill") || !strings.Contains(string(systemPrompt), "the skill reads the mention URI from the current user turn") || !strings.Contains(string(systemPrompt), "Call the exact visible Skill tool when available") || !strings.Contains(string(systemPrompt), "fall back to that materialized skill file") || !strings.Contains(string(systemPrompt), "Do not guess a directory from the plain skill slug") || !strings.Contains(string(systemPrompt), "issue get --issue-id --json") || !strings.Contains(string(systemPrompt), "Claude Code `Monitor` tool is disabled") || !strings.Contains(string(systemPrompt), "bounded shell/script") || !strings.Contains(string(systemPrompt), "agent wait --session-id --json") || !strings.Contains(string(systemPrompt), "agent get --session-id --json") || !strings.Contains(string(systemPrompt), "Agent handoff decisions belong to `$tutti-handoff`") { t.Fatalf("claude system prompt content = %q, want mention handoff fallback guidance", string(systemPrompt)) } if !strings.Contains(string(systemPrompt), "# Host App Context") || !strings.Contains(string(systemPrompt), "Images/videos: use Markdown") || !strings.Contains(string(systemPrompt), "Generated/edited image output: final response must include Markdown image tag.") || !strings.Contains(string(systemPrompt), "Prefer `$CODEX_HOME/generated_images/`") || !strings.Contains(string(systemPrompt), "never use unverified sandbox path") || !strings.Contains(string(systemPrompt), "No inline base64.") || !strings.Contains(string(systemPrompt), "use `[filename](/abs/path)` Markdown links") || !strings.Contains(string(systemPrompt), "No relative paths, line suffixes") || !strings.Contains(string(systemPrompt), "Web URLs: Markdown links") { t.Fatalf("claude system prompt content = %q, want host app rendering guidance", string(systemPrompt)) } if !strings.Contains(string(systemPrompt), "Claude Code skill names may be namespaced") || !strings.Contains(string(systemPrompt), "Claude Code skill listings can omit descriptions") || !strings.Contains(string(systemPrompt), "If a provider Skill tool exists, call the exact visible name") || !strings.Contains(string(systemPrompt), "If the Skill is unavailable, read its materialized `SKILL.md`") || !strings.Contains(string(systemPrompt), "`mention://...` is internal data, not a URL or path") || !strings.Contains(string(systemPrompt), "`mention://agent-target/?workspaceId=...`") || !strings.Contains(string(systemPrompt), "agent get --session-id --json") || !strings.Contains(string(systemPrompt), "issue get --issue-id --json") { t.Fatalf("claude system prompt content = %q, want strict Tutti mention routing", string(systemPrompt)) } if strings.Contains(string(systemPrompt), "CODEX_HOME/skills//SKILL.md") || strings.Contains(string(systemPrompt), ".claude/skills//SKILL.md") || strings.Contains(string(systemPrompt), "`workspace-app/SKILL.md`") || strings.Contains(string(systemPrompt), `args=""`) || strings.Contains(string(systemPrompt), "with the full mention URI") { t.Fatalf("claude system prompt content = %q, want no guessed materialized skill paths", string(systemPrompt)) } pluginDir := envValue(prepared.Env, claudePluginDirEnv) if pluginDir == "" { t.Fatalf("prepared env = %#v, want %s", prepared.Env, claudePluginDirEnv) } if got := envValue(prepared.Env, claudeSkillListingBudgetEnv); got != claudeSkillListingBudgetChars { t.Fatalf("prepared env %s = %q, want %q", claudeSkillListingBudgetEnv, got, claudeSkillListingBudgetChars) } if rel, err := filepath.Rel(cwd, pluginDir); err == nil && !strings.HasPrefix(rel, "..") { t.Fatalf("claude plugin dir = %q, want outside cwd %q", pluginDir, cwd) } pluginManifest, err := os.ReadFile(filepath.Join(pluginDir, ".claude-plugin", "plugin.json")) if err != nil { t.Fatalf("claude plugin manifest missing: %v", err) } if !strings.Contains(string(pluginManifest), `"name": "tutti-cli"`) { t.Fatalf("claude plugin manifest = %q", string(pluginManifest)) } if !strings.Contains(string(pluginManifest), `"author": {`) || !strings.Contains(string(pluginManifest), `"name": "Tutti"`) { t.Fatalf("claude plugin manifest author = %q", string(pluginManifest)) } pluginSkill, err := os.ReadFile(filepath.Join(pluginDir, "skills", "tutti-cli", "SKILL.md")) if err != nil { t.Fatalf("claude plugin skill missing: %v", err) } if !strings.Contains(string(pluginSkill), "`tutti --help`") || !strings.Contains(string(pluginSkill), "this skill's `command-guide.md`") || !strings.Contains(string(pluginSkill), "mention://agent-session") || !strings.Contains(string(pluginSkill), "mention://agent-target") || !strings.Contains(string(pluginSkill), "handed off, not absorbed") || !strings.Contains(string(pluginSkill), "## Route First") || !strings.Contains(string(pluginSkill), "## Call Protocol") || !strings.Contains(string(pluginSkill), "invoke `$issue-manager`") || !strings.Contains(string(pluginSkill), "invoke `$workspace-app`") { t.Fatalf("claude plugin skill content = %q", string(pluginSkill)) } issuePluginSkill, err := os.ReadFile(filepath.Join(pluginDir, "skills", "issue-manager", "SKILL.md")) if err != nil { t.Fatalf("claude issue-manager plugin skill missing: %v", err) } if !strings.Contains(string(issuePluginSkill), "mention://workspace-issue") { t.Fatalf("claude issue-manager plugin skill content = %q", string(issuePluginSkill)) } workspaceAppPluginSkill, err := os.ReadFile(filepath.Join(pluginDir, "skills", "workspace-app", "SKILL.md")) if err != nil { t.Fatalf("claude workspace-app plugin skill missing: %v", err) } if !strings.Contains(string(workspaceAppPluginSkill), "mention://workspace-app") { t.Fatalf("claude workspace-app plugin skill content = %q", string(workspaceAppPluginSkill)) } } func TestDefaultPreparerClaudeCodeSetsFallbackExecutableFromPath(t *testing.T) { binDir := t.TempDir() claudeName := "claude" claudeBody := []byte("#!/bin/sh\n") if runtime.GOOS == "windows" { claudeName = "claude.cmd" claudeBody = []byte("@echo off\r\n") } claudePath := filepath.Join(binDir, claudeName) if err := os.WriteFile(claudePath, claudeBody, 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", binDir) t.Setenv("CLAUDE_CODE_EXECUTABLE", "") prepared, err := newTestPreparer(t.TempDir()).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "claude-code", Cwd: t.TempDir(), }) if err != nil { t.Fatalf("Prepare() error = %v", err) } // A PATH-installed claude is only a fallback: the sidecar prefers a native // SDK binary and the tuttid-provisioned one, so it must arrive via the // fallback env, not the always-wins override. if got := envValue(prepared.Env, claudeCodeFallbackExecutableEnvName); got != claudePath { t.Fatalf("%s = %q, want %q", claudeCodeFallbackExecutableEnvName, got, claudePath) } if got := envValue(prepared.Env, claudeCodeExecutableEnvName); got != "" { t.Fatalf("%s = %q, want empty", claudeCodeExecutableEnvName, got) } } func TestDefaultPreparerClaudeCodePrefersManagedBinaryOverPath(t *testing.T) { binDir := t.TempDir() claudePath := filepath.Join(binDir, "claude") if err := os.WriteFile(claudePath, []byte("#!/bin/sh\n"), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", binDir) t.Setenv("CLAUDE_CODE_EXECUTABLE", "") stateDir := t.TempDir() managed := filepath.Join(stateDir, "agent-providers", "claude-code", "versions", "2.1.201", "claude") writeFakeExecutable(t, managed) writeManagedClaudePointer(t, stateDir, managed) prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "claude-code", Cwd: t.TempDir(), }) if err != nil { t.Fatalf("Prepare() error = %v", err) } if got := envValue(prepared.Env, claudeCodeFallbackExecutableEnvName); got != managed { t.Fatalf("%s = %q, want managed binary %q", claudeCodeFallbackExecutableEnvName, got, managed) } } func TestDefaultPreparerCursorUsesRuntimePluginDir(t *testing.T) { stateDir := t.TempDir() cwd := t.TempDir() agentsPath := filepath.Join(cwd, "AGENTS.md") if err := os.WriteFile(agentsPath, []byte("user cursor guidance\n"), 0o644); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(cwd, ".cursor", "skills"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(cwd, ".cursor", "skills", "user-skill"), []byte("keep"), 0o644); err != nil { t.Fatal(err) } preparer := newTestPreparer(stateDir) preparer.CLICommand = "tutti-dev" prepareInput := PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "cursor-session-1", AgentTargetID: "local:cursor", Provider: "cursor", Cwd: cwd, CLICommand: preparer.CLICommand, BrowserUse: true, } prepared, err := preparer.Prepare(t.Context(), prepareInput) if err != nil { t.Fatalf("Prepare() error = %v", err) } if prepared.Cwd != cwd { t.Fatalf("prepared cwd = %q, want %q", prepared.Cwd, cwd) } if _, err := os.Stat(filepath.Join(cwd, ".cursor", "skills", "tutti-cli")); !os.IsNotExist(err) { t.Fatalf("cursor cwd tutti skill exists after prepare, err = %v", err) } if _, err := os.Stat(filepath.Join(cwd, ".cursor", "skills", "user-skill")); err != nil { t.Fatalf("cursor user skill was modified or removed: %v", err) } agentsContent, err := os.ReadFile(agentsPath) if err != nil { t.Fatalf("cursor AGENTS.md missing after prepare: %v", err) } if string(agentsContent) != "user cursor guidance\n" { t.Fatalf("cursor AGENTS.md content = %q, want user guidance unchanged", string(agentsContent)) } pluginDir := envValue(prepared.Env, cursorPluginDirEnv) if pluginDir == "" { t.Fatalf("prepared env = %#v, want %s", prepared.Env, cursorPluginDirEnv) } if rel, err := filepath.Rel(cwd, pluginDir); err == nil && !strings.HasPrefix(rel, "..") { t.Fatalf("cursor plugin dir = %q, want outside cwd %q", pluginDir, cwd) } pluginManifest, err := os.ReadFile(filepath.Join(pluginDir, ".cursor-plugin", "plugin.json")) if err != nil { t.Fatalf("cursor plugin manifest missing: %v", err) } if !strings.Contains(string(pluginManifest), `"name": "tutti-cli"`) || !strings.Contains(string(pluginManifest), `"skills": "./skills/"`) || !strings.Contains(string(pluginManifest), `"rules": []`) || !strings.Contains(string(pluginManifest), `"displayName": "Tutti CLI"`) { t.Fatalf("cursor plugin manifest = %q", string(pluginManifest)) } if strings.Contains(string(pluginManifest), `"hooks"`) { t.Fatalf("cursor ACP plugin must not advertise unsupported plugin hooks: %q", string(pluginManifest)) } if _, err := os.Stat(filepath.Join(pluginDir, "hooks")); !os.IsNotExist(err) { t.Fatalf("cursor ACP plugin hooks should remain dormant, stat error = %v", err) } contextPath := envValue(prepared.Env, cursorPromptContextFileEnv) if contextPath != filepath.Join(pluginDir, "tutti-context.md") { t.Fatalf("cursor prompt context path = %q, want plugin-owned context file", contextPath) } promptContext, err := os.ReadFile(contextPath) if err != nil { t.Fatalf("cursor prompt context missing: %v", err) } expectedInput := prepareInput expectedInput.hostFacts, err = normalizeHostFacts(preparer.Profile.HostFacts) if err != nil { t.Fatalf("normalizeHostFacts() error = %v", err) } expectedPolicy, err := tuttiCLIPolicy(testResolvedInput(t, expectedInput)) if err != nil { t.Fatalf("tuttiCLIPolicy() error = %v", err) } expectedPolicy = cursorPromptPolicy(expectedPolicy) if !strings.HasPrefix(string(promptContext), strings.TrimSpace(expectedPolicy)+"\n\n## Available Skills\n") { t.Fatalf("cursor prompt context does not start with resolved policy: %s", string(promptContext)) } skillEntries, err := os.ReadDir(filepath.Join(pluginDir, "skills")) if err != nil { t.Fatalf("read cursor plugin skills: %v", err) } catalog := strings.SplitN(string(promptContext), "## Available Skills\n", 2) if len(catalog) != 2 { t.Fatalf("cursor prompt context missing dynamic Skill catalog: %s", string(promptContext)) } if got := strings.Count(catalog[1], "\n- "); got != len(skillEntries) { t.Fatalf("cursor prompt context catalog entries = %d, want %d", got, len(skillEntries)) } for _, entry := range skillEntries { skillFile := filepath.Join(pluginDir, "skills", entry.Name(), "SKILL.md") if !strings.Contains(catalog[1], "`"+skillFile+"`") { t.Fatalf("cursor prompt context missing materialized Skill path %q", skillFile) } } pluginSkill, err := os.ReadFile(filepath.Join(pluginDir, "skills", "tutti-cli", "SKILL.md")) if err != nil { t.Fatalf("cursor plugin skill missing: %v", err) } if !strings.Contains(string(pluginSkill), "`tutti-dev --help`") || !strings.Contains(string(pluginSkill), "mention://agent-session") || !strings.Contains(string(pluginSkill), "mention://agent-target") { t.Fatalf("cursor plugin skill content = %q", string(pluginSkill)) } if _, err := os.Stat(filepath.Join(pluginDir, "skills", "issue-manager", "SKILL.md")); err != nil { t.Fatalf("cursor issue-manager plugin skill missing: %v", err) } if _, err := os.Stat(filepath.Join(pluginDir, "skills", "workspace-app", "SKILL.md")); err != nil { t.Fatalf("cursor workspace-app plugin skill missing: %v", err) } } // Plan mode must NOT override CLAUDE_CONFIG_DIR. Doing so points the CLI at a // fresh config directory without the user's credentials, which made plan turns // fail with "Not logged in · Please run /login" (-32000) for OAuth users while // every other permission mode kept working. Plan mode is applied through the ACP // `set_mode("plan")` call instead. func TestDefaultPreparerClaudePlanModeDoesNotOverrideConfigDir(t *testing.T) { home := t.TempDir() setTestHome(t, home) userClaudeDir := filepath.Join(home, ".claude") if err := os.MkdirAll(userClaudeDir, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userClaudeDir, "settings.json"), []byte(`{ "permissions": { "defaultMode": "default" } }`), 0o600); err != nil { t.Fatal(err) } stateDir := t.TempDir() cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "claude-code", Cwd: cwd, PlanMode: true, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } if got := envValue(prepared.Env, "CLAUDE_CONFIG_DIR"); got != "" { t.Fatalf("prepared env CLAUDE_CONFIG_DIR = %q, want unset so the CLI keeps the user's credentials", got) } } func TestTuttiAgentManagedConfigRemovesOnlyLegacyPinnedProvider(t *testing.T) { input := strings.Join([]string{ `model_provider = "tutti-llm"`, `model = "gpt-5.4"`, `custom_root = "preserved"`, ``, `[model_providers.tutti-llm]`, `name = "Tutti LLM"`, `base_url = "https://llm-api.tutti.sh/v1"`, `wire_api = "responses"`, ``, `[model_providers.custom]`, `name = "Custom"`, ``, `[profiles.custom]`, `model_provider = "tutti-llm"`, `model = "gpt-5.4"`, }, "\n") next, changed := tuttiAgentConfigWithoutLegacyPinnedProvider(input) if !changed { t.Fatalf("changed = false, want true") } for _, removed := range []string{`[model_providers.tutti-llm]`} { if strings.Contains(next, removed) { t.Fatalf("next retained %q: %s", removed, next) } } if !strings.HasPrefix(next, `custom_root = "preserved"`) { t.Fatalf("next retained legacy root assignments: %s", next) } for _, want := range []string{ `custom_root = "preserved"`, `[model_providers.custom]`, `[profiles.custom]`, `model_provider = "tutti-llm"`, `model = "gpt-5.4"`, } { if !strings.Contains(next, want) { t.Fatalf("next removed %q: %s", want, next) } } } func TestTuttiAgentManagedConfigProjectsConnectorMCP(t *testing.T) { configPath := filepath.Join(t.TempDir(), "config.toml") if err := ensureTuttiAgentSessionConfig(configPath, PrepareInput{MCPServers: []MCPServerBinding{{ Name: "connector", Type: "http", URL: "http://127.0.0.1:4321/mcp/connector", Headers: map[string]string{"Authorization": "Bearer test-token"}, }}}); err != nil { t.Fatalf("ensureTuttiAgentSessionConfig() error = %v", err) } raw, err := os.ReadFile(configPath) if err != nil { t.Fatal(err) } content := string(raw) if !strings.Contains(content, "[mcp_servers.connector]") || !strings.Contains(content, `url = "http://127.0.0.1:4321/mcp/connector"`) || !strings.Contains(content, `"Authorization" = "Bearer test-token"`) { t.Fatalf("tutti-agent config = %q", content) } } func TestTuttiAgentManagedConfigPreservesPartialLegacyLookalike(t *testing.T) { input := strings.Join([]string{ `model_provider = "tutti-llm"`, `model = "gpt-5.4"`, ``, `[model_providers.tutti-llm]`, `name = "Custom Tutti Gateway"`, `base_url = "https://llm-api.tutti.sh/v1"`, `wire_api = "responses"`, }, "\n") next, changed := tuttiAgentConfigWithoutLegacyPinnedProvider(input) if changed { t.Fatalf("changed = true, want false: %s", next) } if next != input { t.Fatalf("next changed partial legacy lookalike:\n%s", next) } } func TestTuttiAgentManagedConfigPreservesCommentedLegacySignature(t *testing.T) { input := strings.Join([]string{ `# model_provider = "tutti-llm"`, `# model = "gpt-5.4"`, ``, `[model_providers.tutti-llm]`, `name = "Tutti LLM"`, `base_url = "https://llm-api.tutti.sh/v1"`, `wire_api = "responses"`, }, "\n") next, changed := tuttiAgentConfigWithoutLegacyPinnedProvider(input) if changed { t.Fatalf("changed = true, want false: %s", next) } if next != input { t.Fatalf("next changed commented legacy lookalike:\n%s", next) } } func TestTuttiAgentManagedConfigPreservesLegacyProviderWithExtraKey(t *testing.T) { input := strings.Join([]string{ `model_provider = "tutti-llm"`, `model = "gpt-5.4"`, ``, `[model_providers.tutti-llm]`, `name = "Tutti LLM"`, `base_url = "https://llm-api.tutti.sh/v1"`, `wire_api = "responses"`, `http_headers = { X-Custom = "preserve" }`, }, "\n") next, changed := tuttiAgentConfigWithoutLegacyPinnedProvider(input) if changed { t.Fatalf("changed = true, want false: %s", next) } if next != input { t.Fatalf("next changed customized legacy provider:\n%s", next) } } func TestDefaultPreparerClaudeReconcilesNativeSkillsAcrossRepeatedPrepare(t *testing.T) { preparer := newTestPreparer(t.TempDir()) input := PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:claude-code", Provider: "claude-code", Cwd: t.TempDir(), } first, err := preparer.Prepare(t.Context(), input) if err != nil { t.Fatalf("first Prepare() error = %v", err) } second, err := preparer.Prepare(t.Context(), input) if err != nil { t.Fatalf("second Prepare() error = %v", err) } firstPluginDir := envValue(first.Env, claudePluginDirEnv) secondPluginDir := envValue(second.Env, claudePluginDirEnv) if firstPluginDir != secondPluginDir { t.Fatalf("repeated Prepare() Claude plugin dir = %q and %q, want the same runtime root", firstPluginDir, secondPluginDir) } if _, err := os.Stat(filepath.Join(secondPluginDir, "skills", "tutti-cli", "SKILL.md")); err != nil { t.Fatalf("stable tutti-cli skill missing after repeated Prepare(): %v", err) } if _, err := os.Stat(filepath.Join(secondPluginDir, "skills", "tutti-cli-tutti")); !os.IsNotExist(err) { t.Fatalf("repeated Prepare() created a suffixed tutti-cli skill, err = %v", err) } } func TestDefaultPreparerCleanupRemovesClaudeSystemPromptRuntimeRoot(t *testing.T) { stateDir := t.TempDir() cwd := t.TempDir() preparer := newTestPreparer(stateDir) prepared, err := preparer.Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "claude-code", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } systemPromptPath := envValue(prepared.Env, claudeSystemPromptFileEnv) if _, err := os.Stat(systemPromptPath); err != nil { t.Fatalf("claude system prompt missing before cleanup: %v", err) } pluginDir := envValue(prepared.Env, claudePluginDirEnv) if _, err := os.Stat(filepath.Join(pluginDir, ".claude-plugin", "plugin.json")); err != nil { t.Fatalf("claude plugin manifest missing before cleanup: %v", err) } projectClaudeDir := filepath.Join(cwd, ".claude") if _, err := os.Stat(projectClaudeDir); !os.IsNotExist(err) { t.Fatalf("cwd .claude exists after prepare, err = %v", err) } if _, err := os.Stat(filepath.Join(cwd, "CLAUDE.md")); !os.IsNotExist(err) { t.Fatalf("cwd CLAUDE.md exists after prepare, err = %v", err) } if err := preparer.Cleanup(t.Context(), CleanupInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", }); err != nil { t.Fatalf("Cleanup() error = %v", err) } if _, err := os.Stat(systemPromptPath); !os.IsNotExist(err) { t.Fatalf("claude system prompt still exists, err = %v", err) } if _, err := os.Stat(pluginDir); !os.IsNotExist(err) { t.Fatalf("claude plugin dir still exists, err = %v", err) } if _, err := os.Stat(projectClaudeDir); !os.IsNotExist(err) { t.Fatalf("cwd .claude exists after cleanup, err = %v", err) } if _, err := os.Stat(filepath.Join(cwd, "CLAUDE.md")); !os.IsNotExist(err) { t.Fatalf("cwd CLAUDE.md exists after cleanup, err = %v", err) } } func TestCodexPreparerSkipsUserBrowserSkillWhenBrowserUseEnabled(t *testing.T) { home := t.TempDir() setTestHome(t, home) t.Setenv(browserUseSwitchEnv, "") userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "auth.json"), []byte(`{"token":"test"}`), 0o600); err != nil { t.Fatal(err) } writeSidecarTestFile(t, filepath.Join(userCodexHome, "skills", "browser", "SKILL.md"), "---\nname: browser\n---\nExternal browser\n") writeSidecarTestFile(t, filepath.Join(userCodexHome, "skills", "caveman", "SKILL.md"), "---\nname: caveman\n---\nCaveman mode\n") stateDir := t.TempDir() cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", Cwd: cwd, BrowserUse: true, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") if codexHome == "" { t.Fatalf("prepared env = %#v, want CODEX_HOME", prepared.Env) } if _, err := os.Stat(filepath.Join(codexHome, "skills", "browser")); !os.IsNotExist(err) { t.Fatalf("external browser skill should be omitted when browser-use is enabled, err = %v", err) } if _, err := os.Stat(filepath.Join(codexHome, "skills", "caveman")); err != nil { t.Fatalf("unrelated user skill should still be exposed: %v", err) } if _, err := os.Stat(filepath.Join(codexHome, "skills", "browser-use", "SKILL.md")); err != nil { t.Fatalf("browser-use skill missing: %v", err) } codexAgents, err := os.ReadFile(filepath.Join(codexHome, "AGENTS.md")) if err != nil { t.Fatalf("codex AGENTS.md missing: %v", err) } if !strings.Contains(string(codexAgents), "`$browser-use`") { t.Fatalf("codex AGENTS.md content = %q, want browser-use policy", string(codexAgents)) } } func TestExposeCodexImportedRolloutFileSymlinksMatchingRelativePath(t *testing.T) { home := t.TempDir() setTestHome(t, home) rel := filepath.Join("sessions", "2026", "07", "04", "rollout-abc.jsonl") sourcePath := filepath.Join(home, ".codex", rel) writeSidecarTestFile(t, sourcePath, `{"type":"session_meta"}`) codexHome := t.TempDir() if err := exposeCodexImportedRolloutFile(codexHome, filepath.Join(home, ".codex"), sourcePath); err != nil { t.Fatalf("exposeCodexImportedRolloutFile() error = %v", err) } target := filepath.Join(codexHome, rel) info, err := os.Lstat(target) if err != nil { t.Fatalf("imported rollout file not exposed: %v", err) } if runtime.GOOS != "windows" && info.Mode()&os.ModeSymlink == 0 { t.Fatalf("imported rollout file mode = %v, want symlink", info.Mode()) } if runtime.GOOS != "windows" { linkTarget, err := os.Readlink(target) if err != nil { t.Fatal(err) } if linkTarget != sourcePath { t.Fatalf("symlink target = %q, want %q", linkTarget, sourcePath) } } } func TestExposeCodexImportedRolloutFileNoopWhenSourcePathEmpty(t *testing.T) { home := t.TempDir() setTestHome(t, home) codexHome := t.TempDir() if err := exposeCodexImportedRolloutFile(codexHome, filepath.Join(home, ".codex"), ""); err != nil { t.Fatalf("exposeCodexImportedRolloutFile() error = %v", err) } entries, err := os.ReadDir(codexHome) if err != nil { t.Fatal(err) } if len(entries) != 0 { t.Fatalf("codexHome entries = %#v, want none created for empty source path", entries) } } func TestExposeCodexImportedRolloutFileGracefulWhenSourceFileMissing(t *testing.T) { home := t.TempDir() setTestHome(t, home) sourcePath := filepath.Join(home, ".codex", "sessions", "2026", "07", "04", "rollout-gone.jsonl") codexHome := t.TempDir() if err := exposeCodexImportedRolloutFile(codexHome, filepath.Join(home, ".codex"), sourcePath); err != nil { t.Fatalf("exposeCodexImportedRolloutFile() error = %v, want graceful nil when source is gone", err) } if _, err := os.Lstat(filepath.Join(codexHome, "sessions", "2026", "07", "04", "rollout-gone.jsonl")); !os.IsNotExist(err) { t.Fatalf("expected no symlink for a missing source rollout, err = %v", err) } } func TestExposeCodexImportedRolloutFileGracefulWhenSourceOutsideRealCodexHome(t *testing.T) { home := t.TempDir() setTestHome(t, home) outsidePath := filepath.Join(t.TempDir(), "rollout.jsonl") writeSidecarTestFile(t, outsidePath, `{"type":"session_meta"}`) codexHome := t.TempDir() if err := exposeCodexImportedRolloutFile(codexHome, filepath.Join(home, ".codex"), outsidePath); err != nil { t.Fatalf("exposeCodexImportedRolloutFile() error = %v, want graceful nil for a path outside ~/.codex", err) } entries, err := os.ReadDir(codexHome) if err != nil { t.Fatal(err) } if len(entries) != 0 { t.Fatalf("codexHome entries = %#v, want none created for a source path outside ~/.codex", entries) } } func TestDefaultPreparerCodexExposesImportedRolloutFileFromPrepareInput(t *testing.T) { home := t.TempDir() setTestHome(t, home) rel := filepath.Join("sessions", "2026", "07", "04", "rollout-abc.jsonl") sourcePath := filepath.Join(home, ".codex", rel) writeSidecarTestFile(t, sourcePath, `{"type":"session_meta"}`) stateDir := t.TempDir() cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:codex", Provider: "codex", Cwd: cwd, ExternalRolloutSourcePath: sourcePath, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") if codexHome == "" { t.Fatalf("prepared env = %#v, want CODEX_HOME", prepared.Env) } target := filepath.Join(codexHome, rel) info, err := os.Lstat(target) if err != nil { t.Fatalf("imported rollout file not exposed via Prepare(): %v", err) } if runtime.GOOS != "windows" && info.Mode()&os.ModeSymlink == 0 { t.Fatalf("imported rollout file mode = %v, want symlink", info.Mode()) } } func TestDefaultPreparerCodexSkipsRolloutExposureForNonImportedSession(t *testing.T) { home := t.TempDir() setTestHome(t, home) stateDir := t.TempDir() cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "local:codex", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") if codexHome == "" { t.Fatalf("prepared env = %#v, want CODEX_HOME", prepared.Env) } if _, err := os.Stat(filepath.Join(codexHome, "sessions")); !os.IsNotExist(err) { t.Fatalf("non-imported session should not create a sessions dir, err = %v", err) } } func envValue(env []string, key string) string { prefix := key + "=" for _, item := range env { if strings.HasPrefix(item, prefix) { return strings.TrimPrefix(item, prefix) } } return "" } func assertCodexModelsCacheCleared(t *testing.T, path string, message string) { t.Helper() content, err := os.ReadFile(path) if os.IsNotExist(err) { return } if err != nil { t.Fatalf("%s, err = %v", message, err) } if runtime.GOOS != "windows" || len(content) != 0 { t.Fatalf("%s, content = %q, err = %v", message, content, err) } } func TestDefaultPreparerCodexExposesRelativeModelInstructionsFile(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "auth.json"), []byte(`{"token":"test"}`), 0o600); err != nil { t.Fatal(err) } instructionsName := "gpt5.5-unrestricted.md" instructionsBody := "# Unrestricted model instructions\n" if err := os.WriteFile(filepath.Join(userCodexHome, instructionsName), []byte(instructionsBody), 0o600); err != nil { t.Fatal(err) } userCodexConfig := strings.Join([]string{ `model_instructions_file = "./gpt5.5-unrestricted.md"`, `model = "gpt-5.5"`, "", }, "\n") if err := os.WriteFile(filepath.Join(userCodexHome, "config.toml"), []byte(userCodexConfig), 0o600); err != nil { t.Fatal(err) } stateDir := t.TempDir() cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-instructions", AgentTargetID: "local:codex", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") if codexHome == "" { t.Fatalf("prepared env = %#v, want CODEX_HOME", prepared.Env) } sandboxInstructions := filepath.Join(codexHome, instructionsName) info, err := os.Lstat(sandboxInstructions) if err != nil { t.Fatalf("relative model instructions file not exposed into sandbox: %v", err) } if runtime.GOOS != "windows" { if info.Mode()&os.ModeSymlink == 0 { t.Fatalf("sandbox model instructions file mode = %v, want symlink", info.Mode()) } linkTarget, err := os.Readlink(sandboxInstructions) if err != nil { t.Fatal(err) } if linkTarget != filepath.Join(userCodexHome, instructionsName) { t.Fatalf("sandbox instructions symlink target = %q, want user instructions", linkTarget) } } got, err := os.ReadFile(sandboxInstructions) if err != nil { t.Fatal(err) } if string(got) != instructionsBody { t.Fatalf("sandbox instructions body = %q, want %q", string(got), instructionsBody) } } func TestDefaultPreparerCodexRejectsMissingModelInstructionsFile(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "auth.json"), []byte(`{"token":"test"}`), 0o600); err != nil { t.Fatal(err) } // config.toml references a file that doesn't exist userCodexConfig := strings.Join([]string{ `model_instructions_file = "missing-instructions.md"`, `model = "gpt-5.5"`, "", }, "\n") if err := os.WriteFile(filepath.Join(userCodexHome, "config.toml"), []byte(userCodexConfig), 0o600); err != nil { t.Fatal(err) } stateDir := t.TempDir() cwd := t.TempDir() _, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-missing-instructions", AgentTargetID: "local:codex", Provider: "codex", Cwd: cwd, }) if err == nil { t.Fatalf("Prepare() should fail when model_instructions_file points to a missing file") } if !strings.Contains(err.Error(), "model_instructions_file") { t.Fatalf("Prepare() error = %v, want error mentioning model_instructions_file", err) } var dependencyErr *ConfigDependencyUnavailableError if !errors.As(err, &dependencyErr) { t.Fatalf("Prepare() error = %T %v, want ConfigDependencyUnavailableError", err, err) } if dependencyErr.FailureKind != ConfigDependencyFailureMissing || dependencyErr.DependencyPath != "missing-instructions.md" { t.Fatalf("dependency error = %#v", dependencyErr) } } func TestDefaultPreparerCodexPreservesAbsoluteModelInstructionsFile(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(userCodexHome, "auth.json"), []byte(`{"token":"test"}`), 0o600); err != nil { t.Fatal(err) } absPath := filepath.Join(t.TempDir(), "absolute-instructions.md") if err := os.WriteFile(absPath, []byte("# absolute\n"), 0o600); err != nil { t.Fatal(err) } userCodexConfig := strings.Join([]string{ `model_instructions_file = "` + filepath.ToSlash(absPath) + `"`, `model = "gpt-5.5"`, "", }, "\n") if err := os.WriteFile(filepath.Join(userCodexHome, "config.toml"), []byte(userCodexConfig), 0o600); err != nil { t.Fatal(err) } stateDir := t.TempDir() cwd := t.TempDir() prepared, err := newTestPreparer(stateDir).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-abs-instructions", AgentTargetID: "local:codex", Provider: "codex", Cwd: cwd, }) if err != nil { t.Fatalf("Prepare() error = %v", err) } codexHome := envValue(prepared.Env, "CODEX_HOME") // Absolute paths are not materialized; they're readable from host filesystem. if _, err := os.Stat(filepath.Join(codexHome, "absolute-instructions.md")); !os.IsNotExist(err) { t.Fatalf("absolute instructions file should not be materialized into sandbox, err = %v", err) } } func TestDefaultPreparerCodexRejectsMissingAbsoluteModelInstructionsFile(t *testing.T) { home := t.TempDir() setTestHome(t, home) userCodexHome := filepath.Join(home, ".codex") if err := os.MkdirAll(userCodexHome, 0o700); err != nil { t.Fatal(err) } missingPath := filepath.Join(t.TempDir(), "missing-absolute-instructions.md") config := `model_instructions_file = "` + filepath.ToSlash(missingPath) + `"` + "\n" if err := os.WriteFile(filepath.Join(userCodexHome, "config.toml"), []byte(config), 0o600); err != nil { t.Fatal(err) } _, err := newTestPreparer(t.TempDir()).Prepare(t.Context(), PrepareInput{ WorkspaceID: "workspace-1", AgentSessionID: "session-missing-absolute-instructions", AgentTargetID: "local:codex", Provider: "codex", Cwd: t.TempDir(), }) var dependencyErr *ConfigDependencyUnavailableError if !errors.As(err, &dependencyErr) { t.Fatalf("Prepare() error = %T %v, want ConfigDependencyUnavailableError", err, err) } if dependencyErr.FailureKind != ConfigDependencyFailureMissing || dependencyErr.DependencyPath != filepath.Base(missingPath) { t.Fatalf("dependency error = %#v", dependencyErr) } if strings.Contains(err.Error(), filepath.Dir(missingPath)) { t.Fatalf("public error leaks absolute parent path: %q", err.Error()) } } func writeSidecarTestFile(t *testing.T, path string, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } }