스킬 런타임 2차 고도화와 도구 노출 필터 정비
프로젝트 .claude/skills 재귀 로드와 namespaced SKILL.md 파싱을 추가하고 번들/사용자/프로젝트 스킬을 함께 노출하도록 SkillService와 설정 UI를 확장했다. 슬래시 스킬 호출 시 인자 치환, 스킬 폴더 변수 치환, inline shell 실행, when_to_use 기반 자동 스킬 가이드를 실제 ChatWindow 런타임 경로에 연결했다. blanket deny 권한은 모델 노출 전 활성 도구 목록에서 먼저 제외하도록 AgentLoopService를 보강했고 관련 테스트와 README/DEVELOPMENT 문서를 업데이트했다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase2\\ -p:IntermediateOutputPath=obj\\verify_phase2\\ (경고 0 / 오류 0) 검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase2_tests\\ -p:IntermediateOutputPath=obj\\verify_phase2_tests\\ (통과 16, 기존 WorkspaceContextGeneratorTests nullable 경고 1건 유지)
This commit is contained in:
@@ -78,7 +78,7 @@ public class SkillServiceRuntimePolicyTests
|
||||
var method = typeof(SkillService).GetMethod("ParseSkillFile", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var parsed = method!.Invoke(null, [skillPath]) as SkillDefinition;
|
||||
var parsed = method!.Invoke(null, [skillPath, tempDir]) as SkillDefinition;
|
||||
parsed.Should().NotBeNull();
|
||||
parsed!.Hooks.Should().Contain("lint-pre");
|
||||
parsed.Hooks.Should().Contain("verify-pre");
|
||||
@@ -119,7 +119,7 @@ public class SkillServiceRuntimePolicyTests
|
||||
var method = typeof(SkillService).GetMethod("ParseSkillFile", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var parsed = method!.Invoke(null, [skillPath]) as SkillDefinition;
|
||||
var parsed = method!.Invoke(null, [skillPath, tempDir]) as SkillDefinition;
|
||||
parsed.Should().NotBeNull();
|
||||
parsed!.Hooks.Should().Contain("lint-pre");
|
||||
parsed.Hooks.Should().Contain("verify-post");
|
||||
@@ -153,7 +153,7 @@ public class SkillServiceRuntimePolicyTests
|
||||
var method = typeof(SkillService).GetMethod("ParseSkillFile", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var parsed = method!.Invoke(null, [skillPath]) as SkillDefinition;
|
||||
var parsed = method!.Invoke(null, [skillPath, tempDir]) as SkillDefinition;
|
||||
parsed.Should().NotBeNull();
|
||||
parsed!.IsSample.Should().BeTrue();
|
||||
}
|
||||
@@ -162,4 +162,103 @@ public class SkillServiceRuntimePolicyTests
|
||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseSkillFile_AssignsNamespacedName_ForProjectSkillFolder()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-namespace-" + Guid.NewGuid().ToString("N"));
|
||||
var skillDir = Path.Combine(tempDir, ".claude", "skills", "release", "notes");
|
||||
Directory.CreateDirectory(skillDir);
|
||||
var skillPath = Path.Combine(skillDir, "SKILL.md");
|
||||
try
|
||||
{
|
||||
var content = """
|
||||
---
|
||||
description: release notes helper
|
||||
---
|
||||
|
||||
body
|
||||
""";
|
||||
File.WriteAllText(skillPath, content, Encoding.UTF8);
|
||||
|
||||
var method = typeof(SkillService).GetMethod("ParseSkillFile", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var parsed = method!.Invoke(null, [skillPath, Path.Combine(tempDir, ".claude", "skills")]) as SkillDefinition;
|
||||
parsed.Should().NotBeNull();
|
||||
parsed!.Name.Should().Be("release:notes");
|
||||
parsed.SourceScope.Should().Be("project");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildSlashInvocationAsync_ReplacesArguments_AndSkillDirectoryTokens()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-args-" + Guid.NewGuid().ToString("N"));
|
||||
var skillDir = Path.Combine(tempDir, ".claude", "skills", "release");
|
||||
Directory.CreateDirectory(skillDir);
|
||||
var skillPath = Path.Combine(skillDir, "SKILL.md");
|
||||
try
|
||||
{
|
||||
var content = """
|
||||
---
|
||||
argument-hint: <version> <audience>
|
||||
---
|
||||
|
||||
Release $version for $audience from ${AX_SKILL_DIR}
|
||||
Raw: $ARGUMENTS
|
||||
""";
|
||||
File.WriteAllText(skillPath, content, Encoding.UTF8);
|
||||
|
||||
SkillService.LoadSkills(projectRoot: tempDir);
|
||||
|
||||
var compiled = await SkillService.BuildSlashInvocationAsync("/release 1.2.3 qa-team", tempDir);
|
||||
|
||||
compiled.Should().NotBeNull();
|
||||
compiled!.Skill.Name.Should().Be("release");
|
||||
compiled.SystemPrompt.Should().Contain("Release 1.2.3 for qa-team");
|
||||
compiled.SystemPrompt.Should().Contain(skillDir.Replace('\\', '/'));
|
||||
compiled.SystemPrompt.Should().Contain("Raw: 1.2.3 qa-team");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAutoSkills_IncludesWhenToUseDrivenSkills()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-auto-" + Guid.NewGuid().ToString("N"));
|
||||
var skillDir = Path.Combine(tempDir, ".claude", "skills", "triage");
|
||||
Directory.CreateDirectory(skillDir);
|
||||
var skillPath = Path.Combine(skillDir, "SKILL.md");
|
||||
try
|
||||
{
|
||||
var content = """
|
||||
---
|
||||
user-invocable: false
|
||||
tabs: code
|
||||
when_to_use: build failure test error
|
||||
---
|
||||
|
||||
Investigate failures.
|
||||
""";
|
||||
File.WriteAllText(skillPath, content, Encoding.UTF8);
|
||||
|
||||
SkillService.LoadSkills(projectRoot: tempDir);
|
||||
|
||||
var autoSkills = SkillService.GetAutoSkills("Code");
|
||||
|
||||
autoSkills.Should().Contain(skill => skill.Name == "triage");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user