using AxCopilot.Services.Agent; using FluentAssertions; using Xunit; using System; using System.IO; using System.Reflection; using System.Text; namespace AxCopilot.Tests.Services; public class SkillServiceRuntimePolicyTests { [Fact] public void BuildRuntimeDirective_ReturnsEmpty_WhenNoRuntimeMetadata() { var skill = new SkillDefinition { Name = "plain-skill", SystemPrompt = "do work" }; var directive = SkillService.BuildRuntimeDirective(skill); directive.Should().BeEmpty(); } [Fact] public void BuildRuntimeDirective_ContainsForkAgentEffortAndModelHints() { var skill = new SkillDefinition { Name = "advanced-skill", ExecutionContext = "fork", Agent = "worker", Effort = "high", Model = "gpt-5.4", DisableModelInvocation = true, AllowedTools = "Read, process, WebFetch", Hooks = "lint-pre, verify-post", HookFilters = "lint-pre@pre@file_edit, verify-post@post@*" }; var directive = SkillService.BuildRuntimeDirective(skill); directive.Should().Contain("[Skill Runtime Policy]"); directive.Should().Contain("execution_context: fork"); directive.Should().Contain("preferred_agent: worker"); directive.Should().Contain("reasoning_effort: high"); directive.Should().Contain("preferred_model: gpt-5.4"); directive.Should().Contain("allowed_tools: file_read, http_tool, process"); directive.Should().Contain("hook_names: lint-pre, verify-post"); directive.Should().Contain("hook_filters: lint-pre@pre@file_edit, verify-post@post@*"); directive.Should().Contain("disable_model_invocation"); } [Fact] public void ParseSkillFile_HooksMapAndList_AreNormalizedIntoHooksField() { var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-hooks-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); var skillPath = Path.Combine(tempDir, "SKILL.md"); try { var content = """ --- name: hook-skill hooks: pre: lint-pre, verify-pre post: - verify-post - report-post --- 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]) as SkillDefinition; parsed.Should().NotBeNull(); parsed!.Hooks.Should().Contain("lint-pre"); parsed.Hooks.Should().Contain("verify-pre"); parsed.Hooks.Should().Contain("verify-post"); parsed.Hooks.Should().Contain("report-post"); parsed.HookFilters.Should().Contain("lint-pre@pre@*"); parsed.HookFilters.Should().Contain("verify-pre@pre@*"); parsed.HookFilters.Should().Contain("*@post@*"); } finally { try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } } } [Fact] public void ParseSkillFile_NestedHooksMap_PreservesToolTimingFilters() { var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-hooks-nested-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); var skillPath = Path.Combine(tempDir, "SKILL.md"); try { var content = """ --- name: nested-hook-skill hooks: file_edit: pre: - lint-pre post: verify-post --- 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]) as SkillDefinition; parsed.Should().NotBeNull(); parsed!.Hooks.Should().Contain("lint-pre"); parsed.Hooks.Should().Contain("verify-post"); parsed.HookFilters.Should().Contain("lint-pre@pre@file_edit"); parsed.HookFilters.Should().Contain("verify-post@post@file_edit"); } finally { try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } } } [Fact] public void ParseSkillFile_SampleFlag_SetsIsSample() { var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-sample-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); var skillPath = Path.Combine(tempDir, "SKILL.md"); try { var content = """ --- name: sample-skill sample: true --- 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]) as SkillDefinition; parsed.Should().NotBeNull(); parsed!.IsSample.Should().BeTrue(); } finally { try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } } } }