Files
backend/internal/skill/skill_test.go

96 lines
2.9 KiB
Go

package skill
import (
"encoding/json"
"path/filepath"
"testing"
)
// TestRoundTrip exercises the markdown <-> struct round-trip that is the
// foundation of skill storage. A skill with a JSON-Schema parameters block
// must marshal to YAML frontmatter as a nested map (not a byte array) and
// parse back to an equivalent struct. This guards the two regressions caught
// during Phase 2: parameters serialising as integers, and URL braces being
// percent-encoded.
func TestRoundTrip(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "app", "demo.md")
in := &Skill{
Name: "demo",
Description: "demo skill",
Version: 2,
AppSlug: "app",
Permissions: Permissions{DefaultMode: "readonly", RequireConfirmation: []string{"write"}},
Tools: []Tool{{
Name: "search",
Description: "search things",
Parameters: jsonObject(`{"type":"object","properties":{"q":{"type":"string"}},"required":["q"]}`),
Endpoint: Endpoint{
Method: "GET",
URL: "https://example.com/api/items/{id}",
Path: map[string]string{"id": "{{id}}"},
Query: map[string]string{"q": "{{q}}"},
},
}},
SystemPrompt: "你是一个助手。",
}
// Write through the manager (which delegates to Marshal + Write), then
// read back with the package-level Load to verify the on-disk form.
mgr := NewManager(dir)
if _, err := mgr.Write("app", "demo", in); err != nil {
t.Fatalf("write: %v", err)
}
got, err := Load(path)
if err != nil {
t.Fatalf("load: %v", err)
}
if got.Name != in.Name || got.SystemPrompt != in.SystemPrompt {
t.Fatalf("name/prompt mismatch: %+v", got)
}
if len(got.Tools) != 1 || got.Tools[0].Name != "search" {
t.Fatalf("tools mismatch: %+v", got.Tools)
}
if got.Tools[0].Endpoint.URL != "https://example.com/api/items/{id}" {
t.Fatalf("url braces lost: %s", got.Tools[0].Endpoint.URL)
}
// Parameters must remain a JSON object with the q property.
var params map[string]any
if err := json.Unmarshal(got.Tools[0].Parameters, &params); err != nil {
t.Fatalf("parameters not valid JSON: %v", err)
}
props, _ := params["properties"].(map[string]any)
if _, ok := props["q"]; !ok {
t.Fatalf("parameters lost q: %v", params)
}
}
// TestParseOpenAPIPathParams confirms the OpenAPI importer keeps {id} braces
// in the URL (regression: net/url was percent-encoding them to %7Bid%7D).
func TestParseOpenAPIPathParams(t *testing.T) {
spec := []byte(`
openapi: 3.0.0
info: { title: t }
paths:
/posts/{id}:
get:
operationId: get_post
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
`)
cts, err := ParseOpenAPI(spec, "https://x.com")
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(cts) != 1 {
t.Fatalf("want 1 candidate, got %d", len(cts))
}
want := "https://x.com/posts/{id}"
if cts[0].Endpoint.URL != want {
t.Fatalf("url = %s, want %s", cts[0].Endpoint.URL, want)
}
}