feat(tools): add first-run external tool installer wizard and catalog
첫 실행 시 외부 도구 설치 마법사를 띄우고 7종(FFmpeg·LibreOffice·Pandoc·ImageMagick·H2Orestart·agy·Codex)을 기본 전체 선택 상태로 순차 설치한다. - Core: ExternalToolCatalog(정적 카탈로그, winget ID 실검증), ExternalToolInstaller(winget 무인 실행·재감지 판정, IExternalCommandRunner 시드로 테스트 가능) - Core: ExternalToolDetector에 winget FFmpeg 스캔 + Pandoc/ImageMagick 감지 추가 - App: ToolSetupWindow + ToolSetupViewModel, 첫 실행 플래그(setup.tools.done), 설정 창 '외부 도구 설치/검증' 재진입 버튼 - Tests: ExternalToolInstallerTests 5건 - 기존 미커밋 작업(광고 Ads 지원: AdBannerControl/AdLargeCardControl/AdService, AI 백엔드 agy·switchboard·codex 채팅 클라이언트, 관련 테스트) 포함
This commit is contained in:
parent
f00f6567f4
commit
6a41c4a0dc
37 changed files with 2545 additions and 69 deletions
105
src/Everything2Everything.Tests/AdDesignAuditTests.cs
Normal file
105
src/Everything2Everything.Tests/AdDesignAuditTests.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class AdDesignAuditTests
|
||||
{
|
||||
private static readonly string SolutionRoot = FindSolutionRoot();
|
||||
private static readonly string ViewsDir = Path.Combine(SolutionRoot, "src", "Everything2Everything.App", "Views");
|
||||
|
||||
private static string FindSolutionRoot()
|
||||
{
|
||||
var dir = Directory.GetCurrentDirectory();
|
||||
while (dir != null && !File.Exists(Path.Combine(dir, "Everything2Everything.slnx")))
|
||||
{
|
||||
dir = Directory.GetParent(dir)?.FullName;
|
||||
}
|
||||
return dir ?? throw new DirectoryNotFoundException("솔루션 루트를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdControls_MustExistAndBeWellFormed()
|
||||
{
|
||||
var bannerPath = Path.Combine(ViewsDir, "AdBannerControl.xaml");
|
||||
var largeCardPath = Path.Combine(ViewsDir, "AdLargeCardControl.xaml");
|
||||
|
||||
Assert.True(File.Exists(bannerPath), "AdBannerControl.xaml 이 존재해야 합니다.");
|
||||
Assert.True(File.Exists(largeCardPath), "AdLargeCardControl.xaml 이 존재해야 합니다.");
|
||||
|
||||
var bannerDoc = XDocument.Parse(File.ReadAllText(bannerPath));
|
||||
var largeCardDoc = XDocument.Parse(File.ReadAllText(largeCardPath));
|
||||
|
||||
Assert.NotNull(bannerDoc.Root);
|
||||
Assert.NotNull(largeCardDoc.Root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdControls_MustComplyWithStrictDesignAudit()
|
||||
{
|
||||
var targetFiles = new[]
|
||||
{
|
||||
Path.Combine(ViewsDir, "AdBannerControl.xaml"),
|
||||
Path.Combine(ViewsDir, "AdLargeCardControl.xaml")
|
||||
};
|
||||
|
||||
var rawEmojis = new[] { "⚙", "📁", "🗑", "✖", "🔧", "✨", "🚀", "⚡" };
|
||||
var violations = new List<string>();
|
||||
|
||||
foreach (var file in targetFiles)
|
||||
{
|
||||
if (!File.Exists(file)) continue;
|
||||
|
||||
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||
var fileName = Path.GetFileName(file);
|
||||
|
||||
// 1. 유니코드 이모지 금지 검증
|
||||
var buttons = doc.Descendants().Where(e => e.Name.LocalName is "Button" or "ToggleButton");
|
||||
foreach (var btn in buttons)
|
||||
{
|
||||
var content = btn.Attribute("Content")?.Value;
|
||||
if (!string.IsNullOrEmpty(content) && rawEmojis.Any(emoji => content.Contains(emoji)))
|
||||
{
|
||||
violations.Add($"[{fileName}] 버튼에 이모지 사용 감지: {content}");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 가로 StackPanel 내 아이콘/텍스트 VerticalAlignment="Center" 검증
|
||||
var stackPanels = doc.Descendants().Where(e => e.Name.LocalName == "StackPanel" &&
|
||||
e.Attribute("Orientation")?.Value == "Horizontal");
|
||||
foreach (var sp in stackPanels)
|
||||
{
|
||||
foreach (var child in sp.Elements())
|
||||
{
|
||||
var va = child.Attribute("VerticalAlignment")?.Value;
|
||||
if (va != "Center")
|
||||
{
|
||||
violations.Add($"[{fileName}] 가로 StackPanel의 자식 <{child.Name.LocalName}>에 VerticalAlignment=\"Center\" 누락");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. TextBlock 오버플로 방지 (TextTrimming 또는 TextWrapping) 검증
|
||||
var textBlocks = doc.Descendants().Where(e => e.Name.LocalName == "TextBlock");
|
||||
foreach (var tb in textBlocks)
|
||||
{
|
||||
var text = tb.Attribute("Text")?.Value ?? "";
|
||||
// 바인딩된 동적 텍스트는 Trimming이나 Wrapping 필수
|
||||
if (text.Contains("{Binding") || text.Contains("{x:Bind"))
|
||||
{
|
||||
var trimming = tb.Attribute("TextTrimming")?.Value;
|
||||
var wrapping = tb.Attribute("TextWrapping")?.Value;
|
||||
if (string.IsNullOrEmpty(trimming) && string.IsNullOrEmpty(wrapping))
|
||||
{
|
||||
violations.Add($"[{fileName}] 바인딩 TextBlock에 TextTrimming 또는 TextWrapping 누락: {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(violations.Count == 0,
|
||||
"광고 컨트롤 디자인 감사 위반이 발견되었습니다:\n" + string.Join("\n", violations));
|
||||
}
|
||||
}
|
||||
30
src/Everything2Everything.Tests/AdMobConfigTests.cs
Normal file
30
src/Everything2Everything.Tests/AdMobConfigTests.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using Everything2Everything.Core.Ads;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class AdMobConfigTests
|
||||
{
|
||||
[Fact]
|
||||
public void AdMobConfig_DefaultState_IsValid()
|
||||
{
|
||||
var config = new AdMobConfig();
|
||||
|
||||
Assert.NotNull(config.AppId);
|
||||
Assert.NotNull(config.BannerAdUnitId);
|
||||
Assert.NotNull(config.InterstitialAdUnitId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateAppAdsTxt_ReturnsValidGoogleSpecification()
|
||||
{
|
||||
var config = new AdMobConfig
|
||||
{
|
||||
PublisherId = "pub-1234567890123456"
|
||||
};
|
||||
|
||||
string appAdsTxt = config.GenerateAppAdsTxt();
|
||||
|
||||
Assert.Contains("google.com, pub-1234567890123456, DIRECT, f08c47fec0942fa0", appAdsTxt);
|
||||
}
|
||||
}
|
||||
115
src/Everything2Everything.Tests/AdServiceTests.cs
Normal file
115
src/Everything2Everything.Tests/AdServiceTests.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Everything2Everything.Core.Ads;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class AdServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetBannerAds_ReturnsDefaultHouseAds_NotEmpty()
|
||||
{
|
||||
var service = new AdService();
|
||||
var bannerAds = service.GetBannerAds();
|
||||
|
||||
Assert.NotNull(bannerAds);
|
||||
Assert.NotEmpty(bannerAds);
|
||||
Assert.All(bannerAds, ad =>
|
||||
{
|
||||
Assert.Equal(AdType.Banner, ad.Type);
|
||||
Assert.False(string.IsNullOrWhiteSpace(ad.Title));
|
||||
Assert.False(string.IsNullOrWhiteSpace(ad.Description));
|
||||
Assert.False(string.IsNullOrWhiteSpace(ad.TargetUrl));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLargeCardAds_ReturnsDefaultLargeCardAds_NotEmpty()
|
||||
{
|
||||
var service = new AdService();
|
||||
var largeAds = service.GetLargeCardAds();
|
||||
|
||||
Assert.NotNull(largeAds);
|
||||
Assert.NotEmpty(largeAds);
|
||||
Assert.All(largeAds, ad =>
|
||||
{
|
||||
Assert.Equal(AdType.LargeCard, ad.Type);
|
||||
Assert.False(string.IsNullOrWhiteSpace(ad.Title));
|
||||
Assert.False(string.IsNullOrWhiteSpace(ad.Description));
|
||||
Assert.False(string.IsNullOrWhiteSpace(ad.TargetUrl));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNextBannerAd_RotatesThroughAds()
|
||||
{
|
||||
var service = new AdService();
|
||||
var banners = service.GetBannerAds();
|
||||
|
||||
if (banners.Count > 1)
|
||||
{
|
||||
var first = service.GetNextBannerAd();
|
||||
var second = service.GetNextBannerAd();
|
||||
|
||||
Assert.NotNull(first);
|
||||
Assert.NotNull(second);
|
||||
Assert.NotEqual(first.Id, second.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ad = service.GetNextBannerAd();
|
||||
Assert.NotNull(ad);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenAdUrl_InvalidOrEmptyUrl_ReturnsFalse()
|
||||
{
|
||||
var service = new AdService();
|
||||
|
||||
Assert.False(service.OpenAdUrl(new AdItem { TargetUrl = "" }));
|
||||
Assert.False(service.OpenAdUrl(new AdItem { TargetUrl = "javascript:alert(1)" }));
|
||||
Assert.False(service.OpenAdUrl(new AdItem { TargetUrl = "not-a-valid-url" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadAdsFromJson_ValidJson_PopulatesCustomAds()
|
||||
{
|
||||
var service = new AdService();
|
||||
string json = @"[
|
||||
{
|
||||
""Id"": ""test-banner-1"",
|
||||
""Title"": ""커스텀 파트너스 배너"",
|
||||
""Description"": ""안전하고 빠른 클라우드 스토리지"",
|
||||
""BadgeText"": ""스폰서"",
|
||||
""ImageUrl"": ""https://example.com/banner.png"",
|
||||
""TargetUrl"": ""https://example.com/promo"",
|
||||
""CtaText"": ""지금 확인"",
|
||||
""Type"": 0,
|
||||
""IsActive"": true
|
||||
},
|
||||
{
|
||||
""Id"": ""test-large-1"",
|
||||
""Title"": ""대용량 미디어 솔루션"",
|
||||
""Description"": ""초고속 무손실 변환을 비즈니스에 도입하세요"",
|
||||
""BadgeText"": ""AD"",
|
||||
""ImageUrl"": ""https://example.com/large.png"",
|
||||
""TargetUrl"": ""https://example.com/b2b"",
|
||||
""CtaText"": ""솔루션 보기"",
|
||||
""Type"": 1,
|
||||
""IsActive"": true
|
||||
}
|
||||
]";
|
||||
|
||||
int count = service.LoadAdsFromJson(json);
|
||||
|
||||
Assert.Equal(2, count);
|
||||
var banners = service.GetBannerAds();
|
||||
var largeCards = service.GetLargeCardAds();
|
||||
|
||||
Assert.Contains(banners, b => b.Id == "test-banner-1");
|
||||
Assert.Contains(largeCards, l => l.Id == "test-large-1");
|
||||
}
|
||||
}
|
||||
102
src/Everything2Everything.Tests/AdViewModelTests.cs
Normal file
102
src/Everything2Everything.Tests/AdViewModelTests.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Collections.Generic;
|
||||
using Everything2Everything.App.ViewModels;
|
||||
using Everything2Everything.Core.Ads;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class AdViewModelTests
|
||||
{
|
||||
private class FakeAdService : IAdService
|
||||
{
|
||||
public List<AdItem> Banners { get; } = new()
|
||||
{
|
||||
new AdItem { Id = "b1", Title = "배너 1", TargetUrl = "https://example.com/b1", Type = AdType.Banner },
|
||||
new AdItem { Id = "b2", Title = "배너 2", TargetUrl = "https://example.com/b2", Type = AdType.Banner }
|
||||
};
|
||||
|
||||
public List<AdItem> Cards { get; } = new()
|
||||
{
|
||||
new AdItem { Id = "c1", Title = "카드 1", TargetUrl = "https://example.com/c1", Type = AdType.LargeCard },
|
||||
new AdItem { Id = "c2", Title = "카드 2", TargetUrl = "https://example.com/c2", Type = AdType.LargeCard }
|
||||
};
|
||||
|
||||
public AdItem? LastOpenedAd { get; private set; }
|
||||
private int _bIndex = 0;
|
||||
private int _cIndex = 0;
|
||||
|
||||
public IReadOnlyList<AdItem> GetBannerAds() => Banners;
|
||||
public IReadOnlyList<AdItem> GetLargeCardAds() => Cards;
|
||||
|
||||
public AdItem? GetNextBannerAd()
|
||||
{
|
||||
var ad = Banners[_bIndex % Banners.Count];
|
||||
_bIndex++;
|
||||
return ad;
|
||||
}
|
||||
|
||||
public AdItem? GetNextLargeCardAd()
|
||||
{
|
||||
var ad = Cards[_cIndex % Cards.Count];
|
||||
_cIndex++;
|
||||
return ad;
|
||||
}
|
||||
|
||||
public System.Threading.Tasks.Task<int> RefreshAdsFromFeedAsync(string feedUrl, System.Threading.CancellationToken cancellationToken = default) => System.Threading.Tasks.Task.FromResult(0);
|
||||
public int LoadAdsFromJson(string json) => 0;
|
||||
|
||||
public bool OpenAdUrl(AdItem ad)
|
||||
{
|
||||
LastOpenedAd = ad;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitializesAdsAndVisibility()
|
||||
{
|
||||
var fake = new FakeAdService();
|
||||
var vm = new AdViewModel(fake);
|
||||
|
||||
Assert.NotNull(vm.CurrentBannerAd);
|
||||
Assert.NotNull(vm.CurrentLargeAd);
|
||||
Assert.True(vm.IsBannerVisible);
|
||||
Assert.True(vm.IsLargeCardVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClickBannerAdCommand_OpensAdUrl()
|
||||
{
|
||||
var fake = new FakeAdService();
|
||||
var vm = new AdViewModel(fake);
|
||||
|
||||
vm.ClickBannerAdCommand.Execute(null);
|
||||
|
||||
Assert.NotNull(fake.LastOpenedAd);
|
||||
Assert.Equal(vm.CurrentBannerAd?.Id, fake.LastOpenedAd.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DismissBannerCommand_HidesBanner()
|
||||
{
|
||||
var fake = new FakeAdService();
|
||||
var vm = new AdViewModel(fake);
|
||||
|
||||
Assert.True(vm.IsBannerVisible);
|
||||
vm.DismissBannerCommand.Execute(null);
|
||||
Assert.False(vm.IsBannerVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RotateBannerAd_ChangesCurrentBanner()
|
||||
{
|
||||
var fake = new FakeAdService();
|
||||
var vm = new AdViewModel(fake);
|
||||
|
||||
var firstId = vm.CurrentBannerAd?.Id;
|
||||
vm.RotateBannerAd();
|
||||
var secondId = vm.CurrentBannerAd?.Id;
|
||||
|
||||
Assert.NotEqual(firstId, secondId);
|
||||
}
|
||||
}
|
||||
57
src/Everything2Everything.Tests/AgyChatClientTests.cs
Normal file
57
src/Everything2Everything.Tests/AgyChatClientTests.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Converters;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class AgyChatClientTests
|
||||
{
|
||||
private sealed class FakeStore : ISettingsStore
|
||||
{
|
||||
private readonly Dictionary<string, string> _d = new();
|
||||
public string? Get(string key) => _d.TryGetValue(key, out var v) ? v : null;
|
||||
public void Set(string key, string value) => _d[key] = value;
|
||||
public void Remove(string key) => _d.Remove(key);
|
||||
public bool Contains(string key) => _d.ContainsKey(key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExternalToolDetector_CanDetectAgy()
|
||||
{
|
||||
var available = ExternalToolDetector.IsAgyAvailable(out var path);
|
||||
if (!available) return;
|
||||
Assert.False(string.IsNullOrWhiteSpace(path));
|
||||
Assert.EndsWith("agy.exe", path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AgyChatClient_HasCorrectName()
|
||||
{
|
||||
var client = new AgyChatClient();
|
||||
Assert.Equal("Antigravity CLI (agy)", client.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LlmProvider_ResolveClient_AutoSelectsAgyWhenNoKey()
|
||||
{
|
||||
var store = new FakeStore();
|
||||
var provider = new LlmProvider(store);
|
||||
var (client, model) = provider.ResolveClientForTesting(new AiOptions { Backend = "agy" });
|
||||
Assert.NotNull(client);
|
||||
Assert.IsType<AgyChatClient>(client);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LlmProvider_CheckAvailability_IsReadyWhenAgyAvailable()
|
||||
{
|
||||
if (!ExternalToolDetector.IsAgyAvailable(out _)) return;
|
||||
var store = new FakeStore();
|
||||
var provider = new LlmProvider(store);
|
||||
var availability = await provider.CheckAvailabilityAsync();
|
||||
Assert.True(availability.IsReady);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -26,6 +26,7 @@ public class DependencyInjectionTests
|
|||
|
||||
Assert.NotNull(sp.GetRequiredService<ProviderRegistry>());
|
||||
Assert.NotNull(sp.GetRequiredService<ConversionEngine>());
|
||||
Assert.NotNull(sp.GetRequiredService<Everything2Everything.Core.Ads.IAdService>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -944,8 +944,8 @@ public class DesignAuditAstTests
|
|||
Assert.NotNull(searchBoxCol);
|
||||
|
||||
var widthVal = searchBoxCol.Attribute("Width")?.Value;
|
||||
Assert.True(int.TryParse(widthVal, out var w) && w <= 180,
|
||||
$"SearchBox 컬럼 폭은 180px 이하여야 필터 버튼들이 잘리지 않습니다. 현재: {widthVal}");
|
||||
Assert.True(widthVal == "*" || (int.TryParse(widthVal, out var w) && w <= 180),
|
||||
$"SearchBox 컬럼 폭은 가변(*)이거나 180px 이하여야 필터 컨트롤이 잘리지 않습니다. 현재: {widthVal}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1104,6 +1104,33 @@ public class DesignAuditAstTests
|
|||
Assert.Equal("0", row);
|
||||
Assert.Equal("2", rowSpan);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MainWindow_Toolbar_MustHave_FlexibleSearch_And_CategoryFilterDropdown_Without_Overflow()
|
||||
{
|
||||
// 파일명 검색창과 카테고리 필터가 좁은 창 가로폭(360px)에서도 씹힘/겹침(Overflow Collision)이 발생하지 않도록
|
||||
// 1) 5개 개별 가로 버튼 대신 단일 폴드아웃 드롭다운(CategoryFilterCombo)으로 정합되어야 하며
|
||||
// 2) 검색창은 Width="*" 컬럼에 위치하여 가변 너비를 가져야 하고
|
||||
// 3) CategoryFilterCombo는 VerticalAlignment="Center"를 준수해야 한다.
|
||||
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||
|
||||
// CategoryFilterCombo ComboBox 존재 검증
|
||||
var combo = doc.Descendants().FirstOrDefault(e =>
|
||||
e.Name.LocalName == "ComboBox" &&
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "CategoryFilterCombo");
|
||||
Assert.NotNull(combo);
|
||||
|
||||
var vAlign = combo.Attribute("VerticalAlignment")?.Value;
|
||||
Assert.Equal("Center", vAlign);
|
||||
|
||||
// 이전 5개 개별 가로 버튼(CommandParameter="All" 등)이 툴바에서 제거되어 드롭다운으로 대체되었는지 검증
|
||||
var individualFilterButtons = doc.Descendants().Where(e =>
|
||||
e.Name.LocalName == "Button" &&
|
||||
e.Attribute("Command")?.Value?.Contains("FilterCategoryCommand") == true &&
|
||||
e.Attribute("CommandParameter")?.Value is "All" or "Image" or "Document" or "Media" or "Data").ToList();
|
||||
Assert.Empty(individualFilterButtons);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Windows.Controls.Primitives;
|
|||
using System.Windows.Media;
|
||||
using Everything2Everything.App.Views;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Filters;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
|
@ -398,8 +399,9 @@ public class DesignAuditVisualTreeTests
|
|||
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
|
||||
|
||||
var outDir = @"C:\Users\encep\.gemini\antigravity\brain\a5abaf02-dd4b-45f7-8890-144e9da36bcc";
|
||||
var outPath = System.IO.Path.Combine(outDir, "app_rendered_preview.png");
|
||||
var outDir = @"C:\Users\encep\.gemini\antigravity\brain\b58fd023-a52b-4f4b-aa23-e6df654aa1fb";
|
||||
if (!System.IO.Directory.Exists(outDir)) System.IO.Directory.CreateDirectory(outDir);
|
||||
var outPath = System.IO.Path.Combine(outDir, "rendered_toolbar_fixed.png");
|
||||
using (var fs = new System.IO.FileStream(outPath, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.ReadWrite))
|
||||
{
|
||||
encoder.Save(fs);
|
||||
|
|
@ -410,6 +412,86 @@ public class DesignAuditVisualTreeTests
|
|||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MainWindow_CategoryFilterCombo_ChangesCategory_And_FiltersQueue()
|
||||
{
|
||||
RunOnSta(() =>
|
||||
{
|
||||
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||
var settings = new FakeSettingsStore();
|
||||
var window = new MainWindow(engine, settings);
|
||||
window.ActiveQueue.Add(new QueueItem { SourcePath = @"C:\test.png", FileName = "test.png" });
|
||||
window.ActiveQueue.Add(new QueueItem { SourcePath = @"C:\doc.pdf", FileName = "doc.pdf" });
|
||||
window.ActiveQueue.Add(new QueueItem { SourcePath = @"C:\clip.mp4", FileName = "clip.mp4" });
|
||||
window.ActiveQueue.Add(new QueueItem { SourcePath = @"C:\sheet.xlsx", FileName = "sheet.xlsx" });
|
||||
|
||||
var combo = (ComboBox)window.FindName("CategoryFilterCombo");
|
||||
Assert.NotNull(combo);
|
||||
Assert.Equal(5, combo.Items.Count);
|
||||
|
||||
// 0: 전체 (All)
|
||||
Assert.Equal(FilterCategory.All, window.SelectedCategory);
|
||||
|
||||
// 1: 이미지 (Image)
|
||||
combo.SelectedIndex = 1;
|
||||
Assert.Equal(FilterCategory.Image, window.SelectedCategory);
|
||||
|
||||
var view = System.Windows.Data.CollectionViewSource.GetDefaultView(window.ActiveQueue);
|
||||
var filteredItems = view.Cast<QueueItem>().ToList();
|
||||
Assert.Single(filteredItems);
|
||||
Assert.Equal("test.png", filteredItems[0].FileName);
|
||||
|
||||
// 0: 전체 복귀 (All)
|
||||
combo.SelectedIndex = 0;
|
||||
Assert.Equal(FilterCategory.All, window.SelectedCategory);
|
||||
filteredItems = view.Cast<QueueItem>().ToList();
|
||||
Assert.Equal(4, filteredItems.Count);
|
||||
|
||||
// 가상 레이아웃 검증 (너비 360px 환경에서도 DesiredSize가 정상 계산되는지)
|
||||
var content = (UIElement)window.Content;
|
||||
content.Measure(new Size(360, 600));
|
||||
content.Arrange(new Rect(0, 0, 360, 600));
|
||||
Assert.True(content.DesiredSize.Width > 0);
|
||||
Assert.False(double.IsNaN(content.DesiredSize.Width));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdBannerControl_MeasureAndArrange_HasValidLayoutBounds()
|
||||
{
|
||||
RunOnSta(() =>
|
||||
{
|
||||
var adService = new Everything2Everything.Core.Ads.AdService();
|
||||
var vm = new Everything2Everything.App.ViewModels.AdViewModel(adService);
|
||||
var banner = new AdBannerControl { DataContext = vm };
|
||||
banner.Measure(new Size(800, 100));
|
||||
banner.Arrange(new Rect(0, 0, 800, 100));
|
||||
|
||||
Assert.True(banner.DesiredSize.Width > 0);
|
||||
Assert.True(banner.DesiredSize.Height > 0);
|
||||
Assert.False(double.IsNaN(banner.DesiredSize.Width));
|
||||
Assert.False(double.IsNaN(banner.DesiredSize.Height));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdLargeCardControl_MeasureAndArrange_HasValidLayoutBounds()
|
||||
{
|
||||
RunOnSta(() =>
|
||||
{
|
||||
var adService = new Everything2Everything.Core.Ads.AdService();
|
||||
var vm = new Everything2Everything.App.ViewModels.AdViewModel(adService);
|
||||
var card = new AdLargeCardControl { DataContext = vm };
|
||||
card.Measure(new Size(340, 300));
|
||||
card.Arrange(new Rect(0, 0, 340, 300));
|
||||
|
||||
Assert.True(card.DesiredSize.Width > 0);
|
||||
Assert.True(card.DesiredSize.Height > 0);
|
||||
Assert.False(double.IsNaN(card.DesiredSize.Width));
|
||||
Assert.False(double.IsNaN(card.DesiredSize.Height));
|
||||
});
|
||||
}
|
||||
|
||||
private static IEnumerable<T> FindLogicalChildren<T>(object parent) where T : DependencyObject
|
||||
{
|
||||
if (parent is ContentControl cc && cc.Content != null)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using Everything2Everything.App.ViewModels;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Converters;
|
||||
using Everything2Everything.Core.Providers;
|
||||
using Everything2Everything.Core.Filters;
|
||||
using Xunit;
|
||||
|
|
@ -147,5 +148,56 @@ public class E2EUserScenarioTests
|
|||
Assert.False(MediaConversionNegotiator.CanConvert(".xlsx", ".png"));
|
||||
}
|
||||
|
||||
}
|
||||
[Theory]
|
||||
[InlineData("summarize", null, "요약")]
|
||||
[InlineData("translate", "일본어", "일본어")]
|
||||
[InlineData("proofread", null, "교정")]
|
||||
public void Scenario8_AiQuickPanel_Options_BuildPrompt_FormatsCorrectlyForAllTasks(string task, string? targetLang, string expectedKeyword)
|
||||
{
|
||||
var options = new AiOptions { Task = task, TargetLanguage = targetLang };
|
||||
var (system, user) = LlmProvider.BuildPrompt(options, "테스트 입력 문장");
|
||||
Assert.Contains(expectedKeyword, system);
|
||||
Assert.Equal("테스트 입력 문장", user);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Scenario9_AiConversion_EndToEnd_SwitchboardResolution()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "E2E_AiConversion_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
try
|
||||
{
|
||||
var srcFile = Path.Combine(tempDir, "sample.md");
|
||||
await File.WriteAllTextAsync(srcFile, "# Hello\nEverything2Everything test content.");
|
||||
|
||||
var store = new FakeSettingsStore();
|
||||
store.Set("switchboard.endpoint", "http://192.168.0.225:8787");
|
||||
var provider = new LlmProvider(store);
|
||||
|
||||
var options = new ConvertOptions
|
||||
{
|
||||
Ai = new AiOptions { Task = "summarize", Backend = "switchboard" }
|
||||
};
|
||||
|
||||
var (client, _) = provider.ResolveClientForTesting(options.Ai);
|
||||
Assert.NotNull(client);
|
||||
Assert.Equal("Switchboard Gateway", client.Name);
|
||||
var sbClient = Assert.IsType<SwitchboardChatClient>(client);
|
||||
Assert.Equal("http://192.168.0.225:8787", sbClient.Endpoint);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeSettingsStore : ISettingsStore
|
||||
{
|
||||
private readonly Dictionary<string, string> _d = new();
|
||||
public string? Get(string key) => _d.TryGetValue(key, out var v) ? v : null;
|
||||
public void Set(string key, string value) => _d[key] = value;
|
||||
public void Remove(string key) => _d.Remove(key);
|
||||
public bool Contains(string key) => _d.ContainsKey(key);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
src/Everything2Everything.Tests/ExternalToolInstallerTests.cs
Normal file
117
src/Everything2Everything.Tests/ExternalToolInstallerTests.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using Everything2Everything.App.ViewModels;
|
||||
using Everything2Everything.Core.Converters;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class ExternalToolInstallerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Catalog_CoversAllKnownTools_WithVerifiedWingetIds()
|
||||
{
|
||||
var byKey = ExternalToolCatalog.All.ToDictionary(t => t.Key, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Equal(7, byKey.Count);
|
||||
|
||||
Assert.Equal("Gyan.FFmpeg", byKey["ffmpeg"].WingetId);
|
||||
Assert.Equal("TheDocumentFoundation.LibreOffice", byKey["libreoffice"].WingetId);
|
||||
Assert.Equal("JohnMacFarlane.Pandoc", byKey["pandoc"].WingetId);
|
||||
Assert.Equal("ImageMagick.ImageMagick", byKey["imagemagick"].WingetId);
|
||||
Assert.Equal("Google.AntigravityCLI", byKey["agy"].WingetId);
|
||||
Assert.Equal("OpenAI.Codex", byKey["codex"].WingetId);
|
||||
|
||||
Assert.Equal(ExternalToolInstallKind.Manual, byKey["h2orestart"].Kind);
|
||||
Assert.False(string.IsNullOrWhiteSpace(byKey["h2orestart"].ManualNote));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InstallAsync_WingetTool_RunsSilentWinget_AndReportsInstalled()
|
||||
{
|
||||
var runner = new CapturingRunner();
|
||||
var installer = new ExternalToolInstaller(runner);
|
||||
var tool = WingetTool("test-tool", "Contoso.Demo", () => true);
|
||||
|
||||
var result = await installer.InstallAsync(tool, CancellationToken.None);
|
||||
|
||||
Assert.Equal("winget", runner.LastFileName);
|
||||
Assert.Contains("--id Contoso.Demo", runner.LastArgs);
|
||||
Assert.Contains("--silent", runner.LastArgs);
|
||||
Assert.Contains("--accept-package-agreements", runner.LastArgs);
|
||||
Assert.Contains("--accept-source-agreements", runner.LastArgs);
|
||||
Assert.Contains("--disable-interactivity", runner.LastArgs);
|
||||
Assert.True(result.Success);
|
||||
Assert.Null(result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InstallAsync_WhenStillNotDetectedAfterRun_ReportsFailure()
|
||||
{
|
||||
var runner = new CapturingRunner { ExitCode = 1, Output = "installer failed" };
|
||||
var installer = new ExternalToolInstaller(runner);
|
||||
var tool = WingetTool("test-tool", "Contoso.Demo", () => false);
|
||||
|
||||
var result = await installer.InstallAsync(tool, CancellationToken.None);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("실패", result.Message);
|
||||
Assert.Equal(1, result.ExitCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InstallAsync_ManualKind_DoesNotRunAnything_ReturnsGuidance()
|
||||
{
|
||||
var runner = new CapturingRunner();
|
||||
var installer = new ExternalToolInstaller(runner);
|
||||
var tool = new ExternalToolDefinition
|
||||
{
|
||||
Key = "manual",
|
||||
DisplayName = "Manual",
|
||||
Description = "d",
|
||||
Kind = ExternalToolInstallKind.Manual,
|
||||
ManualNote = "직접 추가하세요",
|
||||
IsInstalled = () => false,
|
||||
};
|
||||
|
||||
var result = await installer.InstallAsync(tool, CancellationToken.None);
|
||||
|
||||
Assert.Null(runner.LastFileName);
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal("직접 추가하세요", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToolSetupViewModel_DefaultsToAllSelected_AndCoversCatalog()
|
||||
{
|
||||
var vm = new ToolSetupViewModel(
|
||||
ExternalToolCatalog.All,
|
||||
new ExternalToolInstaller(new CapturingRunner()));
|
||||
|
||||
Assert.Equal(ExternalToolCatalog.All.Count, vm.Tools.Count);
|
||||
Assert.All(vm.Tools, t => Assert.True(t.IsSelected));
|
||||
Assert.True(vm.SelectAll);
|
||||
}
|
||||
|
||||
private static ExternalToolDefinition WingetTool(string key, string id, Func<bool> installed) => new()
|
||||
{
|
||||
Key = key,
|
||||
DisplayName = key,
|
||||
Description = key,
|
||||
Kind = ExternalToolInstallKind.Winget,
|
||||
WingetId = id,
|
||||
IsInstalled = installed,
|
||||
};
|
||||
|
||||
private sealed class CapturingRunner : IExternalCommandRunner
|
||||
{
|
||||
public string? LastFileName;
|
||||
public string? LastArgs;
|
||||
public int ExitCode { get; set; }
|
||||
public string Output { get; set; } = "";
|
||||
|
||||
public Task<ExternalCommandResult> RunAsync(string fileName, string arguments, string? workingDirectory, CancellationToken ct)
|
||||
{
|
||||
LastFileName = fileName;
|
||||
LastArgs = arguments;
|
||||
return Task.FromResult(new ExternalCommandResult(ExitCode, Output));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,8 +25,8 @@ public class LlmProviderTests
|
|||
[Fact]
|
||||
public async Task NoKey_IsNotReady()
|
||||
{
|
||||
// 환경변수 키 또는 codex CLI(OAuth)가 있으면 AI가 활성화되므로 이 단언은 건너뜀
|
||||
if (EnvHasKey() || ExternalToolDetector.IsCodexAvailable()) return;
|
||||
// 환경변수 키 또는 codex/agy CLI(OAuth) 또는 Switchboard Gateway가 있으면 AI가 활성화되므로 이 단언은 건너뜀
|
||||
if (EnvHasKey() || ExternalToolDetector.IsCodexAvailable() || ExternalToolDetector.IsAgyAvailable(out _) || ExternalToolDetector.IsSwitchboardGatewayAvailable(out _)) return;
|
||||
var p = new LlmProvider(new FakeStore());
|
||||
var a = await p.CheckAvailabilityAsync();
|
||||
Assert.False(a.IsReady);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Converters;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class SwitchboardChatClientTests
|
||||
{
|
||||
private sealed class FakeStore : ISettingsStore
|
||||
{
|
||||
private readonly Dictionary<string, string> _d = new();
|
||||
public string? Get(string key) => _d.TryGetValue(key, out var v) ? v : null;
|
||||
public void Set(string key, string value) => _d[key] = value;
|
||||
public void Remove(string key) => _d.Remove(key);
|
||||
public bool Contains(string key) => _d.ContainsKey(key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SwitchboardChatClient_HasCorrectNameAndEndpoint()
|
||||
{
|
||||
var client = new SwitchboardChatClient();
|
||||
Assert.Equal("Switchboard Gateway", client.Name);
|
||||
Assert.Equal("http://127.0.0.1:8787", client.Endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SwitchboardChatClient_CustomEndpoint_IsPreserved()
|
||||
{
|
||||
var client = new SwitchboardChatClient("http://192.168.1.100:8787/");
|
||||
Assert.Equal("http://192.168.1.100:8787", client.Endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LlmProvider_ResolveClient_SelectsSwitchboardWhenRequested()
|
||||
{
|
||||
var store = new FakeStore();
|
||||
store.Set("switchboard.endpoint", "http://127.0.0.1:8787");
|
||||
var provider = new LlmProvider(store);
|
||||
var (client, model) = provider.ResolveClientForTesting(new AiOptions { Backend = "switchboard" });
|
||||
Assert.NotNull(client);
|
||||
Assert.IsType<SwitchboardChatClient>(client);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckSwitchboardGatewayHealthAsync_ReturnsFalseForUnreachablePort()
|
||||
{
|
||||
var (available, _, _) = await ExternalToolDetector.CheckSwitchboardGatewayHealthAsync("http://127.0.0.1:59999");
|
||||
Assert.False(available);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExternalToolDetector_IsSwitchboardGatewayAvailable_DoesNotThrow()
|
||||
{
|
||||
var available = ExternalToolDetector.IsSwitchboardGatewayAvailable(out var ep);
|
||||
Assert.Equal("http://127.0.0.1:8787", ep);
|
||||
// Returns bool without hanging or throwing
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LlmProvider_CustomEndpoint_IsUsedWhenConfigured()
|
||||
{
|
||||
var store = new FakeStore();
|
||||
store.Set("switchboard.endpoint", "http://192.168.0.25:8787");
|
||||
var provider = new LlmProvider(store);
|
||||
var (client, _) = provider.ResolveClientForTesting(new AiOptions { Backend = "switchboard" });
|
||||
Assert.NotNull(client);
|
||||
var sbClient = Assert.IsType<SwitchboardChatClient>(client);
|
||||
Assert.Equal("http://192.168.0.25:8787", sbClient.Endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExternalToolDetector_IsSwitchboardGatewayAvailable_WithCustomEndpoint_PreservesEndpoint()
|
||||
{
|
||||
var available = ExternalToolDetector.IsSwitchboardGatewayAvailable(out var ep, "http://192.168.0.25:8787/");
|
||||
Assert.Equal("http://192.168.0.25:8787", ep);
|
||||
Assert.False(available);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue