Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,7 @@ func componentPathsWithWorkspaceScoped(homeDir, workspaceDir string, scope Insta
if skills.IsSDDSkill(skillID) {
continue
}
path := skills.SkillPathForAgent(homeDir, adapter, skillID)
path := skills.SkillPathForAgent(targetDir, adapter, skillID)
if path != "" {
paths = append(paths, path)
}
Expand Down
84 changes: 80 additions & 4 deletions internal/cli/run_component_paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func TestComponentPathsWithWorkspaceOpenClawSDDUsesWorkspaceScopedSkills(t *test

func TestComponentPathsOpenClawSkillsSkipsSDDPhaseSkills(t *testing.T) {
home := t.TempDir()
workspace := t.TempDir()
adapters := resolveAdapters([]model.AgentID{model.AgentOpenClaw})
selection := model.Selection{
Skills: []model.SkillID{
Expand All @@ -206,23 +207,98 @@ func TestComponentPathsOpenClawSkillsSkipsSDDPhaseSkills(t *testing.T) {
},
}

paths := componentPathsWithWorkspace(home, t.TempDir(), selection, adapters, model.ComponentSkills)
// OpenClaw always uses workspaceDir when set, independent of scope.
paths := componentPathsWithWorkspace(home, workspace, selection, adapters, model.ComponentSkills)

want := filepath.Join(home, ".openclaw", "skills", "go-testing", "SKILL.md")
want := filepath.Join(workspace, ".openclaw", "skills", "go-testing", "SKILL.md")
if !containsPath(paths, want) {
t.Fatalf("componentPaths(skills,openclaw) missing portable skill path %q\npaths=%v", want, paths)
}

for _, unwanted := range []string{
filepath.Join(home, ".openclaw", "skills", "sdd-init", "SKILL.md"),
filepath.Join(home, ".openclaw", "skills", "sdd-onboard", "SKILL.md"),
filepath.Join(workspace, ".openclaw", "skills", "sdd-init", "SKILL.md"),
filepath.Join(workspace, ".openclaw", "skills", "sdd-onboard", "SKILL.md"),
} {
if containsPath(paths, unwanted) {
t.Fatalf("componentPaths(skills,openclaw) must not verify SDD phase skill path %q\npaths=%v", unwanted, paths)
}
}
}

func TestComponentPathsWorkspaceScopedSkillsUsesWorkspaceDir(t *testing.T) {
home := t.TempDir()
workspace := t.TempDir()
adapters := resolveAdapters([]model.AgentID{model.AgentClaudeCode})
selection := model.Selection{
Skills: []model.SkillID{
model.SkillGoTesting,
model.SkillBranchPR,
},
}

paths := componentPathsWithWorkspaceScoped(home, workspace, ScopeWorkspace, selection, adapters, model.ComponentSkills)

for _, want := range []string{
filepath.Join(workspace, ".claude", "skills", "go-testing", "SKILL.md"),
filepath.Join(workspace, ".claude", "skills", "branch-pr", "SKILL.md"),
} {
if !containsPath(paths, want) {
t.Fatalf("componentPathsWithWorkspaceScoped(skills,claude-code,workspace) missing workspace-scoped path %q\npaths=%v", want, paths)
}
}

for _, unwanted := range []string{
filepath.Join(home, ".claude", "skills", "go-testing", "SKILL.md"),
filepath.Join(home, ".claude", "skills", "branch-pr", "SKILL.md"),
} {
if containsPath(paths, unwanted) {
t.Fatalf("componentPathsWithWorkspaceScoped(skills,claude-code,workspace) must not include home-scoped path %q\npaths=%v", unwanted, paths)
}
}
}

// TestInstallWorkspaceScopeVerificationWithNoGlobalSkills verifies that
// post-apply verification succeeds when --scope=workspace is used and no
// global skill files exist. This is a regression test for issue #785:
// the verifier used to check home-scoped paths even when workspace scope
// was active, causing false failures when only workspace skills existed.
func TestInstallWorkspaceScopeVerificationWithNoGlobalSkills(t *testing.T) {
home := t.TempDir()
workspace := t.TempDir()
adapters := resolveAdapters([]model.AgentID{model.AgentClaudeCode})
selection := model.Selection{
Skills: []model.SkillID{
model.SkillGoTesting,
model.SkillBranchPR,
},
}

// Simulate workspace-scoped install: skills are written to workspace only.
// The verification should check workspace paths, not home paths.
paths := componentPathsWithWorkspaceScoped(home, workspace, ScopeWorkspace, selection, adapters, model.ComponentSkills)

// Verify that workspace paths are included (these should exist after install).
for _, want := range []string{
filepath.Join(workspace, ".claude", "skills", "go-testing", "SKILL.md"),
filepath.Join(workspace, ".claude", "skills", "branch-pr", "SKILL.md"),
} {
if !containsPath(paths, want) {
t.Fatalf("workspace-scoped verification missing workspace path %q\npaths=%v", want, paths)
}
}

// Verify that home paths are NOT included (these would cause false failures
// if checked when only workspace skills exist).
for _, unwanted := range []string{
filepath.Join(home, ".claude", "skills", "go-testing", "SKILL.md"),
filepath.Join(home, ".claude", "skills", "branch-pr", "SKILL.md"),
} {
if containsPath(paths, unwanted) {
t.Fatalf("workspace-scoped verification must not check home path %q when scope=workspace\npaths=%v", unwanted, paths)
}
}
}

func TestComponentPathsSDDKimiIncludesAgentFilesAndGlobalSkills(t *testing.T) {
home := t.TempDir()
adapters := resolveAdapters([]model.AgentID{model.AgentKimi})
Expand Down