Merge branch 'main' into codex/ci-pr-release-compatibility
This commit is contained in:
+623
-41
File diff suppressed because it is too large
Load Diff
@@ -323,7 +323,7 @@ def get_reference_file(product: str) -> str:
|
||||
'calendar': 'calendar.md',
|
||||
'chat': 'chat.md',
|
||||
'contact': 'contact.md',
|
||||
'dev': 'skills/multi/dingtalk-dev/SKILL.md',
|
||||
'dev': 'skills/multi/dingtalk-misc/references/devapp.md',
|
||||
'ding': 'ding.md',
|
||||
'doc': 'doc.md',
|
||||
'drive': 'drive.md',
|
||||
|
||||
@@ -488,7 +488,7 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
|
||||
"needs.lint.outputs.platform_sensitive == 'true'",
|
||||
`COVERAGE_TARGET: "100"`,
|
||||
`COVERAGE_ENFORCE_OVERALL: "false"`,
|
||||
`COVERAGE_OVERALL_TOLERANCE: "0"`,
|
||||
`COVERAGE_OVERALL_TOLERANCE: "0.1"`,
|
||||
`additional_profile=coverage-shortcut.txt`,
|
||||
"run: make test-plan",
|
||||
"run: make format-check",
|
||||
|
||||
@@ -219,7 +219,7 @@ func TestInstallEventScriptStaticExpectations(t *testing.T) {
|
||||
"releases/latest",
|
||||
"EVENT_VERSION",
|
||||
"DWS_SKILLS_ONLY",
|
||||
"dingtalk-event",
|
||||
"dingtalk-misc",
|
||||
"user_im_message_receive_o2o",
|
||||
".config/opencode/skills",
|
||||
"$HOME/.dws/skills/multi/$EVENT_SKILL_NAME",
|
||||
@@ -232,6 +232,7 @@ func TestInstallEventScriptStaticExpectations(t *testing.T) {
|
||||
for _, avoid := range []string{
|
||||
"releases?per_page=30",
|
||||
"select(.tag_name",
|
||||
"dingtalk-event",
|
||||
"dingtalk-dev",
|
||||
"client-secret",
|
||||
"--as app",
|
||||
@@ -269,9 +270,9 @@ func TestInstallEventScriptInstallsBinaryAndEventSkills(t *testing.T) {
|
||||
"dws": "fake-event-binary\n",
|
||||
})
|
||||
writeZip(t, filepath.Join(releaseDir, "dws-skills.zip"), map[string]string{
|
||||
"multi/dingtalk-event/SKILL.md": "event skill user_im_message_receive_o2o\n",
|
||||
"mono/SKILL.md": "mono skill user_im_message_receive_o2o\n",
|
||||
"SKILL.md": "legacy mono root\n",
|
||||
"multi/dingtalk-misc/SKILL.md": "event skill user_im_message_receive_o2o\n",
|
||||
"mono/SKILL.md": "mono skill user_im_message_receive_o2o\n",
|
||||
"SKILL.md": "legacy mono root\n",
|
||||
})
|
||||
writeFakeCurl(t, filepath.Join(stubRoot, "curl"))
|
||||
|
||||
@@ -304,7 +305,7 @@ func TestInstallEventScriptInstallsBinaryAndEventSkills(t *testing.T) {
|
||||
got := string(output)
|
||||
for _, want := range []string{
|
||||
"Version: v1.0.51",
|
||||
"Skill dingtalk-event",
|
||||
"Skill dingtalk-misc",
|
||||
"Skill dws",
|
||||
"dws event consume user_im_message_receive_o2o",
|
||||
} {
|
||||
@@ -322,12 +323,12 @@ func TestInstallEventScriptInstallsBinaryAndEventSkills(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, rel := range []string{
|
||||
".agents/skills/dingtalk-event/SKILL.md",
|
||||
".codex/skills/dingtalk-event/SKILL.md",
|
||||
".config/opencode/skills/dingtalk-event/SKILL.md",
|
||||
".agents/skills/dingtalk-misc/SKILL.md",
|
||||
".codex/skills/dingtalk-misc/SKILL.md",
|
||||
".config/opencode/skills/dingtalk-misc/SKILL.md",
|
||||
".agents/skills/dws/SKILL.md",
|
||||
".codex/skills/dws/SKILL.md",
|
||||
".dws/skills/multi/dingtalk-event/SKILL.md",
|
||||
".dws/skills/multi/dingtalk-misc/SKILL.md",
|
||||
".dws/skills/mono/SKILL.md",
|
||||
} {
|
||||
p := filepath.Join(fakeHome, filepath.FromSlash(rel))
|
||||
@@ -339,6 +340,9 @@ func TestInstallEventScriptInstallsBinaryAndEventSkills(t *testing.T) {
|
||||
t.Fatalf("%s does not contain event skill marker: %q", p, string(data))
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-event")); !os.IsNotExist(err) {
|
||||
t.Fatalf("dingtalk-event should not be installed by install-event.sh, stat err=%v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-dev")); !os.IsNotExist(err) {
|
||||
t.Fatalf("dingtalk-dev should not be installed by install-event.sh, stat err=%v", err)
|
||||
}
|
||||
@@ -357,8 +361,8 @@ func TestInstallEventScriptSkillsOnlySkipsBinary(t *testing.T) {
|
||||
t.Fatalf("MkdirAll(%s) error = %v", releaseDir, err)
|
||||
}
|
||||
writeZip(t, filepath.Join(releaseDir, "dws-skills.zip"), map[string]string{
|
||||
"multi/dingtalk-event/SKILL.md": "event skill user_im_message_receive_o2o\n",
|
||||
"mono/SKILL.md": "mono skill user_im_message_receive_o2o\n",
|
||||
"multi/dingtalk-misc/SKILL.md": "event skill user_im_message_receive_o2o\n",
|
||||
"mono/SKILL.md": "mono skill user_im_message_receive_o2o\n",
|
||||
})
|
||||
writeFakeCurl(t, filepath.Join(stubRoot, "curl"))
|
||||
|
||||
@@ -383,8 +387,8 @@ func TestInstallEventScriptSkillsOnlySkipsBinary(t *testing.T) {
|
||||
if _, err := os.Stat(filepath.Join(installDir, "dws")); !os.IsNotExist(err) {
|
||||
t.Fatalf("DWS_SKILLS_ONLY=1 should not install binary, stat err=%v\noutput:\n%s", err, string(output))
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-event", "SKILL.md")); err != nil {
|
||||
t.Fatalf("skills-only should install event skill: %v\noutput:\n%s", err, string(output))
|
||||
if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-misc", "SKILL.md")); err != nil {
|
||||
t.Fatalf("skills-only should install misc skill (hosts event): %v\noutput:\n%s", err, string(output))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,8 +437,11 @@ func TestInstallEventScriptNoSkillsOnlyInstallsBinary(t *testing.T) {
|
||||
if _, err := os.Stat(filepath.Join(installDir, "dws")); err != nil {
|
||||
t.Fatalf("DWS_NO_SKILLS=1 should install binary: %v\noutput:\n%s", err, string(output))
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-misc")); !os.IsNotExist(err) {
|
||||
t.Fatalf("DWS_NO_SKILLS=1 should not install misc skill, stat err=%v\noutput:\n%s", err, string(output))
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-event")); !os.IsNotExist(err) {
|
||||
t.Fatalf("DWS_NO_SKILLS=1 should not install event skill, stat err=%v\noutput:\n%s", err, string(output))
|
||||
t.Fatalf("DWS_NO_SKILLS=1 should not install retired event skill, stat err=%v\noutput:\n%s", err, string(output))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -845,7 +845,7 @@ print(json.dumps({
|
||||
}))
|
||||
PY
|
||||
;;
|
||||
*/actions/runs/42/attempts/1/jobs*)
|
||||
*/actions/runs/42/attempts/*/jobs*)
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
@@ -891,13 +891,13 @@ for name in required:
|
||||
print(json.dumps({"jobs": jobs}))
|
||||
PY
|
||||
;;
|
||||
*/actions/runs/42/attempts/1)
|
||||
*/actions/runs/42|*/actions/runs/42/attempts/1)
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
print(json.dumps({
|
||||
"id": 42,
|
||||
"run_attempt": 1,
|
||||
"run_attempt": int(os.environ.get("RUN_ATTEMPT", "1")),
|
||||
"repository": {"full_name": "owner/repo"},
|
||||
"path": ".github/workflows/release.yml",
|
||||
"event": "workflow_dispatch",
|
||||
@@ -943,6 +943,10 @@ esac
|
||||
if output, err := run(); err != nil || !strings.Contains(output, "cloud release run 42") {
|
||||
t.Fatalf("tag-bound cloud delivery was rejected: err=%v\noutput:\n%s", err, output)
|
||||
}
|
||||
if output, err := run("RUN_ATTEMPT=2"); err != nil ||
|
||||
!strings.Contains(output, "cloud release run 42") {
|
||||
t.Fatalf("successful rerun of tag-bound cloud delivery was rejected: err=%v\noutput:\n%s", err, output)
|
||||
}
|
||||
if output, err := runWithArgs(
|
||||
[]string{"--channel-repair", "oss", tag, commit},
|
||||
); err != nil || !strings.Contains(output, "cloud release run 42") {
|
||||
|
||||
@@ -83,9 +83,9 @@ var (
|
||||
// Layer A still validates their Cobra registration. Executing them here can
|
||||
// trigger local setup, authentication, upgrades, or long-running consumers.
|
||||
mockUnsupportedRoots = map[string]bool{
|
||||
"api": true, "auth": true, "cache": true, "catalog": true,
|
||||
"api": true, "auth": true, "catalog": true,
|
||||
"completion": true, "config": true, "doctor": true, "event": true,
|
||||
"mcp": true, "plugin": true, "profile": true, "recovery": true,
|
||||
"mcp": true, "plugin": true, "profile": true,
|
||||
"schema": true, "skill": true, "upgrade": true, "version": true,
|
||||
}
|
||||
)
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
|
||||
| `chat` | `references/products/chat.md` | 13 | 31 |
|
||||
| `contact` | `references/products/contact.md` | 7 | 14 |
|
||||
| `devdoc` | `references/products/simple.md` | 2 | 7 |
|
||||
| `dev` | `skills/multi/dingtalk-dev/SKILL.md` | 19 | 25 |
|
||||
| `dev` | `skills/multi/dingtalk-misc/references/devapp.md` | 19 | 25 |
|
||||
| `ding` | `references/products/ding.md` | 2 | 5 |
|
||||
| `event` | `references/products/event.md` | 8 | 11 |
|
||||
| `report` | `references/products/report.md` | 6 | 26 |
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
//
|
||||
// Mono↔multi skill content QA (G1–G4). Spec: docs/skill-mono-multi-qa.md
|
||||
// Contract: skills/content-qa/mono-multi-coverage.yaml
|
||||
|
||||
package unit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type monoMultiQAContract struct {
|
||||
SchemaVersion int `yaml:"schema_version"`
|
||||
Coverage []struct {
|
||||
Mono string `yaml:"mono"`
|
||||
MultiSkill string `yaml:"multi_skill"`
|
||||
MultiRefs []string `yaml:"multi_refs"`
|
||||
} `yaml:"coverage"`
|
||||
OmitCoverage []struct {
|
||||
Mono string `yaml:"mono"`
|
||||
Disposition string `yaml:"disposition"`
|
||||
Via string `yaml:"via"`
|
||||
Reason string `yaml:"reason"`
|
||||
} `yaml:"omit_coverage"`
|
||||
GlobalProtocols []struct {
|
||||
ID string `yaml:"id"`
|
||||
MultiPath string `yaml:"multi_path"`
|
||||
} `yaml:"global_protocols"`
|
||||
OmitGlobal []struct {
|
||||
ID string `yaml:"id"`
|
||||
Disposition string `yaml:"disposition"`
|
||||
Reason string `yaml:"reason"`
|
||||
ExpectedMulti string `yaml:"expected_multi"`
|
||||
} `yaml:"omit_global"`
|
||||
PairedFiles []struct {
|
||||
Mono string `yaml:"mono"`
|
||||
Multi string `yaml:"multi"`
|
||||
Mode string `yaml:"mode"`
|
||||
} `yaml:"paired_files"`
|
||||
OrphanScriptsAllowlist []struct {
|
||||
Path string `yaml:"path"`
|
||||
Disposition string `yaml:"disposition"`
|
||||
Reason string `yaml:"reason"`
|
||||
} `yaml:"orphan_scripts_allowlist"`
|
||||
SkillsWithoutReferencesAllowlist []string `yaml:"skills_without_references_allowlist"`
|
||||
}
|
||||
|
||||
func loadMonoMultiQAContract(t *testing.T) (string, monoMultiQAContract) {
|
||||
t.Helper()
|
||||
root := repoRoot(t)
|
||||
path := filepath.Join(root, "skills", "content-qa", "mono-multi-coverage.yaml")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read contract: %v", err)
|
||||
}
|
||||
var c monoMultiQAContract
|
||||
if err := yaml.Unmarshal(data, &c); err != nil {
|
||||
t.Fatalf("parse contract: %v", err)
|
||||
}
|
||||
if c.SchemaVersion != 1 {
|
||||
t.Fatalf("unsupported schema_version %d", c.SchemaVersion)
|
||||
}
|
||||
return root, c
|
||||
}
|
||||
|
||||
func TestMonoMultiSkillContentG1Shape(t *testing.T) {
|
||||
root, c := loadMonoMultiQAContract(t)
|
||||
multiRoot := filepath.Join(root, "skills", "multi")
|
||||
entries, err := os.ReadDir(multiRoot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
foundShared := false
|
||||
noRefs := map[string]bool{}
|
||||
for _, name := range c.SkillsWithoutReferencesAllowlist {
|
||||
noRefs[name] = true
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
t.Errorf("G1: unexpected non-directory %s under skills/multi", e.Name())
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if !strings.HasPrefix(name, "dingtalk-") {
|
||||
t.Errorf("G1: invalid skill directory name %q (want dingtalk-*)", name)
|
||||
}
|
||||
if name == "dingtalk-shared" {
|
||||
foundShared = true
|
||||
}
|
||||
skillMD := filepath.Join(multiRoot, name, "SKILL.md")
|
||||
if _, err := os.Stat(skillMD); err != nil {
|
||||
t.Errorf("G1: missing SKILL.md for %s: %v", name, err)
|
||||
}
|
||||
refs := filepath.Join(multiRoot, name, "references")
|
||||
if _, err := os.Stat(refs); err != nil && !noRefs[name] {
|
||||
t.Errorf("G1: missing references/ for %s (add dir or skills_without_references_allowlist)", name)
|
||||
}
|
||||
if noRefs[name] {
|
||||
if _, err := os.Stat(refs); err == nil {
|
||||
t.Errorf("G1: %s is on skills_without_references_allowlist but references/ exists; remove allowlist entry", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundShared {
|
||||
t.Error("G1: skills/multi must contain dingtalk-shared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonoMultiSkillContentG2Frontmatter(t *testing.T) {
|
||||
root, _ := loadMonoMultiQAContract(t)
|
||||
multiRoot := filepath.Join(root, "skills", "multi")
|
||||
entries, err := os.ReadDir(multiRoot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
path := filepath.Join(multiRoot, name, "SKILL.md")
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
fm, err := parseSkillFrontmatter(body)
|
||||
if err != nil {
|
||||
t.Errorf("G2: %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
if fm.Name != name {
|
||||
t.Errorf("G2: %s: frontmatter name %q != directory", name, fm.Name)
|
||||
}
|
||||
if strings.TrimSpace(fm.Description) == "" {
|
||||
t.Errorf("G2: %s: empty description", name)
|
||||
}
|
||||
category := fm.Metadata.Category
|
||||
if category == "" {
|
||||
t.Errorf("G2: %s: missing metadata.category", name)
|
||||
} else if category != "product" && category != "shared" {
|
||||
t.Errorf("G2: %s: metadata.category %q want product|shared", name, category)
|
||||
}
|
||||
wantCat := "product"
|
||||
if name == "dingtalk-shared" {
|
||||
wantCat = "shared"
|
||||
}
|
||||
if category != "" && category != wantCat {
|
||||
t.Errorf("G2: %s: metadata.category %q want %q", name, category, wantCat)
|
||||
}
|
||||
bins := fm.Metadata.Requires.Bins
|
||||
hasDWS := false
|
||||
for _, b := range bins {
|
||||
if b == "dws" {
|
||||
hasDWS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasDWS {
|
||||
t.Errorf("G2: %s: metadata.requires.bins must include dws", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonoMultiSkillContentG3Coverage(t *testing.T) {
|
||||
root, c := loadMonoMultiQAContract(t)
|
||||
products := filepath.Join(root, "skills", "mono", "references", "products")
|
||||
stems, err := monoProductStems(products)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
covered := map[string]bool{}
|
||||
for _, row := range c.Coverage {
|
||||
if row.Mono == "" || row.MultiSkill == "" {
|
||||
t.Fatalf("G3: coverage row missing mono/multi_skill: %+v", row)
|
||||
}
|
||||
if covered[row.Mono] {
|
||||
t.Errorf("G3: duplicate coverage for mono %q", row.Mono)
|
||||
}
|
||||
covered[row.Mono] = true
|
||||
skillDir := filepath.Join(root, "skills", "multi", row.MultiSkill)
|
||||
if _, err := os.Stat(filepath.Join(skillDir, "SKILL.md")); err != nil {
|
||||
t.Errorf("G3: coverage %s → %s missing SKILL.md: %v", row.Mono, row.MultiSkill, err)
|
||||
}
|
||||
for _, rel := range row.MultiRefs {
|
||||
p := filepath.Join(skillDir, filepath.FromSlash(rel))
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
t.Errorf("G3: coverage %s → %s missing %s: %v", row.Mono, row.MultiSkill, rel, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
omitted := map[string]bool{}
|
||||
for _, row := range c.OmitCoverage {
|
||||
if row.Mono == "" || strings.TrimSpace(row.Reason) == "" || strings.TrimSpace(row.Disposition) == "" {
|
||||
t.Fatalf("G3: omit_coverage requires mono, disposition, reason: %+v", row)
|
||||
}
|
||||
omitted[row.Mono] = true
|
||||
}
|
||||
for stem := range stems {
|
||||
if covered[stem] || omitted[stem] {
|
||||
continue
|
||||
}
|
||||
t.Errorf("G3: mono product stem %q not in coverage or omit_coverage", stem)
|
||||
}
|
||||
for stem := range covered {
|
||||
if !stems[stem] {
|
||||
t.Errorf("G3: coverage mono %q has no matching skills/mono/references/products entry", stem)
|
||||
}
|
||||
}
|
||||
for stem := range omitted {
|
||||
if !stems[stem] {
|
||||
t.Errorf("G3: omit_coverage mono %q has no matching products entry", stem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonoMultiSkillContentG4Drift(t *testing.T) {
|
||||
root, c := loadMonoMultiQAContract(t)
|
||||
multiRoot := filepath.Join(root, "skills", "multi")
|
||||
|
||||
for _, g := range c.GlobalProtocols {
|
||||
p := filepath.Join(multiRoot, filepath.FromSlash(g.MultiPath))
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
t.Errorf("G4: global protocol %s missing at %s: %v", g.ID, g.MultiPath, err)
|
||||
}
|
||||
}
|
||||
for _, o := range c.OmitGlobal {
|
||||
if strings.TrimSpace(o.ID) == "" || strings.TrimSpace(o.Disposition) == "" || strings.TrimSpace(o.Reason) == "" {
|
||||
t.Fatalf("G4: omit_global requires id, disposition, reason: %+v", o)
|
||||
}
|
||||
}
|
||||
|
||||
for _, pair := range c.PairedFiles {
|
||||
monoPath := filepath.Join(root, "skills", "mono", filepath.FromSlash(pair.Mono))
|
||||
multiPath := filepath.Join(multiRoot, filepath.FromSlash(pair.Multi))
|
||||
monoData, err := os.ReadFile(monoPath)
|
||||
if err != nil {
|
||||
t.Errorf("G4: paired mono %s: %v", pair.Mono, err)
|
||||
continue
|
||||
}
|
||||
multiData, err := os.ReadFile(multiPath)
|
||||
if err != nil {
|
||||
t.Errorf("G4: paired multi %s: %v", pair.Multi, err)
|
||||
continue
|
||||
}
|
||||
mode := pair.Mode
|
||||
if mode == "" {
|
||||
mode = "identical"
|
||||
}
|
||||
switch mode {
|
||||
case "identical":
|
||||
if !bytes.Equal(monoData, multiData) {
|
||||
t.Errorf("G4: paired files differ: mono %s vs multi %s", pair.Mono, pair.Multi)
|
||||
}
|
||||
default:
|
||||
t.Errorf("G4: unknown paired mode %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
allow := map[string]bool{}
|
||||
for _, row := range c.OrphanScriptsAllowlist {
|
||||
if row.Path == "" || row.Disposition == "" || row.Reason == "" {
|
||||
t.Fatalf("G4: orphan allowlist requires path, disposition, reason: %+v", row)
|
||||
}
|
||||
allow[filepath.ToSlash(row.Path)] = true
|
||||
}
|
||||
var orphans []string
|
||||
entries, err := os.ReadDir(multiRoot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
skill := e.Name()
|
||||
scriptsDir := filepath.Join(multiRoot, skill, "scripts")
|
||||
if st, err := os.Stat(scriptsDir); err != nil || !st.IsDir() {
|
||||
continue
|
||||
}
|
||||
blob := skillMarkdownBlob(t, filepath.Join(multiRoot, skill))
|
||||
_ = filepath.WalkDir(scriptsDir, func(path string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil || d.IsDir() {
|
||||
return walkErr
|
||||
}
|
||||
rel, err := filepath.Rel(multiRoot, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relSlash := filepath.ToSlash(rel)
|
||||
base := filepath.Base(path)
|
||||
if strings.Contains(blob, base) || strings.Contains(blob, relSlash) {
|
||||
return nil
|
||||
}
|
||||
if allow[relSlash] {
|
||||
return nil
|
||||
}
|
||||
orphans = append(orphans, relSlash)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
for _, o := range orphans {
|
||||
t.Errorf("G4: orphan script %s (reference from skill markdown or add orphan_scripts_allowlist)", o)
|
||||
}
|
||||
for path := range allow {
|
||||
full := filepath.Join(multiRoot, filepath.FromSlash(path))
|
||||
if _, err := os.Stat(full); err != nil {
|
||||
t.Errorf("G4: orphan allowlist path missing on disk: %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type skillFrontmatter struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Metadata struct {
|
||||
Category string `yaml:"category"`
|
||||
Requires struct {
|
||||
Bins []string `yaml:"bins"`
|
||||
} `yaml:"requires"`
|
||||
} `yaml:"metadata"`
|
||||
}
|
||||
|
||||
func parseSkillFrontmatter(body []byte) (skillFrontmatter, error) {
|
||||
var out skillFrontmatter
|
||||
s := string(body)
|
||||
if !strings.HasPrefix(s, "---\n") && !strings.HasPrefix(s, "---\r\n") {
|
||||
return out, fmt.Errorf("missing YAML frontmatter opener")
|
||||
}
|
||||
rest := s[3:]
|
||||
if strings.HasPrefix(rest, "\r\n") {
|
||||
rest = rest[2:]
|
||||
} else if strings.HasPrefix(rest, "\n") {
|
||||
rest = rest[1:]
|
||||
}
|
||||
end := strings.Index(rest, "\n---")
|
||||
if end < 0 {
|
||||
return out, fmt.Errorf("missing YAML frontmatter closer")
|
||||
}
|
||||
block := rest[:end]
|
||||
if err := yaml.Unmarshal([]byte(block), &out); err != nil {
|
||||
return out, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func monoProductStems(productsDir string) (map[string]bool, error) {
|
||||
entries, err := os.ReadDir(productsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stems := map[string]bool{}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if e.IsDir() {
|
||||
stems[name] = true
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(name, ".md") {
|
||||
stems[strings.TrimSuffix(name, ".md")] = true
|
||||
}
|
||||
}
|
||||
return stems, nil
|
||||
}
|
||||
|
||||
func skillMarkdownBlob(t *testing.T, skillDir string) string {
|
||||
t.Helper()
|
||||
var b strings.Builder
|
||||
_ = filepath.WalkDir(skillDir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
if filepath.Ext(path) != ".md" {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Write(data)
|
||||
b.WriteByte('\n')
|
||||
return nil
|
||||
})
|
||||
return b.String()
|
||||
}
|
||||
@@ -83,8 +83,8 @@ func TestEventSkillUsesFlatOutputContract(t *testing.T) {
|
||||
}
|
||||
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
||||
paths := []string{
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-event", "SKILL.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-event", "references", "event-im.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-misc", "references", "event.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-misc", "references", "event-im.md"),
|
||||
filepath.Join(root, "skills", "mono", "references", "products", "event.md"),
|
||||
}
|
||||
for _, path := range paths {
|
||||
@@ -137,8 +137,8 @@ func TestCrossPlatformCoverageEventSkillPinsSubscriptionRetryOrchestrationContra
|
||||
}
|
||||
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
||||
paths := []string{
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-event", "SKILL.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-event", "references", "event-im.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-misc", "references", "event.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-misc", "references", "event-im.md"),
|
||||
filepath.Join(root, "skills", "mono", "references", "products", "event.md"),
|
||||
filepath.Join(root, "docs", "event-subprocess-contract.md"),
|
||||
}
|
||||
@@ -203,8 +203,8 @@ func TestCrossPlatformCoverageEventSkillDocumentsSubscriptionGuardOperations(t *
|
||||
}
|
||||
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
||||
paths := []string{
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-event", "SKILL.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-event", "references", "event-im.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-misc", "references", "event.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-misc", "references", "event-im.md"),
|
||||
filepath.Join(root, "skills", "mono", "references", "products", "event.md"),
|
||||
filepath.Join(root, "docs", "event-subprocess-contract.md"),
|
||||
}
|
||||
@@ -263,7 +263,7 @@ func TestEventSkillFrontmatterAdvertisesGroupMemberLifecycle(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", ".."))
|
||||
paths := []string{
|
||||
filepath.Join(root, "skills", "mono", "SKILL.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-event", "SKILL.md"),
|
||||
filepath.Join(root, "skills", "multi", "dingtalk-misc", "SKILL.md"),
|
||||
}
|
||||
for _, path := range paths {
|
||||
content, err := os.ReadFile(path)
|
||||
|
||||
Reference in New Issue
Block a user