package mcpsrv_test import ( "fmt" "strings" "testing" "time" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sourcecraft.dev/bigbes/sr-ht-core/auth" "sourcecraft.dev/bigbes/sr-ht-dolt/beads" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" "sourcecraft.dev/bigbes/sr-ht-dolt/core" "sourcecraft.dev/bigbes/sr-ht-dolt/mcpsrv" ) // ready_work: the one tool on this surface that answers about a set of // databases rather than about one. // // What is under test here is not the ready rule — that is beads' and is tested // there, and the /ready page reads it through the very same function — but the // two things only this layer can get wrong: which databases a caller is shown, // and that the two arms of the tool agree about one of them. // // The fixture set below is its own rather than the shared one, because every // property here is a property of a *set*: several owners, one database of each // visibility, one that is not a tracker at all and one whose store will not // open. // --- the fixture ------------------------------------------------------------- const ( daveID = 4 // owns the private tracker bob is granted on erinID = 5 // owns the non-beads database and the broken one frankID = 6 // owns the unlisted tracker ) // The two markers a leak would carry. They are strings no visible database // holds, so "nothing about a database the caller may not list reached the // answer" is a question asked of the serialised payload rather than of a struct. const ( privateMarker = "SECRETREADY" unlistedMarker = "UNLISTEDREADY" ) // readyIssues builds an issues table from (id, title, status, priority, // assignee, is_blocked) tuples, over the same column set the rest of the beads // suite uses. func readyIssues(rows [][6]string) fakeTable { out := make([][]string, 0, len(rows)) for _, r := range rows { out = append(out, row(issueColumns, map[string]string{ "id": r[0], "title": r[1], "status": r[2], "priority": r[3], "assignee": r[4], "is_blocked": r[5], "issue_type": "task", "created_at": "2026-07-01 10:00:00", })) } return beadsTable("issues", issueColumns, out) } // readyTracker is one beads database at head: a "main" branch, one commit, the // issues given and the dependency edges between them. func readyTracker(head string, issues fakeTable, deps [][]string) *fakeSession { return &fakeSession{ branches: []browse.Branch{{Name: "main", Head: head}}, commits: []browse.CommitInfo{{ Hash: head, Author: "alice", Date: headTime, Message: "bd: create (auto-commit)", }}, tables: []fakeTable{ issues, beadsTable("dependencies", []string{"issue_id", "depends_on_issue_id", "type"}, deps), }, } } // readyFixture is one database of the ready fixture set, with its owner: unlike // the shared fixtures, these belong to several people, which is what makes the // listing rule say anything at all. type readyFixture struct { owner string ownerID int name string vis core.Visibility acl map[int]core.AccessMode session *fakeSession // nil is a store that will not open } func (f readyFixture) slug() string { return f.owner + "/" + f.name } // alphaStore is the busiest tracker: two of its five issues are ready. // // a-2 is the case worth naming — it depends on a-5, which is closed, so the // dependency does not block and the card still carries the count. func alphaStore() *fakeSession { issues := readyIssues([][6]string{ {"a-1", "Ready, top priority", "open", "0", "alice", ""}, {"a-2", "Ready, lower", "open", "2", "bob", ""}, {"a-3", "Under way", "in_progress", "0", "alice", ""}, {"a-4", "Waiting on something", "open", "0", "bob", "1"}, {"a-5", "Finished", "closed", "1", "bob", ""}, }) // One long text, so "a listing carries no bodies" is asked of this tool too. issues.rows[0][indexOfColumn("description")] = body("a-1") return readyTracker("h-alpha", issues, [][]string{{"a-2", "a-5", "blocks"}}) } // indexOfColumn is where a named column sits in issueColumns. A name that is not // there panics rather than writing into the wrong cell. func indexOfColumn(name string) int { for i, c := range issueColumns { if c == name { return i } } panic("no issue column named " + name) } func readyFixtures() []readyFixture { return []readyFixture{ {owner: "alice", ownerID: aliceID, name: "alpha", vis: core.VisibilityPublic, session: alphaStore()}, { owner: "bob", ownerID: bobID, name: "beta", vis: core.VisibilityPublic, session: readyTracker("h-beta", readyIssues([][6]string{ {"b-1", "The one ready thing", "open", "1", "carol", ""}, {"b-2", "Finished", "closed", "0", "carol", ""}, }), nil), }, { // PRIVATE: absent from every listing but its owner's and its grantee's, // and readable by neither anonymity nor a stranger. owner: "dave", ownerID: daveID, name: "secrets", vis: core.VisibilityPrivate, acl: map[int]core.AccessMode{bobID: core.AccessRO}, session: readyTracker("h-secrets", readyIssues([][6]string{ {"s-1", privateMarker + " rotate the escrow key", "open", "0", "dave", ""}, }), nil), }, { // UNLISTED: absent from the cross-database arm, which is a listing, and // answered when named directly, which is an address. owner: "frank", ownerID: frankID, name: "hidden", vis: core.VisibilityUnlisted, session: readyTracker("h-hidden", readyIssues([][6]string{ {"u-1", unlistedMarker + " the private draft", "open", "1", "frank", ""}, }), nil), }, { // Not a beads tracker at all: skipped silently, never read for rows. owner: "erin", ownerID: erinID, name: "sensors", vis: core.VisibilityPublic, session: &fakeSession{ branches: []browse.Branch{{Name: "main", Head: "h-sensors"}}, commits: []browse.CommitInfo{{ Hash: "h-sensors", Author: "erin", Date: headTime, Message: "readings", }}, tables: []fakeTable{newTable("measurements", 3)}, }, }, { // A store this daemon cannot open: it costs itself and nothing else. owner: "erin", ownerID: erinID, name: "broken", vis: core.VisibilityPublic, session: nil, }, } } // readyFakes builds the metadata store and the opener over a fixture set, in the // order given — which is the order the listing returns and therefore the order // the aggregation's ceiling takes its candidates in. func readyFakes(fx []readyFixture) (*fakeRepos, *fakeOpener) { repos := &fakeRepos{acl: map[int]map[int]core.AccessMode{}} opener := &fakeOpener{sessions: map[string]*fakeSession{}} for i, f := range fx { id := i + 1 repos.repos = append(repos.repos, &core.Repo{ ID: id, Name: f.name, Description: "the " + f.name + " database", OwnerID: f.ownerID, OwnerName: f.owner, Path: storePath(f.owner, f.name), Visibility: f.vis, }) for userID, mode := range f.acl { if repos.acl[id] == nil { repos.acl[id] = map[int]core.AccessMode{} } repos.acl[id][userID] = mode } if f.session != nil { opener.sessions[storePath(f.owner, f.name)] = f.session } } return repos, opener } // readyServer is the surface over the ready fixture set, with the fakes returned // so a test can count what was opened and read. func readyServer(t *testing.T) (*mcpsrv.Server, *fakeRepos, *fakeOpener) { t.Helper() repos, opener := readyFakes(readyFixtures()) return newServer(t, repos, opener), repos, opener } // --- the shapes a client decodes --------------------------------------------- type ( readyCardResult struct { ID string `json:"id"` Title string `json:"title"` Type string `json:"type"` Priority string `json:"priority"` Assignee string `json:"assignee"` Labels []string `json:"labels"` BlockedBy int `json:"blocked_by"` Blocks int `json:"blocks"` } readyHeadResult struct { Hash string `json:"hash"` Time time.Time `json:"time"` } readyDatabaseResult struct { Owner string `json:"owner"` Name string `json:"name"` Ref string `json:"ref"` Head *readyHeadResult `json:"head"` Ready []readyCardResult `json:"ready"` Count int `json:"count"` } readyUnreadableResult struct { Owner string `json:"owner"` Name string `json:"name"` } readyWorkResult struct { Databases []readyDatabaseResult `json:"databases"` Total int `json:"total"` Limit int `json:"limit"` Truncated bool `json:"truncated"` Considered int `json:"considered"` MaxDatabases int `json:"max_databases"` Capped bool `json:"capped"` Unreadable []readyUnreadableResult `json:"unreadable"` } ) func readyWork(t *testing.T, s *mcp.ClientSession, a map[string]any) readyWorkResult { t.Helper() var out readyWorkResult decode(t, call(t, s, "ready_work", a), &out) return out } // readySlugs is the addresses of the databases in an answer, in the order they // were answered in. func readySlugs(res readyWorkResult) []string { out := make([]string, 0, len(res.Databases)) for _, d := range res.Databases { out = append(out, d.Owner+"/"+d.Name) } return out } func readyIDs(db readyDatabaseResult) []string { out := make([]string, 0, len(db.Ready)) for _, c := range db.Ready { out = append(out, c.ID) } return out } func readyDatabaseNamed(t *testing.T, res readyWorkResult, slug string) readyDatabaseResult { t.Helper() for _, d := range res.Databases { if d.Owner+"/"+d.Name == slug { return d } } t.Fatalf("no database %q in the answer: %v", slug, readySlugs(res)) return readyDatabaseResult{} } // --- the cross-database arm --------------------------------------------------- // The arm the tool exists for: no database named, every tracker the caller may // see, grouped and ordered as the page groups and orders them. func TestReadyWorkAnswersAcrossEveryTracker(t *testing.T) { server, _, _ := readyServer(t) got := readyWork(t, connect(t, server, nil), nil) assert.Equal(t, []string{"alice/alpha", "bob/beta"}, readySlugs(got), "the busier database leads; a tracker with nothing ready is absent rather than empty") assert.Equal(t, 3, got.Total) assert.Equal(t, 200, got.Limit, "the default of docs/DESIGN.mcp.md §9.3") assert.False(t, got.Truncated) assert.False(t, got.Capped) assert.Equal(t, beads.ReadyMaxDatabases, got.MaxDatabases, "the ceiling is beads' and is published so the answer can be read against it") alpha := readyDatabaseNamed(t, got, "alice/alpha") assert.Equal(t, "main", alpha.Ref, "each database is read at its own default branch") require.NotNil(t, alpha.Head, "and says which commit it read") assert.Equal(t, "h-alpha", alpha.Head.Hash) assert.True(t, headTime.Equal(alpha.Head.Time), "got %v", alpha.Head.Time) assert.Equal(t, 2, alpha.Count) assert.Equal(t, []string{"a-1", "a-2"}, readyIDs(alpha), "priority first, then id") assert.Equal(t, readyCardResult{ ID: "a-2", Title: "Ready, lower", Type: "task", Priority: "2", Assignee: "bob", Labels: []string{}, BlockedBy: 1, Blocks: 0, }, alpha.Ready[1], "a closed blocker is still a dependency, and the issue is still ready") beta := readyDatabaseNamed(t, got, "bob/beta") assert.Equal(t, "h-beta", beta.Head.Hash, "each group carries its own database's head") assert.Equal(t, []string{"b-1"}, readyIDs(beta)) } // The listing rule, per caller: what a caller may be shown is exactly what the // cross-database arm shows, and nothing about the rest of the instance reaches // the payload — not a name, not a title, not an id. func TestReadyWorkShowsExactlyWhatTheListingRuleAllows(t *testing.T) { server, _, _ := readyServer(t) for _, tc := range []struct { name string caller *auth.AuthContext want []string }{ {"anonymous", nil, []string{"alice/alpha", "bob/beta"}}, {"a stranger", carol(), []string{"alice/alpha", "bob/beta"}}, {"a grantee", bob(), []string{"alice/alpha", "bob/beta", "dave/secrets"}}, {"an owner of one of them", alice(), []string{"alice/alpha", "bob/beta"}}, } { t.Run(tc.name, func(t *testing.T) { session := connect(t, server, tc.caller) assert.Equal(t, tc.want, readySlugs(readyWork(t, session, nil))) payload := resultJSON(t, call(t, session, "ready_work", nil)) assert.NotContains(t, payload, unlistedMarker, "an unlisted database of somebody else is absent from a listing") assert.NotContains(t, payload, "hidden") assert.NotContains(t, payload, "u-1") if tc.caller == nil || tc.caller.UserID != bobID { assert.NotContains(t, payload, privateMarker, "not a title") assert.NotContains(t, payload, "secrets", "not a name") assert.NotContains(t, payload, "s-1", "not an id") assert.NotContains(t, payload, "dave", "not an owner") } }) } } // The grantee's own answer, so the absences above are a visibility rule and not // a broken tool: bob is granted on the private tracker and reads it. func TestReadyWorkAnswersAGranteeAboutAPrivateTracker(t *testing.T) { server, _, _ := readyServer(t) got := readyWork(t, connect(t, server, bob()), nil) secrets := readyDatabaseNamed(t, got, "dave/secrets") assert.Equal(t, []string{"s-1"}, readyIDs(secrets)) assert.Equal(t, 4, got.Total, "its ready work counts towards the answer") } // Enumerating is two rules and not one, and this is the database where they // differ: db/'s listing query is mode-blind — it lists anything the caller holds // an ACL *row* on — while core.Allowed reads the mode and falls through to // visibility for one it does not recognise. A PRIVATE database carrying such a // row is therefore listed to that caller and must still not be read, which is // the whole job of the second step. func TestReadyWorkAsksTheAccessRuleAndNotOnlyTheListing(t *testing.T) { fixture := func(mode core.AccessMode) []readyFixture { return []readyFixture{{ owner: "dave", ownerID: daveID, name: "quarantine", vis: core.VisibilityPrivate, acl: map[int]core.AccessMode{carolID: mode}, session: readyTracker("h-quarantine", readyIssues([][6]string{ {"q-1", privateMarker + " the quarantined plan", "open", "0", "dave", ""}, }), nil), }} } t.Run("a grant this service does not recognise reads nothing", func(t *testing.T) { repos, opener := readyFakes(fixture(core.AccessMode("ARCHIVED"))) got := readyWork(t, connect(t, newServer(t, repos, opener), carol()), nil) assert.Empty(t, got.Databases) assert.Zero(t, got.Total) assert.Empty(t, got.Unreadable, "it is not a failure either: it is simply absent") assert.Empty(t, opener.opened, "a database the access rule refuses is never opened") }) t.Run("and the same row with a grant it does reads it", func(t *testing.T) { repos, opener := readyFakes(fixture(core.AccessRO)) got := readyWork(t, connect(t, newServer(t, repos, opener), carol()), nil) require.Len(t, got.Databases, 1, "so the refusal above is the mode and not the fixture") assert.Equal(t, []string{"q-1"}, readyIDs(got.Databases[0])) }) } // --- the two arms agree ------------------------------------------------------- // The named arm is the cross arm restricted to one candidate, and this is where // that is asserted rather than assumed: same ref, same head, same cards, same // per-database count. func TestReadyWorkNamedDatabaseAgreesWithTheCrossArm(t *testing.T) { server, _, _ := readyServer(t) session := connect(t, server, nil) across := readyDatabaseNamed(t, readyWork(t, session, nil), "alice/alpha") named := readyWork(t, session, args("alpha")) require.Len(t, named.Databases, 1, "one database was named, so one is answered about") assert.Equal(t, across, named.Databases[0], "one aggregator, two arms: naming a database may not change what it says") assert.Equal(t, 2, named.Total) assert.Equal(t, 1, named.Considered) assert.False(t, named.Capped) assert.Empty(t, named.Unreadable) } // An unlisted database is absent from the listing and answered when addressed — // the rule the dashboard already implements, and the reason the two arms are not // the same question. func TestReadyWorkNamesAnUnlistedTrackerTheListingHides(t *testing.T) { server, _, _ := readyServer(t) session := connect(t, server, nil) assert.NotContains(t, readySlugs(readyWork(t, session, nil)), "frank/hidden") named := readyWork(t, session, map[string]any{"owner": "frank", "name": "hidden"}) require.Len(t, named.Databases, 1) assert.Equal(t, []string{"u-1"}, readyIDs(named.Databases[0])) } // --- the three bounds --------------------------------------------------------- // The head-hash gate: a second call whose heads have not moved reads no rows at // all. Counted on the fakes, never timed — and it holds only because the cache // lives on the server rather than being built per call. func TestReadyWorkHeadHashGateReadsNothingTwice(t *testing.T) { server, _, opener := readyServer(t) session := connect(t, server, nil) alpha := opener.sessions[storePath("alice", "alpha")] beta := opener.sessions[storePath("bob", "beta")] sensors := opener.sessions[storePath("erin", "sensors")] first := readyWork(t, session, nil) reads := alpha.rowReads require.Greater(t, reads, 0, "the first call must read rows") second := readyWork(t, session, nil) assert.Equal(t, reads, alpha.rowReads, "an unmoved head must cost no row reads") assert.Equal(t, 1, alpha.tableReads, "nor a second table listing") assert.Equal(t, 1, alpha.logReads, "nor a second log read") assert.Equal(t, 1, beta.tableReads, "the sibling database is gated too") assert.Equal(t, 1, sensors.tableReads, "and so is the fingerprint of a database that is not a tracker") assert.Equal(t, first, second, "the answer is the same answer, cache or not") // Both stores are still opened and their branches listed: that is what the // gate is gated on, and it is the cheap half. assert.Equal(t, 2, countOpens(opener, storePath("alice", "alpha"))) } // The two arms share one cache, which is the same statement as "they cannot // disagree": a database read by the named arm is not read again by the cross // arm while its head stands. func TestReadyWorkSharesOneCacheBetweenTheArms(t *testing.T) { server, _, opener := readyServer(t) session := connect(t, server, nil) alpha := opener.sessions[storePath("alice", "alpha")] readyWork(t, session, args("alpha")) reads := alpha.rowReads require.Greater(t, reads, 0) readyWork(t, session, nil) assert.Equal(t, reads, alpha.rowReads, "the cross arm read what the named arm had cached") } // The ceiling: more candidate databases than beads.ReadyMaxDatabases and the // answer says so, because a silent cap reads as "that is everything". The // databases past it are never opened. func TestReadyWorkReportsTheCeiling(t *testing.T) { var fx []readyFixture for i := 1; i <= beads.ReadyMaxDatabases+2; i++ { name := fmt.Sprintf("db%02d", i) fx = append(fx, readyFixture{ owner: "alice", ownerID: aliceID, name: name, vis: core.VisibilityPublic, session: readyTracker(fmt.Sprintf("h%02d", i), readyIssues([][6]string{ {fmt.Sprintf("t%02d-1", i), "Ready", "open", "1", "alice", ""}, }), nil), }) } repos, opener := readyFakes(fx) got := readyWork(t, connect(t, newServer(t, repos, opener), nil), nil) assert.True(t, got.Capped, "there were more trackers than one call may open") assert.Equal(t, beads.ReadyMaxDatabases, got.Considered) assert.Equal(t, beads.ReadyMaxDatabases, got.MaxDatabases) assert.Len(t, got.Databases, beads.ReadyMaxDatabases) assert.Equal(t, beads.ReadyMaxDatabases, got.Total) for _, f := range fx[beads.ReadyMaxDatabases:] { assert.Zero(t, countOpens(opener, storePath(f.owner, f.name)), "%s: a database past the ceiling is never opened", f.slug()) } } // --- what one broken database costs ------------------------------------------- // A store that will not open costs itself only: the rest of the answer is the // answer, the gap is admitted by name, and the browse error reaches the log // rather than the caller. func TestReadyWorkSurvivesADatabaseThatCannotBeOpened(t *testing.T) { server, _, _ := readyServer(t) session := connect(t, server, nil) got := readyWork(t, session, nil) assert.Equal(t, []string{"alice/alpha", "bob/beta"}, readySlugs(got)) assert.Equal(t, 3, got.Total) assert.Equal(t, []readyUnreadableResult{{Owner: "erin", Name: "broken"}}, got.Unreadable, "the gap is named so the set is readable as incomplete") payload := resultJSON(t, call(t, session, "ready_work", nil)) assert.NotContains(t, payload, "/stores/", "no on-disk path reaches a caller") assert.NotContains(t, payload, "no store at") } // A database that is not a beads tracker is skipped silently: no group, no note, // and not a row read. func TestReadyWorkSkipsNonBeadsDatabasesSilently(t *testing.T) { server, _, opener := readyServer(t) got := readyWork(t, connect(t, server, nil), nil) assert.NotContains(t, readySlugs(got), "erin/sensors") for _, u := range got.Unreadable { assert.NotEqual(t, "sensors", u.Name, "not being a tracker is not a failure") } assert.Zero(t, opener.sessions[storePath("erin", "sensors")].rowReads, "a database that is not a tracker is never read for rows") } // Naming one is the other half of the same rule: a database the caller is // looking straight at, which is not a tracker, is refused with the sentence that // names the generic tools — never an empty ready set, which would read as // "nothing to do here". func TestReadyWorkRefusesANamedDatabaseThatIsNotATracker(t *testing.T) { server, _, _ := readyServer(t) res := call(t, connect(t, server, nil), "ready_work", map[string]any{"owner": "erin", "name": "sensors"}) require.True(t, res.IsError) text := errorText(res) assert.Contains(t, text, "~erin/sensors") assert.Contains(t, text, "not a beads issue tracker") assert.Contains(t, text, "list_tables", "and names the way to read it anyway") assert.NotContains(t, text, "no database", "the database exists and this caller may read it") } // --- the filters -------------------------------------------------------------- // Each filter narrows the ready set exactly as the page's does: the filtering is // beads.ReadyFilter's, and these are the cases that would catch a second // implementation drifting from it. func TestReadyWorkFiltersNarrowAsThePageDoes(t *testing.T) { server, _, _ := readyServer(t) session := connect(t, server, bob()) for _, tc := range []struct { name string args map[string]any want []string total int }{ {"q over the title", map[string]any{"q": "top priority"}, []string{"alice/alpha"}, 1}, {"q over the id", map[string]any{"q": "b-1"}, []string{"bob/beta"}, 1}, {"q is case-insensitive", map[string]any{"q": "READY, LOWER"}, []string{"alice/alpha"}, 1}, {"assignee", map[string]any{"assignee": "bob"}, []string{"alice/alpha"}, 1}, {"priority", map[string]any{"priority": "0"}, []string{"alice/alpha", "dave/secrets"}, 2}, {"two at once", map[string]any{"priority": "0", "assignee": "dave"}, []string{"dave/secrets"}, 1}, {"a filter nothing matches", map[string]any{"assignee": "nobody"}, nil, 0}, } { t.Run(tc.name, func(t *testing.T) { got := readyWork(t, session, tc.args) assert.ElementsMatch(t, tc.want, readySlugs(got)) assert.Equal(t, tc.total, got.Total, "the total is the matched set, not the instance") }) } } // The filters narrow one named database too, so the arms agree under a filter as // well as without one. func TestReadyWorkFiltersANamedDatabase(t *testing.T) { server, _, _ := readyServer(t) session := connect(t, server, nil) got := readyWork(t, session, args("alpha", "assignee", "bob")) require.Len(t, got.Databases, 1) assert.Equal(t, []string{"a-2"}, readyIDs(got.Databases[0])) assert.Equal(t, 1, got.Databases[0].Count, "the per-database count is the filtered one") } // --- the cap ------------------------------------------------------------------ // The cap of docs/DESIGN.mcp.md §9.3, spent across databases: the answer carries // the limit really applied, the true total, and whether anything was left // behind. Each database keeps its own honest count. func TestReadyWorkCapsTheLimitAndSaysSo(t *testing.T) { server, _, _ := readyServer(t) session := connect(t, server, nil) t.Run("a limit above the cap is answered at the cap", func(t *testing.T) { got := readyWork(t, session, map[string]any{"limit": 5000}) assert.Equal(t, 500, got.Limit, "the cap, reported rather than silently applied") assert.Equal(t, 3, got.Total) assert.False(t, got.Truncated) }) t.Run("a limit below the matches clips and says so", func(t *testing.T) { got := readyWork(t, session, map[string]any{"limit": 1}) assert.Equal(t, 1, got.Limit) require.Len(t, got.Databases, 1, "a database left with nothing is absent, not empty") assert.Equal(t, []string{"a-1"}, readyIDs(got.Databases[0])) assert.Equal(t, 2, got.Databases[0].Count, "and its own count is still the true one") assert.Equal(t, 3, got.Total, "the honest denominator is every match") assert.True(t, got.Truncated) }) t.Run("the limit is spent across databases", func(t *testing.T) { got := readyWork(t, session, map[string]any{"limit": 3}) assert.Equal(t, []string{"alice/alpha", "bob/beta"}, readySlugs(got)) assert.False(t, got.Truncated) }) t.Run("a non-positive limit is refused", func(t *testing.T) { for _, limit := range []int{0, -1} { res := call(t, session, "ready_work", map[string]any{"limit": limit}) require.True(t, res.IsError, "limit %d", limit) assert.Contains(t, errorText(res), "limit must be a positive number of issues") } }) } // --- addressing --------------------------------------------------------------- // Half an address is not an address. Naming neither is the cross-database arm and // naming both is one database; naming one of the two means nothing and is refused // rather than guessed at in either direction. func TestReadyWorkNeedsBothHalvesOfAnAddress(t *testing.T) { server, _, _ := readyServer(t) session := connect(t, server, nil) for _, a := range []map[string]any{{"owner": "alice"}, {"name": "alpha"}} { res := call(t, session, "ready_work", a) require.True(t, res.IsError, "%v", a) assert.Contains(t, errorText(res), "owner") assert.Contains(t, errorText(res), "name") } } // A database the caller may not read answers exactly as one that does not exist, // which is the masked not-found the rest of the surface answers with: a refusal // of its own shape would rebuild the distinction it exists to erase. func TestReadyWorkMasksADatabaseTheCallerMayNotRead(t *testing.T) { server, _, _ := readyServer(t) session := connect(t, server, carol()) masked := call(t, session, "ready_work", map[string]any{"owner": "dave", "name": "secrets"}) require.True(t, masked.IsError) absent := call(t, session, "ready_work", map[string]any{"owner": "dave", "name": "nosuch"}) require.True(t, absent.IsError) assert.Equal(t, strings.Replace(errorText(absent), "nosuch", "secrets", 1), errorText(masked), "a masked database answers exactly as one that does not exist") assert.NotContains(t, errorText(masked), privateMarker) } // --- sessions and bodies ------------------------------------------------------ // One session per database per call, closed by the tool that opened it — in both // arms. The named arm is the one worth counting twice: it opens the store for // the fingerprint and then hands the *same* session to the aggregation, so a // second open would be a store opened twice for one call, and a second close // would be a handle closed twice. func TestReadyWorkClosesEverySessionItOpened(t *testing.T) { t.Run("across every tracker", func(t *testing.T) { server, _, opener := readyServer(t) readyWork(t, connect(t, server, nil), nil) for _, f := range readyFixtures() { if f.session == nil { continue } path := storePath(f.owner, f.name) if f.vis != core.VisibilityPublic { assert.Zero(t, countOpens(opener, path), "%s: a database this caller may not list is never opened", f.slug()) assert.Zero(t, f.session.closes) continue } assert.Equal(t, 1, countOpens(opener, path), "%s: opened once", f.slug()) assert.Equal(t, 1, opener.sessions[path].closes, "%s: and closed once", f.slug()) } }) t.Run("one named tracker", func(t *testing.T) { server, _, opener := readyServer(t) readyWork(t, connect(t, server, nil), args("alpha")) assert.Equal(t, []string{storePath("alice", "alpha")}, opener.opened, "exactly the one database the call named, opened exactly once") assert.Equal(t, 1, opener.sessions[storePath("alice", "alpha")].closes, "the session lent to the aggregation is closed by the handler that opened it, once") }) } // The list/detail split of docs/DESIGN.mcp.md §9.2 holds here too: this is a // listing, so it carries identity and metadata and no prose. Asserted over the // serialised payload, so a field added to the card later would carry a marker // into it and turn this red. func TestReadyWorkCarriesNoIssueBodies(t *testing.T) { server, _, _ := readyServer(t) payload := resultJSON(t, call(t, connect(t, server, nil), "ready_work", nil)) assert.Contains(t, payload, "a-1", "the fixture's body belongs to an issue that is in the answer") assert.NotContains(t, payload, bodyMarker) for _, field := range []string{"description", "design", "acceptance_criteria", "notes"} { assert.NotContains(t, payload, `"`+field+`"`, "the card type has no field a body could arrive in, and that is structural") } } // countOpens is how many times a store path was opened, over the log the fake // opener keeps. func countOpens(o *fakeOpener, path string) int { n := 0 for _, p := range o.opened { if p == path { n++ } } return n }