| 1 | package skill_test |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "regexp" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "reasonix/internal/skill" |
| 11 | ) |
| 12 | |
| 13 | func TestEmbeddedGuideReferencesAreReadableOnDemand(t *testing.T) { |
| 14 | store := skill.New(skill.Options{HomeDir: t.TempDir()}) |
| 15 | loader := skill.NewReadSkillTool(store) |
| 16 | root, err := loader.Execute(context.Background(), json.RawMessage(`{"name":"reasonix-guide"}`)) |
| 17 | if err != nil { |
| 18 | t.Fatal(err) |
| 19 | } |
| 20 | links := regexp.MustCompile(`\]\((references/[^)]+\.md)\)`).FindAllStringSubmatch(root, -1) |
| 21 | if len(links) != 6 { |
| 22 | t.Fatalf("reference routes = %d, want 6", len(links)) |
| 23 | } |
| 24 | for _, link := range links { |
| 25 | args, _ := json.Marshal(map[string]string{"name": "reasonix-guide", "reference": link[1]}) |
| 26 | page, err := loader.Execute(context.Background(), args) |
| 27 | if err != nil || len(page) == 0 { |
| 28 | t.Fatalf("%s: %v", link[1], err) |
| 29 | } |
| 30 | if page == root || strings.Contains(root, strings.SplitN(page, "\n\n", 2)[1]) { |
| 31 | t.Fatalf("%s was not loaded separately", link[1]) |
| 32 | } |
| 33 | again, _ := loader.Execute(context.Background(), args) |
| 34 | if again != page { |
| 35 | t.Fatalf("%s is not stable", link[1]) |
| 36 | } |
| 37 | } |
| 38 | guide, _ := store.Read("reasonix-guide") |
| 39 | if !strings.Contains(skill.CatalogBlock(store.List()), guide.Description) { |
| 40 | t.Fatal("built-in guide trigger description was clipped") |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | func TestEmbeddedReferenceRejectsTraversalAndNonReferenceFiles(t *testing.T) { |
| 45 | loader := skill.NewReadSkillTool(skill.New(skill.Options{HomeDir: t.TempDir()})) |
| 46 | for _, ref := range []string{ |
| 47 | "../SKILL.md", "references/../SKILL.md", "/etc/passwd", |
| 48 | "references/../../other/SKILL.md", "references\\skills.md", |
| 49 | "SKILL.md", "references/missing.md", "references/skills.md/extra", |
| 50 | } { |
| 51 | t.Run(ref, func(t *testing.T) { |
| 52 | args, _ := json.Marshal(map[string]string{"name": "reasonix-guide", "reference": ref}) |
| 53 | if out, err := loader.Execute(context.Background(), args); err == nil || out != "" { |
| 54 | t.Fatalf("reference %q allowed: %q, %v", ref, out, err) |
| 55 | } |
| 56 | }) |
| 57 | } |
| 58 | if _, err := loader.Execute(context.Background(), json.RawMessage(`{"name":"reasonix-guide","reference":"references/skills.md","arguments":"task"}`)); err == nil { |
| 59 | t.Fatal("ambiguous reference/task request accepted") |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func TestEmbeddedReferenceHonorsOverrideAndDisabledSkill(t *testing.T) { |
| 64 | for _, disabled := range []bool{false, true} { |
| 65 | t.Run(map[bool]string{false: "override", true: "disabled"}[disabled], func(t *testing.T) { |
| 66 | opts := skill.Options{HomeDir: t.TempDir(), ProjectRoot: t.TempDir()} |
| 67 | if disabled { |
| 68 | opts.DisabledNames = []string{"reasonix-guide"} |
| 69 | } |
| 70 | store := skill.New(opts) |
| 71 | if !disabled { |
| 72 | _, err := store.CreateWithContent("reasonix-guide", skill.ScopeProject, |
| 73 | "---\nname: reasonix-guide\ndescription: Project guide\n---\nLocal guidance") |
| 74 | if err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | } |
| 78 | loader := skill.NewReadSkillTool(skill.New(opts)) |
| 79 | out, err := loader.Execute(context.Background(), json.RawMessage(`{"name":"reasonix-guide","reference":"references/skills.md"}`)) |
| 80 | if err == nil || out != "" { |
| 81 | t.Fatal("embedded page bypassed the winning/disabled skill") |
| 82 | } |
| 83 | if !disabled { |
| 84 | out, err = loader.Execute(context.Background(), json.RawMessage(`{"name":"reasonix-guide"}`)) |
| 85 | if err != nil || !strings.Contains(out, "Local guidance") { |
| 86 | t.Fatal("legacy body read no longer uses the project override") |
| 87 | } |
| 88 | } |
| 89 | }) |
| 90 | } |
| 91 | } |
| 92 |