package mcpsrv import ( "errors" "fmt" "testing" "github.com/modelcontextprotocol/go-sdk/jsonrpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sourcecraft.dev/bigbes/sr-ht-dolt/db" ) // The mapping of errors.go, tested from inside the package because it is the // contract every address-taking tool is written against and none of them exists // yet: list_databases addresses no single database, so the not-found arm has no // caller until the tools of ch. 9.1 land. Pinning it here is what keeps the two // arms from being decided again, differently, in the first handler that needs // them. func TestMissingOrDeniedSeparatesTheTwoArms(t *testing.T) { const missing = "no database ~alice/notes" t.Run("a miss is an answer the agent reads", func(t *testing.T) { // db/ wraps its misses with what it looked up; the sentence the agent // gets is written from the call's own arguments and never from that. err := missingOrDenied(fmt.Errorf("get repo alice/notes: %w", db.ErrNotFound), "get_database", missing) require.Error(t, err) var protocol *jsonrpc.Error assert.False(t, errors.As(err, &protocol), "a miss is a tool result, not a protocol failure") assert.Equal(t, missing, err.Error()) }) t.Run("anything else is a protocol failure", func(t *testing.T) { // An agent must not read "the metadata store could not answer" as "that // database does not exist" and go rewrite its plan around a database that // is perfectly real. err := missingOrDenied(errors.New("dial tcp 127.0.0.1:5432: connection refused"), "get_database", missing) var protocol *jsonrpc.Error require.ErrorAs(t, err, &protocol) assert.EqualValues(t, jsonrpc.CodeInternalError, protocol.Code) assert.Equal(t, internalMessage, protocol.Message) assert.NotContains(t, protocol.Message, "5432", "the cause is logged, never sent") assert.NotContains(t, protocol.Message, missing, "and a store outage does not borrow the sentence a miss gets") }) }