fix/v02-patch: pr_files_diff, template_name, repo_update #26

Merged
mathias merged 6 commits from fix/v02-patch into main 2026-05-16 22:03:29 +00:00
2 changed files with 74 additions and 9 deletions
Showing only changes of commit 5545d6ab4b - Show all commits

View File

@@ -45,14 +45,15 @@ func NewCreateProjectFromTemplate(c *gitea.Client, a *allowlist.Allowlist, tmplO
func (t *CreateProjectFromTemplate) Descriptor() registry.ToolDescriptor { func (t *CreateProjectFromTemplate) Descriptor() registry.ToolDescriptor {
return registry.ToolDescriptor{ return registry.ToolDescriptor{
Name: "create_project_from_template", Name: "create_project_from_template",
Description: "Create a new project repo from the template, applying placeholder substitutions to known files.", Description: "Create a new project repo from a template, applying placeholder substitutions to known files. Defaults to the server-configured template; pass template_name to override (e.g. template-go-agent).",
InputSchema: json.RawMessage(`{ InputSchema: json.RawMessage(`{
"type":"object", "type":"object",
"properties":{ "properties":{
"owner":{"type":"string"}, "owner":{"type":"string"},
"name":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,38}[a-z0-9]$"}, "name":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,38}[a-z0-9]$"},
"description":{"type":"string"}, "description":{"type":"string"},
"private":{"type":"boolean"} "private":{"type":"boolean"},
"template_name":{"type":"string","description":"Template repo name to generate from. Defaults to the server-configured template."}
}, },
"required":["owner","name"] "required":["owner","name"]
}`), }`),
@@ -60,10 +61,11 @@ func (t *CreateProjectFromTemplate) Descriptor() registry.ToolDescriptor {
} }
type createProjectArgs struct { type createProjectArgs struct {
Owner string `json:"owner"` Owner string `json:"owner"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Private bool `json:"private"` Private bool `json:"private"`
TemplateName string `json:"template_name"`
} }
type createProjectResult struct { type createProjectResult struct {
@@ -91,13 +93,20 @@ func (t *CreateProjectFromTemplate) Call(ctx context.Context, raw json.RawMessag
return nil, fmt.Errorf("name %q does not match pattern %s: %w", args.Name, nameRe.String(), gitea.ErrValidation) return nil, fmt.Errorf("name %q does not match pattern %s: %w", args.Name, nameRe.String(), gitea.ErrValidation)
} }
// Resolve template: per-call override takes precedence over the
// server-configured default. Owner stays server-configured.
tmplName := args.TemplateName
if tmplName == "" {
tmplName = t.templateName
}
// Verify template exists and is marked as a template repo. // Verify template exists and is marked as a template repo.
tmpl, err := t.c.GetRepo(ctx, t.templateOwner, t.templateName) tmpl, err := t.c.GetRepo(ctx, t.templateOwner, tmplName)
if err != nil { if err != nil {
return nil, fmt.Errorf("template lookup: %w", err) return nil, fmt.Errorf("template lookup: %w", err)
} }
if !tmpl.Template { if !tmpl.Template {
return nil, fmt.Errorf("repo %s/%s is not marked as template: %w", t.templateOwner, t.templateName, gitea.ErrValidation) return nil, fmt.Errorf("repo %s/%s is not marked as template: %w", t.templateOwner, tmplName, gitea.ErrValidation)
} }
// Verify destination doesn't already exist. // Verify destination doesn't already exist.
@@ -108,7 +117,7 @@ func (t *CreateProjectFromTemplate) Call(ctx context.Context, raw json.RawMessag
} }
// Generate repo from template. // Generate repo from template.
newRepo, err := t.c.GenerateFromTemplate(ctx, t.templateOwner, t.templateName, gitea.GenerateFromTemplateArgs{ newRepo, err := t.c.GenerateFromTemplate(ctx, t.templateOwner, tmplName, gitea.GenerateFromTemplateArgs{
Owner: args.Owner, Owner: args.Owner,
Name: args.Name, Name: args.Name,
Description: args.Description, Description: args.Description,

View File

@@ -122,6 +122,62 @@ func TestCreateProjectHappyPath(t *testing.T) {
assert.Empty(t, out.PartialFailure) assert.Empty(t, out.PartialFailure)
} }
// TestCreateProjectTemplateNameOverride (issue #24): per-call template_name overrides the
// server-configured default, so the same binary can generate from template-go-web or
// template-go-agent without restart.
func TestCreateProjectTemplateNameOverride(t *testing.T) {
var templateLookups, generateCalls []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/mathias/template-go-agent":
templateLookups = append(templateLookups, "template-go-agent")
_, _ = w.Write([]byte(newTemplateRepoJSON("template-go-agent", true)))
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/mathias/template-go-web":
templateLookups = append(templateLookups, "template-go-web")
_, _ = w.Write([]byte(newTemplateRepoJSON("template-go-web", true)))
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/mathias/new-agent":
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message":"not found"}`))
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/generate"):
generateCalls = append(generateCalls, r.URL.Path)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(newGeneratedRepoJSON("new-agent")))
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v1/repos/mathias/new-agent/contents/"):
filePath := strings.TrimPrefix(r.URL.Path, "/api/v1/repos/mathias/new-agent/contents/")
_, _ = w.Write([]byte(fileContentsJSON(filePath)))
case r.Method == http.MethodPut && strings.HasPrefix(r.URL.Path, "/api/v1/repos/mathias/new-agent/contents/"):
filePath := strings.TrimPrefix(r.URL.Path, "/api/v1/repos/mathias/new-agent/contents/")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(fileWriteResultJSON(filePath)))
default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer srv.Close()
// Server is configured with template-go-web as the default; call overrides to template-go-agent.
tool := newCreateProjectTool(srv.URL)
_, err := tool.Call(context.Background(), json.RawMessage(
`{"owner":"mathias","name":"new-agent","template_name":"template-go-agent"}`,
))
require.NoError(t, err)
assert.Equal(t, []string{"template-go-agent"}, templateLookups,
"override must direct the template lookup, not the server default")
require.Len(t, generateCalls, 1)
assert.Equal(t, "/api/v1/repos/mathias/template-go-agent/generate", generateCalls[0],
"override must direct the /generate call too")
}
// TestCreateProjectNameRegexFailure: invalid name returns ErrValidation without hitting network. // TestCreateProjectNameRegexFailure: invalid name returns ErrValidation without hitting network.
func TestCreateProjectNameRegexFailure(t *testing.T) { func TestCreateProjectNameRegexFailure(t *testing.T) {
tool := tools.NewCreateProjectFromTemplate( tool := tools.NewCreateProjectFromTemplate(