Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

767 lines
22 KiB
Go

package parser
import (
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"go.kenn.io/agentsview/internal/testjsonl"
)
func TestInferCursorSkillNameFromReadFile(t *testing.T) {
path := writeTestSkill(t, "foo", "foo")
_, _, toolCalls := extractAssistantContent([]string{
"[Tool call] ReadFile",
" path=" + path,
})
require.Len(t, toolCalls, 1)
assert.Equal(t, "ReadFile", toolCalls[0].ToolName)
assert.Equal(t, "foo", toolCalls[0].SkillName)
}
func TestInferCursorSkillNameFromShellCommand(t *testing.T) {
path := writeTestSkill(t, "foo", "foo")
_, _, toolCalls := extractAssistantContent([]string{
"[Tool call] Shell",
" command=cat " + path,
})
require.Len(t, toolCalls, 1)
assert.Equal(t, "Shell", toolCalls[0].ToolName)
assert.Equal(t, "foo", toolCalls[0].SkillName)
}
func TestInferCursorSkillNameIgnoresShellWriteCommand(t *testing.T) {
path := writeTestSkill(t, "foo", "foo")
_, _, toolCalls := extractAssistantContent([]string{
"[Tool call] Shell",
" command=cp " + path + " /tmp/SKILL.md",
})
require.Len(t, toolCalls, 1)
assert.Empty(t, toolCalls[0].SkillName)
}
func TestInferCursorSkillNameUsesFrontmatterName(t *testing.T) {
path := writeTestSkill(t, "index", "data-analytics:index")
_, _, toolCalls := extractAssistantContent([]string{
"[Tool call] ReadFile",
` {"path":` + quoteJSON(t, path) + `}`,
})
require.Len(t, toolCalls, 1)
assert.Equal(t, "data-analytics:index", toolCalls[0].SkillName)
}
func TestInferCursorSkillNameIgnoresDiscoveryAndNonSkillPaths(t *testing.T) {
path := writeTestSkill(t, "foo", "foo")
tests := []struct {
name string
line string
}{
{
name: "glob discovery",
line: "[Tool call] Glob\n " +
`{"target_directory":` + quoteJSON(t, filepath.Dir(path)) +
`,"glob_pattern":"**/SKILL.md"}`,
},
{
name: "non skill path",
line: `[Tool call] ReadFile
path=` + filepath.Join(t.TempDir(), "README.md"),
},
{
name: "empty input",
line: "[Tool call] ReadFile",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _, toolCalls := extractAssistantContent(
splitTestLines(tt.line),
)
require.Len(t, toolCalls, 1)
assert.Empty(t, toolCalls[0].SkillName)
})
}
}
func TestInferCodexSkillNameFromReadCommands(t *testing.T) {
path := writeTestSkill(t, "foo", "foo")
for _, cmd := range []string{
"cat " + path,
"sed -n '1,220p' " + path,
"head -40 " + path,
"tail -40 " + path,
"rg name " + path,
"grep name " + path,
"cd /tmp && sed -n '1,220p' " + path,
} {
t.Run(cmd, func(t *testing.T) {
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, cmd)+`}`,
)
assert.Equal(t, "foo", got)
})
}
}
func TestInferCodexSkillNameIgnoresWriteCommands(t *testing.T) {
path := writeTestSkill(t, "foo", "foo")
for _, cmd := range []string{
"cp " + path + " /tmp/SKILL.md",
"mv " + path + " /tmp/SKILL.md",
"mkdir -p " + filepath.Dir(path),
"git add " + path,
"sed -i '' 's/a/b/' " + path,
"sed -ni 's/a/b/' " + path,
"sed -Ei 's/a/b/' " + path,
"echo hi && sed -i '' 's/a/b/' " + path,
"cat > " + path,
} {
t.Run(cmd, func(t *testing.T) {
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, cmd)+`}`,
)
assert.Empty(t, got)
})
}
}
func TestInferCodexSkillNameMixedWriteThenRead(t *testing.T) {
// A write segment earlier in the command must not suppress a real
// skill read in a later segment; each segment is classified on its
// own leading verb rather than rejecting the whole command.
path := writeTestSkill(t, "foo", "data-analytics:foo")
for _, cmd := range []string{
"mkdir -p out && cat " + path,
"git add -A && sed -n '1,40p' " + path,
"touch marker; grep name " + path,
} {
t.Run(cmd, func(t *testing.T) {
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, cmd)+`}`,
)
assert.Equal(t, "data-analytics:foo", got)
})
}
}
func TestInferCodexSkillNameIgnoresGlobDiscovery(t *testing.T) {
for _, cmd := range []string{
"rg --files -g '**/SKILL.md'",
"cat **/SKILL.md",
"sed -n '1,40p' skills/*/SKILL.md",
"grep name skills/?ndex/SKILL.md",
"head -40 skills/[ab]/SKILL.md",
} {
t.Run(cmd, func(t *testing.T) {
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, cmd)+`}`,
)
assert.Empty(t, got)
})
}
}
func TestInferCodexSkillNameBareSkillFileWithWorkdir(t *testing.T) {
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
workdir := filepath.Dir(path)
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, "cat SKILL.md")+
`,"workdir":`+quoteJSON(t, workdir)+`}`,
)
assert.Equal(t, "data-analytics:index", got)
}
func TestInferCodexSkillNameBareSkillFileUsesFallbackCwd(t *testing.T) {
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
cwd := filepath.Dir(path)
got := inferCodexSkillNameWithBase(
"exec_command",
`{"cmd":`+quoteJSON(t, "sed -n '1,40p' SKILL.md")+`}`,
cwd,
)
assert.Equal(t, "data-analytics:index", got)
}
func TestInferCodexSkillNameBareSkillPatternWithoutFileIgnored(t *testing.T) {
// "grep SKILL.md notes.txt" uses SKILL.md as a search pattern, not
// a file. With a workdir that holds no SKILL.md the bare token must
// not be resolved to the workdir name and miscounted as usage.
workdir := t.TempDir()
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, "grep SKILL.md notes.txt")+
`,"workdir":`+quoteJSON(t, workdir)+`}`,
)
assert.Empty(t, got)
}
func TestInferCodexSkillNameBareSkillSearchPatternIgnoredEvenWithFile(t *testing.T) {
// grep/rg take the search pattern as their first operand, so a bare
// "SKILL.md" there is the pattern, not a file to read. It must not be
// inferred even when the workdir holds a readable SKILL.md.
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
workdir := filepath.Dir(path)
for _, cmd := range []string{
"grep SKILL.md notes.txt",
"rg SKILL.md",
"rg SKILL.md notes.txt",
"rg -t md SKILL.md", // SKILL.md is the pattern; md is -t's value
"grep -A 3 SKILL.md notes.txt", // SKILL.md is the pattern; 3 is -A's value
"grep -e SKILL.md notes.txt", // SKILL.md is the -e pattern value
} {
t.Run(cmd, func(t *testing.T) {
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, cmd)+
`,"workdir":`+quoteJSON(t, workdir)+`}`,
)
assert.Empty(t, got)
})
}
}
func TestInferCodexSkillNameSearchCommandReadsFileOperand(t *testing.T) {
// When a pattern precedes SKILL.md (or -e/-f supplies the pattern),
// SKILL.md is a file operand and a workdir-local read is inferred.
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
workdir := filepath.Dir(path)
for _, cmd := range []string{
"grep name SKILL.md",
"rg pattern SKILL.md",
"cat SKILL.md | grep name",
"grep -e foo SKILL.md",
"grep -i name SKILL.md",
} {
t.Run(cmd, func(t *testing.T) {
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, cmd)+
`,"workdir":`+quoteJSON(t, workdir)+`}`,
)
assert.Equal(t, "data-analytics:index", got)
})
}
}
func TestInferCodexSkillNameSearchQuotedPatternIgnored(t *testing.T) {
// A quoted multi-word search pattern containing SKILL.md must not be
// split into fields and read as a file, even with a workdir SKILL.md.
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
workdir := filepath.Dir(path)
for _, cmd := range []string{
`grep "foo SKILL.md" notes.txt`,
`rg 'see SKILL.md for details'`,
`grep -e "read the SKILL.md" notes.txt`,
} {
t.Run(cmd, func(t *testing.T) {
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, cmd)+
`,"workdir":`+quoteJSON(t, workdir)+`}`,
)
assert.Empty(t, got)
})
}
}
func TestInferCodexSkillNameIgnoresNonReadSegments(t *testing.T) {
// A read command elsewhere in the line must not let SKILL.md in an
// unrelated segment (echo, ls, ...) be inferred as a read.
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
workdir := filepath.Dir(path)
for _, cmd := range []string{
"grep foo notes.txt && echo SKILL.md",
"cat notes.txt && echo see SKILL.md",
"grep foo notes.txt; ls SKILL.md",
} {
t.Run(cmd, func(t *testing.T) {
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, cmd)+
`,"workdir":`+quoteJSON(t, workdir)+`}`,
)
assert.Empty(t, got)
})
}
}
func TestTokenizeCommand(t *testing.T) {
tests := []struct {
name string
cmd string
want []string
}{
{
// Backslashes are literal so Windows paths survive intact;
// a full shell lexer such as shlex would consume them.
name: "preserves backslash path",
cmd: `cat C:\Users\me\skills\foo\SKILL.md`,
want: []string{"cat", `C:\Users\me\skills\foo\SKILL.md`},
},
{
name: "double-quoted argument stays one token",
cmd: `grep "foo SKILL.md" notes.txt`,
want: []string{"grep", "foo SKILL.md", "notes.txt"},
},
{
name: "single-quoted argument stays one token",
cmd: `rg 'see SKILL.md for'`,
want: []string{"rg", "see SKILL.md for"},
},
{
name: "drops redirect target attached to operator",
cmd: "cat foo >skills/x/SKILL.md",
want: []string{"cat", "foo"},
},
{
name: "drops redirect operator attached to source",
cmd: "cat file> out",
want: []string{"cat", "file"},
},
{
name: "drops space-separated redirect target",
cmd: "cat a > b",
want: []string{"cat", "a"},
},
{
name: "keeps quoted redirect char as content",
cmd: `grep ">" f`,
want: []string{"grep", ">", "f"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tokenizeCommand(tt.cmd))
})
}
}
func TestInferCodexSkillNameIgnoresRedirectTargets(t *testing.T) {
// A redirect destination (>, >>, 2> ...) is written, not read, so a
// SKILL.md target must not be inferred even with a workdir file. The
// operator may be attached to the target (>SKILL.md) or to the source
// token (file>), so whitespace around it cannot be relied upon.
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
workdir := filepath.Dir(path)
for _, cmd := range []string{
"cat foo > SKILL.md",
"grep name file > SKILL.md",
"cat foo >> SKILL.md",
"cat foo 2> SKILL.md",
"cat foo >SKILL.md",
"cat foo >>SKILL.md",
"cat file> SKILL.md",
"grep name file 2>SKILL.md",
"cat foo >skills/data-analytics/SKILL.md",
} {
t.Run(cmd, func(t *testing.T) {
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, cmd)+
`,"workdir":`+quoteJSON(t, workdir)+`}`,
)
assert.Empty(t, got)
})
}
}
func TestInferCodexSkillNameReadsFileDespiteQuotedRedirectChar(t *testing.T) {
// A quoted ">" is a literal search pattern, not a redirect operator,
// so the SKILL.md file operand is still read.
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, `grep ">" `+path)+`}`,
)
assert.Equal(t, "data-analytics:index", got)
}
func TestInferCodexSkillNameReadsSourceDespiteRedirect(t *testing.T) {
// SKILL.md is the input being read; output is redirected elsewhere,
// so the read is still inferred.
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, "cat "+path+" > out.txt")+`}`,
)
assert.Equal(t, "data-analytics:index", got)
}
func TestInferCodexSkillNameSearchCommandStillReadsPathOperand(t *testing.T) {
// A path-qualified SKILL.md operand to grep/rg is a real file read
// (the pattern is a separate token), so it is still inferred.
path := writeTestSkill(t, "data-analytics", "data-analytics:index")
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, "grep name "+path)+`}`,
)
assert.Equal(t, "data-analytics:index", got)
}
func TestParseCodexSessionInfersSkillName(t *testing.T) {
path := writeTestSkill(t, "index", "data-analytics:index")
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON("skill-read", "/tmp", "user", tsEarly),
testjsonl.CodexMsgJSON("user", "use the dashboard skill", tsEarlyS1),
testjsonl.CodexFunctionCallArgsJSON("exec_command", map[string]any{
"cmd": "sed -n '1,220p' '" + path + "'",
}, tsEarlyS5),
)
_, msgs := runCodexParserTest(t, "skill-read.jsonl", content, false)
require.Len(t, msgs, 2)
require.Len(t, msgs[1].ToolCalls, 1)
assert.Equal(t, "data-analytics:index", msgs[1].ToolCalls[0].SkillName)
}
func TestParseCodexSessionInfersSkillNameFromSessionCwd(t *testing.T) {
path := writeTestSkill(t, "index", "data-analytics:index")
cwd := filepath.Dir(filepath.Dir(filepath.Dir(path)))
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON("skill-read", cwd, "user", tsEarly),
testjsonl.CodexMsgJSON("user", "use the dashboard skill", tsEarlyS1),
testjsonl.CodexFunctionCallArgsJSON("exec_command", map[string]any{
"cmd": "sed -n '1,220p' skills/index/SKILL.md",
}, tsEarlyS5),
)
_, msgs := runCodexParserTest(t, "skill-read.jsonl", content, false)
require.Len(t, msgs, 2)
require.Len(t, msgs[1].ToolCalls, 1)
assert.Equal(t, "data-analytics:index", msgs[1].ToolCalls[0].SkillName)
}
func TestParseCodexSessionFromInfersSkillNameFromSeededCwd(t *testing.T) {
path := writeTestSkill(t, "index", "data-analytics:index")
cwd := filepath.Dir(filepath.Dir(filepath.Dir(path)))
initial := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON("inc-skill", cwd, "codex_cli_rs", tsEarly),
testjsonl.CodexMsgJSON("user", "use the dashboard skill", tsEarlyS1),
)
file := createTestFile(t, "incremental-skill.jsonl", initial)
_, msgs, err := parseCodexTestSession(t, file, "local", false)
require.NoError(t, err)
info, err := os.Stat(file)
require.NoError(t, err)
offset := info.Size()
appended := testjsonl.CodexFunctionCallArgsJSON(
"exec_command", map[string]any{
"cmd": "sed -n '1,220p' skills/index/SKILL.md",
}, tsLateS5)
f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY, 0o644)
require.NoError(t, err)
_, err = f.WriteString(appended)
require.NoError(t, err)
require.NoError(t, f.Close())
newMsgs, _, _, err := parseCodexTestSessionFrom(t, file, offset, len(msgs), false)
require.NoError(t, err)
require.Len(t, newMsgs, 1)
require.Len(t, newMsgs[0].ToolCalls, 1)
assert.Equal(t, "data-analytics:index", newMsgs[0].ToolCalls[0].SkillName)
}
func TestExtractTextContentInfersCursorJSONLSkillName(t *testing.T) {
path := writeTestSkill(t, "planning-and-task-breakdown", "planning-and-task-breakdown")
content := gjson.Parse(
`[{"type":"tool_use","id":"tu_read","name":"Read","input":{"path":` +
quoteJSON(t, path) + `}}]`,
)
_, _, _, _, toolCalls, _ := ExtractTextContent(content)
require.Len(t, toolCalls, 1)
assert.Equal(t, "Read", toolCalls[0].ToolName)
assert.Equal(t, "planning-and-task-breakdown", toolCalls[0].SkillName)
}
func TestExtractTextContentInfersCursorJSONLSkillNameFromFrontmatter(t *testing.T) {
path := writeTestSkill(t, "index", "data-analytics:index")
content := gjson.Parse(
`[{"type":"tool_use","id":"tu_read_file","name":"ReadFile","input":{"path":` +
quoteJSON(t, path) + `}}]`,
)
_, _, _, _, toolCalls, _ := ExtractTextContent(content)
require.Len(t, toolCalls, 1)
assert.Equal(t, "ReadFile", toolCalls[0].ToolName)
assert.Equal(t, "data-analytics:index", toolCalls[0].SkillName)
}
func TestExtractTextContentInfersSkillNameFromPathWithSpaces(t *testing.T) {
path := writeTestSkill(t, "my index", "data-analytics:index")
require.Contains(t, path, " ")
content := gjson.Parse(
`[{"type":"tool_use","id":"tu_read","name":"Read","input":{"path":` +
quoteJSON(t, path) + `}}]`,
)
_, _, _, _, toolCalls, _ := ExtractTextContent(content)
require.Len(t, toolCalls, 1)
assert.Equal(t, "data-analytics:index", toolCalls[0].SkillName)
}
func TestExtractTextContentInfersCursorJSONLSkillNameFromShellRead(t *testing.T) {
path := writeTestSkill(t, "qa", "qa")
content := gjson.Parse(
`[{"type":"tool_use","id":"tu_shell","name":"Shell","input":{"command":` +
quoteJSON(t, "cd /tmp && sed -n '1,120p' "+path) + `}}]`,
)
_, _, _, _, toolCalls, _ := ExtractTextContent(content)
require.Len(t, toolCalls, 1)
assert.Equal(t, "Shell", toolCalls[0].ToolName)
assert.Equal(t, "qa", toolCalls[0].SkillName)
}
func TestExtractTextContentDoesNotInferCursorJSONLNonUsage(t *testing.T) {
path := writeTestSkill(t, "foo", "foo")
templatePath := filepath.Join(t.TempDir(), "SKILL.md.tmpl")
require.NoError(t, os.WriteFile(templatePath, []byte("template"), 0o644))
tests := []struct {
name string
toolName string
input string
}{
{
name: "glob discovery",
toolName: "Glob",
input: `{"glob_pattern":"**/SKILL.md"}`,
},
{
name: "write skill file",
toolName: "Write",
input: `{"path":` + quoteJSON(t, path) + `,"contents":"---"}`,
},
{
name: "str replace skill file",
toolName: "StrReplace",
input: `{"path":` + quoteJSON(t, path) + `,"old_string":"a","new_string":"b"}`,
},
{
name: "apply patch skill file",
toolName: "ApplyPatch",
input: `{"path":` + quoteJSON(t, path) + `,"patch":"*** Begin Patch"}`,
},
{
name: "template file",
toolName: "Read",
input: `{"path":` + quoteJSON(t, templatePath) + `}`,
},
{
name: "shell write command",
toolName: "Shell",
input: `{"command":` + quoteJSON(t, "cp "+path+" /tmp/SKILL.md") + `}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
content := gjson.Parse(
`[{"type":"tool_use","id":"tu","name":` +
quoteJSON(t, tt.toolName) + `,"input":` + tt.input + `}]`,
)
_, _, _, _, toolCalls, _ := ExtractTextContent(content)
require.Len(t, toolCalls, 1)
assert.Empty(t, toolCalls[0].SkillName)
})
}
}
func TestInferCodexSkillNameResolvesRelativePathAgainstWorkdir(t *testing.T) {
path := writeTestSkill(t, "index", "data-analytics:index")
workdir := filepath.Dir(filepath.Dir(filepath.Dir(path)))
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, "sed -n '1,220p' skills/index/SKILL.md")+
`,"workdir":`+quoteJSON(t, workdir)+`}`,
)
assert.Equal(t, "data-analytics:index", got)
}
func TestInferCodexSkillNameWorkdirOverridesFallbackBase(t *testing.T) {
path := writeTestSkill(t, "index", "data-analytics:index")
workdir := filepath.Dir(filepath.Dir(filepath.Dir(path)))
got := inferCodexSkillNameWithBase(
"exec_command",
`{"cmd":`+quoteJSON(t, "cat skills/index/SKILL.md")+
`,"workdir":`+quoteJSON(t, workdir)+`}`,
"/nonexistent/fallback",
)
assert.Equal(t, "data-analytics:index", got)
}
func TestInferCodexSkillNameUsesFallbackBaseWhenNoWorkdir(t *testing.T) {
path := writeTestSkill(t, "index", "data-analytics:index")
fallback := filepath.Dir(filepath.Dir(filepath.Dir(path)))
got := inferCodexSkillNameWithBase(
"exec_command",
`{"cmd":`+quoteJSON(t, "cat skills/index/SKILL.md")+`}`,
fallback,
)
assert.Equal(t, "data-analytics:index", got)
}
func TestInferCodexSkillNameRelativePathNoWorkdirUsesParentFallback(t *testing.T) {
// Frontmatter name ("data-analytics:index") differs from the
// parent dir ("index"). Without a workdir the relative path
// cannot be resolved, so the frontmatter read is skipped and
// the parent-dir fallback is used instead of reading an
// unrelated SKILL.md under the process cwd.
writeTestSkill(t, "index", "data-analytics:index")
got := inferCodexSkillName(
"exec_command",
`{"cmd":`+quoteJSON(t, "cat skills/index/SKILL.md")+`}`,
)
assert.Equal(t, "index", got)
}
func TestExpandSkillHome(t *testing.T) {
home, err := os.UserHomeDir()
require.NoError(t, err)
tests := []struct {
name string
in string
want string
}{
{"bare tilde", "~", home},
{"tilde slash", "~/.claude/skills/foo/SKILL.md",
filepath.Join(home, ".claude/skills/foo/SKILL.md")},
{"absolute unchanged", "/abs/SKILL.md", "/abs/SKILL.md"},
{"relative unchanged", "skills/foo/SKILL.md", "skills/foo/SKILL.md"},
{"tilde user not expanded", "~bob/SKILL.md", "~bob/SKILL.md"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, expandSkillHome(tt.in))
})
}
}
func TestResolveSkillPath(t *testing.T) {
home, err := os.UserHomeDir()
require.NoError(t, err)
absSkillPath := filepath.Join(t.TempDir(), "a", "b", "SKILL.md")
baseDir := filepath.Join(t.TempDir(), "repo")
relativeSkillPath := filepath.Join("s", "SKILL.md")
tests := []struct {
name string
path string
baseDir string
wantPath string
wantReadable bool
}{
{"absolute", absSkillPath, "", absSkillPath, true},
{"tilde expands", "~/s/SKILL.md", "",
filepath.Join(home, "s/SKILL.md"), true},
{"relative joined to base", relativeSkillPath, baseDir,
filepath.Join(baseDir, relativeSkillPath), true},
{"relative no base", relativeSkillPath, "", relativeSkillPath, false},
{"relative with relative base", relativeSkillPath, "rel",
relativeSkillPath, false},
{"tilde base expands", relativeSkillPath, "~/repo",
filepath.Join(home, "repo", relativeSkillPath), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPath, gotReadable := resolveSkillPath(tt.path, tt.baseDir)
assert.Equal(t, tt.wantPath, gotPath)
assert.Equal(t, tt.wantReadable, gotReadable)
})
}
}
func TestSkillNameFromFrontmatterBoundsReadSize(t *testing.T) {
dir := filepath.Join(t.TempDir(), "huge")
require.NoError(t, os.MkdirAll(dir, 0o755))
path := filepath.Join(dir, "SKILL.md")
// Frontmatter terminator sits past the read cap, so the name
// cannot be parsed and resolution falls back to the parent dir.
var sb strings.Builder
sb.WriteString("---\n")
sb.WriteString("description: ")
sb.WriteString(strings.Repeat("x", maxSkillFrontmatterSize))
sb.WriteString("\nname: too-far\n---\n")
require.NoError(t, os.WriteFile(path, []byte(sb.String()), 0o644))
assert.Equal(t, "huge", skillNameFromPath(path, ""))
}
func writeTestSkill(t *testing.T, folder, name string) string {
t.Helper()
dir := filepath.Join(t.TempDir(), "skills", folder)
require.NoError(t, os.MkdirAll(dir, 0o755))
path := filepath.Join(dir, "SKILL.md")
content := "---\nname: " + name + "\ndescription: Test skill\n---\n"
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
return path
}
func splitTestLines(s string) []string {
return strings.Split(s, "\n")
}
func quoteJSON(t *testing.T, s string) string {
t.Helper()
return strconv.Quote(s)
}