Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions client_surface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,74 @@ func TestNewSessionAcceptsPOSIXAbsoluteAgentPath(t *testing.T) {
}
assertJSONKey(t, request.Params, "cwd", `"/"`)
}

// TestNewSessionAcceptsWindowsAbsoluteAgentPath covers the other direction of
// issue #47: a POSIX client must be able to address a Windows agent with a
// drive-absolute or UNC path.
func TestNewSessionAcceptsWindowsAbsoluteAgentPath(t *testing.T) {
ctx := context.Background()
for _, cwd := range []string{`C:\agent\workspace`, `C:/agent/workspace`, `\\server\share`} {
c, requests := singleRequestFixture(t, `{"sessionId":"windows-agent"}`)
created, err := c.NewSessionWithOptions(ctx, allCapabilities(), cwd, NewSessionOptions{})
if err != nil {
t.Fatalf("cwd %q: %v", cwd, err)
}
if created.SessionID != "windows-agent" {
t.Fatalf("cwd %q: session ID = %q", cwd, created.SessionID)
}
request := receiveRequest(t, requests)
if request.Method != schema.SessionNewMethodName {
t.Fatalf("cwd %q: method = %s", cwd, request.Method)
}
want, err := json.Marshal(cwd)
if err != nil {
t.Fatal(err)
}
assertJSONKey(t, request.Params, "cwd", string(want))
}
}

// TestSessionPathsAcceptPOSIXAbsoluteAgentPaths covers issue #47: every
// agent-side session entry point must accept a path that is absolute on the
// agent's platform. On Windows, filepath.IsAbs rejects "/", so these fail if a
// call site stops delegating to acpvalidate.AbsolutePath.
func TestSessionPathsAcceptPOSIXAbsoluteAgentPaths(t *testing.T) {
ctx := context.Background()
init := allCapabilities()

t.Run("load", func(t *testing.T) {
c, requests := singleRequestFixture(t, `{}`)
if _, err := c.LoadSession(ctx, init, schema.LoadSessionRequest{SessionID: "old", Cwd: "/"}); err != nil {
t.Fatal(err)
}
request := receiveRequest(t, requests)
if request.Method != schema.SessionLoadMethodName {
t.Fatalf("method = %s", request.Method)
}
assertJSONKey(t, request.Params, "cwd", `"/"`)
})

t.Run("resume", func(t *testing.T) {
c, requests := singleRequestFixture(t, `{}`)
if _, err := c.ResumeSession(ctx, init, schema.ResumeSessionRequest{SessionID: "old", Cwd: "/"}); err != nil {
t.Fatal(err)
}
request := receiveRequest(t, requests)
if request.Method != schema.SessionResumeMethodName {
t.Fatalf("method = %s", request.Method)
}
assertJSONKey(t, request.Params, "cwd", `"/"`)
})

t.Run("list", func(t *testing.T) {
c, requests := singleRequestFixture(t, `{"sessions":[]}`)
if _, err := c.ListSessions(ctx, init, schema.ListSessionsRequest{Cwd: strPtr("/")}); err != nil {
t.Fatal(err)
}
request := receiveRequest(t, requests)
if request.Method != schema.SessionListMethodName {
t.Fatalf("method = %s", request.Method)
}
assertJSONKey(t, request.Params, "cwd", `"/"`)
})
}
43 changes: 43 additions & 0 deletions mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,46 @@ func TestResumeAndForkGateOnCapabilities(t *testing.T) {
t.Fatalf("method = %s", method)
}
}

