fix(agent): refresh interactive child process metadata
- commit
- b5b7ce14e86e82a073ade67eb72e3b446521ea04
- author
- patriceckhart · · 2026-09-09 05:47 UTC
- parent
- 6506e47
- signature
- unsigned
6 files · +104 -2
M README.md +5 -1
| @@ -814,7 +814,7 @@ Opening `/model` refreshes the router and lists every loaded model under provide | |||
| 814 | 814 | ||
| 815 | 815 | A model installed through Ollama is kept in Ollama's internal storage and is not automatically available as a llama.cpp GGUF file. Download a GGUF copy through `/llama` or place GGUF files in `~/llama-models`, then restart the router so it discovers files added manually. | |
| 816 | 816 | ||
| 817 | - | ## Inline images | |
| 817 | + | ## Child process environment | |
| 818 | 818 | ||
| 819 | 819 | Scripts and shell commands launched by zot inherit runtime metadata: | |
| 820 | 820 | ||
| @@ -831,6 +831,10 @@ Scripts and shell commands launched by zot inherit runtime metadata: | |||
| 831 | 831 | ||
| 832 | 832 | These values contain no credentials. Use `ZOT_AGENT` or `AI_AGENT` to distinguish execution inside zot from a standalone invocation; use the session and model values for diagnostics or attribution. | |
| 833 | 833 | ||
| 834 | + | Interactive shell commands receive model metadata even with `--no-session`; only session ID and file are omitted. Newly launched commands reflect session switches, working-directory changes, and reasoning-level changes. Turning reasoning off removes `ZOT_REASONING_LEVEL`. Already-running subprocesses retain the environment they inherited at launch. | |
| 835 | + | ||
| 836 | + | ## Inline images | |
| 837 | + | ||
| 834 | 838 | When a tool returns an image (for example `read` on a PNG), zot renders it inline on terminals that support it: **Ghostty**, **Kitty**, **iTerm2**, **WezTerm**. On other terminals you see a text placeholder with MIME type, pixel dimensions, and byte size. Control with the `ZOT_INLINE_IMAGES` env var: | |
| 835 | 839 | ||
| 836 | 840 | | Value | Effect | | |
| @@ -814,7 +814,7 @@ Opening `/model` refreshes the router and lists every loaded model under provide | |||||
| 814 | 814 | ||||
| 815 | A model installed through Ollama is kept in Ollama's internal storage and is not automatically available as a llama.cpp GGUF file. Download a GGUF copy through `/llama` or place GGUF files in `~/llama-models`, then restart the router so it discovers files added manually. | 815 | A model installed through Ollama is kept in Ollama's internal storage and is not automatically available as a llama.cpp GGUF file. Download a GGUF copy through `/llama` or place GGUF files in `~/llama-models`, then restart the router so it discovers files added manually. | ||
| 816 | 816 | ||||
| 817 | - | ## Inline images | 817 | + | ## Child process environment |
| 818 | 818 | ||||
| 819 | Scripts and shell commands launched by zot inherit runtime metadata: | 819 | Scripts and shell commands launched by zot inherit runtime metadata: | ||
| 820 | 820 | ||||
| @@ -831,6 +831,10 @@ Scripts and shell commands launched by zot inherit runtime metadata: | |||||
| 831 | 831 | ||||
| 832 | These values contain no credentials. Use `ZOT_AGENT` or `AI_AGENT` to distinguish execution inside zot from a standalone invocation; use the session and model values for diagnostics or attribution. | 832 | These values contain no credentials. Use `ZOT_AGENT` or `AI_AGENT` to distinguish execution inside zot from a standalone invocation; use the session and model values for diagnostics or attribution. | ||
| 833 | 833 | ||||
| 834 | + | Interactive shell commands receive model metadata even with `--no-session`; only session ID and file are omitted. Newly launched commands reflect session switches, working-directory changes, and reasoning-level changes. Turning reasoning off removes `ZOT_REASONING_LEVEL`. Already-running subprocesses retain the environment they inherited at launch. | |||
| 835 | + | ||||
| 836 | + | ## Inline images | |||
| 837 | + | ||||
| 834 | When a tool returns an image (for example `read` on a PNG), zot renders it inline on terminals that support it: **Ghostty**, **Kitty**, **iTerm2**, **WezTerm**. On other terminals you see a text placeholder with MIME type, pixel dimensions, and byte size. Control with the `ZOT_INLINE_IMAGES` env var: | 838 | When a tool returns an image (for example `read` on a PNG), zot renders it inline on terminals that support it: **Ghostty**, **Kitty**, **iTerm2**, **WezTerm**. On other terminals you see a text placeholder with MIME type, pixel dimensions, and byte size. Control with the `ZOT_INLINE_IMAGES` env var: | ||
| 835 | 839 | ||||
| 836 | | Value | Effect | | 840 | | Value | Effect | | ||
M packages/agent/args_test.go +2 -0
| @@ -100,6 +100,8 @@ func TestRunHelpHelperProcess(t *testing.T) { | |||
| 100 | 100 | ||
| 101 | 101 | func TestRunSetsAgentEnvironment(t *testing.T) { | |
| 102 | 102 | t.Setenv("ZOT_AGENT", "") | |
| 103 | + | t.Setenv("AI_AGENT", "") | |
| 104 | + | isolateSessionEnvironment(t) | |
| 103 | 105 | if err := Run([]string{"--help"}, "test"); err != nil { | |
| 104 | 106 | t.Fatalf("Run returned %v", err) | |
| 105 | 107 | } | |
| @@ -100,6 +100,8 @@ func TestRunHelpHelperProcess(t *testing.T) { | |||||
| 100 | 100 | ||||
| 101 | func TestRunSetsAgentEnvironment(t *testing.T) { | 101 | func TestRunSetsAgentEnvironment(t *testing.T) { | ||
| 102 | t.Setenv("ZOT_AGENT", "") | 102 | t.Setenv("ZOT_AGENT", "") | ||
| 103 | + | t.Setenv("AI_AGENT", "") | |||
| 104 | + | isolateSessionEnvironment(t) | |||
| 103 | if err := Run([]string{"--help"}, "test"); err != nil { | 105 | if err := Run([]string{"--help"}, "test"); err != nil { | ||
| 104 | t.Fatalf("Run returned %v", err) | 106 | t.Fatalf("Run returned %v", err) | ||
| 105 | } | 107 | } | ||
M packages/agent/cli.go +9 -1
| @@ -952,7 +952,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 952 | 952 | // (run on the TUI goroutine). Without this, a /sessions swap that | |
| 953 | 953 | // races with a finishing turn could double-write or lose messages. | |
| 954 | 954 | var persistMu sync.Mutex | |
| 955 | - | if !args.NoSess && ag != nil { | |
| 955 | + | if ag != nil { | |
| 956 | 956 | sess, _ = openOrCreateSession(args, r, ag, version) | |
| 957 | 957 | if ag != nil { | |
| 958 | 958 | sessBaselineMsgs = len(ag.Messages()) | |
| @@ -1209,6 +1209,10 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1209 | 1209 | bindAgentSession(newAg, newSess) | |
| 1210 | 1210 | } | |
| 1211 | 1211 | ||
| 1212 | + | r.Provider = newProvider | |
| 1213 | + | r.Model = newModel | |
| 1214 | + | r.Reasoning = newAg.Reasoning | |
| 1215 | + | setZotSessionEnvironment(r, sess) | |
| 1212 | 1216 | startExtensionSession(extMgr, newAg, absPath, "cwd_change") | |
| 1213 | 1217 | ||
| 1214 | 1218 | // Push the new state into the running Interactive. | |
| @@ -1468,6 +1472,10 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1468 | 1472 | }, | |
| 1469 | 1473 | NoYolo: args.NoYolo, | |
| 1470 | 1474 | ConfirmGate: confirmGate, | |
| 1475 | + | OnReasoningChanged: func(level string) { | |
| 1476 | + | r.Reasoning = level | |
| 1477 | + | setZotSessionEnvironment(r, sess) | |
| 1478 | + | }, | |
| 1471 | 1479 | PersistModel: func(providerName, model string) { | |
| 1472 | 1480 | // Update config.json so next launch uses the same pick. | |
| 1473 | 1481 | cfg, _ := LoadConfig() | |
| @@ -952,7 +952,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 952 | // (run on the TUI goroutine). Without this, a /sessions swap that | 952 | // (run on the TUI goroutine). Without this, a /sessions swap that | ||
| 953 | // races with a finishing turn could double-write or lose messages. | 953 | // races with a finishing turn could double-write or lose messages. | ||
| 954 | var persistMu sync.Mutex | 954 | var persistMu sync.Mutex | ||
| 955 | - | if !args.NoSess && ag != nil { | 955 | + | if ag != nil { |
| 956 | sess, _ = openOrCreateSession(args, r, ag, version) | 956 | sess, _ = openOrCreateSession(args, r, ag, version) | ||
| 957 | if ag != nil { | 957 | if ag != nil { | ||
| 958 | sessBaselineMsgs = len(ag.Messages()) | 958 | sessBaselineMsgs = len(ag.Messages()) | ||
| @@ -1209,6 +1209,10 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1209 | bindAgentSession(newAg, newSess) | 1209 | bindAgentSession(newAg, newSess) | ||
| 1210 | } | 1210 | } | ||
| 1211 | 1211 | ||||
| 1212 | + | r.Provider = newProvider | |||
| 1213 | + | r.Model = newModel | |||
| 1214 | + | r.Reasoning = newAg.Reasoning | |||
| 1215 | + | setZotSessionEnvironment(r, sess) | |||
| 1212 | startExtensionSession(extMgr, newAg, absPath, "cwd_change") | 1216 | startExtensionSession(extMgr, newAg, absPath, "cwd_change") | ||
| 1213 | 1217 | ||||
| 1214 | // Push the new state into the running Interactive. | 1218 | // Push the new state into the running Interactive. | ||
| @@ -1468,6 +1472,10 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1468 | }, | 1472 | }, | ||
| 1469 | NoYolo: args.NoYolo, | 1473 | NoYolo: args.NoYolo, | ||
| 1470 | ConfirmGate: confirmGate, | 1474 | ConfirmGate: confirmGate, | ||
| 1475 | + | OnReasoningChanged: func(level string) { | |||
| 1476 | + | r.Reasoning = level | |||
| 1477 | + | setZotSessionEnvironment(r, sess) | |||
| 1478 | + | }, | |||
| 1471 | PersistModel: func(providerName, model string) { | 1479 | PersistModel: func(providerName, model string) { | ||
| 1472 | // Update config.json so next launch uses the same pick. | 1480 | // Update config.json so next launch uses the same pick. | ||
| 1473 | cfg, _ := LoadConfig() | 1481 | cfg, _ := LoadConfig() | ||
M packages/agent/modes/interactive.go +6 -0
| @@ -269,6 +269,9 @@ type InteractiveConfig struct { | |||
| 269 | 269 | // write a new meta row so resume picks up the same model. | |
| 270 | 270 | PersistModel func(providerName, model string) | |
| 271 | 271 | ||
| 272 | + | // OnReasoningChanged reports the normalized level after updating the live agent. | |
| 273 | + | OnReasoningChanged func(level string) | |
| 274 | + | ||
| 272 | 275 | OnAssistant func(m provider.Message) | |
| 273 | 276 | OnToolResult func(id string, r core.ToolResult) | |
| 274 | 277 | ||
| @@ -4196,6 +4199,9 @@ func (i *Interactive) applyReasoningSetting(level string) { | |||
| 4196 | 4199 | i.statusOK = "reasoning level " + label | |
| 4197 | 4200 | i.statusErr = "" | |
| 4198 | 4201 | i.mu.Unlock() | |
| 4202 | + | if i.cfg.OnReasoningChanged != nil { | |
| 4203 | + | i.cfg.OnReasoningChanged(level) | |
| 4204 | + | } | |
| 4199 | 4205 | } | |
| 4200 | 4206 | ||
| 4201 | 4207 | // buildStudyPrompt returns the canned prompt the /study command | |
| @@ -269,6 +269,9 @@ type InteractiveConfig struct { | |||||
| 269 | // write a new meta row so resume picks up the same model. | 269 | // write a new meta row so resume picks up the same model. | ||
| 270 | PersistModel func(providerName, model string) | 270 | PersistModel func(providerName, model string) | ||
| 271 | 271 | ||||
| 272 | + | // OnReasoningChanged reports the normalized level after updating the live agent. | |||
| 273 | + | OnReasoningChanged func(level string) | |||
| 274 | + | ||||
| 272 | OnAssistant func(m provider.Message) | 275 | OnAssistant func(m provider.Message) | ||
| 273 | OnToolResult func(id string, r core.ToolResult) | 276 | OnToolResult func(id string, r core.ToolResult) | ||
| 274 | 277 | ||||
| @@ -4196,6 +4199,9 @@ func (i *Interactive) applyReasoningSetting(level string) { | |||||
| 4196 | i.statusOK = "reasoning level " + label | 4199 | i.statusOK = "reasoning level " + label | ||
| 4197 | i.statusErr = "" | 4200 | i.statusErr = "" | ||
| 4198 | i.mu.Unlock() | 4201 | i.mu.Unlock() | ||
| 4202 | + | if i.cfg.OnReasoningChanged != nil { | |||
| 4203 | + | i.cfg.OnReasoningChanged(level) | |||
| 4204 | + | } | |||
| 4199 | } | 4205 | } | ||
| 4200 | 4206 | ||||
| 4201 | // buildStudyPrompt returns the canned prompt the /study command | 4207 | // buildStudyPrompt returns the canned prompt the /study command | ||
M packages/agent/modes/interactive_model_test.go +17 -0
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "strings" | |
| 7 | 7 | "testing" | |
| 8 | 8 | ||
| 9 | + | "github.com/patriceckhart/zot/packages/core" | |
| 9 | 10 | "github.com/patriceckhart/zot/packages/tui" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| @@ -81,6 +82,22 @@ func TestReasoningCommandOpensDirectSelector(t *testing.T) { | |||
| 81 | 82 | } | |
| 82 | 83 | } | |
| 83 | 84 | ||
| 85 | + | func TestReasoningSettingNotifiesRuntime(t *testing.T) { | |
| 86 | + | i := &Interactive{agent: &core.Agent{Reasoning: "high"}} | |
| 87 | + | var levels []string | |
| 88 | + | i.cfg.OnReasoningChanged = func(level string) { | |
| 89 | + | if i.agent.Reasoning != level { | |
| 90 | + | t.Fatalf("callback ran before agent update: got %q, want %q", i.agent.Reasoning, level) | |
| 91 | + | } | |
| 92 | + | levels = append(levels, level) | |
| 93 | + | } | |
| 94 | + | i.applyReasoningSetting("medium") | |
| 95 | + | i.applyReasoningSetting("off") | |
| 96 | + | if !slices.Equal(levels, []string{"medium", ""}) { | |
| 97 | + | t.Fatalf("reasoning notifications = %q", levels) | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | ||
| 84 | 101 | func TestNormalizeModelQueryKeepsPresetAtSign(t *testing.T) { | |
| 85 | 102 | if got := normalizeModelQuery("@preset/flash"); got != "@presetflash" { | |
| 86 | 103 | t.Fatalf("id = %q; want @presetflash", got) | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "strings" | 6 | "strings" | ||
| 7 | "testing" | 7 | "testing" | ||
| 8 | 8 | ||||
| 9 | + | "github.com/patriceckhart/zot/packages/core" | |||
| 9 | "github.com/patriceckhart/zot/packages/tui" | 10 | "github.com/patriceckhart/zot/packages/tui" | ||
| 10 | ) | 11 | ) | ||
| 11 | 12 | ||||
| @@ -81,6 +82,22 @@ func TestReasoningCommandOpensDirectSelector(t *testing.T) { | |||||
| 81 | } | 82 | } | ||
| 82 | } | 83 | } | ||
| 83 | 84 | ||||
| 85 | + | func TestReasoningSettingNotifiesRuntime(t *testing.T) { | |||
| 86 | + | i := &Interactive{agent: &core.Agent{Reasoning: "high"}} | |||
| 87 | + | var levels []string | |||
| 88 | + | i.cfg.OnReasoningChanged = func(level string) { | |||
| 89 | + | if i.agent.Reasoning != level { | |||
| 90 | + | t.Fatalf("callback ran before agent update: got %q, want %q", i.agent.Reasoning, level) | |||
| 91 | + | } | |||
| 92 | + | levels = append(levels, level) | |||
| 93 | + | } | |||
| 94 | + | i.applyReasoningSetting("medium") | |||
| 95 | + | i.applyReasoningSetting("off") | |||
| 96 | + | if !slices.Equal(levels, []string{"medium", ""}) { | |||
| 97 | + | t.Fatalf("reasoning notifications = %q", levels) | |||
| 98 | + | } | |||
| 99 | + | } | |||
| 100 | + | ||||
| 84 | func TestNormalizeModelQueryKeepsPresetAtSign(t *testing.T) { | 101 | func TestNormalizeModelQueryKeepsPresetAtSign(t *testing.T) { | ||
| 85 | if got := normalizeModelQuery("@preset/flash"); got != "@presetflash" { | 102 | if got := normalizeModelQuery("@preset/flash"); got != "@presetflash" { | ||
| 86 | t.Fatalf("id = %q; want @presetflash", got) | 103 | t.Fatalf("id = %q; want @presetflash", got) | ||
A packages/agent/session_environment_test.go +65 -0
| @@ -0,0 +1,65 @@ | |||
| 1 | + | package agent | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "path/filepath" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/patriceckhart/zot/packages/core" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func isolateSessionEnvironment(t *testing.T) { | |
| 12 | + | t.Helper() | |
| 13 | + | for _, name := range []string{"ZOT_SESSION_ID", "ZOT_SESSION_FILE", "ZOT_PROVIDER", "ZOT_MODEL", "ZOT_REASONING_LEVEL"} { | |
| 14 | + | t.Setenv(name, "inherited") | |
| 15 | + | } | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | func assertSessionEnvironment(t *testing.T, r Resolved, sess *core.Session) { | |
| 19 | + | t.Helper() | |
| 20 | + | want := map[string]string{ | |
| 21 | + | "ZOT_SESSION_ID": "", "ZOT_SESSION_FILE": "", | |
| 22 | + | "ZOT_PROVIDER": r.Provider, "ZOT_MODEL": r.Model, "ZOT_REASONING_LEVEL": r.Reasoning, | |
| 23 | + | } | |
| 24 | + | if sess != nil { | |
| 25 | + | want["ZOT_SESSION_ID"] = sess.ID | |
| 26 | + | want["ZOT_SESSION_FILE"] = sess.Path | |
| 27 | + | } | |
| 28 | + | for name, value := range want { | |
| 29 | + | got, present := os.LookupEnv(name) | |
| 30 | + | if got != value || present != (value != "") { | |
| 31 | + | t.Errorf("%s = %q (present=%v), want %q (present=%v)", name, got, present, value, value != "") | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func TestOpenSessionWithoutPersistencePublishesModelEnvironment(t *testing.T) { | |
| 37 | + | isolateSessionEnvironment(t) | |
| 38 | + | r := Resolved{Provider: "test-provider", Model: "test-model", Reasoning: "high"} | |
| 39 | + | sess, err := openOrCreateSession(Args{NoSess: true}, r, nil, "test") | |
| 40 | + | if err != nil || sess != nil { | |
| 41 | + | t.Fatalf("openOrCreateSession = %v, %v; want nil, nil", sess, err) | |
| 42 | + | } | |
| 43 | + | assertSessionEnvironment(t, r, nil) | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func TestSessionEnvironmentReplacesSessionAndClearsReasoning(t *testing.T) { | |
| 47 | + | isolateSessionEnvironment(t) | |
| 48 | + | r := Resolved{Provider: "old-provider", Model: "old-model", Reasoning: "high"} | |
| 49 | + | oldSession := &core.Session{ID: "old", Path: filepath.Join(t.TempDir(), "old.jsonl")} | |
| 50 | + | setZotSessionEnvironment(r, oldSession) | |
| 51 | + | assertSessionEnvironment(t, r, oldSession) | |
| 52 | + | ||
| 53 | + | // A directory change replaces the session and may rebuild a different model. | |
| 54 | + | r.Provider, r.Model, r.Reasoning = "new-provider", "new-model", "medium" | |
| 55 | + | newSession := &core.Session{ID: "new", Path: filepath.Join(t.TempDir(), "new.jsonl")} | |
| 56 | + | setZotSessionEnvironment(r, newSession) | |
| 57 | + | assertSessionEnvironment(t, r, newSession) | |
| 58 | + | ||
| 59 | + | r.Reasoning = "" | |
| 60 | + | setZotSessionEnvironment(r, newSession) | |
| 61 | + | assertSessionEnvironment(t, r, newSession) | |
| 62 | + | ||
| 63 | + | setZotSessionEnvironment(r, nil) | |
| 64 | + | assertSessionEnvironment(t, r, nil) | |
| 65 | + | } | |
| @@ -0,0 +1,65 @@ | |||||
| 1 | + | package agent | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/patriceckhart/zot/packages/core" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func isolateSessionEnvironment(t *testing.T) { | |||
| 12 | + | t.Helper() | |||
| 13 | + | for _, name := range []string{"ZOT_SESSION_ID", "ZOT_SESSION_FILE", "ZOT_PROVIDER", "ZOT_MODEL", "ZOT_REASONING_LEVEL"} { | |||
| 14 | + | t.Setenv(name, "inherited") | |||
| 15 | + | } | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func assertSessionEnvironment(t *testing.T, r Resolved, sess *core.Session) { | |||
| 19 | + | t.Helper() | |||
| 20 | + | want := map[string]string{ | |||
| 21 | + | "ZOT_SESSION_ID": "", "ZOT_SESSION_FILE": "", | |||
| 22 | + | "ZOT_PROVIDER": r.Provider, "ZOT_MODEL": r.Model, "ZOT_REASONING_LEVEL": r.Reasoning, | |||
| 23 | + | } | |||
| 24 | + | if sess != nil { | |||
| 25 | + | want["ZOT_SESSION_ID"] = sess.ID | |||
| 26 | + | want["ZOT_SESSION_FILE"] = sess.Path | |||
| 27 | + | } | |||
| 28 | + | for name, value := range want { | |||
| 29 | + | got, present := os.LookupEnv(name) | |||
| 30 | + | if got != value || present != (value != "") { | |||
| 31 | + | t.Errorf("%s = %q (present=%v), want %q (present=%v)", name, got, present, value, value != "") | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func TestOpenSessionWithoutPersistencePublishesModelEnvironment(t *testing.T) { | |||
| 37 | + | isolateSessionEnvironment(t) | |||
| 38 | + | r := Resolved{Provider: "test-provider", Model: "test-model", Reasoning: "high"} | |||
| 39 | + | sess, err := openOrCreateSession(Args{NoSess: true}, r, nil, "test") | |||
| 40 | + | if err != nil || sess != nil { | |||
| 41 | + | t.Fatalf("openOrCreateSession = %v, %v; want nil, nil", sess, err) | |||
| 42 | + | } | |||
| 43 | + | assertSessionEnvironment(t, r, nil) | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func TestSessionEnvironmentReplacesSessionAndClearsReasoning(t *testing.T) { | |||
| 47 | + | isolateSessionEnvironment(t) | |||
| 48 | + | r := Resolved{Provider: "old-provider", Model: "old-model", Reasoning: "high"} | |||
| 49 | + | oldSession := &core.Session{ID: "old", Path: filepath.Join(t.TempDir(), "old.jsonl")} | |||
| 50 | + | setZotSessionEnvironment(r, oldSession) | |||
| 51 | + | assertSessionEnvironment(t, r, oldSession) | |||
| 52 | + | ||||
| 53 | + | // A directory change replaces the session and may rebuild a different model. | |||
| 54 | + | r.Provider, r.Model, r.Reasoning = "new-provider", "new-model", "medium" | |||
| 55 | + | newSession := &core.Session{ID: "new", Path: filepath.Join(t.TempDir(), "new.jsonl")} | |||
| 56 | + | setZotSessionEnvironment(r, newSession) | |||
| 57 | + | assertSessionEnvironment(t, r, newSession) | |||
| 58 | + | ||||
| 59 | + | r.Reasoning = "" | |||
| 60 | + | setZotSessionEnvironment(r, newSession) | |||
| 61 | + | assertSessionEnvironment(t, r, newSession) | |||
| 62 | + | ||||
| 63 | + | setZotSessionEnvironment(r, nil) | |||
| 64 | + | assertSessionEnvironment(t, r, nil) | |||
| 65 | + | } | |||