Implements the first two slices of the OpenSpec change mcp-resource-templates (merged via PR #148): Slice 1 — mcp-resources-core - server.WithResourceCapabilities(false, false) enabled per design.md D5 - new operation/resource/ package with: - parse.go: typed URI parsers for owner/repo/commit/issue/pr/comment/status enforcing 40-hex sha, numeric index, kind ∈ {issue, pr} - bound.go: EmbeddedListCap = 30 + generic Bounded[T] helper with sentinel - errors.go: MapForgejoError maps 403 → -32002, 404 → -32003 - register.go: thin AddTemplate wrapper over mcp.NewResourceTemplate - RegisterCoreResources stub wired from Run() Slice 2 — mcp-resource-commit - operation/repo/resources_commit.go registers template forgejo://repo/{owner}/{repo}/commit/{sha} - Handler returns application/json metadata + text/markdown sidecar (D9) - Short-sha rejected at parse layer; SDK 404 surfaces via MapForgejoError - RegisterCommitResource wired from RegisterCoreResources - README "Resources" section added with one row for commit Tests - 8 new test files across operation/resource/ and operation/repo/ - All passing under go test -count=1 ./... Tasks - 17/47 checkboxes ticked in openspec/changes/mcp-resource-templates/tasks.md (slice 1+2; 1.11 manual client check deferred) Driven by team:dev-loop. Closes the slice 1+2 portion of forgejo-mcp-13x.
58 lines
1.3 KiB
Go
58 lines
1.3 KiB
Go
package resource
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBounded_UnderCap(t *testing.T) {
|
|
items := []string{"a", "b", "c"}
|
|
r := Bounded(items, 10, "list_tool")
|
|
if r.Truncated {
|
|
t.Fatal("expected not truncated")
|
|
}
|
|
if len(r.Items) != 3 {
|
|
t.Errorf("expected 3 items, got %d", len(r.Items))
|
|
}
|
|
if r.Sentinel() != "" {
|
|
t.Errorf("expected empty sentinel, got %q", r.Sentinel())
|
|
}
|
|
}
|
|
|
|
func TestBounded_AtCap(t *testing.T) {
|
|
items := make([]string, 30)
|
|
for i := range items {
|
|
items[i] = "x"
|
|
}
|
|
r := Bounded(items, 30, "list_tool")
|
|
if r.Truncated {
|
|
t.Fatal("expected not truncated at exact cap")
|
|
}
|
|
if r.Sentinel() != "" {
|
|
t.Errorf("expected empty sentinel at cap, got %q", r.Sentinel())
|
|
}
|
|
}
|
|
|
|
func TestBounded_OverCap_SentinelContents(t *testing.T) {
|
|
items := make([]string, 35)
|
|
for i := range items {
|
|
items[i] = "x"
|
|
}
|
|
r := Bounded(items, 30, "list_issues")
|
|
if !r.Truncated {
|
|
t.Fatal("expected truncated")
|
|
}
|
|
if len(r.Items) != 30 {
|
|
t.Errorf("expected 30 items, got %d", len(r.Items))
|
|
}
|
|
s := r.Sentinel()
|
|
if s == "" {
|
|
t.Fatal("expected non-empty sentinel")
|
|
}
|
|
if !strings.Contains(s, "list_issues") {
|
|
t.Errorf("sentinel must name the list tool, got %q", s)
|
|
}
|
|
if !strings.Contains(s, "30 of 35") {
|
|
t.Errorf("sentinel must include cap/total, got %q", s)
|
|
}
|
|
}
|