diff --git a/internal/tools/create_project_from_template.go b/internal/tools/create_project_from_template.go index 0f548aa..f8df046 100644 --- a/internal/tools/create_project_from_template.go +++ b/internal/tools/create_project_from_template.go @@ -45,14 +45,15 @@ func NewCreateProjectFromTemplate(c *gitea.Client, a *allowlist.Allowlist, tmplO func (t *CreateProjectFromTemplate) Descriptor() registry.ToolDescriptor { return registry.ToolDescriptor{ 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(`{ "type":"object", "properties":{ "owner":{"type":"string"}, "name":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,38}[a-z0-9]$"}, "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"] }`), @@ -60,10 +61,11 @@ func (t *CreateProjectFromTemplate) Descriptor() registry.ToolDescriptor { } type createProjectArgs struct { - Owner string `json:"owner"` - Name string `json:"name"` - Description string `json:"description"` - Private bool `json:"private"` + Owner string `json:"owner"` + Name string `json:"name"` + Description string `json:"description"` + Private bool `json:"private"` + TemplateName string `json:"template_name"` } 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) } + // 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. - tmpl, err := t.c.GetRepo(ctx, t.templateOwner, t.templateName) + tmpl, err := t.c.GetRepo(ctx, t.templateOwner, tmplName) if err != nil { return nil, fmt.Errorf("template lookup: %w", err) } 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. @@ -108,7 +117,7 @@ func (t *CreateProjectFromTemplate) Call(ctx context.Context, raw json.RawMessag } // 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, Name: args.Name, Description: args.Description, diff --git a/internal/tools/create_project_from_template_test.go b/internal/tools/create_project_from_template_test.go index 5c34d92..a5de491 100644 --- a/internal/tools/create_project_from_template_test.go +++ b/internal/tools/create_project_from_template_test.go @@ -122,6 +122,62 @@ func TestCreateProjectHappyPath(t *testing.T) { 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. func TestCreateProjectNameRegexFailure(t *testing.T) { tool := tools.NewCreateProjectFromTemplate(