From 19a0e6ecf171e21253dc61bed22111e1dda76e81 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 4 Nov 2026 11:38:51 +0000 Subject: [PATCH] test(studio): cover the app tool call time limit below the e2e suite The platform e2e spec "ends a tool call that outlasts the 60 second limit" has failed on master since connector tool calls got the MCP tool timeout (360 s by default) instead of the connector package default of 60 s. The fixture tool sleeps 90 s, the call is no longer cut at 60 s, and it returns 200 with "slept". The new limit is intended, and a standalone server has no flag to shorten it, so the spec can only pass by waiting more than six minutes. Move the check to the studio service tests: a connector service with a one second call time limit, and an app tool that sleeps past it, must end with ErrMCPAppFailed and no credential in the text. Remove the e2e spec. Assisted-by: Claude:claude-sonnet-5-5 --- core/services/studio/mcpapps_test.go | 30 ++++++++++++++++++- .../services/studio/mcpconnectors_e2e_test.go | 10 +++++-- tests/e2e-platform/mcp_apps_test.go | 10 ------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/core/services/studio/mcpapps_test.go b/core/services/studio/mcpapps_test.go index 9e42dddf21..25a38daeba 100644 --- a/core/services/studio/mcpapps_test.go +++ b/core/services/studio/mcpapps_test.go @@ -44,6 +44,14 @@ func appsMCPServer() *httptest.Server { add("refresh", mcp.Meta{"ui": map[string]any{"resourceUri": "ui://clock/app.html", "visibility": []any{"app"}}}, "refreshed ") add("model_only", mcp.Meta{"ui": map[string]any{"resourceUri": "ui://clock/app.html", "visibility": []any{"model"}}}, "model ") add("plain", nil, "plain ") + mcp.AddTool(srv, &mcp.Tool{Name: "slow", Meta: mcp.Meta{"ui": map[string]any{"resourceUri": "ui://clock/app.html"}}}, + func(ctx context.Context, _ *mcp.CallToolRequest, _ appIn) (*mcp.CallToolResult, any, error) { + select { + case <-time.After(30 * time.Second): + case <-ctx.Done(): + } + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: "slept"}}}, nil, nil + }) add("big", mcp.Meta{"ui": map[string]any{"resourceUri": "ui://big/app.html"}}, "big ") add("json", mcp.Meta{"ui": map[string]any{"resourceUri": "ui://json/app.html"}}, "json ") page := func(uri, mime, body string, meta mcp.Meta) { @@ -73,8 +81,18 @@ type appsEnv struct { } func newAppsEnv(turns ...[]sessiontest.Step) *appsEnv { + return newAppsEnvWith(nil, turns...) +} + +// newAppsEnvWith is newAppsEnv with the options of the connector service +// changed by tune. +func newAppsEnvWith(tune func(*mcpconns.Options), turns ...[]sessiontest.Step) *appsEnv { ts := appsMCPServer() - conns, _ := realConnectors(true) + var tuners []func(*mcpconns.Options) + if tune != nil { + tuners = append(tuners, tune) + } + conns, _ := realConnectors(true, tuners...) in := mcpconns.Input{Name: "Clock", Transport: mcpconns.TransportHTTP, URL: ts.URL, Enabled: true} token := "tok-clock-secret" in.Token = &token @@ -132,6 +150,16 @@ func toolEntries(svc *studio.Service, user, id string) []session.Entry { } var _ = Describe("mcpapps", func() { + It("ends an app tool call that outlasts the call time limit as a failure with no credential", func() { + e := newAppsEnvWith(func(o *mcpconns.Options) { o.CallTimeout = time.Second }) + id := create(e.svc, alice) + start := time.Now() + _, err := e.svc.MCPAppCall(context.Background(), alice, id, e.ref(), "slow", json.RawMessage(`{"zone":"UTC"}`)) + Expect(err).To(MatchError(studio.ErrMCPAppFailed)) + Expect(time.Since(start)).To(BeNumerically("<", 10*time.Second)) + Expect(err.Error()).NotTo(ContainSubstring("tok-clock-secret")) + }) + It("a connector tool with a view gives the transcript an app reference", func() { // The model's own server has the same tool names, so the connector's tools // take the connector's name in front; the app still names the tool of the server. diff --git a/core/services/studio/mcpconnectors_e2e_test.go b/core/services/studio/mcpconnectors_e2e_test.go index a3a5a33e08..a269cccb39 100644 --- a/core/services/studio/mcpconnectors_e2e_test.go +++ b/core/services/studio/mcpconnectors_e2e_test.go @@ -39,14 +39,18 @@ func e2eServer() (*httptest.Server, *atomic.Int32) { return ts, &reached } -func realConnectors(allowPrivate bool) (*mcpconns.Service, mcpconns.Store) { +func realConnectors(allowPrivate bool, tune ...func(*mcpconns.Options)) (*mcpconns.Service, mcpconns.Store) { ms, err := mcpconns.NewJSON("") Expect(err).NotTo(HaveOccurred()) cs, _ := connectors.NewJSON("") - svc, err := mcpconns.New(mcpconns.Options{ + o := mcpconns.Options{ Store: ms, Key: func(ctx context.Context) ([]byte, error) { return cs.EnsureKey(ctx, connectors.NewKey) }, AllowPrivate: func() bool { return allowPrivate }, - }) + } + for _, t := range tune { + t(&o) + } + svc, err := mcpconns.New(o) Expect(err).NotTo(HaveOccurred()) DeferCleanup(svc.Close) return svc, ms diff --git a/tests/e2e-platform/mcp_apps_test.go b/tests/e2e-platform/mcp_apps_test.go index aa252185f1..3431cec9e8 100644 --- a/tests/e2e-platform/mcp_apps_test.go +++ b/tests/e2e-platform/mcp_apps_test.go @@ -6,7 +6,6 @@ import ( "net/http" "net/url" "strings" - "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -180,15 +179,6 @@ var _ = Describe("MCP Apps", Label("platform", "mcp", "mcp-apps"), Ordered, func Expect(code).To(Equal(http.StatusForbidden)) }) - It("ends a tool call that outlasts the 60 second limit with a 502 and no credential", Label("slow"), func() { - start := time.Now() - code, out := call(alice, chat, server, "slow", obj{"millis": 90000}) - Expect(code).To(Equal(http.StatusBadGateway), string(out)) - Expect(time.Since(start)).To(BeNumerically("<", 80*time.Second)) - Expect(string(out)).To(ContainSubstring("mcp_app_failed")) - Expect(string(out)).NotTo(ContainSubstring("APP-SECRET")) - }) - It("refuses both routes without a credential", func() { anon := newAPI(in.api.base) code, _ := anon.do(http.MethodGet, resPath(chat, server, "ui://clock/app.html"), nil)