feat(tools): make repo the canonical repo-identifier arg; name now an alias (#38)
Every caller (claude.ai connector, LLMs primed on gitea/GitHub) sends the repo identifier as `repo`, but the 33 per-repo identifier tools advertised `name`. The v0.2.8 shim aliased repo->name so it worked, yet the advertised inputSchema still said `name` — a misleading contract, with the shim load-bearing. - Flip all 33 identifier tools: schema property + required + struct field/tag from `name` to `repo`. A compliant `repo` caller now matches the struct directly; the shim is pure back-compat. - normalizeAliases is now bidirectional (name<->repo), so legacy `name` callers still resolve, and repo_create / create_project_from_template — whose `name` means "name of the NEW repo", not an existing-repo id — keep `name` and still accept `repo`. - `number`/`index` left as-is (out of scope; separate pre-existing quirk where pr_merge advertises `index` rather than `number`). - Tests: schema-canonical assertion + flipped alias round-trip (explicit `repo` wins over `name`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,20 +14,20 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// #36: every caller (claude.ai connector, gitea's own convention) sends the
|
||||
// repo identifier as `repo` and issue/PR index as `index`, but the tools
|
||||
// declare them as `name` and `number`. Unmatched fields zero-valued the path
|
||||
// segment and produced gitea's misleading /api/swagger 404. parseArgs now
|
||||
// aliases repo->name and index->number so the idiomatic call works.
|
||||
// #38: `repo` (and `number`) are the canonical, idiomatic gitea/GitHub arg names
|
||||
// that identifier tools advertise. `name` is kept as a back-compat alias via the
|
||||
// bidirectional shim, so old `name` callers still work. `index`->`number` stays.
|
||||
// An explicit canonical (`repo`) always wins over its alias (`name`).
|
||||
func TestRepoAndIndexAliasesResolve(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
wantPath string
|
||||
}{
|
||||
{"repo+index aliases", `{"owner":"mathias","repo":"infra","index":7}`, "/api/v1/repos/mathias/infra/issues/7"},
|
||||
{"canonical name+number", `{"owner":"mathias","name":"infra","number":7}`, "/api/v1/repos/mathias/infra/issues/7"},
|
||||
{"explicit name wins over repo", `{"owner":"mathias","name":"infra","repo":"ignored","number":7}`, "/api/v1/repos/mathias/infra/issues/7"},
|
||||
{"canonical repo+number", `{"owner":"mathias","repo":"infra","number":7}`, "/api/v1/repos/mathias/infra/issues/7"},
|
||||
{"repo+index alias", `{"owner":"mathias","repo":"infra","index":7}`, "/api/v1/repos/mathias/infra/issues/7"},
|
||||
{"legacy name alias", `{"owner":"mathias","name":"infra","number":7}`, "/api/v1/repos/mathias/infra/issues/7"},
|
||||
{"explicit repo wins over name", `{"owner":"mathias","name":"ignored","repo":"infra","number":7}`, "/api/v1/repos/mathias/infra/issues/7"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -47,3 +47,28 @@ func TestRepoAndIndexAliasesResolve(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// #38: identifier tools must ADVERTISE `repo` (not `name`) in their schema, so
|
||||
// the contract a client reads matches what every caller sends. The two create
|
||||
// tools keep `name` because there it means "name of the new repo", not an
|
||||
// existing-repo identifier.
|
||||
func TestRepoIsCanonicalInAdvertisedSchema(t *testing.T) {
|
||||
c := gitea.NewClient("http://unused", "")
|
||||
a := allowlist.New([]string{"mathias"})
|
||||
|
||||
identifier := map[string]json.RawMessage{
|
||||
"repo_get": tools.NewRepoGet(c, a).Descriptor().InputSchema,
|
||||
"issue_get": tools.NewIssueGet(c, a).Descriptor().InputSchema,
|
||||
"pr_merge": tools.NewPRMerge(c, a).Descriptor().InputSchema,
|
||||
"repo_delete": tools.NewRepoDelete(c, a).Descriptor().InputSchema,
|
||||
"file_read": tools.NewFileRead(c, a).Descriptor().InputSchema,
|
||||
}
|
||||
for name, sch := range identifier {
|
||||
s := string(sch)
|
||||
assert.Contains(t, s, `"repo":`, name+" must advertise repo")
|
||||
assert.NotContains(t, s, `"name":`, name+" must not advertise name")
|
||||
}
|
||||
|
||||
// create tools keep `name` (new resource name, not an existing-repo id)
|
||||
assert.Contains(t, string(tools.NewRepoCreate(c, a).Descriptor().InputSchema), `"name":`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user