feat(agentic): add plan template compatibility tools
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
27928fc9b4
commit
9d90e7532f
4 changed files with 594 additions and 61 deletions
|
|
@ -14,7 +14,6 @@ import (
|
|||
"dappco.re/go/core/forge"
|
||||
coremcp "forge.lthn.ai/core/mcp/pkg/mcp"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// options := agentic.AgentOptions{}
|
||||
|
|
@ -202,6 +201,9 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result {
|
|||
c.Action("state.set", s.handleStateSet).Description = "Store shared plan state for later sessions"
|
||||
c.Action("state.get", s.handleStateGet).Description = "Read shared plan state by key"
|
||||
c.Action("state.list", s.handleStateList).Description = "List shared plan state for a plan"
|
||||
c.Action("template.list", s.handleTemplateList).Description = "List available YAML plan templates"
|
||||
c.Action("template.preview", s.handleTemplatePreview).Description = "Preview a YAML plan template with variable substitution"
|
||||
c.Action("template.create_plan", s.handleTemplateCreatePlan).Description = "Create a stored plan from a YAML template"
|
||||
|
||||
c.Action("agentic.prompt", s.handlePrompt).Description = "Read a system prompt by slug"
|
||||
c.Action("agentic.task", s.handleTask).Description = "Read a task plan by slug"
|
||||
|
|
@ -308,6 +310,7 @@ func (s *PrepSubsystem) RegisterTools(server *mcp.Server) {
|
|||
s.registerStateTools(server)
|
||||
s.registerPhaseTools(server)
|
||||
s.registerTaskTools(server)
|
||||
s.registerTemplateTools(server)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "agentic_scan",
|
||||
|
|
@ -778,68 +781,11 @@ func (s *PrepSubsystem) pullWikiContent(ctx context.Context, org, repo string) s
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) renderPlan(templateSlug string, variables map[string]string, task string) string {
|
||||
r := lib.Template(templateSlug)
|
||||
if !r.OK {
|
||||
definition, err := loadPlanTemplateDefinition(templateSlug, variables)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
content := r.Value.(string)
|
||||
for key, value := range variables {
|
||||
content = core.Replace(content, core.Concat("{{", key, "}}"), value)
|
||||
content = core.Replace(content, core.Concat("{{ ", key, " }}"), value)
|
||||
}
|
||||
|
||||
var tmpl struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Guidelines []string `yaml:"guidelines"`
|
||||
Phases []struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Tasks []any `yaml:"tasks"`
|
||||
} `yaml:"phases"`
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(content), &tmpl); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
plan := core.NewBuilder()
|
||||
plan.WriteString(core.Concat("# ", tmpl.Name, "\n\n"))
|
||||
if task != "" {
|
||||
plan.WriteString(core.Concat("**Task:** ", task, "\n\n"))
|
||||
}
|
||||
if tmpl.Description != "" {
|
||||
plan.WriteString(core.Concat(tmpl.Description, "\n\n"))
|
||||
}
|
||||
|
||||
if len(tmpl.Guidelines) > 0 {
|
||||
plan.WriteString("## Guidelines\n\n")
|
||||
for _, g := range tmpl.Guidelines {
|
||||
plan.WriteString(core.Concat("- ", g, "\n"))
|
||||
}
|
||||
plan.WriteString("\n")
|
||||
}
|
||||
|
||||
for i, phase := range tmpl.Phases {
|
||||
plan.WriteString(core.Sprintf("## Phase %d: %s\n\n", i+1, phase.Name))
|
||||
if phase.Description != "" {
|
||||
plan.WriteString(core.Concat(phase.Description, "\n\n"))
|
||||
}
|
||||
for _, t := range phase.Tasks {
|
||||
switch v := t.(type) {
|
||||
case string:
|
||||
plan.WriteString(core.Concat("- [ ] ", v, "\n"))
|
||||
case map[string]any:
|
||||
if name, ok := v["name"].(string); ok {
|
||||
plan.WriteString(core.Concat("- [ ] ", name, "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
plan.WriteString("\n")
|
||||
}
|
||||
|
||||
return plan.String()
|
||||
return renderPlanMarkdown(definition, task)
|
||||
}
|
||||
|
||||
func detectLanguage(repoPath string) string {
|
||||
|
|
|
|||
452
pkg/agentic/template.go
Normal file
452
pkg/agentic/template.go
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"dappco.re/go/agent/pkg/lib"
|
||||
core "dappco.re/go/core"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// variable := agentic.TemplateVariable{Name: "feature_name", Required: true}
|
||||
type TemplateVariable struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
|
||||
// summary := agentic.TemplateSummary{Slug: "bug-fix", Name: "Bug Fix", PhasesCount: 6}
|
||||
type TemplateSummary struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
PhasesCount int `json:"phases_count"`
|
||||
Variables []TemplateVariable `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.TemplateListInput{Category: "development"}
|
||||
type TemplateListInput struct {
|
||||
Category string `json:"category,omitempty"`
|
||||
}
|
||||
|
||||
// out := agentic.TemplateListOutput{Success: true, Total: 2}
|
||||
type TemplateListOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Templates []TemplateSummary `json:"templates"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// input := agentic.TemplatePreviewInput{Template: "new-feature", Variables: map[string]string{"feature_name": "Auth"}}
|
||||
type TemplatePreviewInput struct {
|
||||
Template string `json:"template,omitempty"`
|
||||
TemplateSlug string `json:"template_slug,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Variables map[string]string `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
// out := agentic.TemplatePreviewOutput{Success: true, Template: "bug-fix"}
|
||||
type TemplatePreviewOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Template string `json:"template"`
|
||||
Preview string `json:"preview"`
|
||||
}
|
||||
|
||||
// input := agentic.TemplateCreatePlanInput{Template: "new-feature", Variables: map[string]string{"feature_name": "Auth"}}
|
||||
type TemplateCreatePlanInput struct {
|
||||
Template string `json:"template,omitempty"`
|
||||
TemplateSlug string `json:"template_slug,omitempty"`
|
||||
Variables map[string]string `json:"variables,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Activate bool `json:"activate,omitempty"`
|
||||
}
|
||||
|
||||
// out := agentic.TemplateCreatePlanOutput{Success: true, Plan: agentic.PlanCompatibilitySummary{Slug: "auth-feature"}}
|
||||
type TemplateCreatePlanOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Plan PlanCompatibilitySummary `json:"plan"`
|
||||
Commands map[string]string `json:"commands,omitempty"`
|
||||
}
|
||||
|
||||
type planTemplateDefinition struct {
|
||||
Slug string `yaml:"-"`
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Category string `yaml:"category"`
|
||||
Variables map[string]planTemplateVariableDef `yaml:"variables"`
|
||||
Guidelines []string `yaml:"guidelines"`
|
||||
Phases []planTemplatePhaseDef `yaml:"phases"`
|
||||
}
|
||||
|
||||
type planTemplateVariableDef struct {
|
||||
Description string `yaml:"description"`
|
||||
Required bool `yaml:"required"`
|
||||
}
|
||||
|
||||
type planTemplatePhaseDef struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Tasks []any `yaml:"tasks"`
|
||||
}
|
||||
|
||||
// result := c.Action("template.list").Run(ctx, core.NewOptions())
|
||||
func (s *PrepSubsystem) handleTemplateList(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.templateList(ctx, nil, TemplateListInput{
|
||||
Category: optionStringValue(options, "category"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("template.preview").Run(ctx, core.NewOptions(core.Option{Key: "template", Value: "bug-fix"}))
|
||||
func (s *PrepSubsystem) handleTemplatePreview(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.templatePreview(ctx, nil, TemplatePreviewInput{
|
||||
Template: optionStringValue(options, "template"),
|
||||
TemplateSlug: optionStringValue(options, "template_slug", "template-slug", "slug"),
|
||||
Variables: optionStringMapValue(options, "variables"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("template.create_plan").Run(ctx, core.NewOptions(core.Option{Key: "template", Value: "bug-fix"}))
|
||||
func (s *PrepSubsystem) handleTemplateCreatePlan(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.templateCreatePlan(ctx, nil, TemplateCreatePlanInput{
|
||||
Template: optionStringValue(options, "template"),
|
||||
TemplateSlug: optionStringValue(options, "template_slug", "template-slug"),
|
||||
Variables: optionStringMapValue(options, "variables"),
|
||||
Slug: optionStringValue(options, "slug", "plan_slug", "plan-slug"),
|
||||
Title: optionStringValue(options, "title"),
|
||||
Activate: optionBoolValue(options, "activate"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) registerTemplateTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "template_list",
|
||||
Description: "List available plan templates with variables, category, and phase counts.",
|
||||
}, s.templateList)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "template_preview",
|
||||
Description: "Preview a plan template with variable substitution before creating a stored plan.",
|
||||
}, s.templatePreview)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "template_create_plan",
|
||||
Description: "Create a stored plan from an embedded YAML template, with optional activation.",
|
||||
}, s.templateCreatePlan)
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) templateList(_ context.Context, _ *mcp.CallToolRequest, input TemplateListInput) (*mcp.CallToolResult, TemplateListOutput, error) {
|
||||
templates := make([]TemplateSummary, 0, len(lib.ListTasks()))
|
||||
for _, slug := range lib.ListTasks() {
|
||||
definition, err := loadPlanTemplateDefinition(slug, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if input.Category != "" && definition.Category != input.Category {
|
||||
continue
|
||||
}
|
||||
templates = append(templates, templateSummaryFromDefinition(definition))
|
||||
}
|
||||
|
||||
sort.Slice(templates, func(i, j int) bool {
|
||||
return templates[i].Slug < templates[j].Slug
|
||||
})
|
||||
|
||||
return nil, TemplateListOutput{
|
||||
Success: true,
|
||||
Templates: templates,
|
||||
Total: len(templates),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) templatePreview(_ context.Context, _ *mcp.CallToolRequest, input TemplatePreviewInput) (*mcp.CallToolResult, TemplatePreviewOutput, error) {
|
||||
templateName := templateNameValue(input.Template, input.TemplateSlug, input.Slug)
|
||||
if templateName == "" {
|
||||
return nil, TemplatePreviewOutput{}, core.E("templatePreview", "template is required", nil)
|
||||
}
|
||||
|
||||
definition, err := loadPlanTemplateDefinition(templateName, input.Variables)
|
||||
if err != nil {
|
||||
return nil, TemplatePreviewOutput{}, err
|
||||
}
|
||||
|
||||
return nil, TemplatePreviewOutput{
|
||||
Success: true,
|
||||
Template: definition.Slug,
|
||||
Preview: renderPlanMarkdown(definition, ""),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) templateCreatePlan(ctx context.Context, _ *mcp.CallToolRequest, input TemplateCreatePlanInput) (*mcp.CallToolResult, TemplateCreatePlanOutput, error) {
|
||||
templateName := templateNameValue(input.Template, input.TemplateSlug)
|
||||
if templateName == "" {
|
||||
return nil, TemplateCreatePlanOutput{}, core.E("templateCreatePlan", "template is required", nil)
|
||||
}
|
||||
if input.Variables == nil {
|
||||
return nil, TemplateCreatePlanOutput{}, core.E("templateCreatePlan", "variables are required", nil)
|
||||
}
|
||||
|
||||
definition, err := loadPlanTemplateDefinition(templateName, input.Variables)
|
||||
if err != nil {
|
||||
return nil, TemplateCreatePlanOutput{}, err
|
||||
}
|
||||
|
||||
if missing := missingTemplateVariables(definition, input.Variables); len(missing) > 0 {
|
||||
return nil, TemplateCreatePlanOutput{}, core.E("templateCreatePlan", core.Concat("missing required variables: ", core.Join(", ", missing...)), nil)
|
||||
}
|
||||
|
||||
title := core.Trim(input.Title)
|
||||
if title == "" {
|
||||
title = definition.Name
|
||||
}
|
||||
|
||||
contextData := map[string]any{
|
||||
"template": definition.Slug,
|
||||
}
|
||||
if len(input.Variables) > 0 {
|
||||
contextData["variables"] = input.Variables
|
||||
}
|
||||
|
||||
_, created, err := s.planCreate(ctx, nil, PlanCreateInput{
|
||||
Title: title,
|
||||
Slug: input.Slug,
|
||||
Objective: definition.Description,
|
||||
Description: definition.Description,
|
||||
Context: contextData,
|
||||
Phases: templatePlanPhases(definition),
|
||||
Notes: templateGuidelinesNote(definition.Guidelines),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, TemplateCreatePlanOutput{}, err
|
||||
}
|
||||
|
||||
plan, err := readPlan(PlansRoot(), created.ID)
|
||||
if err != nil {
|
||||
return nil, TemplateCreatePlanOutput{}, err
|
||||
}
|
||||
|
||||
if input.Activate {
|
||||
_, updated, updateErr := s.planUpdate(ctx, nil, PlanUpdateInput{
|
||||
Slug: plan.Slug,
|
||||
Status: planCompatibilityInputStatus("active"),
|
||||
})
|
||||
if updateErr != nil {
|
||||
return nil, TemplateCreatePlanOutput{}, updateErr
|
||||
}
|
||||
plan = &updated.Plan
|
||||
}
|
||||
|
||||
return nil, TemplateCreatePlanOutput{
|
||||
Success: true,
|
||||
Plan: planCompatibilitySummary(*plan),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func templateNameValue(values ...string) string {
|
||||
for _, value := range values {
|
||||
if trimmed := core.Trim(value); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadPlanTemplateDefinition(slug string, variables map[string]string) (planTemplateDefinition, error) {
|
||||
result := lib.Task(slug)
|
||||
if !result.OK {
|
||||
err, _ := result.Value.(error)
|
||||
if err == nil {
|
||||
err = core.E("loadPlanTemplateDefinition", core.Concat("template not found: ", slug), nil)
|
||||
}
|
||||
return planTemplateDefinition{}, err
|
||||
}
|
||||
|
||||
content := applyTemplateVariables(result.Value.(string), variables)
|
||||
|
||||
var definition planTemplateDefinition
|
||||
if err := yaml.Unmarshal([]byte(content), &definition); err != nil {
|
||||
return planTemplateDefinition{}, core.E("loadPlanTemplateDefinition", core.Concat("invalid plan template: ", slug), err)
|
||||
}
|
||||
if definition.Name == "" || len(definition.Phases) == 0 {
|
||||
return planTemplateDefinition{}, core.E("loadPlanTemplateDefinition", core.Concat("invalid plan template: ", slug), nil)
|
||||
}
|
||||
|
||||
definition.Slug = slug
|
||||
return definition, nil
|
||||
}
|
||||
|
||||
func applyTemplateVariables(content string, variables map[string]string) string {
|
||||
for key, value := range variables {
|
||||
content = core.Replace(content, core.Concat("{{", key, "}}"), value)
|
||||
content = core.Replace(content, core.Concat("{{ ", key, " }}"), value)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func renderPlanMarkdown(definition planTemplateDefinition, task string) string {
|
||||
plan := core.NewBuilder()
|
||||
plan.WriteString(core.Concat("# ", definition.Name, "\n\n"))
|
||||
if task != "" {
|
||||
plan.WriteString(core.Concat("**Task:** ", task, "\n\n"))
|
||||
}
|
||||
if definition.Description != "" {
|
||||
plan.WriteString(core.Concat(definition.Description, "\n\n"))
|
||||
}
|
||||
|
||||
if len(definition.Guidelines) > 0 {
|
||||
plan.WriteString("## Guidelines\n\n")
|
||||
for _, guideline := range definition.Guidelines {
|
||||
plan.WriteString(core.Concat("- ", guideline, "\n"))
|
||||
}
|
||||
plan.WriteString("\n")
|
||||
}
|
||||
|
||||
for index, phase := range definition.Phases {
|
||||
plan.WriteString(core.Sprintf("## Phase %d: %s\n\n", index+1, phase.Name))
|
||||
if phase.Description != "" {
|
||||
plan.WriteString(core.Concat(phase.Description, "\n\n"))
|
||||
}
|
||||
for _, taskItem := range templatePlanTasks(phase.Tasks) {
|
||||
plan.WriteString(core.Concat("- [ ] ", taskItem.Title, "\n"))
|
||||
}
|
||||
plan.WriteString("\n")
|
||||
}
|
||||
|
||||
return plan.String()
|
||||
}
|
||||
|
||||
func templateSummaryFromDefinition(definition planTemplateDefinition) TemplateSummary {
|
||||
return TemplateSummary{
|
||||
Slug: definition.Slug,
|
||||
Name: definition.Name,
|
||||
Description: definition.Description,
|
||||
Category: definition.Category,
|
||||
PhasesCount: len(definition.Phases),
|
||||
Variables: templateVariableList(definition),
|
||||
}
|
||||
}
|
||||
|
||||
func templateVariableList(definition planTemplateDefinition) []TemplateVariable {
|
||||
if len(definition.Variables) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(definition.Variables))
|
||||
for name := range definition.Variables {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
variables := make([]TemplateVariable, 0, len(names))
|
||||
for _, name := range names {
|
||||
definitionValue := definition.Variables[name]
|
||||
variables = append(variables, TemplateVariable{
|
||||
Name: name,
|
||||
Description: definitionValue.Description,
|
||||
Required: definitionValue.Required,
|
||||
})
|
||||
}
|
||||
return variables
|
||||
}
|
||||
|
||||
func missingTemplateVariables(definition planTemplateDefinition, variables map[string]string) []string {
|
||||
var missing []string
|
||||
for name, variable := range definition.Variables {
|
||||
if !variable.Required {
|
||||
continue
|
||||
}
|
||||
if core.Trim(variables[name]) == "" {
|
||||
missing = append(missing, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(missing)
|
||||
return missing
|
||||
}
|
||||
|
||||
func templatePlanPhases(definition planTemplateDefinition) []Phase {
|
||||
phases := make([]Phase, 0, len(definition.Phases))
|
||||
for index, phaseDefinition := range definition.Phases {
|
||||
phases = append(phases, Phase{
|
||||
Number: index + 1,
|
||||
Name: phaseDefinition.Name,
|
||||
Description: phaseDefinition.Description,
|
||||
Status: "pending",
|
||||
Tasks: templatePlanTasks(phaseDefinition.Tasks),
|
||||
})
|
||||
}
|
||||
return phases
|
||||
}
|
||||
|
||||
func templatePlanTasks(items []any) []PlanTask {
|
||||
tasks := make([]PlanTask, 0, len(items))
|
||||
for index, item := range items {
|
||||
task := templatePlanTask(item, index+1)
|
||||
if task.Title == "" {
|
||||
continue
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
func templatePlanTask(item any, number int) PlanTask {
|
||||
switch value := item.(type) {
|
||||
case string:
|
||||
return PlanTask{
|
||||
ID: core.Sprint(number),
|
||||
Title: core.Trim(value),
|
||||
Status: "pending",
|
||||
}
|
||||
case map[string]any:
|
||||
title := stringValue(value["title"])
|
||||
if title == "" {
|
||||
title = stringValue(value["name"])
|
||||
}
|
||||
status := stringValue(value["status"])
|
||||
if status == "" {
|
||||
status = "pending"
|
||||
}
|
||||
taskID := stringValue(value["id"])
|
||||
if taskID == "" {
|
||||
taskID = core.Sprint(number)
|
||||
}
|
||||
return PlanTask{
|
||||
ID: taskID,
|
||||
Title: title,
|
||||
Description: stringValue(value["description"]),
|
||||
Status: status,
|
||||
Notes: stringValue(value["notes"]),
|
||||
}
|
||||
}
|
||||
return PlanTask{}
|
||||
}
|
||||
|
||||
func templateGuidelinesNote(guidelines []string) string {
|
||||
cleaned := cleanStrings(guidelines)
|
||||
if len(cleaned) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
builder := core.NewBuilder()
|
||||
builder.WriteString("Guidelines:\n")
|
||||
for _, guideline := range cleaned {
|
||||
builder.WriteString(core.Concat("- ", guideline, "\n"))
|
||||
}
|
||||
return core.TrimSuffix(builder.String(), "\n")
|
||||
}
|
||||
20
pkg/agentic/template_example_test.go
Normal file
20
pkg/agentic/template_example_test.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Example_templateVariableList() {
|
||||
variables := templateVariableList(planTemplateDefinition{
|
||||
Variables: map[string]planTemplateVariableDef{
|
||||
"feature_name": {Required: true},
|
||||
"description": {Required: false},
|
||||
},
|
||||
})
|
||||
|
||||
fmt.Println(variables[0].Name, variables[0].Required)
|
||||
fmt.Println(variables[1].Name, variables[1].Required)
|
||||
// Output:
|
||||
// description false
|
||||
// feature_name true
|
||||
}
|
||||
115
pkg/agentic/template_test.go
Normal file
115
pkg/agentic/template_test.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTemplate_HandleTemplateList_Good(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "")
|
||||
result := subsystem.handleTemplateList(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "category", Value: "development"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(TemplateListOutput)
|
||||
require.True(t, ok)
|
||||
assert.True(t, output.Success)
|
||||
assert.NotZero(t, output.Total)
|
||||
|
||||
found := false
|
||||
for _, summary := range output.Templates {
|
||||
assert.Equal(t, "development", summary.Category)
|
||||
if summary.Slug == "bug-fix" {
|
||||
found = true
|
||||
assert.Equal(t, "Bug Fix", summary.Name)
|
||||
assert.NotZero(t, summary.PhasesCount)
|
||||
assert.NotEmpty(t, summary.Variables)
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
func TestTemplate_HandleTemplatePreview_Good(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "")
|
||||
result := subsystem.handleTemplatePreview(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "template", Value: "new-feature"},
|
||||
core.Option{Key: "variables", Value: `{"feature_name":"Authentication"}`},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(TemplatePreviewOutput)
|
||||
require.True(t, ok)
|
||||
assert.True(t, output.Success)
|
||||
assert.Equal(t, "new-feature", output.Template)
|
||||
assert.Contains(t, output.Preview, "Authentication")
|
||||
assert.Contains(t, output.Preview, "Phase 1")
|
||||
}
|
||||
|
||||
func TestTemplate_HandleTemplatePreview_Bad(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "")
|
||||
result := subsystem.handleTemplatePreview(context.Background(), core.NewOptions())
|
||||
assert.False(t, result.OK)
|
||||
}
|
||||
|
||||
func TestTemplate_HandleTemplatePreview_Ugly_MissingVariables(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "")
|
||||
result := subsystem.handleTemplatePreview(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "template", Value: "new-feature"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(TemplatePreviewOutput)
|
||||
require.True(t, ok)
|
||||
assert.Contains(t, output.Preview, "{{ feature_name }}")
|
||||
}
|
||||
|
||||
func TestTemplate_HandleTemplateCreatePlan_Good(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "")
|
||||
result := subsystem.handleTemplateCreatePlan(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "template", Value: "new-feature"},
|
||||
core.Option{Key: "variables", Value: `{"feature_name":"Authentication"}`},
|
||||
core.Option{Key: "title", Value: "Authentication Rollout"},
|
||||
core.Option{Key: "plan_slug", Value: "auth-rollout"},
|
||||
core.Option{Key: "activate", Value: true},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(TemplateCreatePlanOutput)
|
||||
require.True(t, ok)
|
||||
assert.True(t, output.Success)
|
||||
assert.Equal(t, "auth-rollout", output.Plan.Slug)
|
||||
assert.Equal(t, "active", output.Plan.Status)
|
||||
|
||||
plan, err := readPlan(PlansRoot(), "auth-rollout")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Authentication Rollout", plan.Title)
|
||||
assert.Equal(t, "in_progress", plan.Status)
|
||||
require.NotEmpty(t, plan.Phases)
|
||||
require.NotEmpty(t, plan.Phases[0].Tasks)
|
||||
assert.Equal(t, "pending", plan.Phases[0].Tasks[0].Status)
|
||||
assert.Equal(t, "new-feature", stringValue(plan.Context["template"]))
|
||||
}
|
||||
|
||||
func TestTemplate_HandleTemplateCreatePlan_Bad(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "")
|
||||
result := subsystem.handleTemplateCreatePlan(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "template", Value: "new-feature"},
|
||||
))
|
||||
assert.False(t, result.OK)
|
||||
}
|
||||
|
||||
func TestTemplate_HandleTemplateCreatePlan_Ugly_UnknownTemplate(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "")
|
||||
result := subsystem.handleTemplateCreatePlan(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "template", Value: "unknown-template"},
|
||||
core.Option{Key: "variables", Value: `{"feature_name":"Authentication"}`},
|
||||
))
|
||||
assert.False(t, result.OK)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue