using System.IO; using AxCopilot.Services.Agent; using FluentAssertions; using Xunit; namespace AxCopilot.Tests.Services; public class WorkspaceContextGeneratorTests { // ═══════════════════════════════════════════ // LoadContext // ═══════════════════════════════════════════ [Fact] public void LoadContext_NullFolder_ReturnsNull() { WorkspaceContextGenerator.LoadContext(null).Should().BeNull(); } [Fact] public void LoadContext_EmptyFolder_ReturnsNull() { WorkspaceContextGenerator.LoadContext("").Should().BeNull(); } [Fact] public void LoadContext_NonExistentFile_ReturnsNull() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { // .ax-context.md가 없으면 null WorkspaceContextGenerator.LoadContext(tempDir).Should().BeNull(); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public void LoadContext_ExistingFile_ReturnsContent() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { var contextPath = Path.Combine(tempDir, ".ax-context.md"); File.WriteAllText(contextPath, "# Test Context\nHello"); var result = WorkspaceContextGenerator.LoadContext(tempDir); result.Should().NotBeNull(); result.Should().Contain("Test Context"); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public void LoadContext_LargeFile_Truncated() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { var contextPath = Path.Combine(tempDir, ".ax-context.md"); // 4000자 제한 초과하는 내용 작성 File.WriteAllText(contextPath, new string('A', 5000)); var result = WorkspaceContextGenerator.LoadContext(tempDir); result.Should().NotBeNull(); result!.Should().Contain("(truncated)"); result!.Length.Should().BeLessOrEqualTo(4100); // 4000 + truncation message } finally { Directory.Delete(tempDir, recursive: true); } } // ═══════════════════════════════════════════ // EnsureContextAsync — 멱등성 // ═══════════════════════════════════════════ [Fact] public async Task EnsureContextAsync_NullFolder_ReturnsNull() { var result = await WorkspaceContextGenerator.EnsureContextAsync(null!); result.Should().BeNull(); } [Fact] public async Task EnsureContextAsync_NonExistentFolder_ReturnsNull() { var result = await WorkspaceContextGenerator.EnsureContextAsync("/nonexistent/path/xyz"); result.Should().BeNull(); } [Fact] public async Task EnsureContextAsync_ExistingContextFile_ReturnsWithoutRegeneration() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { var contextPath = Path.Combine(tempDir, ".ax-context.md"); File.WriteAllText(contextPath, "# Pre-existing context"); var result = await WorkspaceContextGenerator.EnsureContextAsync(tempDir); result.Should().Contain("Pre-existing context"); } finally { Directory.Delete(tempDir, recursive: true); } } // ═══════════════════════════════════════════ // GenerateAsync — 실제 생성 // ═══════════════════════════════════════════ [Fact] public async Task GenerateAsync_CreatesContextFile() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { // 테스트 파일 구조 생성 File.WriteAllText(Path.Combine(tempDir, "test.cs"), "class Test {}"); File.WriteAllText(Path.Combine(tempDir, "helper.cs"), "class Helper {}"); var subDir = Path.Combine(tempDir, "src"); Directory.CreateDirectory(subDir); File.WriteAllText(Path.Combine(subDir, "main.cs"), "class Main {}"); var result = await WorkspaceContextGenerator.GenerateAsync(tempDir); result.Should().NotBeNullOrEmpty(); result.Should().Contain("# Workspace Context (auto-generated)"); result.Should().Contain("## Project"); result.Should().Contain("## Structure"); result.Should().Contain(".cs"); // 파일 생성 확인 File.Exists(Path.Combine(tempDir, ".ax-context.md")).Should().BeTrue(); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task GenerateAsync_DetectsDotNetBuildSystem() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { File.WriteAllText(Path.Combine(tempDir, "MyProject.sln"), "solution content"); var result = await WorkspaceContextGenerator.GenerateAsync(tempDir); result.Should().Contain(".NET"); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task GenerateAsync_DetectsNodeJsBuildSystem() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { File.WriteAllText(Path.Combine(tempDir, "package.json"), """{"name": "test"}"""); var result = await WorkspaceContextGenerator.GenerateAsync(tempDir); result.Should().Contain("Node.js"); result.Should().Contain("## Key Manifests"); result.Should().Contain("Node package"); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task GenerateAsync_IncludesLanguageSnapshot() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { File.WriteAllText(Path.Combine(tempDir, "main.go"), "package main"); File.WriteAllText(Path.Combine(tempDir, "worker.go"), "package main"); File.WriteAllText(Path.Combine(tempDir, "helper.py"), "print('hi')"); var result = await WorkspaceContextGenerator.GenerateAsync(tempDir); result.Should().Contain("## Language Snapshot"); result.Should().Contain("Go:"); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task GenerateAsync_IncludesReadmeSummary() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { File.WriteAllText(Path.Combine(tempDir, "README.md"), "# My Project\n\nThis is a test project for unit testing."); var result = await WorkspaceContextGenerator.GenerateAsync(tempDir); result.Should().Contain("## README Summary"); result.Should().Contain("test project"); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task GenerateAsync_DetectsContextFiles() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { File.WriteAllText(Path.Combine(tempDir, "AGENTS.md"), "Agent rules here"); var skillsDir = Path.Combine(tempDir, ".claude", "skills", "deck"); Directory.CreateDirectory(skillsDir); File.WriteAllText(Path.Combine(skillsDir, "SKILL.md"), "# skill"); var result = await WorkspaceContextGenerator.GenerateAsync(tempDir); result.Should().Contain("## Existing Context Files"); result.Should().Contain("AGENTS.md"); result.Should().Contain("## Agent Context"); result.Should().Contain(".claude/skills"); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task GenerateAsync_SkipsHiddenDirs() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { // node_modules는 건너뛰어야 함 var nodeModules = Path.Combine(tempDir, "node_modules"); Directory.CreateDirectory(nodeModules); File.WriteAllText(Path.Combine(nodeModules, "package.json"), "{}"); // src는 표시되어야 함 var src = Path.Combine(tempDir, "src"); Directory.CreateDirectory(src); File.WriteAllText(Path.Combine(src, "main.cs"), "class Main {}"); var result = await WorkspaceContextGenerator.GenerateAsync(tempDir); result.Should().Contain("src/"); result.Should().NotContain("node_modules/"); } finally { Directory.Delete(tempDir, recursive: true); } } // ═══════════════════════════════════════════ // 취소 지원 // ═══════════════════════════════════════════ [Fact] public async Task EnsureContextAsync_Cancellation_Throws() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { using var cts = new CancellationTokenSource(); cts.Cancel(); // 이미 취소된 토큰으로 호출 — 파일이 없으므로 GenerateAsync 진입 // OperationCanceledException 또는 정상 반환 (구현에 따라) // 최소한 크래시하지 않으면 OK try { await WorkspaceContextGenerator.EnsureContextAsync(tempDir, cts.Token); } catch (OperationCanceledException) { // 예상된 동작 } } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task GenerateAsync_IncludesLanguageWorkflowHints() { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { File.WriteAllText(Path.Combine(tempDir, "Cargo.toml"), "[package]\nname='sample'"); File.WriteAllText(Path.Combine(tempDir, "main.rs"), "fn main() {}"); var result = await WorkspaceContextGenerator.GenerateAsync(tempDir); result.Should().Contain("## Language Workflow"); result.Should().Contain("Rust:"); result.Should().Contain("Cargo.toml"); result.Should().Contain("cargo build"); } finally { Directory.Delete(tempDir, recursive: true); } } }