using System; using System.Globalization; using System.IO; using System.Security.Cryptography; using NUnit.Framework; using UnityEngine; namespace Streamingle.Editor.Tests { public sealed class AICameraGeneratorWindowTests { [Serializable] private sealed class DirectiveDocument { public string schemaVersion; public DirectiveShot[] shots; } [Serializable] private sealed class DirectiveShot { public int index; public string shotSize; public string motion; public string composition; public float distanceMeters; public float motionIntensity; public float bodyFollowStrength; } private static string HybridMetadataJson( string generationMode = "hierarchical_hybrid_retarget_v12", string plannerMode = "hierarchical", double targetAspectRatio = 16d / 9d) { return "{" + "\"schemaVersion\":\"3.6\"," + $"\"generationMode\":\"{generationMode}\"," + "\"songId\":\"song-id\"," + "\"datasetId\":\"dataset-id\"," + "\"frameCount\":6," + "\"sampleRate\":60," + "\"shotCount\":2," + $"\"plannerMode\":\"{plannerMode}\"," + "\"targetAspectRatio\":" + targetAspectRatio.ToString( "R", CultureInfo.InvariantCulture) + "," + "\"worldCameraFile\":\"generated_camera_world.f32\"," + "\"timeFile\":\"time.f64\"," + "\"shotsFile\":\"shots.json\"," + "\"outputSha256\":{" + "\"worldCamera\":\"camera-hash\"," + "\"time\":\"time-hash\"," + "\"shots\":\"shots-hash\"}}"; } private static string HybridShotsJson(string schemaVersion = "1.2") { return "{" + $"\"schemaVersion\":\"{schemaVersion}\"," + "\"shots\":[" + "{\"index\":0,\"startFrame\":0,\"endFrameExclusive\":3}," + "{\"index\":1,\"startFrame\":3,\"endFrameExclusive\":6}]}"; } [Test] public void DefaultCwAiRootPrefersConfiguredThenSiblingRepository() { var root = Path.Combine( Path.GetTempPath(), "cw-ai-root-tests-" + Guid.NewGuid().ToString("N")); var project = Path.Combine(root, "UnityProject"); var configured = Path.Combine(root, "ConfiguredCWAI"); var sibling = Path.Combine(root, "CW-AI"); Directory.CreateDirectory(project); CreateCwAiRootMarker(configured); CreateCwAiRootMarker(sibling); try { Assert.That( AICameraGeneratorWindow.ResolveDefaultCwAiRoot( project, configured, Path.Combine(root, "MissingPackage")), Is.EqualTo(Path.GetFullPath(configured))); Assert.That( AICameraGeneratorWindow.ResolveDefaultCwAiRoot( project, string.Empty, Path.Combine(root, "MissingPackage")), Is.EqualTo(Path.GetFullPath(sibling))); } finally { Directory.Delete(root, true); } } [Test] public void DefaultCwAiRootSkipsProjectCopyWithoutTrainingIndex() { var root = Path.Combine( Path.GetTempPath(), "cw-ai-incomplete-root-tests-" + Guid.NewGuid().ToString("N")); var project = Path.Combine(root, "UnityProject"); var sibling = Path.Combine(root, "CW-AI"); Directory.CreateDirectory(Path.Combine(project, "DatasetExports")); Directory.CreateDirectory(Path.Combine( project, "MachineLearning", "CameraDirector")); CreateCwAiRootMarker(sibling); try { Assert.That( AICameraGeneratorWindow.IsGenerationLibraryRoot(project), Is.False); Assert.That( AICameraGeneratorWindow.ResolveDefaultCwAiRoot( project, project, Path.Combine(root, "MissingPackage")), Is.EqualTo(Path.GetFullPath(sibling))); } finally { Directory.Delete(root, true); } } [Test] public void DefaultCwAiRootSkipsMalformedConfiguredPath() { var root = Path.Combine( Path.GetTempPath(), "cw-ai-malformed-root-tests-" + Guid.NewGuid().ToString("N")); var project = Path.Combine(root, "UnityProject"); var sibling = Path.Combine(root, "CW-AI"); Directory.CreateDirectory(project); CreateCwAiRootMarker(sibling); try { Assert.That( AICameraGeneratorWindow.ResolveDefaultCwAiRoot( project, "\0invalid", Path.Combine(root, "MissingPackage")), Is.EqualTo(Path.GetFullPath(sibling))); } finally { Directory.Delete(root, true); } } [Test] public void ProjectCacheRootDefaultsToWritableProjectLibrary() { var project = Path.Combine( Path.GetTempPath(), "cw-ai-cache-root-tests-" + Guid.NewGuid().ToString("N")); var configured = Path.Combine(project, "ConfiguredCache"); Assert.That( AICameraGeneratorWindow.ResolveProjectCacheRoot( project, string.Empty), Is.EqualTo(Path.Combine( Path.GetFullPath(project), "Library", "CWAI"))); Assert.That( AICameraGeneratorWindow.ResolveProjectCacheRoot( project, configured), Is.EqualTo(Path.GetFullPath(configured))); } private static void CreateCwAiRootMarker(string root) { Directory.CreateDirectory(Path.Combine(root, "DatasetExports")); Directory.CreateDirectory(Path.Combine( root, "MachineLearning", "CameraDirector")); var reports = Directory.CreateDirectory(Path.Combine(root, "reports")); File.WriteAllText( Path.Combine(reports.FullName, "training_index.json"), "{}"); var models = Directory.CreateDirectory(Path.Combine(root, "models")); File.WriteAllText( Path.Combine(models.FullName, "cut_ranker_v2.json"), "{}"); } [Test] public void DefaultCwAiRootReplacesStalePackageCacheRevision() { var root = Path.Combine( Path.GetTempPath(), "cw-ai-package-root-tests-" + Guid.NewGuid().ToString("N")); var project = Path.Combine(root, "UnityProject"); var staleRuntime = Path.Combine( project, "Library", "PackageCache", "com.mingle.cw-ai@old-revision", "RuntimeData~"); var currentPackage = Path.Combine( project, "Library", "PackageCache", "com.mingle.cw-ai@current-revision"); var currentRuntime = Path.Combine(currentPackage, "RuntimeData~"); CreateCwAiRootMarker(staleRuntime); CreateCwAiRootMarker(currentRuntime); try { Assert.That( AICameraGeneratorWindow.ResolveDefaultCwAiRoot( project, staleRuntime, currentPackage), Is.EqualTo(Path.GetFullPath(currentRuntime))); } finally { Directory.Delete(root, true); } } [Test] public void DefaultCwAiRootPreservesValidExternalSelection() { var root = Path.Combine( Path.GetTempPath(), "cw-ai-external-root-tests-" + Guid.NewGuid().ToString("N")); var project = Path.Combine(root, "UnityProject"); var external = Path.Combine(root, "ExternalCWAI"); var currentPackage = Path.Combine( project, "Library", "PackageCache", "com.mingle.cw-ai@current-revision"); CreateCwAiRootMarker(external); CreateCwAiRootMarker(Path.Combine(currentPackage, "RuntimeData~")); try { Assert.That( AICameraGeneratorWindow.ResolveDefaultCwAiRoot( project, external, currentPackage), Is.EqualTo(Path.GetFullPath(external))); } finally { Directory.Delete(root, true); } } [Test] public void CliExecutableReplacesStalePackageCacheRevision() { var root = Path.Combine( Path.GetTempPath(), "cw-ai-worker-path-tests-" + Guid.NewGuid().ToString("N")); var project = Path.Combine(root, "UnityProject"); var staleWorker = Path.Combine( project, "Library", "PackageCache", "com.mingle.cw-ai@old-revision", "Tools~", "CWCameraWorker", AICameraCliRunner.ExecutableName); var currentPackage = Path.Combine( project, "Library", "PackageCache", "com.mingle.cw-ai@current-revision"); var currentWorker = Path.Combine( currentPackage, "Tools~", "CWCameraWorker", AICameraCliRunner.ExecutableName); Directory.CreateDirectory(Path.GetDirectoryName(staleWorker)); Directory.CreateDirectory(Path.GetDirectoryName(currentWorker)); File.WriteAllText(staleWorker, string.Empty); File.WriteAllText(currentWorker, string.Empty); try { Assert.That( AICameraGeneratorWindow.ResolveCliExecutablePath( staleWorker, string.Empty, project, currentPackage), Is.EqualTo(Path.GetFullPath(currentWorker))); } finally { Directory.Delete(root, true); } } [Test] public void CliExecutablePreservesValidExternalSelection() { var root = Path.Combine( Path.GetTempPath(), "cw-ai-external-worker-tests-" + Guid.NewGuid().ToString("N")); var project = Path.Combine(root, "UnityProject"); var externalWorker = Path.Combine( root, "ExternalWorker", AICameraCliRunner.ExecutableName); var currentPackage = Path.Combine( project, "Library", "PackageCache", "com.mingle.cw-ai@current-revision"); var currentWorker = Path.Combine( currentPackage, "Tools~", "CWCameraWorker", AICameraCliRunner.ExecutableName); Directory.CreateDirectory(Path.GetDirectoryName(externalWorker)); Directory.CreateDirectory(Path.GetDirectoryName(currentWorker)); File.WriteAllText(externalWorker, string.Empty); File.WriteAllText(currentWorker, string.Empty); try { Assert.That( AICameraGeneratorWindow.ResolveCliExecutablePath( externalWorker, string.Empty, project, currentPackage), Is.EqualTo(Path.GetFullPath(externalWorker))); } finally { Directory.Delete(root, true); } } [Test] public void CliWorkerIsTheOnlyHighQualityBackend() { Assert.That( AICameraGeneratorWindow.HybridQualityBackendLabel, Does.Contain("CWCameraWorker")); Assert.That( AICameraGeneratorWindow.HybridQualityBackendDescription, Does.Contain("별도 Python 설치가 필요하지 않습니다")); } [TestCase(0, "auto")] [TestCase(1, "slow")] [TestCase(2, "normal")] [TestCase(3, "fast")] public void CutRhythmUsesStableDataPlannerCliValues( int value, string expected) { Assert.That( AICameraGeneratorWindow.CutDensityCliValueForTests(value), Is.EqualTo(expected)); Assert.That( AICameraGeneratorWindow.CutRhythmExplanation, Does.Contain("음악")); } [TestCase("auto", "balanced")] [TestCase("center", "centered")] [TestCase("centered", "centered")] [TestCase("left_third", "left_third")] [TestCase("right-third", "right_third")] public void HybridHorizontalFramingPreservesExplicitDirection( string composition, string expected) { Assert.That( AICameraGeneratorWindow.HybridHorizontalFraming(composition), Is.EqualTo(expected)); } [Test] public void FullGenerationIgnoresPersistedManualOverrides() { var unusedOutput = Path.Combine( Path.GetTempPath(), "cw-ai-full-scope-" + Guid.NewGuid().ToString("N")); var scope = AICameraGeneratorWindow.CreateGenerationControlScope( false, 12, unusedOutput, "close", "orbit_right", "left_third", 9.5f, 2f, 1f); Assert.That(scope.ShotSize, Is.EqualTo("auto")); Assert.That(scope.Motion, Is.EqualTo("auto")); Assert.That(scope.Composition, Is.EqualTo("auto")); Assert.That(scope.DistanceMeters, Is.Zero); Assert.That(scope.MotionIntensity, Is.EqualTo(1f)); Assert.That(scope.BodyFollowStrength, Is.EqualTo(0.18f)); Assert.That(scope.DirectivesPath, Is.Empty); Assert.That(scope.UsesSelectedShotOverrides, Is.False); Assert.That(Directory.Exists(unusedOutput), Is.False); } [Test] public void SelectedShotRegenerationSerializesOnlyThatShotsOverrides() { var output = Path.Combine( Path.GetTempPath(), "cw-ai-selected-scope-" + Guid.NewGuid().ToString("N")); try { var scope = AICameraGeneratorWindow.CreateGenerationControlScope( true, 7, output, "medium", "truck_left", "right_third", 6.25f, 1.4f, 0.08f); Assert.That(scope.ShotSize, Is.EqualTo("auto")); Assert.That(scope.Motion, Is.EqualTo("auto")); Assert.That(scope.Composition, Is.EqualTo("auto")); Assert.That(scope.DistanceMeters, Is.Zero); Assert.That(scope.MotionIntensity, Is.EqualTo(1f)); Assert.That(scope.BodyFollowStrength, Is.EqualTo(0.18f)); Assert.That(scope.UsesSelectedShotOverrides, Is.True); Assert.That(File.Exists(scope.DirectivesPath), Is.True); var document = JsonUtility.FromJson( File.ReadAllText(scope.DirectivesPath)); Assert.That( document.schemaVersion, Is.EqualTo("camera-directives-v1")); Assert.That(document.shots, Has.Length.EqualTo(1)); Assert.That(document.shots[0].index, Is.EqualTo(7)); Assert.That(document.shots[0].shotSize, Is.EqualTo("medium")); Assert.That(document.shots[0].motion, Is.EqualTo("truck_left")); Assert.That( document.shots[0].composition, Is.EqualTo("right_third")); Assert.That(document.shots[0].distanceMeters, Is.EqualTo(6.25f)); Assert.That(document.shots[0].motionIntensity, Is.EqualTo(1.4f)); Assert.That(document.shots[0].bodyFollowStrength, Is.EqualTo(0.08f)); } finally { if (Directory.Exists(output)) { Directory.Delete(output, true); } } } [Test] public void FullGenerationDoesNotEmitSelectedShotOneShotArguments() { Assert.That( AICameraGeneratorWindow.BuildSelectedShotOneShotArguments( -1, string.Empty), Is.Empty); } [Test] public void SelectedShotOneShotArgumentsIncludeIndexAndBaseGeneration() { var baseGeneration = Path.Combine( Path.GetTempPath(), "cw ai base generation"); var arguments = AICameraGeneratorWindow.BuildSelectedShotOneShotArguments( 7, baseGeneration); Assert.That(arguments, Does.Contain("--only-shot-index")); Assert.That(arguments, Does.Contain("7")); Assert.That(arguments, Does.Contain("--base-generation")); Assert.That( arguments, Does.Contain(Path.GetFullPath(baseGeneration))); } [Test] public void SelectedShotTimingUsesSeparateOneShotHistory() { var text = AICameraGeneratorWindow.BuildSelectedShotGenerationTimingText( 4d, 10d, AICameraGeneratorWindow.CacheObservation.Hit, AICameraGeneratorWindow.CacheObservation.Miss); Assert.That(text, Does.Contain("선택 카메라 1개만 계산 중")); Assert.That(text, Does.Contain("최근 단일 샷 기준 총")); Assert.That(text, Does.Contain("새 후보 풀 계산 중")); } [Test] public void FailedImportPreservesPreviousGeneratedFolder() { var previous = Path.Combine(Path.GetTempPath(), "previous-base"); var failedCandidate = Path.Combine( Path.GetTempPath(), "failed-candidate"); Assert.That( AICameraGeneratorWindow.GeneratedFolderAfterImport( previous, failedCandidate, false), Is.EqualTo(previous)); } [Test] public void SuccessfulImportAdvancesGeneratedFolder() { var candidate = Path.Combine(Path.GetTempPath(), "new-base"); Assert.That( AICameraGeneratorWindow.GeneratedFolderAfterImport( "previous-base", candidate, true), Is.EqualTo(Path.GetFullPath(candidate))); } [Test] public void HybridBaseContractAcceptsFullCompositeSchema() { Assert.DoesNotThrow(() => AICameraGeneratorWindow .ValidateHybridBaseGenerationContractForTests( HybridMetadataJson(), HybridShotsJson())); } [Test] public void HybridBaseContractRejectsNonHybridOutputBeforeWorkerStarts() { var exception = Assert.Throws(() => AICameraGeneratorWindow .ValidateHybridBaseGenerationContractForTests( HybridMetadataJson( generationMode: "unity_native_fast", plannerMode: string.Empty, targetAspectRatio: 0d), HybridShotsJson("1.3"))); Assert.That(exception.Message, Does.Contain("Hybrid")); } [Test] public void HybridBaseContractRequiresTargetAspectRatio() { var exception = Assert.Throws(() => AICameraGeneratorWindow .ValidateHybridBaseGenerationContractForTests( HybridMetadataJson(targetAspectRatio: 0d), HybridShotsJson())); Assert.That(exception.Message, Does.Contain("targetAspectRatio")); } [Test] public void HybridBaseContractRequiresShotsSchema12() { var exception = Assert.Throws(() => AICameraGeneratorWindow .ValidateHybridBaseGenerationContractForTests( HybridMetadataJson(), HybridShotsJson("1.3"))); Assert.That(exception.Message, Does.Contain("schema 1.2")); } [Test] public void WindowProvenanceRecoveryPrefersCwAiRelativePathAfterMove() { var newRoot = Path.Combine( Path.GetTempPath(), "cw-ai-moved-" + Guid.NewGuid().ToString("N")); var relative = Path.Combine( "examples", "UnityGenerated", "camera_result"); var movedFolder = Path.Combine(newRoot, relative); Directory.CreateDirectory(movedFolder); try { Assert.That( AICameraGeneratorWindow .ResolveGeneratedFolderFromProvenance( newRoot, relative.Replace('\\', '/'), @"D:\OldPc\CW-AI\examples\UnityGenerated\camera_result"), Is.EqualTo(Path.GetFullPath(movedFolder))); } finally { Directory.Delete(newRoot, true); } } [Test] public void WindowProvenanceRecoveryUsesAbsoluteFallback() { var fallback = Path.Combine( Path.GetTempPath(), "cw-ai-fallback-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(fallback); try { Assert.That( AICameraGeneratorWindow .ResolveGeneratedFolderFromProvenance( Path.Combine(fallback, "missing-root"), "examples/UnityGenerated/missing", fallback), Is.EqualTo(Path.GetFullPath(fallback))); } finally { Directory.Delete(fallback, true); } } [Test] public void WindowProvenanceRejectsMetadataChangedBehindStoredHash() { var generatedFolder = Path.Combine( Path.GetTempPath(), "cw-ai-provenance-hash-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(generatedFolder); var metadataPath = Path.Combine(generatedFolder, "metadata.json"); try { File.WriteAllText(metadataPath, "{\"generation\":1}"); string expected; using (var algorithm = SHA256.Create()) { expected = BitConverter.ToString( algorithm.ComputeHash(File.ReadAllBytes(metadataPath))) .Replace("-", string.Empty) .ToLowerInvariant(); } Assert.DoesNotThrow(() => AICameraGeneratorWindow.ValidateProvenanceMetadataSha256( generatedFolder, expected)); File.WriteAllText(metadataPath, "{\"generation\":2}"); Assert.That( () => AICameraGeneratorWindow .ValidateProvenanceMetadataSha256( generatedFolder, expected), Throws.TypeOf() .With.Message.Contains("does not match")); } finally { Directory.Delete(generatedFolder, true); } } [Test] public void ColdStartHybridEstimateIsBroadAndMarkedInitial() { var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate( 204.48d, 0d, 0, -1d, 0d); Assert.That(estimate.IsAvailable, Is.True); Assert.That(estimate.IsInitial, Is.True); Assert.That( estimate.MinimumTotalSeconds, Is.EqualTo(79.4112d).Within(1e-4)); Assert.That( estimate.MaximumTotalSeconds, Is.EqualTo(148.23424d).Within(1e-4)); Assert.That( estimate.MinimumRemainingSeconds, Is.EqualTo(79.4112d).Within(1e-4)); Assert.That( estimate.MaximumRemainingSeconds, Is.EqualTo(148.23424d).Within(1e-4)); } [Test] public void HistoricalEstimateUsesObservedRateAndElapsedTime() { var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate( 200d, 20d, 4, 0.5d, 0.05d); Assert.That(estimate.IsAvailable, Is.True); Assert.That(estimate.IsInitial, Is.False); Assert.That(estimate.MinimumTotalSeconds, Is.EqualTo(85d)); Assert.That(estimate.MaximumTotalSeconds, Is.EqualTo(115d)); Assert.That(estimate.MinimumRemainingSeconds, Is.EqualTo(65d)); Assert.That(estimate.MaximumRemainingSeconds, Is.EqualTo(95d)); } [Test] public void CandidateCacheHitReplacesGeneralHistoryWithShortTailBudget() { var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate( 200d, 10d, 4, 0.5d, 0.05d, AICameraGeneratorWindow.CacheObservation.Hit, AICameraGeneratorWindow.CacheObservation.Hit, 3, false); Assert.That(estimate.MinimumTotalSeconds, Is.EqualTo(13d)); Assert.That(estimate.MaximumTotalSeconds, Is.EqualTo(30d)); Assert.That(estimate.MinimumRemainingSeconds, Is.EqualTo(3d)); Assert.That(estimate.MaximumRemainingSeconds, Is.EqualTo(20d)); } [Test] public void CandidateCacheHitTailCountsDownFromObservationTime() { var initial = AICameraGeneratorWindow.CalculateRuntimeEstimate( 200d, 10d, 4, 0.5d, 0.05d, AICameraGeneratorWindow.CacheObservation.Hit, AICameraGeneratorWindow.CacheObservation.Hit, 3, false, 10d); var later = AICameraGeneratorWindow.CalculateRuntimeEstimate( 200d, 15d, 4, 0.5d, 0.05d, AICameraGeneratorWindow.CacheObservation.Hit, AICameraGeneratorWindow.CacheObservation.Hit, 3, false, 10d); Assert.That(initial.MaximumRemainingSeconds, Is.EqualTo(20d)); Assert.That(later.MaximumRemainingSeconds, Is.EqualTo(15d)); Assert.That( later.MaximumRemainingSeconds, Is.LessThan(initial.MaximumRemainingSeconds)); Assert.That(later.MaximumTotalSeconds, Is.EqualTo(30d)); } [Test] public void PreparationCacheMissAddsColdPreparationPenalty() { var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate( 200d, 20d, 4, 0.5d, 0.05d, AICameraGeneratorWindow.CacheObservation.Miss, AICameraGeneratorWindow.CacheObservation.Miss, 2, false); Assert.That(estimate.MinimumTotalSeconds, Is.EqualTo(103d)); Assert.That(estimate.MaximumTotalSeconds, Is.EqualTo(164d)); Assert.That(estimate.MinimumRemainingSeconds, Is.EqualTo(83d)); Assert.That(estimate.MaximumRemainingSeconds, Is.EqualTo(144d)); } [Test] public void ImportStageBoundsStaleRemainingEstimate() { var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate( 200d, 20d, 4, 0.5d, 0.05d, AICameraGeneratorWindow.CacheObservation.Hit, AICameraGeneratorWindow.CacheObservation.Miss, 4, true); Assert.That(estimate.MinimumRemainingSeconds, Is.Zero); Assert.That(estimate.MaximumRemainingSeconds, Is.EqualTo(24d)); Assert.That(estimate.MaximumTotalSeconds, Is.EqualTo(44d)); } [Test] public void ImportStageTailCountsDownFromObservationTime() { var initial = AICameraGeneratorWindow.CalculateRuntimeEstimate( 200d, 20d, 4, 0.5d, 0.05d, AICameraGeneratorWindow.CacheObservation.Hit, AICameraGeneratorWindow.CacheObservation.Miss, 4, true, double.NaN, 20d); var later = AICameraGeneratorWindow.CalculateRuntimeEstimate( 200d, 21d, 4, 0.5d, 0.05d, AICameraGeneratorWindow.CacheObservation.Hit, AICameraGeneratorWindow.CacheObservation.Miss, 4, true, double.NaN, 20d); Assert.That(initial.MaximumRemainingSeconds, Is.EqualTo(24d)); Assert.That(later.MaximumRemainingSeconds, Is.EqualTo(23d)); Assert.That( later.MaximumRemainingSeconds, Is.LessThan(initial.MaximumRemainingSeconds)); Assert.That(initial.MaximumTotalSeconds, Is.EqualTo(44d)); Assert.That(later.MaximumTotalSeconds, Is.EqualTo(44d)); } [Test] public void InvalidDurationDoesNotClaimAnEstimate() { var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate( 0d, 10d, 2, 0.5d, 0.1d); Assert.That(estimate.IsAvailable, Is.False); Assert.That(estimate.MinimumTotalSeconds, Is.Zero); Assert.That(estimate.MaximumTotalSeconds, Is.Zero); Assert.That(estimate.MinimumRemainingSeconds, Is.Zero); Assert.That(estimate.MaximumRemainingSeconds, Is.Zero); } [Test] public void TimingTextShowsElapsedTotalRemainingAndCacheState() { var estimate = AICameraGeneratorWindow.CalculateRuntimeEstimate( 200d, 10d, 4, 0.5d, 0.05d, AICameraGeneratorWindow.CacheObservation.Hit, AICameraGeneratorWindow.CacheObservation.Hit, 3, false); var text = AICameraGeneratorWindow.BuildGenerationTimingText( 10d, estimate, AICameraGeneratorWindow.CacheObservation.Hit, AICameraGeneratorWindow.CacheObservation.Hit); Assert.That(text, Does.Contain("경과 10초")); Assert.That(text, Does.Contain("최근 실측 예상 총")); Assert.That(text, Does.Contain("남은 시간")); Assert.That(text, Does.Contain("안전 후보 캐시 사용 중")); } [TestCase("Candidate cache hit: abc", "candidate", 1)] [TestCase("[candidate-cache] miss: none", "candidate", 2)] [TestCase( "Candidate cache runtime validation failed; falling back", "candidate", 2)] [TestCase("Preparation cache invalid: sha mismatch", "preparation", 2)] [TestCase("unrelated message", "candidate", 0)] public void CacheLogObservationParsesStableSignals( string message, string cacheName, int expected) { Assert.That( AICameraGeneratorWindow.ParseCacheObservation( message, cacheName), Is.EqualTo( (AICameraGeneratorWindow.CacheObservation)expected)); } [Test] public void CandidateCacheModeDefaultsToAutoAndFreshPoolUsesOff() { Assert.That( AICameraGeneratorWindow.CandidateCacheMode(false), Is.EqualTo("auto")); Assert.That( AICameraGeneratorWindow.CandidateCacheMode(true), Is.EqualTo("off")); } [Test] public void AppliedSeedSummarySeparatesCurrentResultFromNextGeneration() { var summary = AICameraGeneratorWindow.BuildAppliedSeedSummary( 20489696, 20489697, false, -1); Assert.That( summary, Is.EqualTo( "현재 전체 생성 결과 Seed: 20489696\n" + "다음 생성에 사용할 Seed: 20489697")); } [Test] public void AppliedSeedSummaryMarksSelectedShotAsMixedResult() { var summary = AICameraGeneratorWindow.BuildAppliedSeedSummary( 20489696, 20489697, true, 8); Assert.That(summary, Does.Contain("카메라: 008")); Assert.That(summary, Does.Contain("해당 카메라 Seed: 20489696")); Assert.That(summary, Does.Contain("나머지 카메라는 기존 결과")); } [Test] public void SeedAndReapplyLabelsDescribeDifferentActions() { Assert.That( AICameraGeneratorWindow.NextGenerationSeedLabel, Does.Contain("다음 생성")); Assert.That( AICameraGeneratorWindow.SelectedCameraRegenerationButtonLabel, Does.Contain("안전 후보")); Assert.That( AICameraGeneratorWindow.ListedCameraRegenerationButtonLabel, Does.Contain("안전 후보")); Assert.That( AICameraGeneratorWindow.ReapplyCurrentResultButtonLabel, Does.Contain("Seed 사용 안 함")); Assert.That( AICameraGeneratorWindow.ReapplyCurrentResultWarning, Does.Contain("새 후보를 생성하지 않으며")); Assert.That( AICameraGeneratorWindow.SeedCandidateExplanation, Does.Contain("안전 후보 풀을 재사용")); Assert.That( AICameraGeneratorWindow.FreshCandidatePoolOptionLabel, Does.Contain("후보 풀 캐시 무시하고 재계산")); Assert.That( AICameraGeneratorWindow.FullAutonomousGenerationButtonLabel, Does.Contain("자율 생성")); Assert.That( AICameraGeneratorWindow.FullGenerationAutonomyExplanation, Does.Contain("전체 생성에 적용되지 않습니다")); Assert.That( AICameraGeneratorWindow.SelectedShotOverrideExplanation, Does.Contain("선택한 카메라를 다시 생성할 때만")); } } }