// TestSessionPathsAcceptPOSIXAbsoluteAgentPaths covers issue #47 for the
// unstable v1 MCP session helpers: each one must accept "/" even when the
// client host treats it as relative.
func TestSessionPathsAcceptPOSIXAbsoluteAgentPaths(t *testing.T) {
ctx := context.Background()
initialization := schema.InitializeResponse{AgentCapabilities: &schema.AgentCapabilities{
SessionCapabilities: &schema.SessionCapabilities{
Resume: &schema.SessionResumeCapabilities{},
Fork: &schema.SessionForkCapabilities{},
},
}}

t.Run("new", func(t *testing.T) {
connection, requests := fixture(t, func(string) any { return map[string]any{"sessionId": "s"} })
if _, err := mcp.NewSession(ctx, connection, initialization, "/", mcp.NewSessionOptions{}); err != nil {
t.Fatal(err)
}
if method := <-requests; method != schema.SessionNewMethodName {
t.Fatalf("method = %s", method)
}
})

t.Run("resume", func(t *testing.T) {
connection, requests := fixture(t, func(string) any { return map[string]any{} })
if _, err := mcp.ResumeSession(ctx, connection, initialization, schema.ResumeSessionRequest{Cwd: "/", SessionID: "old"}); err != nil {
t.Fatal(err)
}
if method := <-requests; method != schema.SessionResumeMethodName {
t.Fatalf("method = %s", method)
}
})

t.Run("fork", func(t *testing.T) {
connection, requests := fixture(t, func(string) any { return map[string]any{"sessionId": "forked"} })
if _, err := mcp.ForkSession(ctx, connection, initialization, schema.ForkSessionRequest{Cwd: "/", SessionID: "old"}); err != nil {
t.Fatal(err)
}
if method := <-requests; method != schema.SessionForkMethodName {
t.Fatalf("method = %s", method)
}
})
}
8 changes: 8 additions & 0 deletions unstable/unstable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ func TestForkSessionGatesAndEncodes(t *testing.T) {
if request := <-requests; request.method != schema.SessionForkMethodName {
t.Fatalf("method = %s", request.method)
}
// Issue #47: a Windows client must be able to fork into a POSIX agent
// working directory even though filepath.IsAbs rejects "/" there.
if _, err := acpunstable.ForkSession(ctx, connection, initialization, schema.ForkSessionRequest{Cwd: "/", SessionID: "old"}); err != nil {
t.Fatal(err)
}
if request := <-requests; request.method != schema.SessionForkMethodName {
t.Fatalf("method = %s", request.method)
}
}

type testHandler struct{}
Expand Down
33 changes: 33 additions & 0 deletions v2/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,36 @@ func TestV2NewSessionUsesStoredInitialization(t *testing.T) {
t.Fatalf("session ID = %q", session.SessionID)
}
}

// TestV2SessionPathsAcceptPOSIXAbsoluteAgentPaths covers issue #47 for the
// draft-v2 session entry points besides NewSession.
func TestV2SessionPathsAcceptPOSIXAbsoluteAgentPaths(t *testing.T) {
ctx := context.Background()
client, _ := pipeClient(t, func(_ context.Context, method string, raw json.RawMessage) (any, error) {
switch method {
case schema.SessionResumeMethodName:
request := decode[schema.ResumeSessionRequest](t, raw)
if request.Cwd != "/" {
t.Errorf("resume cwd = %q", request.Cwd)
}
return schema.ResumeSessionResponse{}, nil
case schema.SessionListMethodName:
request := decode[schema.ListSessionsRequest](t, raw)
if request.Cwd == nil || *request.Cwd != "/" {
t.Errorf("list cwd = %v", request.Cwd)
}
return schema.ListSessionsResponse{}, nil
default:
return nil, &acp.RPCError{Code: -32601}
}
}, nil)

initialization := sessionInitialization()
if _, err := client.ResumeSession(ctx, initialization, schema.ResumeSessionRequest{SessionID: "old", Cwd: "/"}); err != nil {
t.Fatal(err)
}
cwd := schema.AbsolutePath("/")
if _, err := client.ListSessions(ctx, initialization, schema.ListSessionsRequest{Cwd: &cwd}); err != nil {
t.Fatal(err)
}
}
1 change: 1 addition & 0 deletions v2/mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func TestValidatePathsAcceptsAbsolutePathsFromEitherPlatform(t *testing.T) {
}
if err := validateServers(initialization(true), []schema.McpServer{
NewStdioServer("local", "/opt/mcp", nil, nil),
NewStdioServer("windows", `C:\tools\mcp.exe`, nil, nil),
}); err != nil {
t.Fatal(err)
}
Expand Down
Loading