mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
68d798d2f4
- Add internal/fences package for detecting markdown code regions (fenced blocks and inline code spans) with ReplaceOutside/StripCode - SubstituteArgs, HasArgPlaceholders, RequiredArgs now skip $ placeholders inside ``` fences and `inline` code spans - ProcessFileAttachments skips @file tokens inside code regions - Add $+ placeholder: expands like $@ but requires at least 1 argument - Add RequiredArgs() method; expandPromptTemplate validates arg count and re-populates input on failure instead of submitting - Update feature-request, file-issue, new-prompt to use $+
333 lines
8.9 KiB
Go
333 lines
8.9 KiB
Go
package prompts
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestParseCommandArgs(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected []string
|
|
}{
|
|
{"", []string{}},
|
|
{"hello", []string{"hello"}},
|
|
{"hello world", []string{"hello", "world"}},
|
|
{`"hello world"`, []string{"hello world"}},
|
|
{`'hello world'`, []string{"hello world"}},
|
|
{`hello "world foo" bar`, []string{"hello", "world foo", "bar"}},
|
|
{`hello 'world foo' bar`, []string{"hello", "world foo", "bar"}},
|
|
{`hello \"world\"`, []string{"hello", `"world"`}},
|
|
{`hello \\world`, []string{"hello", `\world`}},
|
|
{` hello world `, []string{"hello", "world"}},
|
|
{`Button "onClick handler" "disabled support"`, []string{"Button", "onClick handler", "disabled support"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
got := ParseCommandArgs(tt.input)
|
|
if len(got) != len(tt.expected) {
|
|
t.Errorf("ParseCommandArgs(%q) = %v, want %v", tt.input, got, tt.expected)
|
|
return
|
|
}
|
|
for i := range got {
|
|
if got[i] != tt.expected[i] {
|
|
t.Errorf("ParseCommandArgs(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.expected[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSubstituteArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
args []string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "no placeholders",
|
|
content: "Hello world",
|
|
args: []string{},
|
|
expected: "Hello world",
|
|
},
|
|
{
|
|
name: "positional $1",
|
|
content: "Hello $1",
|
|
args: []string{"world"},
|
|
expected: "Hello world",
|
|
},
|
|
{
|
|
name: "positional $1 $2",
|
|
content: "$1 and $2",
|
|
args: []string{"first", "second"},
|
|
expected: "first and second",
|
|
},
|
|
{
|
|
name: "missing arg",
|
|
content: "Hello $1 and $2",
|
|
args: []string{"world"},
|
|
expected: "Hello world and ",
|
|
},
|
|
{
|
|
name: "$@ wildcard",
|
|
content: "Args: $@",
|
|
args: []string{"a", "b", "c"},
|
|
expected: "Args: a b c",
|
|
},
|
|
{
|
|
name: "$ARGUMENTS wildcard",
|
|
content: "Args: $ARGUMENTS",
|
|
args: []string{"a", "b", "c"},
|
|
expected: "Args: a b c",
|
|
},
|
|
{
|
|
name: "${@} all args",
|
|
content: "Args: ${@}",
|
|
args: []string{"a", "b", "c"},
|
|
expected: "Args: a b c",
|
|
},
|
|
{
|
|
name: "${@:2} slice from index 2",
|
|
content: "Rest: ${@:2}",
|
|
args: []string{"a", "b", "c", "d"},
|
|
expected: "Rest: b c d",
|
|
},
|
|
{
|
|
name: "${@:1:2} slice with length",
|
|
content: "First two: ${@:1:2}",
|
|
args: []string{"a", "b", "c", "d"},
|
|
expected: "First two: a b",
|
|
},
|
|
{
|
|
name: "${@:0} from start",
|
|
content: "All: ${@:0}",
|
|
args: []string{"a", "b", "c"},
|
|
expected: "All: a b c",
|
|
},
|
|
{
|
|
name: "${@:3:1} single arg",
|
|
content: "Third: ${@:3:1}",
|
|
args: []string{"a", "b", "c", "d"},
|
|
expected: "Third: c",
|
|
},
|
|
{
|
|
name: "combined placeholders",
|
|
content: "Create $1 with features: $ARGUMENTS",
|
|
args: []string{"Button", "onClick", "disabled"},
|
|
expected: "Create Button with features: Button onClick disabled",
|
|
},
|
|
{
|
|
name: "slice beyond bounds",
|
|
content: "${@:10}",
|
|
args: []string{"a", "b"},
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "empty args with wildcard",
|
|
content: "Args: $@",
|
|
args: []string{},
|
|
expected: "Args: ",
|
|
},
|
|
{
|
|
name: "$1 inside code block preserved",
|
|
content: "Use $1 here\n```bash\necho $1\n```\ndone",
|
|
args: []string{"foo"},
|
|
expected: "Use foo here\n```bash\necho $1\n```\ndone",
|
|
},
|
|
{
|
|
name: "$@ inside code block preserved",
|
|
content: "Run $@\n```\necho $@\n```\n",
|
|
args: []string{"a", "b"},
|
|
expected: "Run a b\n```\necho $@\n```\n",
|
|
},
|
|
{
|
|
name: "all placeholders inside code block",
|
|
content: "Prompt\n```\n$1 $2 $@\n```\n",
|
|
args: []string{"x"},
|
|
expected: "Prompt\n```\n$1 $2 $@\n```\n",
|
|
},
|
|
{
|
|
name: "$1 inside inline code preserved",
|
|
content: "Use `$1` here and $1 outside",
|
|
args: []string{"foo"},
|
|
expected: "Use `$1` here and foo outside",
|
|
},
|
|
{
|
|
name: "$+ required variadic",
|
|
content: "Args: $+",
|
|
args: []string{"a", "b", "c"},
|
|
expected: "Args: a b c",
|
|
},
|
|
{
|
|
name: "$+ with empty args",
|
|
content: "Args: $+",
|
|
args: []string{},
|
|
expected: "Args: ",
|
|
},
|
|
{
|
|
name: "all placeholders in inline code",
|
|
content: "Use `$1` and `$@` for args",
|
|
args: []string{"x"},
|
|
expected: "Use `$1` and `$@` for args",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := SubstituteArgs(tt.content, tt.args)
|
|
if got != tt.expected {
|
|
t.Errorf("SubstituteArgs(%q, %v) = %q, want %q", tt.content, tt.args, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseFrontmatter(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
wantDesc string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "simple description",
|
|
content: "description: Review code\n",
|
|
wantDesc: "Review code",
|
|
},
|
|
{
|
|
name: "empty",
|
|
content: "",
|
|
wantDesc: "",
|
|
},
|
|
{
|
|
name: "invalid yaml",
|
|
content: "description: [unclosed",
|
|
wantDesc: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fm, err := ParseFrontmatter(tt.content)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ParseFrontmatter() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
if fm.Description != tt.wantDesc {
|
|
t.Errorf("ParseFrontmatter() Description = %q, want %q", fm.Description, tt.wantDesc)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPromptTemplateExpand(t *testing.T) {
|
|
tpl := &PromptTemplate{
|
|
Name: "component",
|
|
Description: "Create a component",
|
|
Content: "Create a React component named $1 with features: $ARGUMENTS",
|
|
}
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
input: "Button",
|
|
expected: "Create a React component named Button with features: Button",
|
|
},
|
|
{
|
|
input: `Button "onClick handler"`,
|
|
expected: "Create a React component named Button with features: Button onClick handler",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
got := tpl.Expand(tt.input)
|
|
if got != tt.expected {
|
|
t.Errorf("Expand(%q) = %q, want %q", tt.input, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHasArgPlaceholders(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
want bool
|
|
}{
|
|
{"no placeholders", "Just a plain prompt with no args", false},
|
|
{"$1 placeholder", "Create a $1 component", true},
|
|
{"$@ placeholder", "Run with args: $@", true},
|
|
{"$ARGUMENTS placeholder", "Features: $ARGUMENTS", true},
|
|
{"${1} placeholder", "Name: ${1}", true},
|
|
{"${ARGUMENTS} placeholder", "All: ${ARGUMENTS}", true},
|
|
{"${@:1} placeholder", "Rest: ${@:1}", true},
|
|
{"${@:1:2} placeholder", "Slice: ${@:1:2}", true},
|
|
{"dollar in text", "Cost is one hundred dollars", false},
|
|
{"empty content", "", false},
|
|
{"$1 inside code block only", "Prompt\n```\necho $1\n```\n", false},
|
|
{"$1 outside and inside code block", "Use $1 here\n```\necho $1\n```\n", true},
|
|
{"$@ inside code block only", "Prompt\n```bash\necho $@\n```\n", false},
|
|
{"$+ placeholder", "Run with args: $+", true},
|
|
{"$+ inside inline code only", "Use `$+` for required args", false},
|
|
{"$1 inside inline code only", "Use `$1` for positional args", false},
|
|
{"$1 outside and in inline code", "Create $1 (see `$1` syntax)", true},
|
|
{"$@ outside $1 in inline code", "Run $@ with `$1` syntax", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tpl := &PromptTemplate{Content: tt.content}
|
|
if got := tpl.HasArgPlaceholders(); got != tt.want {
|
|
t.Errorf("HasArgPlaceholders() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRequiredArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
want int
|
|
}{
|
|
{"no placeholders", "Just a plain prompt", 0},
|
|
{"$1 only", "Create a $1 component", 1},
|
|
{"$1 and $2", "Create $1 with $2", 2},
|
|
{"$3 skipping $2", "Use $1 and $3", 3},
|
|
{"${1} braced", "Name: ${1}", 1},
|
|
{"${2} braced", "Name: ${1} Desc: ${2}", 2},
|
|
{"$@ only", "Run with: $@", 0},
|
|
{"$ARGUMENTS only", "Features: $ARGUMENTS", 0},
|
|
{"${ARGUMENTS} only", "All: ${ARGUMENTS}", 0},
|
|
{"$1 and $@", "Create $1 with extras: $@", 1},
|
|
{"${@:1} slice only", "Rest: ${@:1}", 0},
|
|
{"${@:1:2} slice only", "Slice: ${@:1:2}", 0},
|
|
{"mixed $1 $2 and $@", "Create $1 named $2: $@", 2},
|
|
{"empty content", "", 0},
|
|
{"$2 inside code block only", "Prompt\n```\n$1 $2\n```\n", 0},
|
|
{"$1 outside $2 inside code block", "Use $1\n```\n$2 inside\n```\n", 1},
|
|
{"$+ only", "Run with: $+", 1},
|
|
{"$+ and $2", "Create $2 with: $+", 2},
|
|
{"$+ inside inline code only", "Use `$+` for required args", 0},
|
|
{"$1 and $2 in inline code only", "Use `$1` and `$2` for args", 0},
|
|
{"$1 outside $2 in inline code", "Create $1 (see `$2`)", 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tpl := &PromptTemplate{Content: tt.content}
|
|
if got := tpl.RequiredArgs(); got != tt.want {
|
|
t.Errorf("RequiredArgs() = %d, want %d", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|