package mcpsrv_test import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" "sourcecraft.dev/bigbes/sr-ht-dolt/core" "sourcecraft.dev/bigbes/sr-ht-dolt/mcpsrv" ) // read_rows and the one question a rendered string cannot answer: is there a // value in this cell at all? // // browse renders a real NULL as the text "NULL", which is what a row storing // those four characters renders as too. On a page the two are interchangeable — // a reader sees the same thing either way — but this tool hands rows to a // machine with no schema beside them, and an agent cannot recover the // distinction afterwards. So the mask browse carries beside its rows is spent // here: an absent value is JSON null, a stored "NULL" is the string. // nullsTable is a table holding both halves of the ambiguity in one column: // row 2 stores no value, row 3 stores the text "NULL", and browse renders both // as "NULL". Only the mask tells them apart. func nullsTable() fakeTable { return fakeTable{ name: "cells", cols: []browse.ColumnInfo{ {Name: "id", Type: "int", PrimaryKey: true}, {Name: "body", Type: "text", Nullable: true}, }, rows: [][]string{ {"1", "plain"}, {"2", "NULL"}, // no value: browse rendered the placeholder {"3", "NULL"}, // the four characters, actually stored }, nulls: [][]bool{ {false, false}, {false, true}, {false, false}, }, } } // nullableRowsResult is read_rows decoded into cells that can be null, which is // the shape this tool answers: [][]*string, where a nil cell is a JSON null. type nullableRowsResult struct { Columns []string `json:"columns"` Rows [][]*string `json:"rows"` } func TestReadRowsAnswersARealNullAsNullAndAStoredNullAsAString(t *testing.T) { res := call(t, connect(t, nullsSurface(t), nil), "read_rows", args("cells", "table", "cells")) require.False(t, res.IsError, "%s", errorText(res)) var out nullableRowsResult decode(t, res, &out) require.Equal(t, []string{"id", "body"}, out.Columns) require.Len(t, out.Rows, 3) require.NotNil(t, out.Rows[0][1]) assert.Equal(t, "plain", *out.Rows[0][1]) assert.Nil(t, out.Rows[1][1], "a cell that holds no value is null, not the string \"NULL\"") require.NotNil(t, out.Rows[2][1], "a row that stores the text \"NULL\" still has a value") assert.Equal(t, "NULL", *out.Rows[2][1]) // A key is never null, and neither is a cell that holds an ordinary value: // the mask is spent on the one column that needed it and nowhere else. for i, row := range out.Rows { require.NotNilf(t, row[0], "row %d: the primary key holds a value", i) } } // The distinction has to survive the wire, not merely the Go struct: what an // agent reads is the JSON, so the null is asserted there too. func TestReadRowsPutsANullOnTheWire(t *testing.T) { res := call(t, connect(t, nullsSurface(t), nil), "read_rows", args("cells", "table", "cells")) require.False(t, res.IsError, "%s", errorText(res)) require.NotNil(t, res.StructuredContent) raw, err := json.Marshal(res.StructuredContent) require.NoError(t, err) assert.Contains(t, string(raw), `[["1","plain"],["2",null],["3","NULL"]]`, "the absent value is a JSON null and the stored text is a string") } // nullsSurface is the server over the cells database. func nullsSurface(t *testing.T) *mcpsrv.Server { t.Helper() repos := &fakeRepos{acl: map[int]map[int]core.AccessMode{}} repos.repos = append(repos.repos, &core.Repo{ ID: 1, Name: "cells", OwnerID: aliceID, OwnerName: "alice", Path: storePath("alice", "cells"), Visibility: core.VisibilityPublic, }) opener := &fakeOpener{sessions: map[string]*fakeSession{ storePath("alice", "cells"): smallStore("main", "cccc", []fakeTable{nullsTable()}), }} return newServer(t, repos, opener) }