package cmd import ( "os" "path/filepath" "strings" "testing" "github.com/onyx-dot-app/onyx/cli/internal/embedded" "github.com/onyx-dot-app/onyx/cli/internal/testutil" ) func TestInstallSkillCmd_BasicInstall(t *testing.T) { tmpDir := t.TempDir() origDir, err := os.Getwd() if err != nil { t.Fatalf("failed to get working directory: %v", err) } if err := os.Chdir(tmpDir); err != nil { t.Fatalf("failed to chdir: %v", err) } t.Cleanup(func() { if err := os.Chdir(origDir); err != nil { t.Logf("warning: failed to restore working directory: %v", err) } }) ios, out, _ := testutil.TestIOStreams() cmd := newInstallSkillCmd(ios) cmd.SilenceErrors = true cmd.SilenceUsage = true if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error: %v", err) } canonicalPath := filepath.Join(tmpDir, ".agents", "skills", "onyx-cli", "SKILL.md") content, err := os.ReadFile(canonicalPath) if err != nil { t.Fatalf("expected canonical file at %s, got error: %v", canonicalPath, err) } if string(content) != embedded.SkillMD { t.Fatalf("canonical file content does not match embedded SKILL.md") } output := out.String() if !strings.Contains(output, "Installed") { t.Errorf("expected 'Installed' in output, got: %s", output) } } func TestInstallSkillCmd_GlobalInstall(t *testing.T) { tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) ios, out, _ := testutil.TestIOStreams() cmd := newInstallSkillCmd(ios) cmd.SetArgs([]string{"--global"}) cmd.SilenceErrors = true cmd.SilenceUsage = true if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error: %v", err) } canonicalPath := filepath.Join(tmpHome, ".agents", "skills", "onyx-cli", "SKILL.md") content, err := os.ReadFile(canonicalPath) if err != nil { t.Fatalf("expected canonical file at %s, got error: %v", canonicalPath, err) } if string(content) != embedded.SkillMD { t.Fatalf("canonical file content does not match embedded SKILL.md") } output := out.String() if !strings.Contains(output, "Installed") { t.Errorf("expected 'Installed' in output, got: %s", output) } } func TestInstallSkillCmd_UpToDate(t *testing.T) { tmpDir := t.TempDir() origDir, err := os.Getwd() if err != nil { t.Fatalf("failed to get working directory: %v", err) } if err := os.Chdir(tmpDir); err != nil { t.Fatalf("failed to chdir: %v", err) } t.Cleanup(func() { if err := os.Chdir(origDir); err != nil { t.Logf("warning: failed to restore working directory: %v", err) } }) // First install. ios1, _, _ := testutil.TestIOStreams() cmd1 := newInstallSkillCmd(ios1) cmd1.SilenceErrors = true cmd1.SilenceUsage = true if err := cmd1.Execute(); err != nil { t.Fatalf("first install failed: %v", err) } // Second install should report "Up to date". ios2, out2, _ := testutil.TestIOStreams() cmd2 := newInstallSkillCmd(ios2) cmd2.SilenceErrors = true cmd2.SilenceUsage = true if err := cmd2.Execute(); err != nil { t.Fatalf("second install failed: %v", err) } output := out2.String() if !strings.Contains(output, "Up to date") { t.Errorf("expected 'Up to date' in output, got: %s", output) } } func TestInstallSkillCmd_CopyMode(t *testing.T) { tmpDir := t.TempDir() origDir, err := os.Getwd() if err != nil { t.Fatalf("failed to get working directory: %v", err) } if err := os.Chdir(tmpDir); err != nil { t.Fatalf("failed to chdir: %v", err) } t.Cleanup(func() { if err := os.Chdir(origDir); err != nil { t.Logf("warning: failed to restore working directory: %v", err) } }) ios, out, _ := testutil.TestIOStreams() cmd := newInstallSkillCmd(ios) cmd.SetArgs([]string{"--copy"}) cmd.SilenceErrors = true cmd.SilenceUsage = true if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error: %v", err) } // Verify canonical copy exists. canonicalPath := filepath.Join(tmpDir, ".agents", "skills", "onyx-cli", "SKILL.md") content, err := os.ReadFile(canonicalPath) if err != nil { t.Fatalf("expected canonical file at %s, got error: %v", canonicalPath, err) } if string(content) != embedded.SkillMD { t.Fatalf("canonical file content does not match embedded SKILL.md") } // Verify claude-code agent copy exists and is a real file, not a symlink. claudePath := filepath.Join(tmpDir, ".claude", "skills", "onyx-cli", "SKILL.md") info, err := os.Lstat(claudePath) if err != nil { t.Fatalf("expected claude-code copy at %s, got error: %v", claudePath, err) } if info.Mode()&os.ModeSymlink != 0 { t.Fatalf("expected regular file for --copy mode, got symlink at %s", claudePath) } claudeContent, err := os.ReadFile(claudePath) if err != nil { t.Fatalf("failed to read claude-code copy: %v", err) } if string(claudeContent) != embedded.SkillMD { t.Fatalf("claude-code copy content does not match embedded SKILL.md") } output := out.String() if !strings.Contains(output, "Copied") { t.Errorf("expected 'Copied' in output, got: %s", output) } } func TestInstallSkillCmd_RefusesSymlinkedDestinations(t *testing.T) { cases := []struct { name string args []string plant func(t *testing.T, base, target string) }{ { name: "canonical link to an existing file", plant: func(t *testing.T, base, target string) { writeTestFile(t, target, "original") linkTestFile(t, target, filepath.Join(base, ".agents", "skills", "onyx-cli", "SKILL.md")) }, }, { name: "dangling canonical link", plant: func(t *testing.T, base, target string) { linkTestFile(t, target, filepath.Join(base, ".agents", "skills", "onyx-cli", "SKILL.md")) }, }, { name: "link inside a real agent dir", plant: func(t *testing.T, base, target string) { writeTestFile(t, target, "original") linkTestFile(t, target, filepath.Join(base, ".claude", "skills", "onyx-cli", "SKILL.md")) }, }, { name: "link inside a real agent dir with --copy", args: []string{"--copy"}, plant: func(t *testing.T, base, target string) { writeTestFile(t, target, "original") linkTestFile(t, target, filepath.Join(base, ".claude", "skills", "onyx-cli", "SKILL.md")) }, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { base := t.TempDir() t.Chdir(base) target := filepath.Join(t.TempDir(), "target") c.plant(t, base, target) ios, _, _ := testutil.TestIOStreams() cmd := newInstallSkillCmd(ios) cmd.SilenceErrors = true cmd.SilenceUsage = true cmd.SetArgs(c.args) if err := cmd.Execute(); err == nil { t.Fatal("expected install-skill to refuse the symlinked destination") } got, err := os.ReadFile(target) if err == nil && string(got) != "original" { t.Fatalf("link target was overwritten: %q", got) } }) } } func TestInstallSkillCmd_CommittedAgentDirLink(t *testing.T) { for _, args := range [][]string{nil, {"--copy"}} { t.Run(strings.Join(append([]string{"args"}, args...), " "), func(t *testing.T) { testCommittedAgentDirLink(t, args) }) } } func testCommittedAgentDirLink(t *testing.T, args []string) { // The Onyx repo commits .claude/skills -> ../.agents/skills. base := t.TempDir() t.Chdir(base) if err := os.MkdirAll(filepath.Join(base, ".agents", "skills"), 0o755); err != nil { t.Fatal(err) } linkTestFile(t, filepath.Join("..", ".agents", "skills"), filepath.Join(base, ".claude", "skills")) ios, out, _ := testutil.TestIOStreams() cmd := newInstallSkillCmd(ios) cmd.SilenceErrors = true cmd.SilenceUsage = true cmd.SetArgs(args) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out.String(), "Up to date "+filepath.Join(base, ".claude", "skills", "onyx-cli")) { t.Errorf("expected the linked agent dir reported up to date, got: %s", out.String()) } } func writeTestFile(t *testing.T, path, content string) { t.Helper() if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } } func linkTestFile(t *testing.T, target, link string) { t.Helper() if err := os.MkdirAll(filepath.Dir(link), 0o755); err != nil { t.Fatal(err) } if err := os.Symlink(target, link); err != nil { t.Fatal(err) } } func TestInstallSkillCmd_RefusesAgentDirLinkedOutside(t *testing.T) { for _, args := range [][]string{nil, {"--copy"}} { t.Run(strings.Join(append([]string{"args"}, args...), " "), func(t *testing.T) { base := t.TempDir() t.Chdir(base) outside := t.TempDir() // An existing entry there that a link step would remove. victim := filepath.Join(outside, "skills", "onyx-cli") writeTestFileMkdir(t, victim, "original") linkTestFile(t, outside, filepath.Join(base, ".claude")) ios, _, _ := testutil.TestIOStreams() cmd := newInstallSkillCmd(ios) cmd.SilenceErrors = true cmd.SilenceUsage = true cmd.SetArgs(args) if err := cmd.Execute(); err == nil { t.Fatal("expected a .claude link outside the project to be refused") } info, err := os.Lstat(victim) if err != nil || !info.Mode().IsRegular() { t.Fatalf("entry outside the project was changed: %v", err) } }) } } func writeTestFileMkdir(t *testing.T, path, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } writeTestFile(t, path, content) } func TestInstallSkillCmd_CopyReplacesEarlierLink(t *testing.T) { base := t.TempDir() t.Chdir(base) for _, args := range [][]string{nil, {"--copy"}} { ios, _, _ := testutil.TestIOStreams() cmd := newInstallSkillCmd(ios) cmd.SilenceErrors = true cmd.SilenceUsage = true cmd.SetArgs(args) if err := cmd.Execute(); err != nil { t.Fatalf("install-skill %v: %v", args, err) } } info, err := os.Lstat(filepath.Join(base, ".claude", "skills", "onyx-cli")) if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { t.Fatalf("expected --copy to replace the earlier link with a real directory: %v %v", info, err) } }