Initial commit: CarrotAssistant backend (Go + Gin + SQLite, OpenAI-compatible agent runtime)
This commit is contained in:
95
internal/skill/skill_test.go
Normal file
95
internal/skill/skill_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user