Files
tutti/packages/agent/daemon/runtime/codex_appserver_fork_test.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

685 lines
20 KiB
Go

package agentruntime
import (
"context"
"encoding/json"
"errors"
"testing"
)
func TestCodexAppServerForkCapabilitiesRequireExactSupportedRuntime(t *testing.T) {
t.Run("supported codex", func(t *testing.T) {
adapter, source, transport := startForkCapableCodexAdapter(t)
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
if err != nil {
t.Fatalf("ForkCapabilities: %v", err)
}
if capabilities.FullSession || !capabilities.ThroughTurn {
t.Fatalf("capabilities = %#v, want through-turn only", capabilities)
}
if spawned, live := transport.snapshot(); spawned != 1 || len(live) != 1 {
t.Fatalf(
"capability probe processes = spawned %d/live %d, want existing 1/1",
spawned,
len(live),
)
}
})
t.Run("older codex", func(t *testing.T) {
transport := &multiProcAppServerTransport{}
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.143.9"
})
adapter := NewCodexAppServerAdapter(transport)
source := testAppServerSession()
if _, err := adapter.Start(context.Background(), source); err != nil {
t.Fatalf("Start: %v", err)
}
source.ProviderSessionID = "codex-thread-1"
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
if err != nil {
t.Fatalf("ForkCapabilities: %v", err)
}
if capabilities.FullSession || capabilities.ThroughTurn {
t.Fatalf("capabilities = %#v, want unsupported", capabilities)
}
})
t.Run("supported tutti agent", func(t *testing.T) {
transport := &multiProcAppServerTransport{}
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = tuttiAgentForkUserAgent()
})
adapter := NewTuttiAgentAppServerAdapterWithHostMetadata(
transport,
LegacyHostMetadata(),
)
source := testAppServerSession()
source.Provider = ProviderTuttiAgent
if _, err := adapter.Start(context.Background(), source); err != nil {
t.Fatalf("Start: %v", err)
}
source.ProviderSessionID = "codex-thread-1"
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
if err != nil {
t.Fatalf("ForkCapabilities: %v", err)
}
if capabilities.FullSession || !capabilities.ThroughTurn {
t.Fatalf("capabilities = %#v, want through-turn only", capabilities)
}
})
t.Run("older tutti agent", func(t *testing.T) {
transport := &multiProcAppServerTransport{}
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "tutti_agent/0.0.9"
})
adapter := NewTuttiAgentAppServerAdapterWithHostMetadata(
transport,
LegacyHostMetadata(),
)
source := testAppServerSession()
source.Provider = ProviderTuttiAgent
if _, err := adapter.Start(context.Background(), source); err != nil {
t.Fatalf("Start: %v", err)
}
source.ProviderSessionID = "tutti-thread-1"
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
if err != nil {
t.Fatalf("ForkCapabilities: %v", err)
}
if capabilities.FullSession || capabilities.ThroughTurn {
t.Fatalf("capabilities = %#v, want unsupported", capabilities)
}
})
}
func TestCodexAppServerForkCapabilitiesUsePersistedRuntimeAttestation(t *testing.T) {
transport := &multiProcAppServerTransport{}
adapter := NewCodexAppServerAdapter(transport)
source := testAppServerSession()
source.ProviderSessionID = "codex-thread-1"
source.RuntimeContext = map[string]any{
"agent": map[string]any{"userAgent": "codex/0.144.1"},
}
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
if err != nil {
t.Fatalf("ForkCapabilities: %v", err)
}
if capabilities.FullSession || !capabilities.ThroughTurn {
t.Fatalf("capabilities = %#v, want through-turn only", capabilities)
}
if spawned, live := transport.snapshot(); spawned != 0 || len(live) != 0 {
t.Fatalf(
"capability projection processes = spawned %d/live %d, want 0/0",
spawned,
len(live),
)
}
}
func TestTuttiAgentAppServerForkCapabilitiesUsePersistedRuntimeAttestation(
t *testing.T,
) {
transport := &multiProcAppServerTransport{}
adapter := NewTuttiAgentAppServerAdapterWithHostMetadata(
transport,
LegacyHostMetadata(),
)
source := testAppServerSession()
source.Provider = ProviderTuttiAgent
source.ProviderSessionID = "codex-thread-1"
source.RuntimeContext = map[string]any{
"agent": map[string]any{"userAgent": tuttiAgentForkUserAgent()},
}
capabilities, err := adapter.ForkCapabilities(t.Context(), source)
if err != nil {
t.Fatalf("ForkCapabilities: %v", err)
}
if capabilities.FullSession || !capabilities.ThroughTurn {
t.Fatalf("capabilities = %#v, want through-turn only", capabilities)
}
if spawned, live := transport.snapshot(); spawned != 0 || len(live) != 0 {
t.Fatalf(
"capability projection processes = spawned %d/live %d, want 0/0",
spawned,
len(live),
)
}
}
func TestCodexAppServerForkCapabilitiesRequirePersistedAttestation(t *testing.T) {
t.Run("missing attestation", func(t *testing.T) {
transport := &multiProcAppServerTransport{}
adapter := NewCodexAppServerAdapter(transport)
source := testAppServerSession()
source.ProviderSessionID = "codex-thread-1"
capabilities, err := adapter.ForkCapabilities(t.Context(), source)
if err != nil || capabilities.ThroughTurn {
t.Fatalf("capabilities=%#v error=%v, want unsupported", capabilities, err)
}
if spawned, _ := transport.snapshot(); spawned != 0 {
t.Fatalf("capability projection spawned=%d, want 0", spawned)
}
})
t.Run("older persisted runtime", func(t *testing.T) {
transport := &multiProcAppServerTransport{}
adapter := NewCodexAppServerAdapter(transport)
source := testAppServerSession()
source.ProviderSessionID = "codex-thread-1"
source.RuntimeContext = map[string]any{
"agent": map[string]any{"userAgent": "codex/0.143.9"},
}
capabilities, err := adapter.ForkCapabilities(t.Context(), source)
if err != nil || capabilities.ThroughTurn {
t.Fatalf("capabilities=%#v error=%v, want unsupported", capabilities, err)
}
if spawned, _ := transport.snapshot(); spawned != 0 {
t.Fatalf("capability projection spawned=%d, want 0", spawned)
}
})
}
func TestCodexAppServerForkThroughProviderTurn(t *testing.T) {
adapter, source, transport := startForkCapableCodexAdapter(t)
result, err := adapter.Fork(context.Background(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
})
if err != nil {
t.Fatalf("Fork: %v", err)
}
if result.ProviderSessionID != "codex-thread-fork" ||
result.ForkedFromProviderSessionID != source.ProviderSessionID ||
result.ThroughProviderTurnID != "provider-turn-2" {
t.Fatalf("result = %#v", result)
}
request := appServerRequestParams(
t,
transport.conn(1),
appServerMethodThreadFork,
)
if got := asString(request["threadId"]); got != source.ProviderSessionID {
t.Fatalf("threadId = %q, want %q", got, source.ProviderSessionID)
}
if got := asString(request["lastTurnId"]); got != "provider-turn-2" {
t.Fatalf("lastTurnId = %q, want provider-turn-2", got)
}
for _, forbidden := range []string{"path", "cwd", "model", "config"} {
if _, ok := request[forbidden]; ok {
t.Fatalf("fork request unexpectedly contains %q: %#v", forbidden, request)
}
}
spawned, live := transport.snapshot()
if spawned != 2 || len(live) != 1 {
t.Fatalf("processes = spawned %d/live %d, want 2/1", spawned, len(live))
}
if !adapter.HasLiveSession(source) {
t.Fatal("source live session was closed by fork")
}
}
func TestTuttiAgentAppServerForkThroughProviderTurn(t *testing.T) {
transport := &multiProcAppServerTransport{}
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = tuttiAgentForkUserAgent()
server.threadReadTurnIDs = []string{
"provider-turn-1",
"provider-turn-2",
}
})
adapter := NewTuttiAgentAppServerAdapterWithHostMetadata(
transport,
LegacyHostMetadata(),
)
source := testAppServerSession()
source.Provider = ProviderTuttiAgent
if _, err := adapter.Start(t.Context(), source); err != nil {
t.Fatalf("Start: %v", err)
}
source.ProviderSessionID = "codex-thread-1"
controller := NewController([]Adapter{adapter}, nil)
result, err := controller.Fork(t.Context(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
ProviderTurnBindingJSON: json.RawMessage(`{"schemaVersion":1}`),
})
if err != nil {
t.Fatalf("Fork: %v", err)
}
if result.ProviderSessionID != "codex-thread-fork" ||
result.ForkedFromProviderSessionID != source.ProviderSessionID ||
result.ThroughProviderTurnID != "provider-turn-2" {
t.Fatalf("result = %#v", result)
}
request := appServerRequestParams(
t,
transport.conn(1),
appServerMethodThreadFork,
)
if got := asString(request["lastTurnId"]); got != "provider-turn-2" {
t.Fatalf("lastTurnId = %q, want provider-turn-2", got)
}
}
func TestCodexAppServerForkedChildCanResumeAndStartTurn(t *testing.T) {
adapter, source, transport := startForkCapableCodexAdapter(t)
result, err := adapter.Fork(t.Context(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
})
if err != nil {
t.Fatalf("Fork: %v", err)
}
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.144.1"
server.holdTurn = true
})
target := source
target.AgentSessionID = "agent-session-fork"
target.ProviderSessionID = result.ProviderSessionID
if err := adapter.Resume(t.Context(), target); err != nil {
t.Fatalf("Resume forked child: %v", err)
}
if !adapter.HasLiveSession(target) {
t.Fatal("forked child did not retain a live resumed session")
}
if err := adapter.ExecAsync(
t.Context(),
target,
[]PromptContentBlock{{Type: "text", Text: "continue from fork"}},
"",
"canonical-fork-turn-1",
nil,
nil,
); err != nil {
t.Fatalf("ExecAsync forked child: %v", err)
}
targetConn := transport.conn(2)
waitForCondition(t, func() bool {
return len(appServerRequestParamsList(
t,
targetConn,
appServerMethodTurnStart,
)) == 1
})
turnStart := appServerRequestParams(
t,
targetConn,
appServerMethodTurnStart,
)
if got := asString(turnStart["threadId"]); got != result.ProviderSessionID {
t.Fatalf(
"forked child turn/start threadId = %q, want %q",
got,
result.ProviderSessionID,
)
}
if !adapter.HasLiveSession(source) {
t.Fatal("resuming forked child replaced the source live session")
}
if err := adapter.Close(t.Context(), target); err != nil {
t.Fatalf("Close forked child: %v", err)
}
}
func TestCodexAppServerForkRejectsUnavailableBoundaryBeforeProviderMutation(
t *testing.T,
) {
adapter, source, transport := startForkCapableCodexAdapter(t)
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.144.1"
server.threadReadTurnIDs = []string{"provider-turn-1"}
})
result, err := adapter.Fork(t.Context(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
})
if err == nil ||
result.DeliveryDisposition != SessionForkDeliveryNotStarted {
t.Fatalf("Fork() result=%#v error=%v, want not-started", result, err)
}
if requests := appServerRequestParamsList(
t,
transport.conn(1),
appServerMethodThreadFork,
); len(requests) != 0 {
t.Fatalf("thread/fork requests=%d, want zero", len(requests))
}
}
func TestCodexAppServerForkRejectsUnverifiedChild(t *testing.T) {
tests := []struct {
name string
configure func(*fakeCodexAppServer)
}{
{
name: "lineage missing",
configure: func(server *fakeCodexAppServer) {
server.omitForkedFromThreadID = true
},
},
{
name: "lineage empty",
configure: func(server *fakeCodexAppServer) {
server.emptyForkedFromThreadID = true
},
},
{
name: "lineage mismatch",
configure: func(server *fakeCodexAppServer) {
server.forkedFromThreadID = "different-source"
},
},
{
name: "boundary mismatch",
configure: func(server *fakeCodexAppServer) {
server.forkResponseLastTurnID = "different-turn"
},
},
{
name: "source returned as child",
configure: func(server *fakeCodexAppServer) {
server.forkChildThreadID = "codex-thread-1"
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
adapter, source, transport := startForkCapableCodexAdapter(t)
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.144.1"
test.configure(server)
})
result, err := adapter.Fork(context.Background(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
})
if err == nil {
t.Fatal("Fork succeeded, want verification error")
}
if result.DeliveryDisposition != SessionForkDeliveryUnknown {
t.Fatalf(
"delivery disposition = %q, want %q",
result.DeliveryDisposition,
SessionForkDeliveryUnknown,
)
}
})
}
}
func TestCodexAppServerForkClassifiesExplicitRPCRejection(t *testing.T) {
adapter, source, transport := startForkCapableCodexAdapter(t)
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.144.1"
server.forkRPCError = true
})
result, err := adapter.Fork(context.Background(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
})
if err == nil {
t.Fatal("Fork succeeded, want explicit provider rejection")
}
if result.DeliveryDisposition != SessionForkDeliveryRejected {
t.Fatalf(
"delivery disposition = %q, want %q",
result.DeliveryDisposition,
SessionForkDeliveryRejected,
)
}
}
func TestCodexAppServerForkVerifiesSelectedProviderTurnBinding(t *testing.T) {
adapter, source, transport := startForkCapableCodexAdapter(t)
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.144.1"
server.forkResponseTurnIDs = []string{"provider-turn-1", "provider-turn-2"}
})
result, err := adapter.Fork(context.Background(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
})
if err != nil {
t.Fatalf("Fork: %v", err)
}
if result.DeliveryDisposition != SessionForkDeliveryAccepted {
t.Fatalf("result = %#v", result)
}
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.144.1"
server.forkResponseTurnIDs = []string{"provider-turn-2"}
})
selectedOnly, err := adapter.Fork(context.Background(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
})
if err != nil || selectedOnly.DeliveryDisposition != SessionForkDeliveryAccepted {
t.Fatalf("selected-only Fork result=%#v error=%v", selectedOnly, err)
}
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.144.1"
server.forkResponseTurnIDs = []string{"provider-turn-1"}
})
rejected, err := adapter.Fork(context.Background(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
})
if err == nil || rejected.DeliveryDisposition != SessionForkDeliveryUnknown {
t.Fatalf("missing selected binding result=%#v error=%v", rejected, err)
}
}
func TestControllerForkUsesOptionalSessionForkAdapter(t *testing.T) {
transport := &multiProcAppServerTransport{}
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.144.1"
})
adapter := NewCodexAppServerAdapter(transport)
source := testAppServerSession()
source.ProviderSessionID = "codex-thread-1"
source.RuntimeContext = map[string]any{
"agent": map[string]any{"userAgent": "codex/0.144.1"},
}
controller := NewController([]Adapter{adapter}, nil)
capabilities, err := controller.ForkCapabilities(context.Background(), source)
if err != nil {
t.Fatalf("ForkCapabilities: %v", err)
}
if !capabilities.ThroughTurn {
t.Fatalf("capabilities = %#v", capabilities)
}
result, err := controller.Fork(context.Background(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
ProviderTurnBindingJSON: []byte(`{"schemaVersion":1}`),
})
if err != nil {
t.Fatalf("Fork: %v", err)
}
if result.ProviderSessionID != "codex-thread-fork" {
t.Fatalf("result = %#v", result)
}
spawned, live := transport.snapshot()
if spawned != 1 || len(live) != 0 {
t.Fatalf("processes = spawned %d/live %d, want 1/0", spawned, len(live))
}
if adapter.HasLiveSession(source) {
t.Fatal("historical fork registered a live source session")
}
controller.mu.Lock()
controller.turns[sessionKey(source.RoomID, source.AgentSessionID)] = activeTurn{
turnID: "canonical-turn-running",
}
controller.mu.Unlock()
activeResult, err := controller.Fork(context.Background(), SessionForkInput{
Source: source,
ProviderTurnID: "provider-turn-2",
ProviderTurnBindingJSON: []byte(`{"schemaVersion":1}`),
})
if err != nil || activeResult.DeliveryDisposition != SessionForkDeliveryAccepted {
t.Fatalf("Fork active turn result=%#v error=%v", activeResult, err)
}
}
func TestControllerForkClassifiesSourcePreparationFailureAsNotStarted(t *testing.T) {
source := testAppServerSession()
source.ProviderSessionID = "codex-thread-1"
tests := []struct {
name string
controller *Controller
mutate func(*Session)
}{
{
name: "invalid source",
controller: NewController(nil, nil),
mutate: func(source *Session) {
source.Provider = ""
},
},
{
name: "adapter resolution failure",
controller: NewControllerWithAdapterResolver(
nil,
nil,
sessionForkErrorAdapterResolver{},
),
mutate: func(*Session) {},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testSource := source
test.mutate(&testSource)
result, err := test.controller.Fork(
context.Background(),
SessionForkInput{
Source: testSource,
ProviderTurnID: "provider-turn-2",
},
)
if err == nil {
t.Fatal("Fork succeeded, want source preparation error")
}
if result.DeliveryDisposition != SessionForkDeliveryNotStarted {
t.Fatalf(
"delivery disposition = %q, want %q",
result.DeliveryDisposition,
SessionForkDeliveryNotStarted,
)
}
})
}
}
type sessionForkErrorAdapterResolver struct{}
func (sessionForkErrorAdapterResolver) ResolveAdapter(
context.Context,
AdapterResolveInput,
) (Adapter, error) {
return nil, errors.New("session fork adapter resolution failed")
}
func TestCodexAppServerUserAgentVersionGate(t *testing.T) {
strategy := appServerForkStrategyForTest(t, ProviderCodex)
tests := []struct {
userAgent string
want bool
}{
{userAgent: "codex/0.144.0", want: true},
{userAgent: "codex-cli 0.145.2", want: true},
{userAgent: "codex/0.143.99", want: false},
{userAgent: "codex/0.146.0-alpha.3.1", want: true},
{userAgent: "tutti-agent/9.0.0", want: false},
{userAgent: "", want: false},
}
for _, test := range tests {
t.Run(test.userAgent, func(t *testing.T) {
version, ok := appServerForkVersion(
strategy,
map[string]any{"userAgent": test.userAgent},
)
got := ok && versionAtLeast(
version,
strategy.throughTurnMinimumVersion,
)
if got != test.want {
t.Fatalf("gate(%q) = %v, want %v", test.userAgent, got, test.want)
}
})
}
}
func TestTuttiAgentAppServerUserAgentVersionGate(t *testing.T) {
strategy := appServerForkStrategyForTest(t, ProviderTuttiAgent)
tests := []struct {
userAgent string
want bool
}{
{userAgent: tuttiAgentForkUserAgent(), want: true},
{
userAgent: tuttiAgentForkUserAgent() + " (Mac OS 26.5.0; arm64) dumb",
want: true,
},
{userAgent: "tutti-agent/0.0.9", want: false},
{userAgent: "codex/0.144.0", want: false},
{userAgent: "", want: false},
}
for _, test := range tests {
t.Run(test.userAgent, func(t *testing.T) {
version, ok := appServerForkVersion(
strategy,
map[string]any{"userAgent": test.userAgent},
)
got := ok && versionAtLeast(
version,
strategy.throughTurnMinimumVersion,
)
if got != test.want {
t.Fatalf(
"gate(%q) = %v, want %v",
test.userAgent,
got,
test.want,
)
}
})
}
}
func startForkCapableCodexAdapter(
t *testing.T,
) (*CodexAppServerAdapter, Session, *multiProcAppServerTransport) {
t.Helper()
transport := &multiProcAppServerTransport{}
transport.setConfigure(func(server *fakeCodexAppServer) {
server.userAgent = "codex/0.144.1"
server.threadReadTurnIDs = []string{"provider-turn-1", "provider-turn-2"}
})
adapter := NewCodexAppServerAdapter(transport)
source := testAppServerSession()
if _, err := adapter.Start(context.Background(), source); err != nil {
t.Fatalf("Start: %v", err)
}
source.ProviderSessionID = "codex-thread-1"
return adapter, source, transport
}