Merge pull request #11646 from KHG420/codex/studio-scenario-skill-frontmatter

fix(studio): keep scenario skill frontmatter valid YAML
This commit is contained in:
YHH authored and GitHub committed 2026-10-01 20:45:18 -07:00
commit 580339980f
7 files changed
+84 -18

No files matched your search

@@ -1,8 +1,8 @@
---
name: api-notes
description: Turn a user-selected local OpenAPI JSON document into a Markdown guide with source pointers and an explicit review record.
owner: @esengine
backup: @SivanCola
owner: "@esengine"
backup: "@SivanCola"
status: active
reviewed: 2026-10-01
---
@@ -1,8 +1,8 @@
---
name: frontend-page
description: Build a frontend from selected requirements and hand it off with observed browser acceptance and remaining issues.
owner: @esengine
backup: @SivanCola
owner: "@esengine"
backup: "@SivanCola"
status: active
reviewed: 2026-10-01
---
@@ -1,8 +1,8 @@
---
name: issue-fix
description: Reproduce a repository issue, make the smallest correct fix, and draft a pull request from observed evidence.
owner: @esengine
backup: @SivanCola
owner: "@esengine"
backup: "@SivanCola"
status: active
reviewed: 2026-10-01
---
@@ -1,8 +1,8 @@
---
name: release-note
description: Draft a release note from user-selected changes, citing only verified behavior.
owner: @esengine
backup: @SivanCola
owner: "@esengine"
backup: "@SivanCola"
status: active
reviewed: 2026-09-29
---
@@ -0,0 +1,53 @@
package skill
import (
"os"
"path/filepath"
"testing"
"reasonix/internal/base/testenv"
)
func TestScenarioExamplesDiscoverWithoutInvalidFrontmatter(t *testing.T) {
for _, tc := range []struct {
plugin string
name string
}{
{plugin: "release-note-kit", name: "release-note"},
{plugin: "issue-fix-kit", name: "issue-fix"},
{plugin: "frontend-page-kit", name: "frontend-page"},
{plugin: "api-notes-kit", name: "api-notes"},
} {
t.Run(tc.plugin, func(t *testing.T) {
root := testenv.TempDir(t)
source := filepath.Join("..", "..", "..", "examples", tc.plugin)
if err := os.CopyFS(root, os.DirFS(source)); err != nil {
t.Fatal(err)
}
skillsRoot := filepath.Join(root, "skills")
store := New(Options{
HomeDir: testenv.TempDir(t),
CustomPaths: []string{skillsRoot},
PluginPaths: map[string][]string{skillsRoot: {tc.plugin}},
DisableBuiltins: true,
})
skills := store.SlashList()
if len(skills) != 1 {
t.Fatalf("discovered skills = %v, want one scenario skill", skills)
}
sk := skills[0]
if sk.Name != tc.name || sk.SlashName() != tc.plugin+":"+tc.name {
t.Fatalf("skill identity = %q / %q", sk.Name, sk.SlashName())
}
if sk.Description == "" || sk.Body == "" {
t.Fatal("scenario skill lost its description or instructions")
}
if len(sk.Invalid) != 0 {
t.Fatalf("scenario skill frontmatter diagnostics = %v", sk.Invalid)
}
if err := store.ValidateInvocation(sk); err != nil {
t.Fatalf("scenario skill invocation: %v", err)
}
})
}
}
+10 -1
View File
@@ -12,6 +12,8 @@ import (
"slices"
"strings"
"unicode"
"gopkg.in/yaml.v3"
)
type docClass int
@@ -168,7 +170,14 @@ func docMetadata(lines []string) (map[string]string, bool) {
return meta, true
}
if key, value, ok := strings.Cut(line, ":"); ok {
meta[strings.TrimSpace(key)] = strings.TrimSpace(value)
value = strings.TrimSpace(value)
if strings.HasPrefix(value, `"`) || strings.HasPrefix(value, "'") {
var scalar string
if err := yaml.Unmarshal([]byte(value), &scalar); err == nil {
value = scalar
}
}
meta[strings.TrimSpace(key)] = value
}
}
return nil, false
+13 -9
View File
@@ -87,21 +87,25 @@ func TestADocumentNamesItsOwnersAsCodeOwnersDoes(t *testing.T) {
return "---\nowner: " + owner + "\nbackup: " + backup + "\nstatus: active\nreviewed: 2026-09-17\n---\n# T\n"
}
root := docRepo(t, map[string]string{
".github/CODEOWNERS": "* @a @b\n/docs/ @b @a\n",
"docs/good.md": header("@b", "@a"),
"docs/swapped.md": header("@a", "@b"),
"docs/bare.md": "# No header\n",
"top.md": header("@a", "@b"),
"README.md": "# Landing page, owned through CODEOWNERS only\n",
"REASONIX.md": "# Read by a model verbatim\n",
".github/CODEOWNERS": "* @a @b\n/docs/ @b @a\n",
"docs/good.md": header("@b", "@a"),
"docs/double.md": header(`"@b"`, `"@a"`),
"docs/single.md": header("'@b'", "'@a'"),
"docs/swapped.md": header("@a", "@b"),
"docs/quoted-swap.md": header(`"@a"`, `"@b"`),
"docs/broken.md": header(`"@b`, `"@a"`),
"docs/bare.md": "# No header\n",
"top.md": header("@a", "@b"),
"README.md": "# Landing page, owned through CODEOWNERS only\n",
"REASONIX.md": "# Read by a model verbatim\n",
})
got := ruleFindings(checkDocs(root), ruleDocOwner)
flagged := map[string]string{}
for _, f := range got {
flagged[f.File] = f.Msg
}
if len(flagged) != 2 || flagged["docs/swapped.md"] == "" || flagged["docs/bare.md"] == "" {
t.Fatalf("flagged = %v, want exactly the swapped owners and the missing header", flagged)
if len(flagged) != 4 || flagged["docs/swapped.md"] == "" || flagged["docs/quoted-swap.md"] == "" || flagged["docs/broken.md"] == "" || flagged["docs/bare.md"] == "" {
t.Fatalf("flagged = %v, want swapped owners, malformed quotes and the missing header", flagged)
}
if !strings.Contains(flagged["docs/swapped.md"], "CODEOWNERS line 2") {
t.Fatalf("the finding must point at the rule it disagrees with: %s", flagged["docs/swapped.md"])