Three compounding bugs prevented tool calling with llama.cpp:
- Stream parser set argsComplete on partial JSON (e.g. "{"), dropping
subsequent argument deltas — fix: use json.Valid to detect completeness
- Missing tool_choice default — llama.cpp needs explicit "auto" to
activate its GBNF grammar constraint; now set when tools are present
- Tool names in history used internal format (fs.ls) while definitions
used API format (fs_ls) — now re-sanitized in translateMessage
Additional changes:
- Disable SDK retries for local providers (500s are deterministic)
- Dynamic capability probing via /props (llama.cpp) and /api/show
(Ollama), replacing hardcoded model prefix list
- Engine respects forced arm ToolUse capability when router is active
- Bundled /init skill with Go template blocks, context-aware for local
vs cloud models, deduplication rules against CLAUDE.md
- Tool result compaction for local models — previous round results
replaced with size markers to stay within small context windows
- Text-only fallback when tool-parse errors occur on local models
- "text-only" TUI indicator when model lacks tool support
- Session ResetError for retry after stream failures
- AllowedTools per-turn filtering in engine buildRequest
239 lines
5.3 KiB
Go
239 lines
5.3 KiB
Go
package skill
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestBundledSkills_NotEmpty(t *testing.T) {
|
|
skills, err := BundledSkills()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(skills) == 0 {
|
|
t.Error("expected at least one bundled skill")
|
|
}
|
|
}
|
|
|
|
func TestBundledSkills_BatchExists(t *testing.T) {
|
|
skills, err := BundledSkills()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
var batch *Skill
|
|
for _, s := range skills {
|
|
if s.Frontmatter.Name == "batch" {
|
|
batch = s
|
|
break
|
|
}
|
|
}
|
|
if batch == nil {
|
|
t.Fatal("batch skill not found in bundled skills")
|
|
}
|
|
if batch.Frontmatter.Description == "" {
|
|
t.Error("batch skill missing description")
|
|
}
|
|
if batch.Body == "" {
|
|
t.Error("batch skill has empty body")
|
|
}
|
|
if batch.Source != "bundled" {
|
|
t.Errorf("batch skill source = %q, want %q", batch.Source, "bundled")
|
|
}
|
|
}
|
|
|
|
func TestBundledSkills_AllParseClean(t *testing.T) {
|
|
skills, err := BundledSkills()
|
|
if err != nil {
|
|
t.Fatalf("BundledSkills() error: %v", err)
|
|
}
|
|
for _, s := range skills {
|
|
if s.Frontmatter.Name == "" {
|
|
t.Errorf("bundled skill has empty name: %+v", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBundledSkills_InitExists(t *testing.T) {
|
|
skills, err := BundledSkills()
|
|
if err != nil {
|
|
t.Fatalf("BundledSkills() error: %v", err)
|
|
}
|
|
var init *Skill
|
|
for _, s := range skills {
|
|
if s.Frontmatter.Name == "init" {
|
|
init = s
|
|
break
|
|
}
|
|
}
|
|
if init == nil {
|
|
t.Fatal("init skill not found in bundled skills")
|
|
}
|
|
if init.Frontmatter.Description == "" {
|
|
t.Error("init skill missing description")
|
|
}
|
|
if init.Body == "" {
|
|
t.Error("init skill has empty body")
|
|
}
|
|
}
|
|
|
|
func TestBundledSkills_InitRender_Local(t *testing.T) {
|
|
skills, err := BundledSkills()
|
|
if err != nil {
|
|
t.Fatalf("BundledSkills() error: %v", err)
|
|
}
|
|
var init *Skill
|
|
for _, s := range skills {
|
|
if s.Frontmatter.Name == "init" {
|
|
init = s
|
|
break
|
|
}
|
|
}
|
|
if init == nil {
|
|
t.Fatal("init skill not found")
|
|
}
|
|
|
|
rendered, err := init.Render(TemplateData{
|
|
ProjectRoot: "/tmp/myproject",
|
|
Local: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Render() error: %v", err)
|
|
}
|
|
|
|
// Local mode should use sequential fs_* tools, not spawn_elfs for orchestration
|
|
if !contains(rendered, "fs_ls") {
|
|
t.Error("local render should contain fs_ls")
|
|
}
|
|
if !contains(rendered, "fs_read") {
|
|
t.Error("local render should contain fs_read")
|
|
}
|
|
if contains(rendered, "Use spawn_elfs") {
|
|
t.Error("local render should NOT instruct to use spawn_elfs")
|
|
}
|
|
if !contains(rendered, "/tmp/myproject") {
|
|
t.Error("local render should contain ProjectRoot")
|
|
}
|
|
}
|
|
|
|
func TestBundledSkills_InitRender_Cloud(t *testing.T) {
|
|
skills, err := BundledSkills()
|
|
if err != nil {
|
|
t.Fatalf("BundledSkills() error: %v", err)
|
|
}
|
|
var init *Skill
|
|
for _, s := range skills {
|
|
if s.Frontmatter.Name == "init" {
|
|
init = s
|
|
break
|
|
}
|
|
}
|
|
if init == nil {
|
|
t.Fatal("init skill not found")
|
|
}
|
|
|
|
rendered, err := init.Render(TemplateData{
|
|
ProjectRoot: "/tmp/myproject",
|
|
Local: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Render() error: %v", err)
|
|
}
|
|
|
|
// Cloud mode should use spawn_elfs
|
|
if !contains(rendered, "spawn_elfs") {
|
|
t.Error("cloud render should contain spawn_elfs")
|
|
}
|
|
if !contains(rendered, "Elf 1") {
|
|
t.Error("cloud render should contain Elf 1")
|
|
}
|
|
if !contains(rendered, "/tmp/myproject") {
|
|
t.Error("cloud render should contain ProjectRoot")
|
|
}
|
|
if !contains(rendered, "creating") {
|
|
t.Error("cloud render (no Args) should say 'creating'")
|
|
}
|
|
}
|
|
|
|
func TestBundledSkills_InitRender_CloudUpdate(t *testing.T) {
|
|
skills, err := BundledSkills()
|
|
if err != nil {
|
|
t.Fatalf("BundledSkills() error: %v", err)
|
|
}
|
|
var init *Skill
|
|
for _, s := range skills {
|
|
if s.Frontmatter.Name == "init" {
|
|
init = s
|
|
break
|
|
}
|
|
}
|
|
if init == nil {
|
|
t.Fatal("init skill not found")
|
|
}
|
|
|
|
rendered, err := init.Render(TemplateData{
|
|
ProjectRoot: "/tmp/myproject",
|
|
Args: "/tmp/myproject/AGENTS.md",
|
|
Local: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Render() error: %v", err)
|
|
}
|
|
|
|
// Cloud update mode should have Elf 4 for review
|
|
if !contains(rendered, "Elf 4") {
|
|
t.Error("cloud update render should contain Elf 4")
|
|
}
|
|
if !contains(rendered, "updating") {
|
|
t.Error("cloud update render should say 'updating'")
|
|
}
|
|
if !contains(rendered, "/tmp/myproject/AGENTS.md") {
|
|
t.Error("cloud update render should contain existing path")
|
|
}
|
|
}
|
|
|
|
func TestBundledSkills_InitRender_LocalUpdate(t *testing.T) {
|
|
skills, err := BundledSkills()
|
|
if err != nil {
|
|
t.Fatalf("BundledSkills() error: %v", err)
|
|
}
|
|
var init *Skill
|
|
for _, s := range skills {
|
|
if s.Frontmatter.Name == "init" {
|
|
init = s
|
|
break
|
|
}
|
|
}
|
|
if init == nil {
|
|
t.Fatal("init skill not found")
|
|
}
|
|
|
|
rendered, err := init.Render(TemplateData{
|
|
ProjectRoot: "/tmp/myproject",
|
|
Args: "/tmp/myproject/AGENTS.md",
|
|
Local: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Render() error: %v", err)
|
|
}
|
|
|
|
// Local update should mention existing file
|
|
if !contains(rendered, "existing AGENTS.md") {
|
|
t.Error("local update render should mention existing file")
|
|
}
|
|
if contains(rendered, "Use spawn_elfs") {
|
|
t.Error("local update render should NOT instruct to use spawn_elfs")
|
|
}
|
|
}
|
|
|
|
func contains(s, substr string) bool {
|
|
return len(s) > 0 && len(substr) > 0 && stringContains(s, substr)
|
|
}
|
|
|
|
func stringContains(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|