be08938f1f
Finding 6 of REVIEW-2026-07-30.md. runWithTimeout could not cancel anything, because Provider.Execute took no context: the goroutine ran on to the HTTP client's 60s timeout, outliving the 30s capability timeout. Provider.Execute now takes a context carrying that timeout, and the workspace provider issues its tool call with http.NewRequestWithContext, so a timed-out execution actually tears the request down. A read-only capability whose provider call times out now resolves to failed rather than unknown. Spec §4.3 reserves unknown for executions whose side effect may or may not have landed, and never retries them — which made read failures both unretryable and indistinguishable from genuinely ambiguous mutations, for calls that by definition have no side effect. Mutating capabilities still resolve to unknown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uea55zaiWuEByEDC4UBSdd
61 lines
1.8 KiB
Go
61 lines
1.8 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/kami/hexis/internal/domain"
|
|
)
|
|
|
|
func newWorkspaceTestServer(t *testing.T, status int, body map[string]any) (*httptest.Server, *WorkspaceMCPProvider) {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(body)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
allowlist := ToolAllowlist{Tools: map[string]ToolMapping{
|
|
"workspace_tool": {Capability: "ws.do_thing", Risk: "write", ReadOnly: false},
|
|
}}
|
|
return srv, NewWorkspaceMCPProvider(srv.URL, allowlist)
|
|
}
|
|
|
|
func TestExecute_SucceedsOnPlainResult(t *testing.T) {
|
|
_, p := newWorkspaceTestServer(t, http.StatusOK, map[string]any{
|
|
"summary": "ok", "items": []any{}, "warnings": []any{}, "truncated": false,
|
|
})
|
|
|
|
cap := &domain.Capability{Name: "ws.do_thing"}
|
|
result, err := p.Execute(context.Background(), cap, &domain.ExecuteRequest{})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
}
|
|
|
|
// TestExecute_ErrorEnvelopeReportedAsFailure verifies that a 200 response
|
|
// carrying workspace-mcp's ERROR-coded warning envelope (its convention for
|
|
// tool-handler exceptions) is treated as a failed execution, not a success.
|
|
func TestExecute_ErrorEnvelopeReportedAsFailure(t *testing.T) {
|
|
_, p := newWorkspaceTestServer(t, http.StatusOK, map[string]any{
|
|
"summary": "boom",
|
|
"items": []any{},
|
|
"warnings": []any{
|
|
map[string]any{"code": "ERROR", "message": "boom"},
|
|
},
|
|
"truncated": false,
|
|
})
|
|
|
|
cap := &domain.Capability{Name: "ws.do_thing"}
|
|
_, err := p.Execute(context.Background(), cap, &domain.ExecuteRequest{})
|
|
if err == nil {
|
|
t.Fatal("expected error for ERROR-coded warning envelope, got nil")
|
|
}
|
|
}
|