feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization

This commit is contained in:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -0,0 +1,107 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Paca.Avalonia;
using Paca.Mobile.Core;
using Xunit;
namespace Paca.Tests.Platform;
public class AvaloniaAndMobileDiscoveryGateTests
{
private static string SolutionRoot
{
get
{
var dir = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(dir) && !File.Exists(Path.Combine(dir, "Paca.slnx")) && !File.Exists(Path.Combine(dir, "Paca.sln")))
{
var parent = Directory.GetParent(dir);
if (parent == null) break;
dir = parent.FullName;
}
return dir ?? AppContext.BaseDirectory;
}
}
// =========================================================================
// Gate 1: Avalonia MainViewModel CTS 리소스 누수 방지 검증
// =========================================================================
[Fact]
public async Task Gate_Avalonia_MainViewModel_Cts_Is_Disposed_And_Nulled_After_Execution()
{
var vm = new MainViewModel();
vm.Url = "https://example.com/not-supported"; // 즉시 실패/완료 유도
await vm.RunDownloadAsync();
Assert.False(vm.IsBusy);
Assert.False(vm.HasActiveCts, "다운로드 완료 또는 실패 후 CTS는 Dispose되고 null 처리되어야 합니다.");
}
// =========================================================================
// Gate 2: Avalonia MainViewModel Log 컬렉션 슬라이딩 윈도우 상한(200개) 검증
// =========================================================================
[Fact]
public void Gate_Avalonia_MainViewModel_Log_Sliding_Window_Limit()
{
var vm = new MainViewModel();
for (int i = 0; i < 300; i++)
{
vm.AddLogEntry($"Log item {i}");
}
Assert.True(vm.Log.Count <= 200, $"Log 항목 수는 최대 200개 이하로 캡핑되어야 합니다. 현재: {vm.Log.Count}");
}
// =========================================================================
// Gate 3: Avalonia MainWindow.axaml 바인딩 무결성 (CompileBindings 및 WPF 잔재 제거)
// =========================================================================
[Fact]
public void Gate_Avalonia_MainWindow_Axaml_Clean_Bindings()
{
var axamlPath = Path.Combine(SolutionRoot, "src", "Paca.Avalonia", "MainWindow.axaml");
Assert.True(File.Exists(axamlPath), $"MainWindow.axaml must exist at {axamlPath}");
var content = File.ReadAllText(axamlPath);
Assert.DoesNotContain("x:CompileBindings=\"False\"", content);
Assert.DoesNotContain("UpdateSourceTrigger=PropertyChanged", content);
}
// =========================================================================
// Gate 4: LanDiscoveryMessage 디바이스명에 파이프(|) 포함 시 파싱 무결성 검증
// =========================================================================
[Fact]
public void Gate_LanDiscoveryMessage_Parses_DeviceName_With_Pipes()
{
var devNameWithPipes = "My|Studio|PC";
var endpoint = "192.168.1.100:5000";
var rawPayload = $"PACA_DISCOVERY|PONG|{devNameWithPipes}|{endpoint}|1|abc123fingerprint";
var parsed = LanDiscoveryMessage.Parse(rawPayload);
Assert.NotNull(parsed);
Assert.Equal(LanDiscoveryMessageType.Pong, parsed.Type);
Assert.Equal(devNameWithPipes, parsed.DeviceName);
Assert.Equal(endpoint, parsed.Endpoint);
Assert.True(parsed.IsPairingAllowed);
Assert.Equal("abc123fingerprint", parsed.Fingerprint);
}
// =========================================================================
// Gate 5: LanDiscoveryProber IPv6 엔드포인트 브래킷 포맷팅 검증
// =========================================================================
[Fact]
public void Gate_LanDiscoveryProber_Formats_IPv6_Endpoint_With_Brackets()
{
using var prober = new LanDiscoveryProber();
DiscoveredLanDevice? foundDevice = null;
prober.OnDeviceFound += d => foundDevice = d;
// 원격 송신자가 0.0.0.0:5000을 보냈고, 송신자 IP가 IPv6인 경우
var ipv6Sender = "fe80::1";
prober.HandleReceivedPayload("PACA_DISCOVERY|PONG|TestNode|0.0.0.0:5000|1|fp", ipv6Sender);
Assert.NotNull(foundDevice);
Assert.Contains("[fe80::1]:5000", foundDevice.Endpoint);
Assert.Equal("http://[fe80::1]:5000", foundDevice.HttpBaseUrl);
}
}