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) } }