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)
}