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
This commit is contained in:
Ettore Di Giacinto committed 2026-11-04 11:38:51 +00:00
1 parent 388be1fa1d
commit 19a0e6ecf1
3 files changed
+36 -14

No files matched your search

+29 -1
View File
@@ -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.
@@ -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
-10
View File
@@ -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)