diff --git a/pkg/mcp/agentic/watch.go b/pkg/mcp/agentic/watch.go index 802f55a..9f6863b 100644 --- a/pkg/mcp/agentic/watch.go +++ b/pkg/mcp/agentic/watch.go @@ -14,7 +14,8 @@ import ( const ( defaultWatchPollInterval = 5 * time.Second - defaultWatchTimeout = 30 * time.Minute + defaultWatchTimeout = 60 * time.Second + maxWatchTimeout = 30 * time.Minute ) // WatchInput is the input for agentic_watch. @@ -57,10 +58,7 @@ func (s *PrepSubsystem) watch(ctx context.Context, req *mcp.CallToolRequest, inp pollInterval = defaultWatchPollInterval } - timeout := time.Duration(input.Timeout) * time.Second - if timeout <= 0 { - timeout = defaultWatchTimeout - } + timeout := resolveWatchTimeout(input) start := time.Now() deadline := start.Add(timeout) @@ -166,6 +164,19 @@ func (s *PrepSubsystem) watch(ctx context.Context, req *mcp.CallToolRequest, inp }, nil } +func resolveWatchTimeout(input WatchInput) time.Duration { + if input.Timeout <= 0 { + return defaultWatchTimeout + } + + maxSeconds := int(maxWatchTimeout / time.Second) + if input.Timeout > maxSeconds { + return maxWatchTimeout + } + + return time.Duration(input.Timeout) * time.Second +} + func (s *PrepSubsystem) findActiveWorkspaces() []string { wsDirs := s.listWorkspaceDirs() if len(wsDirs) == 0 { diff --git a/pkg/mcp/agentic/watch_test.go b/pkg/mcp/agentic/watch_test.go index 6af8bb3..bf8aee0 100644 --- a/pkg/mcp/agentic/watch_test.go +++ b/pkg/mcp/agentic/watch_test.go @@ -7,11 +7,35 @@ import ( "time" ) -func TestWatchDefaults_Good_RFCThirtyMinuteTimeout(t *testing.T) { - if defaultWatchTimeout != 30*time.Minute { - t.Fatalf("expected default watch timeout to be 30m, got %s", defaultWatchTimeout) +func TestWatchDefaults_Good_RFCOneMinuteTimeout(t *testing.T) { + if defaultWatchTimeout != 60*time.Second { + t.Fatalf("expected default watch timeout to be 60s, got %s", defaultWatchTimeout) } if defaultWatchPollInterval != 5*time.Second { t.Fatalf("expected default poll interval to be 5s, got %s", defaultWatchPollInterval) } + if maxWatchTimeout != 30*time.Minute { + t.Fatalf("expected max watch timeout to be 30m, got %s", maxWatchTimeout) + } +} + +func TestResolveWatchTimeout_Good_HonorsInputTimeout(t *testing.T) { + got := resolveWatchTimeout(WatchInput{Timeout: 10}) + if got != 10*time.Second { + t.Fatalf("expected input timeout to be honored as 10s, got %s", got) + } +} + +func TestResolveWatchTimeout_Good_ClampsInputTimeout(t *testing.T) { + got := resolveWatchTimeout(WatchInput{Timeout: int((10 * time.Hour) / time.Second)}) + if got != 30*time.Minute { + t.Fatalf("expected input timeout to clamp to 30m, got %s", got) + } +} + +func TestResolveWatchTimeout_Good_ZeroUsesDefault(t *testing.T) { + got := resolveWatchTimeout(WatchInput{Timeout: 0}) + if got != defaultWatchTimeout { + t.Fatalf("expected zero timeout to use default %s, got %s", defaultWatchTimeout, got) + } }