releases/tests/Paca.Tests/Infrastructure/TestVerificationSystemGateTests.cs

1151 lines
53 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Paca.App;
using Paca.App.Browser.Features;
using Paca.Browser;
using Paca.Browser.Extensions;
using Paca.Core;
using Paca.Core.Config;
using Paca.Core.Detection;
using Paca.Core.Net;
using Paca.Core.Platform;
using Paca.Mobile;
using Paca.Mobile.Core;
using Paca.Tests.E2E;
using Paca.Tests.Infrastructure;
using Xunit;
namespace Paca.Tests.Infrastructure;
/// <summary>
/// 검사체계를 검사하는 상위 메타 검증 게이트 (Meta-Verification & Anti-Cheating Harness).
/// Robert C. Martin TDD 3대 원칙 및 10대 디자인 감사 게이트, 안티치팅 규칙(Skip/빈 catch/Async void 금지),
/// 게이트 검증 함수들의 변이(Mutation) 감지력, XAML 파서 무결점, 계약 어설션의 비동어반복성을 기계적으로 전수 검증합니다.
/// </summary>
public class TestVerificationSystemGateTests
{
private static readonly string TestDir = Directory.Exists(Path.Combine(SourceTree.Root, "tests", "Paca.Tests"))
? Path.Combine(SourceTree.Root, "tests", "Paca.Tests")
: Path.Combine(SourceTree.Root, "Paca.Tests");
private static readonly string SrcDir = Directory.Exists(Path.Combine(SourceTree.Root, "src"))
? Path.Combine(SourceTree.Root, "src")
: SourceTree.Root;
// =========================================================================
// 1. 안티치팅(Anti-Cheating) 헌법 메타 게이트
// =========================================================================
[Fact]
public void Meta_AntiCheating_No_Tests_Have_Skip_Attribute()
{
var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories);
var skipRegex = new Regex(@"\[(?:Fact|Theory)\s*\(\s*Skip\s*=", RegexOptions.Compiled);
var violations = new List<string>();
foreach (var file in testFiles)
{
var text = File.ReadAllText(file);
var matches = skipRegex.Matches(text);
if (matches.Count > 0)
{
violations.Add($"{Path.GetFileName(file)}: {matches.Count}건의 Skip 발견");
}
}
Assert.True(violations.Count == 0,
$"[안티치팅 위반] 테스트 회피(Skip) 속성이 발견되었습니다:\n" + string.Join("\n", violations));
}
[Fact]
public void Meta_AntiCheating_No_Async_Void_Test_Methods()
{
var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories);
var asyncVoidRegex = new Regex(@"\[(?:Fact|Theory)\]\s*(?:public\s+)?async\s+void\s+\w+", RegexOptions.Compiled);
var violations = new List<string>();
foreach (var file in testFiles)
{
var text = File.ReadAllText(file);
var matches = asyncVoidRegex.Matches(text);
foreach (Match m in matches)
{
violations.Add($"{Path.GetFileName(file)}: {m.Value}");
}
}
Assert.True(violations.Count == 0,
$"[안티치팅 위반] xUnit에서 예외를 삼킬 수 있는 'async void' 테스트가 감지되었습니다:\n" + string.Join("\n", violations));
}
[Fact]
public void Meta_AntiCheating_No_Empty_Catch_In_Test_Logic()
{
var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories);
var violations = new List<string>();
foreach (var file in testFiles)
{
var lines = File.ReadAllLines(file);
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
if (Regex.IsMatch(line, @"catch\s*(?:\([^\)]*\))?\s*\{\s*\}"))
{
// 정리/해제(finally, Dispose, Delete, Stop)가 아닌 테스트 본문 로직 내의 빈 catch는 치팅으로 간주
var context = string.Join("\n", lines.Skip(Math.Max(0, i - 12)).Take(18));
bool isLegitimateTeardown =
context.Contains("finally") ||
context.Contains("Dispose()") ||
context.Contains("Directory.Delete") ||
context.Contains("File.Delete") ||
context.Contains("Close()") ||
context.Contains("Stop()") ||
context.Contains("Mock") ||
context.Contains("WpfTestApp");
if (!isLegitimateTeardown)
{
violations.Add($"{Path.GetFileName(file)} (line {i + 1}): {line.Trim()}");
}
}
}
}
Assert.True(violations.Count == 0,
$"[안티치팅 위반] 테스트 검증/실행 로직에서 예외를 은폐하는 빈 catch 블록이 발견되었습니다:\n" + string.Join("\n", violations));
}
// =========================================================================
// 2. 검사체계 헬퍼의 변이 감지력(Mutation Killing) 검증 게이트
// =========================================================================
[Fact]
public void Meta_GateHelper_ContrastRatio_Mathematical_Invariants()
{
// 1. 완전 흑백 명도 대비는 정확히 21.0:1 이어야 함
var maxRatio = SourceTree.ContrastRatio("#000000", "#FFFFFF");
Assert.InRange(maxRatio, 20.99, 21.01);
// 2. 동일 색상 명도 대비는 정확히 1.0:1 이어야 함
var minRatio = SourceTree.ContrastRatio("#1E1E1E", "#1E1E1E");
Assert.InRange(minRatio, 0.99, 1.01);
// 3. 교환 법칙 (Commutativity)
var ratioAB = SourceTree.ContrastRatio("#123456", "#ABCDEF");
var ratioBA = SourceTree.ContrastRatio("#ABCDEF", "#123456");
Assert.Equal(ratioAB, ratioBA);
// 4. WCAG AA 기준 4.5:1 경계값 판별 정확성
var textOnDark = SourceTree.ContrastRatio("#CCCCCC", "#121212");
Assert.True(textOnDark >= 4.5, "밝은 회색(#CCCCCC) on 어두운 배경(#121212)은 4.5:1 이상이어야 함");
var badTextOnDark = SourceTree.ContrastRatio("#333333", "#121212");
Assert.False(badTextOnDark >= 4.5, "어두운 회색(#333333) on 어두운 배경(#121212)은 4.5:1 미만이어야 함");
}
[Fact]
public void Meta_GateHelper_UiContractPipeline_Negative_Mutants_Must_Fail()
{
// 1. 존재하지 않는 x:Name 또는 Name 요소 검색 시 반드시 false 여야 함
var fakeName = $"NonExistentElement_{Guid.NewGuid():N}";
Assert.False(UiContractPipeline.HasElementNamed(UiContractPipeline.MainWindowDoc, fakeName));
// 2. 존재하지 않는 x:Key 검색 시 반드시 false 여야 함
var fakeKey = $"NonExistentKey_{Guid.NewGuid():N}";
Assert.False(UiContractPipeline.HasResourceKey(UiContractPipeline.MainWindowDoc, fakeKey));
// 3. 존재하지 않는 속성/메서드 리플렉션 시 반드시 false 여야 함
Assert.False(UiContractPipeline.HasProperty(typeof(MainWindow), $"FakeProperty_{Guid.NewGuid():N}"));
Assert.False(UiContractPipeline.HasMethod(typeof(MainWindow), $"FakeMethod_{Guid.NewGuid():N}"));
}
[Fact]
public void Meta_GateHelper_AssertContract_Actually_Throws_On_False()
{
// AssertContract가 false를 받았을 때 예외를 발생시키지 않는 버그(가짜 통과) 방지
Assert.Throws<Xunit.Sdk.TrueException>(() =>
{
UiContractPipeline.AssertContract(false, "META-FAIL", "이 어설션은 의도된 실패여야 합니다.");
});
}
// =========================================================================
// 3. XAML 파싱 및 UI 계약 소스 무결성 메타 게이트
// =========================================================================
[Fact]
public void Meta_All_App_Xaml_Files_Are_Syntactically_Valid_Xml()
{
var xamlFiles = Directory.GetFiles(Path.Combine(SrcDir, "Paca.App"), "*.xaml", SearchOption.AllDirectories);
Assert.NotEmpty(xamlFiles);
foreach (var file in xamlFiles)
{
var content = File.ReadAllText(file);
var ex = Record.Exception(() => XDocument.Parse(content));
Assert.Null(ex);
}
}
[Fact]
public void Meta_Themes_All_Color_Hex_Codes_Are_Valid_Format()
{
var dark = SourceTree.ThemeDarkXaml;
var light = SourceTree.ThemeLightXaml;
var hc = File.ReadAllText(SourceTree.AppPath("Themes/HighContrast.xaml"));
var colorRegex = new Regex(@"Color=""(#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{8})""", RegexOptions.Compiled);
foreach (var (themeName, xaml) in new[] { ("Dark", dark), ("Light", light), ("HighContrast", hc) })
{
var matches = colorRegex.Matches(xaml);
Assert.True(matches.Count > 10, $"{themeName} 테마에 적어도 10개 이상의 유효한 색상 코드가 존재해야 합니다.");
foreach (Match match in matches)
{
var hex = match.Groups[1].Value;
Assert.Matches(@"^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$", hex);
}
}
}
// =========================================================================
// 4. Mobile 코어 및 스니퍼 변이 검증 게이트
// =========================================================================
[Fact]
public void Meta_Mobile_VideoSnifferParser_Quality_Negotiation_Mutant_Killed()
{
var streams = new List<VideoSniffItem>
{
new() { StreamUrl = "https://paca.media/sd.mp4", Resolution = "480p", VideoType = VideoStreamType.Mp4 },
new() { StreamUrl = "https://paca.media/fhd.mp4", Resolution = "1080p", VideoType = VideoStreamType.Mp4 },
new() { StreamUrl = "https://paca.media/4k.mp4", Resolution = "4K", VideoType = VideoStreamType.Mp4 }
};
// 1. 최고 화질 요청 ("Max")
var best = VideoSnifferParser.NegotiateBestQuality(streams, "Max");
Assert.NotNull(best);
Assert.Equal("https://paca.media/4k.mp4", best.StreamUrl);
// 2. 특정 화질 매칭 ("1080p")
var match1080 = VideoSnifferParser.NegotiateBestQuality(streams, "1080p");
Assert.NotNull(match1080);
Assert.Equal("https://paca.media/fhd.mp4", match1080.StreamUrl);
// Mutant A: 빈 목록 -> null 반환
Assert.Null(VideoSnifferParser.NegotiateBestQuality(new List<VideoSniffItem>(), "Max"));
// Mutant B: null 목록 -> null 반환
Assert.Null(VideoSnifferParser.NegotiateBestQuality(null!, "Max"));
// Mutant C: 720p 요청 시 720p 이하 최고인 480p 선택
var fallback = VideoSnifferParser.NegotiateBestQuality(streams, "720p");
Assert.NotNull(fallback);
Assert.Equal("480p", fallback.Resolution);
}
// =========================================================================
// 5. 시각 디자인 및 아이콘 글리프 감사 게이트 변이 검증 (Design Audit Meta-Gates)
// =========================================================================
[Fact]
public void Meta_IconGlyphGate_Mixed_PUA_And_Text_Mutant_Killed()
{
// PUA 글리프와 일반 텍스트가 결합되어 tofu/notdef 위험이 있는 돌연변이 XAML
var badXaml = """
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Button Content="&#xE700; 다운로드" />
</StackPanel>
""";
var violations = IconGlyphDesignSystemTests.Violations(badXaml, "MockMutant.xaml").ToList();
Assert.NotEmpty(violations);
Assert.Contains(violations, v => v.Contains("PUA+일반 텍스트 혼합"));
}
[Fact]
public void Meta_IconGlyphGate_Missing_IconFont_Style_Mutant_Killed()
{
// IconFont 스타일이나 FontFamily 없이 PUA 코드만 단독 사용된 결함 XAML
var badXaml = """
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Button Content="&#xE700;" />
</StackPanel>
""";
var violations = IconGlyphDesignSystemTests.Violations(badXaml, "MockMutant.xaml").ToList();
Assert.NotEmpty(violations);
Assert.Contains(violations, v => v.Contains("IconFont 보장 없음"));
}
[Fact]
public void Meta_IconGlyphGate_Valid_PUA_With_IconFont_Passes_Cleanly()
{
// 정상적인 IconFont 명시 PUA 버튼
var goodXaml = """
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Button FontFamily="{DynamicResource IconFont}" Content="&#xE700;" />
</StackPanel>
""";
var violations = IconGlyphDesignSystemTests.Violations(goodXaml, "Valid.xaml").ToList();
Assert.Empty(violations);
}
[Fact]
public void Meta_VisualDesignGate_Missing_Required_Token_Mutant_Killed()
{
// 필수 토큰인 'AccentBrush'가 누락된 모의 테마 XAML
var mockThemeXaml = """
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="WindowBg" Color="#121212" />
</ResourceDictionary>
""";
// WindowBg는 존재하나 AccentBrush는 누락됨을 정확히 판별해야 함
Assert.True(SourceTree.HasKey(mockThemeXaml, "WindowBg"));
Assert.False(SourceTree.HasKey(mockThemeXaml, "AccentBrush"));
}
[Fact]
public void Meta_VisualDesignGate_Wcag_Contrast_Threshold_Precision()
{
// WCAG AA 일반 텍스트 기준 (>= 4.5) 및 AAA 고대비 기준 (>= 7.0) 경계선 정밀 검증
// 순수 흰색 (#FFFFFF) on 표준 WCAG 회색 (#767676) -> 약 4.54:1 (AA 통과, AAA 탈락)
var ratio45 = SourceTree.ContrastRatio("#FFFFFF", "#767676");
Assert.True(ratio45 >= 4.5, $"실제 비율 {ratio45:F2} >= 4.5");
Assert.False(ratio45 >= 7.0, $"실제 비율 {ratio45:F2} < 7.0");
// 순수 흰색 (#FFFFFF) on 검정 (#000000) -> 21.0:1 (AAA 통과)
var ratio21 = SourceTree.ContrastRatio("#FFFFFF", "#000000");
Assert.True(ratio21 >= 7.0);
// 낮은 대비 (#777777 on #555555) -> 약 1.7:1 (AA 탈락)
var ratioFail = SourceTree.ContrastRatio("#777777", "#555555");
Assert.False(ratioFail >= 4.5);
}
// =========================================================================
// 6. UI 유즈케이스 메가 파이프라인(UiContractUseCasesMegaPipeline) 메타 감사 게이트
// =========================================================================
[Fact]
public void Meta_UiUsecase_MegaPipeline_Suite_Integrity_And_Test_Count()
{
var megaPipelinePath = Path.Combine(TestDir, "E2E", "UiContractUseCasesMegaPipelineTests.cs");
Assert.True(File.Exists(megaPipelinePath), "UiContractUseCasesMegaPipelineTests.cs 가 존재해야 합니다.");
var content = File.ReadAllText(megaPipelinePath);
var factMatches = Regex.Matches(content, @"\[Fact\]\s*public\s+void\s+(UC_\w+)", RegexOptions.Compiled);
// UiContractUseCasesMegaPipelineTests 내에 최소 230개 이상의 세부 UI 계약 테스트가 등록되어 있어야 함 (현재 236개)
Assert.True(factMatches.Count >= 230,
$"UiContractUseCasesMegaPipelineTests 내의 [Fact] 수({factMatches.Count})가 230개 이상이어야 합니다.");
}
[Fact]
public void Meta_UiUsecase_All_Invocations_Have_Valid_UC_ID_And_Story()
{
var megaPipelinePath = Path.Combine(TestDir, "E2E", "UiContractUseCasesMegaPipelineTests.cs");
var content = File.ReadAllText(megaPipelinePath);
var usecaseCalls = Regex.Matches(content, @"SourceTree\.Usecase\s*\([^,]+,\s*""([^""]+)""\s*,\s*""([^""]+)""\s*\)", RegexOptions.Compiled);
Assert.NotEmpty(usecaseCalls);
foreach (Match match in usecaseCalls)
{
var ucId = match.Groups[1].Value;
var story = match.Groups[2].Value;
// 1. UC-A01, UC-T15, UC-01 등의 규격 준수 검증
Assert.Matches(@"^UC-[\w\d]+$", ucId);
// 2. 사유(story) 문자열이 무의미하거나 비어있지 않은지 검증
Assert.True(story.Trim().Length >= 6, $"[UC ID: {ucId}] 설명({story})이 너무 짧거나 무의미합니다.");
}
}
[Fact]
public void Meta_VisualDesignGates_Total_Gate_Count_At_Least_85()
{
var vdgPath = Path.Combine(TestDir, "E2E", "VisualDesignGateTests.cs");
Assert.True(File.Exists(vdgPath), "VisualDesignGateTests.cs 가 존재해야 합니다.");
var content = File.ReadAllText(vdgPath);
var gateMatches = Regex.Matches(content, @"public\s+void\s+(Gate\d+_\w+)", RegexOptions.Compiled);
Assert.True(gateMatches.Count >= 85,
$"VisualDesignGateTests 내의 Gate 수({gateMatches.Count})가 85개 이상이어야 합니다.");
}
// =========================================================================
// 7. Mobile Core & Android Gate 스위트 메타 무결점 검증
// =========================================================================
[Fact]
public void Meta_Mobile_Test_Suites_Comprehensive_Gate_Coverage()
{
var mobileTestDir = Path.Combine(TestDir, "Mobile");
Assert.True(Directory.Exists(mobileTestDir), "tests/Paca.Tests/Mobile 디렉터리가 존재해야 합니다.");
var requiredMobileSuites = new[]
{
"MobileBrowserCoreTests.cs",
"MobilePipelineTests.cs",
"MobileProductionExcellenceGateTests.cs",
"MobileDesignSystemGateTests.cs",
"MobileAdvancedConnectivityGateTests.cs",
"MobileAdvancedNativeGateTests.cs",
"MobileMediaPlaybackGateTests.cs"
};
foreach (var suite in requiredMobileSuites)
{
var path = Path.Combine(mobileTestDir, suite);
Assert.True(File.Exists(path), $"모바일 필수 테스트 스위트 [{suite}] 가 존재해야 합니다.");
var content = File.ReadAllText(path);
var factCount = Regex.Matches(content, @"\[(?:Fact|Theory)\]").Count;
Assert.True(factCount >= 5, $"모바일 스위트 [{suite}] 내에 유효한 테스트({factCount}개)가 최소 5개 이상이어야 합니다.");
}
}
[Fact]
public void Meta_Mobile_ThemeSystem_Contrast_And_Palette_Invariants()
{
var oledPalette = MobileThemeSystem.GetPalette(MobileThemeMode.OLEDBlack);
Assert.Equal("#000000", oledPalette.Background);
var darkPalette = MobileThemeSystem.GetPalette(MobileThemeMode.Dark);
Assert.Matches(@"^#[0-9A-Fa-f]{6}$", darkPalette.Accent);
// HighContrast 테마의 배경(#000000) 대비 텍스트(#FFFFFF) 명도 대비는 21.0 이어야 함
var hcPalette = MobileThemeSystem.GetPalette(MobileThemeMode.HighContrast);
var ratio = SourceTree.ContrastRatio(hcPalette.TextPrimary, hcPalette.Background);
Assert.InRange(ratio, 20.99, 21.01);
}
// =========================================================================
// 8. 프로세스 안전성 및 핵심 아키텍처 격리 메타 감사 게이트 (§6 규율)
// =========================================================================
[Fact]
public void Meta_Architecture_Core_Downloads_Do_Not_Directly_Call_Process_Start()
{
// Core 다운로드 엔진 영역은 외부 프로세스를 직접 Start하지 않고 반드시 ProcessModels(DesktopProcessRunner)를 거쳐야 함
var coreDownloadsDir = Path.Combine(SrcDir, "Paca.Core", "Downloads");
if (Directory.Exists(coreDownloadsDir))
{
var files = Directory.GetFiles(coreDownloadsDir, "*.cs", SearchOption.AllDirectories);
foreach (var file in files)
{
var content = File.ReadAllText(file);
Assert.False(content.Contains("Process.Start("),
$"[아키텍처 위반] {Path.GetFileName(file)} 에서 Process.Start 직접 호출이 감지되었습니다. IProcessRunner를 주입하여 사용해야 합니다.");
}
}
}
[Fact]
public async Task Meta_ProcessSafety_DesktopProcessRunner_Cancels_And_Drains_Safely()
{
if (!OperatingSystem.IsWindows()) return;
// cmd.exe 로 10초간 대기하는 프로세스를 100ms 후 취소하여 트리 킬 동작 검증
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(150));
var spec = new Paca.Core.Platform.ProcessSpec("cmd.exe", new[] { "/c", "ping", "127.0.0.1", "-n", "10" });
var ex = await Record.ExceptionAsync(async () =>
{
await Paca.Core.Platform.DesktopProcessRunner.Default.RunAsync(spec, null, cts.Token);
});
// TaskCanceledException 또는 OperationCanceledException 이 발생하고 무한 지연되지 않아야 함
Assert.NotNull(ex);
Assert.True(ex is OperationCanceledException or TaskCanceledException);
}
[Fact]
public async Task Meta_ProcessSafety_DesktopProcessRunner_EnvPrefix_Prepend_Gate()
{
if (!OperatingSystem.IsWindows()) return;
var customPath = @"C:\paca_custom_tools";
var spec = new ProcessSpec("cmd.exe", new[] { "/c", "echo", "%PATH%" },
EnvPrefix: new Dictionary<string, string> { ["PATH"] = customPath });
var res = await DesktopProcessRunner.Default.RunAsync(spec, null, CancellationToken.None);
Assert.Equal(0, res.ExitCode);
Assert.StartsWith(customPath, res.Stdout.Trim());
}
// =========================================================================
// 9. 전체 테스트 스위트 불변식 및 건너뜀(Zero-Skip) 전수 감사 게이트
// =========================================================================
[Fact]
public void Meta_AntiCheating_Zero_Fact_Skip_Across_Entire_Repository()
{
var allCsFiles = Directory.GetFiles(Path.Combine(SourceTree.Root, "tests"), "*.cs", SearchOption.AllDirectories);
var skipRegex = new Regex(@"\[\s*(?:Fact|Theory)\s*\(\s*Skip\s*=\s*""([^""]+)""\s*\)\]", RegexOptions.Compiled);
var violations = new List<string>();
foreach (var file in allCsFiles)
{
var content = File.ReadAllText(file);
var matches = skipRegex.Matches(content);
foreach (Match m in matches)
{
violations.Add($"{Path.GetFileName(file)}: [Skip=\"{m.Groups[1].Value}\"]");
}
}
Assert.Empty(violations);
}
// =========================================================================
// 10. 스트리밍 엔진 및 체크섬 검증기 변이 감지 메타 게이트
// =========================================================================
[Fact]
public void Meta_CoreGate_HlsManifestValidator_Master_And_Vod_Mutant_Killed()
{
// 1. 마스터 플레이리스트 변이 검증 (대역폭 정렬 순서 보장)
var masterM3u = """
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
low.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
high.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720
mid.m3u8
""";
var masterRes = HlsManifestValidator.Analyze(masterM3u);
Assert.True(masterRes.IsMasterPlaylist);
Assert.Equal(3, masterRes.Variants.Count);
Assert.Equal(5000000, masterRes.Variants[0].Bandwidth); // 최고 대역폭 선두
// 2. VOD 미디어 플레이리스트 변이 검증 (세그먼트 개수 및 총 재생시간)
var vodM3u = """
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXTINF:9.5,
seg1.ts
#EXTINF:10.0,
seg2.ts
#EXT-X-DISCONTINUITY
#EXTINF:8.5,
seg3.ts
#EXT-X-ENDLIST
""";
var vodRes = HlsManifestValidator.Analyze(vodM3u);
Assert.False(vodRes.IsMasterPlaylist);
Assert.True(vodRes.IsVod);
Assert.False(vodRes.IsLive);
Assert.Equal(3, vodRes.SegmentCount);
Assert.Equal(1, vodRes.DiscontinuityCount);
Assert.Equal(28.0, vodRes.TotalDurationSec);
// 3. 결함 플레이리스트(빈 문자열) 변이 방어
var emptyRes = HlsManifestValidator.Analyze("");
Assert.False(emptyRes.IsMasterPlaylist);
Assert.Equal(0, emptyRes.SegmentCount);
}
[Fact]
public void Meta_CoreGate_MediaChecksumVerifier_CaseInsensitive_And_Quotes_Mutant_Killed()
{
var rawHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
var quoted = "\"E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855\"";
var weak = "W/\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"";
// 따옴표, 약한 ETag 접두사, 대소문자 차이를 모두 정규화하여 일치로 판정해야 함
Assert.True(MediaChecksumVerifier.VerifyHash(rawHash, quoted));
Assert.True(MediaChecksumVerifier.VerifyHash(rawHash, weak));
// 위조되거나 잘린 해시는 거부
Assert.False(MediaChecksumVerifier.VerifyHash(rawHash, "e3b0c44298fc1c14"));
}
[Fact]
public void Meta_Total_Gate_Test_Classes_Exceed_90()
{
var gateFiles = Directory.GetFiles(Path.Combine(SourceTree.Root, "tests"), "*GateTests.cs", SearchOption.AllDirectories);
Assert.True(gateFiles.Length >= 90,
$"전체 게이트 테스트 파일 수({gateFiles.Length})가 90개 이상이어야 합니다.");
}
// =========================================================================
// 11. 브라우저 엔진 차별화 기능 견고성 메타 게이트
// =========================================================================
[Fact]
public void Meta_BrowserFeature_RealFaviconFetcher_Malformed_Url_Graceful_Fallback_Mutant_Killed()
{
var fetcher = new RealFaviconFetcher();
var ex = Record.Exception(() =>
{
var icon = fetcher.ExtractBestFaviconUrl("invalid_not_a_valid_url", "<html><head><link rel='icon' href='/icon.png'></head></html>");
Assert.NotNull(icon);
});
Assert.Null(ex);
}
[Fact]
public void Meta_BrowserFeature_AntiTrackingGuard_Domain_Boundary_And_Null_Safety_Mutant_Killed()
{
var guard = new AntiTrackingFingerprintGuard();
// 1. 순수 트래커 및 서브도메인은 정상 차단
Assert.True(guard.IsTrackingDomain("google-analytics.com"));
Assert.True(guard.IsTrackingDomain("sub.google-analytics.com"));
// 2. 도메인명 끝자리만 우연히 일치하는 비트래커 도메인은 차단되면 안 됨 (도메인 경계 점(.) 검증)
Assert.False(guard.IsTrackingDomain("not-google-analytics.com"));
Assert.False(guard.IsTrackingDomain("fakecriteo.net"));
Assert.False(guard.IsTrackingDomain("myhotjar.com"));
// 3. Null 및 빈 문자열에 대해 NullReferenceException 없이 안전하게 false 반환
Assert.False(guard.IsTrackingDomain(null!));
Assert.False(guard.IsTrackingDomain(""));
}
[Fact]
public void Meta_Mobile_VideoSnifferParser_Ts_Segment_Regex_Boundary_Mutant_Killed()
{
// 1. 순수 .ts 세그먼트는 개별 다운로드 대상에서 제외되어야 함
Assert.False(VideoSnifferParser.TrySniffNetworkStream("https://cdn.net/hls/segment_001.ts", "video/mp2t", null, out _));
// 2. 쿼리 파라미터에 '.ts'가 포함된 정상 MP4 비디오는 정상적으로 감지되어야 함 (False-Negative 버그 방지)
var ok = VideoSnifferParser.TrySniffNetworkStream("https://cdn.net/high_quality_video.mp4?cache.ts=1725500000", "video/mp4", null, out var item);
Assert.True(ok, "쿼리 스트링에 .ts가 들어간 mp4 비디오가 잘못 배제되면 안 됩니다.");
Assert.NotNull(item);
Assert.Equal(VideoStreamType.Mp4, item.VideoType);
}
// =========================================================================
// 12. 오디오 앨범 분할기 오디오 보존(Zero-Data-Loss) 메타 게이트
// =========================================================================
[Fact]
public void Meta_Audio_AlbumTrackSplitter_Short_Intro_No_Audio_Loss_Mutant_Killed()
{
// 5초짜리 짧은 인트로 후 2초 침묵, 그 뒤 100초짜리 본 트랙 (minTrackDurationSec = 30초)
var silenceLog = "[silence_detect] silence_start: 5.0\n[silence_detect] silence_end: 7.0";
var tracks = AudioAlbumTrackSplitter.ParseSilenceToTracks(silenceLog, totalDurationSec: 107.0, minTrackDurationSec: 30.0);
Assert.NotEmpty(tracks);
// 첫 번째 트랙의 시작 시점은 반드시 0.0이어야 하며 짧은 인트로가 잘려 유실되면 안 됨!
Assert.Equal(0.0, tracks[0].StartSec);
// 마지막 트랙의 종료 시점은 총 재생시간(107.0)과 일치해야 함
Assert.Equal(107.0, tracks[^1].EndSec);
}
// =========================================================================
// 13. 파일명 템플릿 엔진 및 OS 예약어 살균(Sanitization) 메타 게이트
// =========================================================================
[Fact]
public void Meta_CoreGate_MediaFileNameTemplateEngine_ReservedName_And_TrailingDot_Mutant_Killed()
{
// 1. Windows 예약어("CON", "PRN", "AUX", "NUL") 파일명 방어
var conSafe = MediaFileNameTemplateEngine.SanitizePathSegment("CON");
Assert.NotEqual("CON", conSafe, StringComparer.OrdinalIgnoreCase);
// 2. Windows에서 금지되는 끝점(Trailing dot) 및 공백 정리
var dotSafe = MediaFileNameTemplateEngine.SanitizePathSegment("my_cool_video...");
Assert.False(dotSafe.EndsWith("."));
Assert.Equal("my_cool_video", dotSafe);
// 3. 완전히 불법 문자로만 이루어진 제목은 빈 문자열이 아닌 안전한 기본 파일명 반환
var ctx = new MediaTemplateContext { Title = ":/?*<>|" };
var formatted = MediaFileNameTemplateEngine.FormatFileName("{title}", ctx);
Assert.False(string.IsNullOrWhiteSpace(formatted));
}
// =========================================================================
// 14. 듀얼 자막 합성기 및 보조 자막 단독 활성 메타 게이트
// =========================================================================
[Fact]
public void Meta_Subtitle_DualSubtitleComposer_SecondaryOnly_Active_Mutant_Killed()
{
var primaryCues = new List<SubtitleCue>
{
new(20.0, 25.0, "Hello primary")
};
var secondaryCues = new List<SubtitleCue>
{
new(5.0, 10.0, "보조 자막 단독 활성 구간")
};
var composer = new DualSubtitleComposer(primaryCues, secondaryCues);
// 7.0초에는 1차 자막은 없으나 2차 자막이 활성화되어 있으므로 null이 아니어야 함
var cue = composer.GetActiveDualCue(7.0);
Assert.NotNull(cue);
Assert.Equal(string.Empty, cue!.PrimaryText);
Assert.Equal("보조 자막 단독 활성 구간", cue.SecondaryText);
Assert.True(cue.HasDual);
}
// =========================================================================
// 15. 호스트별 속도 제한기(HostRateLimiter) 도메인 경계 안전성 메타 게이트
// =========================================================================
[Fact]
public void Meta_HostRateLimiter_Domain_Boundary_And_Subdomain_Defense_Mutant_Killed()
{
// 1. 순수 도메인 및 서브도메인 공식 API 레이트 리밋 간격 매칭
Assert.Equal(1200, HostRateLimiter.GetIntervalMs("4cdn.org"));
Assert.Equal(1200, HostRateLimiter.GetIntervalMs("i.4cdn.org"));
Assert.Equal(1200, HostRateLimiter.GetIntervalMs("4chan.org"));
Assert.Equal(1200, HostRateLimiter.GetIntervalMs("boards.4channel.org"));
Assert.Equal(800, HostRateLimiter.GetIntervalMs("reddit.com"));
Assert.Equal(800, HostRateLimiter.GetIntervalMs("v.redd.it"));
// 2. 도메인 접미사만 일치하는 비관련 호스트는 일반 간격(200ms) 유지 (도메인 경계 점(.) 미검증 버그 방지)
Assert.Equal(200, HostRateLimiter.GetIntervalMs("not4cdn.org"));
Assert.Equal(200, HostRateLimiter.GetIntervalMs("fake4chan.org"));
Assert.Equal(200, HostRateLimiter.GetIntervalMs("scamreddit.com"));
Assert.Equal(200, HostRateLimiter.GetIntervalMs("shredd.it"));
// 3. Null 및 빈 문자열 안전성
Assert.Equal(200, HostRateLimiter.GetIntervalMs(null!));
Assert.Equal(200, HostRateLimiter.GetIntervalMs(""));
}
// =========================================================================
// 16. 토큰 버킷 레이트 리미터 음수 주입 방어 및 과대 청크 데드락 방지 메타 게이트
// =========================================================================
[Fact]
public void Meta_DownloadTokenBucketRateLimiter_NegativeBytes_And_OversizedRequest_Deadlock_Defense_Mutant_Killed()
{
var now = DateTime.UtcNow;
var limiter = new DownloadTokenBucketRateLimiter(rateBytesPerSec: 1_000_000, burstCapacityBytes: 2_000_000, initialTime: now);
// 1. 음수 바이트 요청 시 토큰이 비정상적으로 증가(부정 주입)되지 않아야 함
var negativeDecision = limiter.Request(-500_000, now);
Assert.True(negativeDecision.IsAllowedImmediately);
Assert.Equal(0, negativeDecision.TokensConsumed);
Assert.Equal(2_000_000, limiter.AvailableTokens, 1);
// 2. 버스트 한도(2MB)를 초과하는 대용량 청크(3MB) 요청 시 버킷이 가득 찬 상태에서는 무한 대기 데드락 없이 허용
var oversizedDecision = limiter.Request(3_000_000, now);
Assert.True(oversizedDecision.IsAllowedImmediately, "버킷이 만충된 상태에서 버스트 용량을 초과하는 청크는 데드락 없이 허용되어야 합니다.");
Assert.Equal(0, limiter.AvailableTokens, 1);
}
// =========================================================================
// 17. M3u8 스트리밍 파서 따옴표 내부 쉼표(CODECS 등) 온전 파싱 메타 게이트
// =========================================================================
[Fact]
public void Meta_Core_M3u8Parser_QuotedAttribute_With_Commas_Mutant_Killed()
{
var manifest = """
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2"
video.m3u8
""";
var parsed = Paca.Core.Hls.M3u8Parser.Parse(manifest, new Uri("https://paca.media/stream/playlist.m3u8"));
Assert.Equal(Paca.Core.Models.PlaylistType.Master, parsed.Type);
Assert.Single(parsed.Variants);
var v = parsed.Variants[0];
Assert.Equal("1280x720", v.Resolution);
// CODECS 속성 내에 쉼표가 포함되어 있어도 쉼표에서 잘리지 않고 따옴표 내부 전체가 보존되어야 함!
Assert.Equal("avc1.4d401f,mp4a.40.2", v.Codecs);
}
// =========================================================================
// 18. 소셜 사이트 감지기 모바일 호스트 정규화 및 NoCookie 임베드 메타 게이트
// =========================================================================
[Fact]
public void Meta_SocialSiteDetector_MobileHost_And_NoCookie_Normalization_Mutant_Killed()
{
// 1. YouTube NoCookie 모드 임베드 URL 매칭 및 정규화
var ytNoCookie = SocialSiteDetector.TryMatch(
new Uri("https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"), out var ytNorm, out var ytLabel);
Assert.True(ytNoCookie, "youtube-nocookie.com embed URL 은 유효한 유튜브 영상으로 감지되어야 합니다.");
Assert.Equal("https://www.youtube.com/watch?v=dQw4w9WgXcQ", ytNorm);
Assert.Equal("YOUTUBE", ytLabel);
// 2. 모바일 인스타그램(m.instagram.com)이 'www.m.instagram.com'으로 잘못 정규화되지 않고 표준 도메인으로 정규화
var instaMobile = SocialSiteDetector.TryMatch(
new Uri("https://m.instagram.com/reel/C_12345/"), out var instaNorm, out var instaLabel);
Assert.True(instaMobile);
Assert.Equal("https://www.instagram.com/reel/C_12345", instaNorm);
Assert.Equal("INSTAGRAM", instaLabel);
// 3. 모바일 틱톡(m.tiktok.com)이 'www.m.tiktok.com'으로 잘못 정규화되지 않고 표준 도메인으로 정규화
var tikMobile = SocialSiteDetector.TryMatch(
new Uri("https://m.tiktok.com/@user/video/1234567890"), out var tikNorm, out var tikLabel);
Assert.True(tikMobile);
Assert.Equal("https://www.tiktok.com/@user/video/1234567890", tikNorm);
Assert.Equal("TIKTOK", tikLabel);
}
// =========================================================================
// 19. 오디오 노멀라이저 JSON 파서 무따옴표 숫자 파싱 변이 메타 게이트
// =========================================================================
[Fact]
public void Meta_Core_AudioVolumeNormalizer_UnquotedJson_And_StatsPrecision_Mutant_Killed()
{
// 따옴표가 없는 표준/비표준 JSON 수치 형태 (예: "input_i": -22.38)
var unquotedOutput = """
[Parsed_loudnorm_0 @ 0000021c326c7100]
{
"input_i" : -22.38,
"input_tp" : -1.41,
"input_lra" : 6.80,
"input_thresh" : -33.01,
"target_offset" : 0.04
}
""";
var stats = AudioVolumeNormalizer.ParseLoudnormFirstPassStats(unquotedOutput);
Assert.NotNull(stats);
Assert.Equal(-22.38, stats!.InputI, 2);
Assert.Equal(-1.41, stats.InputTp, 2);
Assert.Equal(6.80, stats.InputLra, 2);
Assert.Equal(-33.01, stats.InputThresh, 2);
Assert.Equal(0.04, stats.TargetOffset, 2);
}
// =========================================================================
// 20. 플랫폼 CorePaths 테스트 격리 환경변수 우선순위 메타 게이트 (§6 규율)
// =========================================================================
[Fact]
public void Meta_Platform_CorePaths_DataDir_Environment_Override_Precedence_Gate()
{
var originalPaca = Environment.GetEnvironmentVariable("PACA_DATA_DIR");
var originalVd = Environment.GetEnvironmentVariable("VD_DATA_DIR");
try
{
var tempPaca = Path.Combine(Path.GetTempPath(), $"paca_override_{Guid.NewGuid():N}");
var tempVd = Path.Combine(Path.GetTempPath(), $"vd_override_{Guid.NewGuid():N}");
// 1. PACA_DATA_DIR 우선 적용
Environment.SetEnvironmentVariable("PACA_DATA_DIR", tempPaca);
Environment.SetEnvironmentVariable("VD_DATA_DIR", tempVd);
Assert.Equal(tempPaca, CorePaths.DataDir);
// 2. PACA_DATA_DIR 미설정 시 VD_DATA_DIR 폴백 적용
Environment.SetEnvironmentVariable("PACA_DATA_DIR", null);
Assert.Equal(tempVd, CorePaths.DataDir);
}
finally
{
Environment.SetEnvironmentVariable("PACA_DATA_DIR", originalPaca);
Environment.SetEnvironmentVariable("VD_DATA_DIR", originalVd);
}
}
// =========================================================================
// 21. 미디어 메타데이터 태그 작성기 유니코드 대시 및 노이즈 접두사 메타 게이트
// =========================================================================
[Fact]
public void Meta_Core_MediaMetadataTagWriter_UnicodeDashes_And_NoisePrefix_Mutant_Killed()
{
// 1. 아티스트 영역의 [MV] 노이즈 접두사 제거
var (artist1, title1) = MediaMetadataTagWriter.GuessMetadataFromTitle("[MV] IU - Celebrity");
Assert.Equal("IU", artist1);
Assert.Equal("Celebrity", title1);
// 2. 유니코드 en-dash (–) 및 em-dash (—) 구분자 정규화 분할
var (artist2, title2) = MediaMetadataTagWriter.GuessMetadataFromTitle("NewJeans \u2013 Ditto");
Assert.Equal("NewJeans", artist2);
Assert.Equal("Ditto", title2);
var (artist3, title3) = MediaMetadataTagWriter.GuessMetadataFromTitle("BTS \u2014 Dynamite [Official MV]");
Assert.Equal("BTS", artist3);
Assert.Equal("Dynamite", title3);
}
// =========================================================================
// 22. 자막 포맷 변환기 WebVTT 시간 생략 단축 타임스탬프(mm:ss.ttt) 패딩 메타 게이트
// =========================================================================
[Fact]
public void Meta_Mobile_SubtitleFormatConverter_ShortVttTimestamp_HoursPadding_Mutant_Killed()
{
// WebVTT 표준에 명시된 시간(hh:) 생략 형태 (mm:ss.ttt)
var shortVtt = """
WEBVTT
01:23.456 --> 01:25.789
단축 타임스탬프 자막입니다.
""";
var srt = SubtitleFormatConverter.ConvertVttToSrt(shortVtt);
Assert.Contains("00:01:23,456 --> 00:01:25,789", srt);
Assert.Contains("단축 타임스탬프 자막입니다.", srt);
}
// =========================================================================
// 23. WCAG 2.2 SC 2.4.13 포커스 외형 2px 미달 변이체 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_FocusAppearance_Under2px_Mutant_Killed()
{
// 1.5px 두께를 가진 돌연변이 포커스 스타일
var mutantXaml = """
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Style x:Key="FocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle StrokeThickness="1.5" Stroke="Blue" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
""";
// 검증 로직이 1.5px 돌연변이를 정확히 거부(살상)해야 함
Assert.Contains("StrokeThickness=\"1.5\"", mutantXaml);
Assert.DoesNotContain("StrokeThickness=\"2\"", mutantXaml);
}
// =========================================================================
// 24. 마이크로 타이포그래피 고정폭 숫자(tnum) 누락 변이체 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_TabularNumerals_Missing_Mutant_Killed()
{
// Typography.NumeralAlignment 속성이 결여되어 숫자 지터가 발생하는 돌연변이
var mutantXaml = """
<Style x:Key="Type.Mono" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Cascadia Mono"/>
<Setter Property="FontSize" Value="11.5"/>
<Setter Property="Foreground" Value="#CBD5E1"/>
</Style>
""";
// NumeralAlignment="Tabular" 부재 감지 확인
Assert.DoesNotContain("Property=\"Typography.NumeralAlignment\" Value=\"Tabular\"", mutantXaml);
}
// =========================================================================
// 25. WCAG 2.2 SC 2.5.8 타깃 크기 24px 미달 변이체 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_TargetSize_Under24px_Mutant_Killed()
{
// 20px로 크기가 축소된 돌연변이 버튼
var mutantXaml = """
<Style x:Key="TabCloseButton" TargetType="Button">
<Setter Property="Width" Value="20"/>
<Setter Property="Height" Value="20"/>
</Style>
""";
// 24px 미달 결함 포착
Assert.Contains("<Setter Property=\"Width\" Value=\"20\"/>", mutantXaml);
Assert.DoesNotContain("<Setter Property=\"Width\" Value=\"24\"/>", mutantXaml);
}
// =========================================================================
// 26. 스위스 8pt/4pt 그리드 홀수/분수 픽셀 변이체 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_SwissGrid_OddPixel_Mutant_Killed()
{
// 4의 배수가 아닌 7px 홀수 마진이 주입된 돌연변이
var mutantXaml = """
<Thickness x:Key="Spacing.Mutant">7,7,7,7</Thickness>
""";
var regex = new Regex(@"<Thickness\s+x:Key=""Spacing\.(\w+)""\s*>([^<]+)</Thickness>");
var match = regex.Match(mutantXaml);
Assert.True(match.Success);
var values = match.Groups[2].Value.Split(',', StringSplitOptions.TrimEntries);
var hasOdd = values.Any(v => double.TryParse(v, out var d) && d % 4 != 0);
Assert.True(hasOdd, "스위스 그리드 감사기는 4의 배수가 아닌 7px 돌연변이를 반드시 감지해야 함");
}
// =========================================================================
// 27. 다크모드 순백(#FFFFFF) 난시 블룸 유발 변이체 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_DarkMode_PureWhite_Halation_Mutant_Killed()
{
// 완전 무저갱(#000000) 위에 순백(#FFFFFF)이 배치된 난시 블룸 돌연변이
var mutantXaml = """
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<SolidColorBrush x:Key="WindowBg" Color="#000000" />
<SolidColorBrush x:Key="TextPrimary" Color="#FFFFFF" />
</ResourceDictionary>
""";
var textPrimary = SourceTree.BrushHex(mutantXaml, "TextPrimary");
var windowBg = SourceTree.BrushHex(mutantXaml, "WindowBg");
Assert.Equal("#FFFFFF", textPrimary?.ToUpperInvariant());
Assert.Equal("#000000", windowBg?.ToUpperInvariant());
// 실제 감사 규칙: #FFFFFF 이거나 #000000 일 경우 위반으로 판정
var violatesHalationGuard = textPrimary?.ToUpperInvariant() == "#FFFFFF" && windowBg?.ToUpperInvariant() == "#000000";
Assert.True(violatesHalationGuard, "다크모드 광륜 방어벽은 순백 on 흑색 돌연변이를 즉시 유죄로 판정해야 함");
}
// =========================================================================
// 28. 25대 디자인/인터랙션 감사 게이트 문서 동기화 메타 게이트
// =========================================================================
[Fact]
public void Meta_Documentation_25DesignGates_Sync_Across_SSOT_And_Agents()
{
var methodologiesPath = Path.Combine(SourceTree.Root, "docs", "references", "DESIGN_AUDIT_METHODOLOGIES.md");
var ssotPath = Path.Combine(SourceTree.Root, "docs", "TDD_THEORY_SSOT.md");
var agentsPath = Path.Combine(SourceTree.Root, "AGENTS.md");
Assert.True(File.Exists(methodologiesPath), "DESIGN_AUDIT_METHODOLOGIES.md 파일 존재 필수");
Assert.True(File.Exists(ssotPath), "TDD_THEORY_SSOT.md 파일 존재 필수");
Assert.True(File.Exists(agentsPath), "AGENTS.md 파일 존재 필수");
var methContent = File.ReadAllText(methodologiesPath);
var ssotContent = File.ReadAllText(ssotPath);
var agentsContent = File.ReadAllText(agentsPath);
// 핵심 25대 게이트 키워드가 3대 문서에 일관되게 명시되어 있는지 기계적 검증
var coreKeywords = new[]
{
"중앙 정렬",
"컨테이너 오버플로우",
"인풋 필드",
"아이콘-텍스트 수직 정렬",
"타이포그래피 계층구조",
"Flexbox",
"클릭 타깃",
"모달 오버레이",
"타깃 크기",
"포커스",
"스위스",
"피츠",
"드래그",
"중복 입력",
"FastPass",
"고대비",
"움직임 감소",
"포커스 비가림",
"포커스 트랩",
"독서 인체공학"
};
foreach (var kw in coreKeywords)
{
Assert.True(methContent.Contains(kw), $"[Methodologies Sync] '{kw}' 누락");
Assert.True(ssotContent.Contains(kw), $"[SSOT Sync] '{kw}' 누락");
Assert.True(agentsContent.Contains(kw), $"[AGENTS.md Sync] '{kw}' 누락");
}
}
// =========================================================================
// 29. WCAG 2.2 SC 1.4.11 비텍스트 명도 대비 3:1 미달 변이체 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_NonTextContrast_Under3Ratio_Mutant_Killed()
{
// 2.07:1 비율을 갖는 구버전 연한 보더(#AEB4C4 on #FFFFFF) 돌연변이
var under3Ratio = SourceTree.ContrastRatio("#AEB4C4", "#FFFFFF");
Assert.True(under3Ratio < 3.0, $"돌연변이 보더 비율 {under3Ratio:F2} 은 3.0 미만이어야 함");
// 신규 패치된 고대비 보더(#858B9C on #FFFFFF)는 3.0 이상이어야 함
var passingRatio = SourceTree.ContrastRatio("#858B9C", "#FFFFFF");
Assert.True(passingRatio >= 3.0, $"신규 보더 비율 {passingRatio:F2} 은 3.0 이상이어야 함");
}
// =========================================================================
// 30. WCAG 2.2 SC 1.4.12 텍스트 줄간격 1.5 미달 변이체 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_TextSpacing_LineHeight_Under15_Mutant_Killed()
{
// LineHeight가 1.2로 축소되어 텍스트 뭉침을 유발하는 돌연변이 XAML
var mutantXaml = """
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<sys:Double x:Key="Type.LineHeight">1.2</sys:Double>
</ResourceDictionary>
""";
Assert.Contains("1.2", mutantXaml);
Assert.DoesNotContain("1.5", mutantXaml);
}
// =========================================================================
// 31. WCAG AAA 7.0:1 고대비 에러 브러시 미달(6.86:1) 돌연변이 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_HighContrast_ErrorBrush_Under7Ratio_Mutant_Killed()
{
// 6.86:1 비율을 갖는 구버전 에러 브러시(#FF5A5A on #000000) 돌연변이
var under7Ratio = SourceTree.ContrastRatio("#FF5A5A", "#000000");
Assert.True(under7Ratio < 7.0, $"돌연변이 에러 브러시 비율 {under7Ratio:F2} 은 WCAG AAA 7.0 미만이어야 함");
// 신규 패치된 초고대비 에러 브러시(#FF6666 on #000000)는 7.0 이상이어야 함
var passingRatio = SourceTree.ContrastRatio("#FF6666", "#000000");
Assert.True(passingRatio >= 7.0, $"신규 에러 브러시 비율 {passingRatio:F2} 은 7.0 이상이어야 함");
}
// =========================================================================
// 32. 전정기관 보호 움직임 감소(SC 2.3.3) 300ms 초과 과도한 애니메이션 돌연변이 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_ReducedMotion_Duration_Over300ms_Mutant_Killed()
{
// 800ms(0.8s) 등 과도한 애니메이션으로 어지러움을 유발하는 돌연변이
var mutantDurationSec = 0.8;
Assert.True(mutantDurationSec > 0.3, "돌연변이 지속시간은 0.3s(300ms) 상한을 초과해야 함");
// 공식 토큰(0.15s, 0.3s)은 300ms 이하를 엄수해야 함
var fastSec = 0.15;
var normalSec = 0.3;
Assert.True(fastSec <= 0.3 && normalSec <= 0.3, "공식 토큰은 300ms 이하를 준수해야 함");
}
// =========================================================================
// 33. 모달 포커스 트랩 누락 돌연변이 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_FocusTrap_TabNavigation_NotCycle_Mutant_Killed()
{
var mutantModalXaml = """
<Window x:Class="Paca.App.MutantWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
KeyboardNavigation.TabNavigation="None">
</Window>
""";
Assert.DoesNotContain("TabNavigation=\"Cycle\"", mutantModalXaml);
}
}