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;
///
/// 검사체계를 검사하는 상위 메타 검증 게이트 (Meta-Verification & Anti-Cheating Harness).
/// Robert C. Martin TDD 3대 원칙 및 10대 디자인 감사 게이트, 안티치팅 규칙(Skip/빈 catch/Async void 금지),
/// 게이트 검증 함수들의 변이(Mutation) 감지력, XAML 파서 무결점, 계약 어설션의 비동어반복성을 기계적으로 전수 검증합니다.
///
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();
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();
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();
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(() =>
{
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
{
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(), "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 = """
""";
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 = """
""";
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 = """
""";
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 = """
""";
// 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 { ["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();
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", "");
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
{
new(20.0, 25.0, "Hello primary")
};
var secondaryCues = new List
{
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 = """
""";
// 검증 로직이 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 = """
""";
// 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 = """
""";
// 24px 미달 결함 포착
Assert.Contains("", mutantXaml);
Assert.DoesNotContain("", mutantXaml);
}
// =========================================================================
// 26. 스위스 8pt/4pt 그리드 홀수/분수 픽셀 변이체 살상 메타 게이트
// =========================================================================
[Fact]
public void Meta_VisualDesignGate_SwissGrid_OddPixel_Mutant_Killed()
{
// 4의 배수가 아닌 7px 홀수 마진이 주입된 돌연변이
var mutantXaml = """
7,7,7,7
""";
var regex = new Regex(@"([^<]+)");
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 = """
""";
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 = """
1.2
""";
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 = """
""";
Assert.DoesNotContain("TabNavigation=\"Cycle\"", mutantModalXaml);
}
}