package agentruntime import ( "context" "encoding/json" "strings" "sync" "testing" "time" activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events" ) type sideRoutingObserver struct { mu sync.Mutex events map[string][]StreamEvent } func TestCodexSideSourceForLaunchRestoresPersistedCodexHome(t *testing.T) { source, err := codexHistoricalSideSourceForLaunch(Session{ RoomID: "workspace", AgentSessionID: "canonical", Env: []string{"CODEX_HOME=/stale", "EXISTING=value"}, RuntimeContext: map[string]any{"agent": map[string]any{ "codexHome": "/persisted/codex-home", }}, }, Session{ RoomID: "workspace", AgentSessionID: "side-history", RootAgentSessionID: "side-history", Scope: RuntimeSessionScopeSide, SourceAgentSessionID: "canonical", SideRequestID: "request", }) if err != nil { t.Fatal(err) } if source.AgentSessionID != "side-history" || source.RootAgentSessionID != "side-history" || source.SourceAgentSessionID != "canonical" || source.Scope != RuntimeSessionScopeSide || source.Resumable { t.Fatalf("historical launch identity = %#v", source) } if got := envValueLast(source.Env, "CODEX_HOME"); got != "/persisted/codex-home" { t.Fatalf("CODEX_HOME = %q, want persisted home", got) } if got := envValueLast(source.Env, "EXISTING"); got != "value" { t.Fatalf("EXISTING = %q, want preserved", got) } } func TestCodexHistoricalSideCapabilitiesRequirePersistedCodexHome(t *testing.T) { adapter := NewCodexAppServerAdapter(nil) capabilities, err := adapter.SideCapabilities(t.Context(), Session{ AgentSessionID: "historical", ProviderSessionID: "thread-1", RuntimeContext: map[string]any{"agent": map[string]any{ "userAgent": "codex/0.144.1", }}, }) if err != nil { t.Fatal(err) } if capabilities.Supported { t.Fatalf("capabilities = %#v, want fail closed without codexHome", capabilities) } } func (o *sideRoutingObserver) ObserveRuntimeStreamEvents( _ context.Context, _ string, sessionID string, events []StreamEvent, ) error { o.mu.Lock() defer o.mu.Unlock() if o.events == nil { o.events = make(map[string][]StreamEvent) } o.events[sessionID] = append(o.events[sessionID], events...) return nil } func (*sideRoutingObserver) ForgetSideConversation(string, string) {} func (o *sideRoutingObserver) containsText( sessionID string, text string, ) bool { o.mu.Lock() defer o.mu.Unlock() for _, event := range o.events[sessionID] { raw, _ := json.Marshal(event) if strings.Contains(string(raw), text) { return true } } return false } func TestCodexAppServerSideUsesEphemeralForkAndInjectedBoundary(t *testing.T) { transport := &multiProcAppServerTransport{} spawned := 0 transport.setConfigure(func(server *fakeCodexAppServer) { spawned++ server.userAgent = "codex/0.144.1" if spawned == 1 { server.holdTurn = true server.forkNotificationBeforeResponse = true } }) adapter := NewCodexAppServerAdapter(transport) controller := NewController([]Adapter{adapter}, nil) canonicalObserver := &sideRoutingObserver{} sideObserver := &sideRoutingObserver{} controller.SetStreamEventObserver(canonicalObserver) controller.SetSideStreamEventObserver(sideObserver) started, err := controller.Start(t.Context(), StartInput{ RoomID: "workspace-side-codex", AgentSessionID: "parent", Provider: ProviderCodex, CWD: "/workspace", }) if err != nil { t.Fatal(err) } if started.Session.ProviderSessionID != "codex-thread-1" { t.Fatalf("source provider session = %q", started.Session.ProviderSessionID) } if _, err := controller.Exec(t.Context(), ExecInput{ RoomID: "workspace-side-codex", AgentSessionID: "parent", TurnID: "parent-turn", Content: []PromptContentBlock{{Type: "text", Text: "continue parent"}}, TuttiModeSnapshot: testActiveTuttiModeSnapshot(), }); err != nil { t.Fatal(err) } activeDeadline := time.Now().Add(5 * time.Second) for adapter.sessionActiveTurnID("parent") != "turn-1" && time.Now().Before(activeDeadline) { time.Sleep(time.Millisecond) } if got := adapter.sessionActiveTurnID("parent"); got != "turn-1" { t.Fatalf("parent provider active turn = %q, want turn-1", got) } opened, err := controller.OpenSide(t.Context(), SideConversationOpenInput{ RoomID: "workspace-side-codex", SourceAgentSessionID: "parent", SideAgentSessionID: "side-1", RequestID: "side-request-1", }) if err != nil { t.Fatal(err) } if opened.Session.ProviderSessionID != "codex-thread-fork" || !opened.Capabilities.ActiveSourceTurn || !opened.Capabilities.Ephemeral || !opened.Capabilities.HideInheritedTurns || !opened.Capabilities.ModelBoundaryInjected { t.Fatalf("opened side = %#v", opened) } if !controller.HasActiveTurn("workspace-side-codex", "parent") { t.Fatal("Codex parent stopped while Side was opened") } if !sideObserver.containsText("side-1", "Early Side title") { t.Fatal("pre-fork-response child notification was not replayed to Side") } if canonicalObserver.containsText("parent", "Early Side title") { t.Fatal("pre-fork-response child notification leaked to parent") } if count, live := transport.snapshot(); count != 1 || len(live) != 1 { t.Fatalf("processes = spawned %d/live %d, want source-owned 1/1", count, len(live)) } sideConn := transport.conn(0) fork := appServerRequestParams(t, sideConn, appServerMethodThreadFork) if asString(fork["threadId"]) != "codex-thread-1" || asString(fork["lastTurnId"]) != "" || fork["ephemeral"] != true || fork["excludeTurns"] != true || !strings.Contains( asString(fork["developerInstructions"]), "Only user instructions submitted after the Side boundary", ) || !strings.Contains( asString(fork["developerInstructions"]), "Do not create or delegate to sub-agents", ) || !strings.Contains( asString(fork["developerInstructions"]), "= 2 && adapter.sessionActiveTurn("side-1") == nil { break } time.Sleep(time.Millisecond) } if active := adapter.sessionActiveTurn("side-1"); active != nil { t.Fatal("Side provider turn did not settle") } if len(turnStarts) < 2 || asString(turnStarts[len(turnStarts)-1]["threadId"]) != "codex-thread-fork" { t.Fatalf("Side turn/start requests = %#v", turnStarts) } sideCollaboration := payloadObject( turnStarts[len(turnStarts)-1]["collaborationMode"], ) sideSettings := payloadObject(sideCollaboration["settings"]) sideDeveloperInstructions := asString( sideSettings["developer_instructions"], ) if !strings.Contains(sideDeveloperInstructions, "