96 lines
2.9 KiB
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, ¶ms); 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)
|
|
}
|
|
}
|