fix(create_project_from_template): accept per-call template_name override
The template name was hardcoded into the binary at startup via
NewCreateProjectFromTemplate("mathias", "template-go-web"), so
generating from a different template (e.g. template-go-agent)
required a code change and restart. The constructor already
parameterised it correctly — the gap was at the tool's input
schema, which never exposed template_name to the caller.
Adds an optional template_name input field. When set, it overrides
the server-configured default for that call only; when omitted,
behavior is unchanged. Template owner stays server-configured —
only the repo name is per-call.
Server-side validation already verifies the resolved template
exists and is marked as a template repo, so no enum constraint
is added — keeps the door open for future templates (go-ml,
go-service, ...) without redeploys.
Adds TestCreateProjectTemplateNameOverride verifying the override
directs both the template lookup and the /generate POST.
Closes #24
This commit is contained in:
@@ -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"]
|
||||
}`),
|
||||
@@ -64,6 +65,7 @@ type createProjectArgs struct {
|
||||
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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user