1200 lines
43 KiB
Go
1200 lines
43 KiB
Go
package update
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/git"
|
|
"github.com/cli/cli/v2/internal/config"
|
|
"github.com/cli/cli/v2/internal/gh"
|
|
"github.com/cli/cli/v2/internal/prompter"
|
|
"github.com/cli/cli/v2/internal/skills/registry"
|
|
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/httpmock"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/google/shlex"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewCmdUpdate_Help(t *testing.T) {
|
|
ios, _, _, _ := iostreams.Test()
|
|
f := &cmdutil.Factory{
|
|
IOStreams: ios,
|
|
Prompter: &prompter.PrompterMock{},
|
|
GitClient: &git.Client{},
|
|
}
|
|
|
|
cmd := NewCmdUpdate(f, func(opts *UpdateOptions) error {
|
|
return nil
|
|
})
|
|
|
|
assert.Equal(t, "update [<skill>...] [flags]", cmd.Use)
|
|
assert.NotEmpty(t, cmd.Short)
|
|
assert.NotEmpty(t, cmd.Long)
|
|
assert.NotEmpty(t, cmd.Example)
|
|
}
|
|
|
|
func TestNewCmdUpdate_Flags(t *testing.T) {
|
|
ios, _, _, _ := iostreams.Test()
|
|
f := &cmdutil.Factory{IOStreams: ios, Prompter: &prompter.PrompterMock{}, GitClient: &git.Client{}}
|
|
cmd := NewCmdUpdate(f, func(_ *UpdateOptions) error { return nil })
|
|
|
|
flags := []string{"all", "force", "dry-run", "dir", "unpin"}
|
|
for _, name := range flags {
|
|
assert.NotNil(t, cmd.Flags().Lookup(name), "missing flag: --%s", name)
|
|
}
|
|
}
|
|
|
|
func TestNewCmdUpdate_ArgsPassedToOptions(t *testing.T) {
|
|
ios, _, stdout, stderr := iostreams.Test()
|
|
f := &cmdutil.Factory{IOStreams: ios, Prompter: &prompter.PrompterMock{}, GitClient: &git.Client{}}
|
|
|
|
var gotOpts *UpdateOptions
|
|
cmd := NewCmdUpdate(f, func(opts *UpdateOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
|
|
args, _ := shlex.Split("mcp-cli git-commit --all --force")
|
|
cmd.SetArgs(args)
|
|
cmd.SetOut(stdout)
|
|
cmd.SetErr(stderr)
|
|
err := cmd.Execute()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"mcp-cli", "git-commit"}, gotOpts.Skills)
|
|
assert.True(t, gotOpts.All)
|
|
assert.True(t, gotOpts.Force)
|
|
}
|
|
|
|
func TestScanInstalledSkills(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T, dir string)
|
|
verify func(t *testing.T, skills []installedSkill, err error)
|
|
}{
|
|
{
|
|
name: "happy path with metadata, no metadata, and pinned skills",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
|
|
// Skill with full metadata
|
|
skillDir := filepath.Join(dir, "git-commit")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
content := heredoc.Doc(`
|
|
---
|
|
name: git-commit
|
|
description: Git commit helper
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/awesome-copilot
|
|
github-tree-sha: abc123
|
|
github-path: skills/git-commit
|
|
---
|
|
Body content
|
|
`)
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(content), 0o644))
|
|
|
|
// Skill without metadata
|
|
noMetaDir := filepath.Join(dir, "unknown-skill")
|
|
require.NoError(t, os.MkdirAll(noMetaDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(noMetaDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: unknown-skill
|
|
---
|
|
No metadata here
|
|
`)), 0o644))
|
|
|
|
// Pinned skill
|
|
pinnedDir := filepath.Join(dir, "pinned-skill")
|
|
require.NoError(t, os.MkdirAll(pinnedDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(pinnedDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: pinned-skill
|
|
metadata:
|
|
github-repo: https://github.com/octocat/hubot-skills
|
|
github-tree-sha: def456
|
|
github-pinned: v1.0.0
|
|
---
|
|
Pinned content
|
|
`)), 0o644))
|
|
},
|
|
verify: func(t *testing.T, skills []installedSkill, err error) {
|
|
t.Helper()
|
|
require.NoError(t, err)
|
|
assert.Len(t, skills, 3)
|
|
|
|
byName := make(map[string]installedSkill)
|
|
for _, s := range skills {
|
|
byName[s.name] = s
|
|
}
|
|
|
|
gc := byName["git-commit"]
|
|
assert.Equal(t, "monalisa", gc.owner)
|
|
assert.Equal(t, "awesome-copilot", gc.repo)
|
|
assert.Equal(t, "github.com", gc.repoHost)
|
|
assert.Equal(t, "abc123", gc.treeSHA)
|
|
assert.Equal(t, "skills/git-commit", gc.sourcePath)
|
|
assert.Empty(t, gc.pinned)
|
|
|
|
us := byName["unknown-skill"]
|
|
assert.Empty(t, us.owner)
|
|
assert.Empty(t, us.repo)
|
|
|
|
ps := byName["pinned-skill"]
|
|
assert.Equal(t, "github.com", ps.repoHost)
|
|
assert.Equal(t, "v1.0.0", ps.pinned)
|
|
},
|
|
},
|
|
{
|
|
name: "unsupported host metadata returns error",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "enterprise-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: enterprise-skill
|
|
metadata:
|
|
github-repo: https://acme.ghes.com/monalisa/octocat-skills
|
|
github-tree-sha: abc123
|
|
---
|
|
body
|
|
`)), 0o644))
|
|
},
|
|
verify: func(t *testing.T, skills []installedSkill, err error) {
|
|
t.Helper()
|
|
require.NoError(t, err)
|
|
require.Len(t, skills, 1)
|
|
require.Error(t, skills[0].metadataErr)
|
|
assert.Contains(t, skills[0].metadataErr.Error(), "does not currently support GitHub Enterprise Server")
|
|
},
|
|
},
|
|
{
|
|
name: "non-existent directory returns nil",
|
|
// no setup needed; dir does not exist
|
|
verify: func(t *testing.T, skills []installedSkill, err error) {
|
|
t.Helper()
|
|
require.NoError(t, err)
|
|
assert.Nil(t, skills)
|
|
},
|
|
},
|
|
{
|
|
name: "corrupted YAML is skipped gracefully",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "corrupt")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
not: valid: yaml: [broken
|
|
---
|
|
body
|
|
`)), 0o644))
|
|
},
|
|
verify: func(t *testing.T, skills []installedSkill, err error) {
|
|
t.Helper()
|
|
require.NoError(t, err)
|
|
require.Len(t, skills, 1)
|
|
assert.Equal(t, "corrupt", skills[0].name)
|
|
assert.ErrorContains(t, skills[0].metadataErr, "invalid SKILL.md")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// For the non-existent directory case, pass a path that doesn't exist
|
|
dir := filepath.Join(t.TempDir(), "skills")
|
|
if tt.setup != nil {
|
|
require.NoError(t, os.MkdirAll(dir, 0o755))
|
|
tt.setup(t, dir)
|
|
}
|
|
|
|
skills, err := scanInstalledSkills(dir, nil, "")
|
|
tt.verify(t, skills, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPromptForSkillOrigin(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantOK bool
|
|
wantOwner string
|
|
wantRepo string
|
|
wantReason string
|
|
}{
|
|
{
|
|
name: "valid owner/repo",
|
|
input: "monalisa/awesome-copilot",
|
|
wantOK: true,
|
|
wantOwner: "monalisa",
|
|
wantRepo: "awesome-copilot",
|
|
},
|
|
{
|
|
name: "empty input skips",
|
|
input: "",
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "invalid format returns reason",
|
|
input: "just-a-name",
|
|
wantOK: false,
|
|
wantReason: "invalid repository",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pm := &prompter.PrompterMock{
|
|
InputFunc: func(prompt string, defaultValue string) (string, error) {
|
|
return tt.input, nil
|
|
},
|
|
}
|
|
|
|
owner, repo, reason, ok, err := promptForSkillOrigin(pm, "test-skill")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.wantOK, ok)
|
|
assert.Equal(t, tt.wantOwner, owner)
|
|
assert.Equal(t, tt.wantRepo, repo)
|
|
if tt.wantReason != "" {
|
|
assert.Contains(t, reason, tt.wantReason)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScanAllAgentsDeduplicatesSharedProjectDirs(t *testing.T) {
|
|
repoDir := t.TempDir()
|
|
homeDir := t.TempDir()
|
|
|
|
sharedSkillDir := filepath.Join(repoDir, ".agents", "skills", "git-commit")
|
|
require.NoError(t, os.MkdirAll(sharedSkillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(sharedSkillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: git-commit
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/octocat-skills
|
|
github-tree-sha: abc123
|
|
---
|
|
Body
|
|
`)), 0o644))
|
|
|
|
claudeSkillDir := filepath.Join(repoDir, ".claude", "skills", "code-review")
|
|
require.NoError(t, os.MkdirAll(claudeSkillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(claudeSkillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: code-review
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/octocat-skills
|
|
github-tree-sha: def456
|
|
---
|
|
Body
|
|
`)), 0o644))
|
|
|
|
skills := scanAllAgents(repoDir, homeDir)
|
|
require.Len(t, skills, 2)
|
|
|
|
byName := make(map[string]installedSkill)
|
|
for _, skill := range skills {
|
|
byName[skill.name] = skill
|
|
}
|
|
|
|
assert.Equal(t, registry.ScopeProject, byName["git-commit"].scope)
|
|
assert.Equal(t, registry.ScopeProject, byName["code-review"].scope)
|
|
}
|
|
|
|
func TestUpdateRun(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T, dir string)
|
|
stubs func(reg *httpmock.Registry)
|
|
opts func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions
|
|
verify func(t *testing.T, dir string)
|
|
wantErr string
|
|
wantStderr string
|
|
wantStdout string
|
|
}{
|
|
{
|
|
name: "scans all agents when no --dir is set",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
t.Setenv("HOME", dir)
|
|
t.Setenv("USERPROFILE", dir)
|
|
skillDir := filepath.Join(dir, ".agents", "skills", "code-review")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: code-review
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/octocat-skills
|
|
github-tree-sha: currentsha
|
|
github-path: skills/code-review
|
|
---
|
|
Installed content
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v1.0.0"}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/ref/tags/v1.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "commit1", "type": "commit"}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/commit1"),
|
|
httpmock.StringResponse(`{"sha": "commit1", "tree": [{"path": "skills/code-review", "type": "tree", "sha": "currentsha"}, {"path": "skills/code-review/SKILL.md", "type": "blob", "sha": "blob1"}, {"path": "skills", "type": "tree", "sha": "treeshaX"}], "truncated": false}`))
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
}
|
|
},
|
|
wantStderr: "All skills are up to date.",
|
|
},
|
|
{
|
|
name: "no installed skills",
|
|
stubs: func(reg *httpmock.Registry) {},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
}
|
|
},
|
|
wantStderr: "No installed skills found.",
|
|
},
|
|
{
|
|
name: "specific skill not installed",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "octocat-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: octocat-skill
|
|
metadata:
|
|
github-repo: https://github.com/octocat/hubot-skills
|
|
github-tree-sha: abc
|
|
---
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
Skills: []string{"nonexistent"},
|
|
}
|
|
},
|
|
wantErr: "none of the specified skills are installed",
|
|
},
|
|
{
|
|
name: "pinned skills are skipped",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "pinned-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: pinned-skill
|
|
metadata:
|
|
github-repo: https://github.com/octocat/hubot-skills
|
|
github-tree-sha: abc123
|
|
github-pinned: v1.0.0
|
|
---
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Prompter: &prompter.PrompterMock{},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
}
|
|
},
|
|
wantStderr: "pinned",
|
|
},
|
|
{
|
|
name: "no metadata skips in non-interactive mode",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "manual-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: manual-skill
|
|
---
|
|
No metadata
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
ios.SetStdinTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
}
|
|
},
|
|
wantStderr: "no GitHub metadata",
|
|
},
|
|
{
|
|
name: "all up to date",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "monalisa-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: monalisa-skill
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/octocat-skills
|
|
github-tree-sha: abc123def456
|
|
github-path: skills/monalisa-skill
|
|
---
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v1.0.0"}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/ref/tags/v1.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "commitsha123", "type": "commit"}}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/commitsha123"),
|
|
httpmock.StringResponse(fmt.Sprintf(`{"sha": "commitsha123", "tree": [{"path": "skills/monalisa-skill/SKILL.md", "type": "blob", "sha": "blobsha1"}, {"path": "skills/monalisa-skill", "type": "tree", "sha": "abc123def456"}, {"path": "skills", "type": "tree", "sha": "treeshaX"}], "truncated": false}`)),
|
|
)
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
}
|
|
},
|
|
wantStderr: "All skills are up to date.",
|
|
},
|
|
{
|
|
name: "dry run reports available updates",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "hubot-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: hubot-skill
|
|
metadata:
|
|
github-repo: https://github.com/hubot/octocat-skills
|
|
github-tree-sha: oldsha123
|
|
github-path: skills/hubot-skill
|
|
---
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/hubot/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v2.0.0"}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/hubot/octocat-skills/git/ref/tags/v2.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "newcommit456", "type": "commit"}}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/hubot/octocat-skills/git/trees/newcommit456"),
|
|
httpmock.StringResponse(`{"sha": "newcommit456", "tree": [{"path": "skills/hubot-skill/SKILL.md", "type": "blob", "sha": "blobsha2"}, {"path": "skills/hubot-skill", "type": "tree", "sha": "newsha456"}, {"path": "skills", "type": "tree", "sha": "treeshaY"}], "truncated": false}`),
|
|
)
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Prompter: &prompter.PrompterMock{},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
DryRun: true,
|
|
}
|
|
},
|
|
wantStderr: "1 update(s) available:",
|
|
wantStdout: "hubot-skill",
|
|
},
|
|
{
|
|
name: "non-interactive without --all errors",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "hubot-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: hubot-skill
|
|
metadata:
|
|
github-repo: https://github.com/hubot/octocat-skills
|
|
github-tree-sha: oldsha123
|
|
github-path: skills/hubot-skill
|
|
---
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/hubot/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v2.0.0"}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/hubot/octocat-skills/git/ref/tags/v2.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "newcommit456", "type": "commit"}}`),
|
|
)
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/hubot/octocat-skills/git/trees/newcommit456"),
|
|
httpmock.StringResponse(`{"sha": "newcommit456", "tree": [{"path": "skills/hubot-skill/SKILL.md", "type": "blob", "sha": "blobsha2"}, {"path": "skills/hubot-skill", "type": "tree", "sha": "newsha456"}, {"path": "skills", "type": "tree", "sha": "treeshaY"}], "truncated": false}`),
|
|
)
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
ios.SetStdinTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
}
|
|
},
|
|
wantErr: "updates available; re-run with --all to apply, or run interactively to confirm",
|
|
},
|
|
{
|
|
name: "force update rewrites SKILL.md on disk",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
t.Setenv("USERPROFILE", homeDir)
|
|
skillDir := filepath.Join(dir, "code-review")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: code-review
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/octocat-skills
|
|
github-tree-sha: oldsha000
|
|
github-path: skills/code-review
|
|
---
|
|
Old content
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v3.0.0"}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/ref/tags/v3.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "newcommit789", "type": "commit"}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newcommit789"),
|
|
httpmock.StringResponse(`{"sha": "newcommit789", "tree": [{"path": "skills/code-review/SKILL.md", "type": "blob", "sha": "newblob1"}, {"path": "skills/code-review", "type": "tree", "sha": "newsha999"}, {"path": "skills", "type": "tree", "sha": "treeshaZ"}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newsha999"),
|
|
httpmock.StringResponse(`{"sha": "newsha999", "tree": [{"path": "SKILL.md", "type": "blob", "sha": "newblob1", "size": 20}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/blobs/newblob1"),
|
|
httpmock.StringResponse(fmt.Sprintf(`{"sha": "newblob1", "encoding": "base64", "content": "%s"}`,
|
|
"IyBDb2RlIFJldmlldyBVcGRhdGVk")))
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
All: true,
|
|
Force: true,
|
|
}
|
|
},
|
|
verify: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
content, err := os.ReadFile(filepath.Join(dir, "code-review", "SKILL.md"))
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(content), "github-repo: https://github.com/monalisa/octocat-skills")
|
|
assert.NotContains(t, string(content), "Old content")
|
|
},
|
|
wantStdout: "Updated code-review",
|
|
},
|
|
{
|
|
name: "namespaced skill with --dir resolves install base correctly",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
t.Setenv("USERPROFILE", homeDir)
|
|
skillDir := filepath.Join(dir, "monalisa", "code-review")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: code-review
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/octocat-skills
|
|
github-tree-sha: oldsha000
|
|
github-path: skills/monalisa/code-review
|
|
---
|
|
Old namespaced content
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v3.0.0"}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/ref/tags/v3.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "newcommit789", "type": "commit"}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newcommit789"),
|
|
httpmock.StringResponse(`{"sha": "newcommit789", "tree": [{"path": "skills/monalisa/code-review/SKILL.md", "type": "blob", "sha": "newblob1"}, {"path": "skills/monalisa/code-review", "type": "tree", "sha": "newsha999"}, {"path": "skills/monalisa", "type": "tree", "sha": "nstresha"}, {"path": "skills", "type": "tree", "sha": "treeshaZ"}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newsha999"),
|
|
httpmock.StringResponse(`{"sha": "newsha999", "tree": [{"path": "SKILL.md", "type": "blob", "sha": "newblob1", "size": 20}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/blobs/newblob1"),
|
|
httpmock.StringResponse(fmt.Sprintf(`{"sha": "newblob1", "encoding": "base64", "content": "%s"}`,
|
|
"IyBOYW1lc3BhY2VkIFNraWxsIFVwZGF0ZWQ=")))
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
All: true,
|
|
Force: true,
|
|
}
|
|
},
|
|
verify: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
// After update, skill should be installed flat (not namespaced).
|
|
content, err := os.ReadFile(filepath.Join(dir, "code-review", "SKILL.md"))
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(content), "github-repo: https://github.com/monalisa/octocat-skills")
|
|
assert.NotContains(t, string(content), "Old namespaced content")
|
|
// Old namespaced directory should be cleaned up.
|
|
_, err = os.Stat(filepath.Join(dir, "monalisa", "code-review"))
|
|
assert.True(t, os.IsNotExist(err), "old namespaced directory should be removed")
|
|
},
|
|
wantStdout: "Updated monalisa/code-review",
|
|
},
|
|
{
|
|
name: "install failure during update reports error and continues",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
t.Setenv("USERPROFILE", homeDir)
|
|
skillDir := filepath.Join(dir, "code-review")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: code-review
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/octocat-skills
|
|
github-tree-sha: oldsha000
|
|
github-path: skills/code-review
|
|
---
|
|
Original content
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v3.0.0"}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/ref/tags/v3.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "newcommit789", "type": "commit"}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newcommit789"),
|
|
httpmock.StringResponse(`{"sha": "newcommit789", "tree": [{"path": "skills/code-review/SKILL.md", "type": "blob", "sha": "newblob1"}, {"path": "skills/code-review", "type": "tree", "sha": "newsha999"}, {"path": "skills", "type": "tree", "sha": "treeshaZ"}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newsha999"),
|
|
httpmock.StringResponse(`{"sha": "newsha999", "tree": [{"path": "SKILL.md", "type": "blob", "sha": "newblob1", "size": 20}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/blobs/newblob1"),
|
|
httpmock.StatusStringResponse(500, "server error"))
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
All: true,
|
|
}
|
|
},
|
|
verify: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
content, err := os.ReadFile(filepath.Join(dir, "code-review", "SKILL.md"))
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(content), "Original content", "file should not be modified on failure")
|
|
},
|
|
wantStderr: "Failed to update code-review",
|
|
wantErr: "SilentError",
|
|
},
|
|
{
|
|
name: "interactive confirm applies update",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
t.Setenv("USERPROFILE", homeDir)
|
|
skillDir := filepath.Join(dir, "code-review")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: code-review
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/octocat-skills
|
|
github-tree-sha: oldsha000
|
|
github-path: skills/code-review
|
|
---
|
|
Old content
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v3.0.0"}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/ref/tags/v3.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "newcommit789", "type": "commit"}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newcommit789"),
|
|
httpmock.StringResponse(`{"sha": "newcommit789", "tree": [{"path": "skills/code-review/SKILL.md", "type": "blob", "sha": "newblob1"}, {"path": "skills/code-review", "type": "tree", "sha": "newsha999"}, {"path": "skills", "type": "tree", "sha": "treeshaZ"}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newsha999"),
|
|
httpmock.StringResponse(`{"sha": "newsha999", "tree": [{"path": "SKILL.md", "type": "blob", "sha": "newblob1", "size": 20}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/blobs/newblob1"),
|
|
httpmock.StringResponse(fmt.Sprintf(`{"sha": "newblob1", "encoding": "base64", "content": "%s"}`,
|
|
"IyBDb2RlIFJldmlldyBVcGRhdGVk")))
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStdinTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Prompter: &prompter.PrompterMock{
|
|
ConfirmFunc: func(msg string, defaultVal bool) (bool, error) {
|
|
return true, nil
|
|
},
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
}
|
|
},
|
|
verify: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
content, err := os.ReadFile(filepath.Join(dir, "code-review", "SKILL.md"))
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, string(content), "Old content")
|
|
},
|
|
wantStdout: "Updated code-review",
|
|
},
|
|
{
|
|
name: "interactive confirm cancelled",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "code-review")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: code-review
|
|
metadata:
|
|
github-repo: https://github.com/monalisa/octocat-skills
|
|
github-tree-sha: oldsha000
|
|
github-path: skills/code-review
|
|
---
|
|
Old content
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v3.0.0"}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/ref/tags/v3.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "newcommit789", "type": "commit"}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newcommit789"),
|
|
httpmock.StringResponse(`{"sha": "newcommit789", "tree": [{"path": "skills/code-review/SKILL.md", "type": "blob", "sha": "newblob1"}, {"path": "skills/code-review", "type": "tree", "sha": "newsha999"}, {"path": "skills", "type": "tree", "sha": "treeshaZ"}], "truncated": false}`))
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStdinTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Prompter: &prompter.PrompterMock{
|
|
ConfirmFunc: func(msg string, defaultVal bool) (bool, error) {
|
|
return false, nil
|
|
},
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
}
|
|
},
|
|
wantErr: "CancelError",
|
|
wantStderr: "Update cancelled",
|
|
},
|
|
{
|
|
name: "no-metadata skill prompted interactively and skipped",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "manual-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: manual-skill
|
|
---
|
|
No metadata
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStdinTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Prompter: &prompter.PrompterMock{
|
|
InputFunc: func(prompt string, defaultValue string) (string, error) {
|
|
return "", nil
|
|
},
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
}
|
|
},
|
|
wantStderr: "no GitHub metadata",
|
|
},
|
|
{
|
|
name: "no-metadata skill enriched via prompt then updated",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
t.Setenv("USERPROFILE", homeDir)
|
|
skillDir := filepath.Join(dir, "manual-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: manual-skill
|
|
---
|
|
Old manual content
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v1.0.0"}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/ref/tags/v1.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "commit123", "type": "commit"}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/commit123"),
|
|
httpmock.StringResponse(`{"sha": "commit123", "tree": [{"path": "skills/manual-skill/SKILL.md", "type": "blob", "sha": "blob1"}, {"path": "skills/manual-skill", "type": "tree", "sha": "newtree1"}, {"path": "skills", "type": "tree", "sha": "treeshaX"}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/trees/newtree1"),
|
|
httpmock.StringResponse(`{"sha": "newtree1", "tree": [{"path": "SKILL.md", "type": "blob", "sha": "blob1", "size": 20}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/monalisa/octocat-skills/git/blobs/blob1"),
|
|
httpmock.StringResponse(fmt.Sprintf(`{"sha": "blob1", "encoding": "base64", "content": "%s"}`,
|
|
"IyBNYW51YWwgU2tpbGwgVXBkYXRlZA==")))
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStdinTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Prompter: &prompter.PrompterMock{
|
|
InputFunc: func(prompt string, defaultValue string) (string, error) {
|
|
return "monalisa/octocat-skills", nil
|
|
},
|
|
ConfirmFunc: func(msg string, defaultVal bool) (bool, error) {
|
|
return true, nil
|
|
},
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
}
|
|
},
|
|
verify: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
content, err := os.ReadFile(filepath.Join(dir, "manual-skill", "SKILL.md"))
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, string(content), "Old manual content")
|
|
assert.Contains(t, string(content), "github-repo: https://github.com/monalisa/octocat-skills")
|
|
},
|
|
wantStdout: "Updated manual-skill",
|
|
},
|
|
{
|
|
name: "unpin clears pin and applies update",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
t.Setenv("USERPROFILE", homeDir)
|
|
skillDir := filepath.Join(dir, "pinned-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: pinned-skill
|
|
metadata:
|
|
github-repo: https://github.com/octocat/hubot-skills
|
|
github-tree-sha: oldsha000
|
|
github-pinned: v1.0.0
|
|
github-path: skills/pinned-skill
|
|
---
|
|
Pinned content
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/octocat/hubot-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v2.0.0"}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/octocat/hubot-skills/git/ref/tags/v2.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "newcommit789", "type": "commit"}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/octocat/hubot-skills/git/trees/newcommit789"),
|
|
httpmock.StringResponse(`{"sha": "newcommit789", "tree": [{"path": "skills/pinned-skill/SKILL.md", "type": "blob", "sha": "newblob1"}, {"path": "skills/pinned-skill", "type": "tree", "sha": "newsha999"}, {"path": "skills", "type": "tree", "sha": "treeshaZ"}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/octocat/hubot-skills/git/trees/newsha999"),
|
|
httpmock.StringResponse(`{"sha": "newsha999", "tree": [{"path": "SKILL.md", "type": "blob", "sha": "newblob1", "size": 20}], "truncated": false}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/octocat/hubot-skills/git/blobs/newblob1"),
|
|
httpmock.StringResponse(fmt.Sprintf(`{"sha": "newblob1", "encoding": "base64", "content": "%s"}`,
|
|
"IyBVbnBpbm5lZCBhbmQgVXBkYXRlZA==")))
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(false)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
All: true,
|
|
Unpin: true,
|
|
}
|
|
},
|
|
verify: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
content, err := os.ReadFile(filepath.Join(dir, "pinned-skill", "SKILL.md"))
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, string(content), "Pinned content")
|
|
assert.NotContains(t, string(content), "github-pinned")
|
|
},
|
|
wantStdout: "Updated pinned-skill",
|
|
},
|
|
{
|
|
name: "pinned skills still skipped without --unpin",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "pinned-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: pinned-skill
|
|
metadata:
|
|
github-repo: https://github.com/octocat/hubot-skills
|
|
github-tree-sha: abc123
|
|
github-pinned: v1.0.0
|
|
---
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Prompter: &prompter.PrompterMock{},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
Unpin: false,
|
|
}
|
|
},
|
|
wantStderr: "pinned",
|
|
},
|
|
{
|
|
name: "unpin with dry-run reports update without modifying files",
|
|
setup: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
skillDir := filepath.Join(dir, "pinned-skill")
|
|
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(heredoc.Doc(`
|
|
---
|
|
name: pinned-skill
|
|
metadata:
|
|
github-repo: https://github.com/octocat/hubot-skills
|
|
github-tree-sha: oldsha000
|
|
github-pinned: v1.0.0
|
|
github-path: skills/pinned-skill
|
|
---
|
|
Pinned content
|
|
`)), 0o644))
|
|
},
|
|
stubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/octocat/hubot-skills/releases/latest"),
|
|
httpmock.StringResponse(`{"tag_name": "v2.0.0"}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/octocat/hubot-skills/git/ref/tags/v2.0.0"),
|
|
httpmock.StringResponse(`{"object": {"sha": "newcommit789", "type": "commit"}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "repos/octocat/hubot-skills/git/trees/newcommit789"),
|
|
httpmock.StringResponse(`{"sha": "newcommit789", "tree": [{"path": "skills/pinned-skill/SKILL.md", "type": "blob", "sha": "newblob1"}, {"path": "skills/pinned-skill", "type": "tree", "sha": "newsha999"}, {"path": "skills", "type": "tree", "sha": "treeshaZ"}], "truncated": false}`))
|
|
},
|
|
opts: func(ios *iostreams.IOStreams, dir string, reg *httpmock.Registry) *UpdateOptions {
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
return &UpdateOptions{
|
|
IO: ios,
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Prompter: &prompter.PrompterMock{},
|
|
GitClient: &git.Client{RepoDir: dir},
|
|
Dir: dir,
|
|
DryRun: true,
|
|
Unpin: true,
|
|
}
|
|
},
|
|
verify: func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
content, err := os.ReadFile(filepath.Join(dir, "pinned-skill", "SKILL.md"))
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(content), "github-pinned: v1.0.0", "dry-run should not modify files")
|
|
},
|
|
wantStderr: "1 update(s) available:",
|
|
wantStdout: "pinned-skill",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ios, _, stdout, stderr := iostreams.Test()
|
|
|
|
dir := t.TempDir()
|
|
if tt.setup != nil {
|
|
tt.setup(t, dir)
|
|
}
|
|
|
|
reg := &httpmock.Registry{}
|
|
defer reg.Verify(t)
|
|
if tt.stubs != nil {
|
|
tt.stubs(reg)
|
|
}
|
|
|
|
opts := tt.opts(ios, dir, reg)
|
|
err := updateRun(opts)
|
|
|
|
if tt.wantErr != "" {
|
|
assert.EqualError(t, err, tt.wantErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
if tt.wantStderr != "" {
|
|
assert.Contains(t, stderr.String(), tt.wantStderr)
|
|
}
|
|
if tt.wantStdout != "" {
|
|
assert.Contains(t, stdout.String(), tt.wantStdout)
|
|
}
|
|
if tt.verify != nil {
|
|
tt.verify(t, dir)
|
|
}
|
|
})
|
|
}
|
|
}
|