#if UNITY_EDITOR using System; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using AibisDream.SaveSystem; using Newtonsoft.Json; using NUnit.Framework; namespace AibisDream.DeveloperMode.Editor.Tests { public sealed class TestSaveSystemTests { private string _root; private TestSaveRepository _repository; [SetUp] public void SetUp() { _root = Path.Combine( Path.GetTempPath(), "AibisDream-TestSaveTests", Guid.NewGuid().ToString("N")); _repository = new TestSaveRepository(_root); } [TearDown] public void TearDown() { if (Directory.Exists(_root)) { Directory.Delete(_root, true); } } [Test] public void Record_SameAnchor_ReplacesSnapshotAndKeepsFirstSeenOrder() { var first = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneOne")); var second = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneTwo")); var scan = _repository.Scan("1.0"); Assert.That(scan.Entries.Count, Is.EqualTo(1)); Assert.That(second.Meta.firstSeenOrder, Is.EqualTo(first.Meta.firstSeenOrder)); Assert.That(scan.Entries[0].Meta.sceneName, Is.EqualTo("SceneTwo")); Assert.That(_repository.TryLoad(scan.Entries[0], out var snapshot, out var error), Is.True, error); Assert.That(snapshot.scene.sceneName, Is.EqualTo("SceneTwo")); } [Test] public void Record_ReplacingEarlierEntry_DoesNotMoveItsOrder() { _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB")); _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA2")); var entries = _repository.Scan().Entries.ToArray(); Assert.That(entries.Select(item => item.NodeName), Is.EqualTo(new[] { "NodeA", "NodeB" })); Assert.That(entries[0].Meta.firstSeenOrder, Is.LessThan(entries[1].Meta.firstSeenOrder)); } [Test] public void Scan_RetainsMissingSnapshotAsInvalid() { var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); File.Delete(entry.SnapshotPath); var scan = _repository.Scan(); Assert.That(scan.ValidCount, Is.Zero); Assert.That(scan.InvalidCount, Is.EqualTo(1)); Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.MissingSnapshot)); } [Test] public void Scan_RetainsOrphanSnapshotWithBrokenMeta() { var directory = Path.Combine(_root, "chapter", "broken"); Directory.CreateDirectory(directory); File.WriteAllText( Path.Combine(directory, TestSaveRepository.SnapshotFileName), "{}", Encoding.UTF8); var scan = _repository.Scan(); Assert.That(scan.InvalidCount, Is.EqualTo(1)); Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptMeta)); } [Test] public void Validate_CorruptSnapshot_IsRetainedAndDisabledOnNextScan() { var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); File.WriteAllText(entry.SnapshotPath, "{broken", Encoding.UTF8); Assert.That(_repository.Validate(entry, out var error), Is.False); var rescanned = _repository.Scan(); Assert.That(error, Does.Contain("无法解析")); Assert.That(rescanned.InvalidCount, Is.EqualTo(1)); Assert.That(rescanned.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptSnapshot)); } [Test] public void Scan_RecoversCompleteStagingDirectory() { var final = Path.Combine(_root, "chapter", "node"); var staging = final + ".__staging"; Directory.CreateDirectory(staging); var request = CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"); var meta = CreateMeta(request, 1); File.WriteAllText( Path.Combine(staging, TestSaveRepository.MetaFileName), JsonConvert.SerializeObject(meta), Encoding.UTF8); File.WriteAllText( Path.Combine(staging, TestSaveRepository.SnapshotFileName), SnapshotPersistence.Serialize(request.Snapshot), Encoding.UTF8); var scan = _repository.Scan(); Assert.That(Directory.Exists(final), Is.True); Assert.That(Directory.Exists(staging), Is.False); Assert.That(scan.ValidCount, Is.EqualTo(1)); } [Test] public void Record_ConcurrentRequests_ProducesCompleteUniqueEntries() { var tasks = Enumerable.Range(0, 12) .Select(index => Task.Run(() => _repository.Record(CreateRequest( "ChapterA", "ProjectA", $"Node{index:00}", $"Scene{index:00}")))) .ToArray(); Task.WaitAll(tasks); var scan = _repository.Scan(); Assert.That(scan.ValidCount, Is.EqualTo(12)); Assert.That(scan.InvalidCount, Is.Zero); Assert.That(scan.Entries.Select(item => item.Meta.firstSeenOrder).Distinct().Count(), Is.EqualTo(12)); } [Test] public void DeleteInvalid_RemovesOnlyInvalidEntries() { _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); var invalid = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB")); File.Delete(invalid.SnapshotPath); var removed = _repository.DeleteInvalid(out var error); var scan = _repository.Scan(); Assert.That(error, Is.Null); Assert.That(removed, Is.EqualTo(1)); Assert.That(scan.ValidCount, Is.EqualTo(1)); Assert.That(scan.InvalidCount, Is.Zero); } [TestCase(new[] { "hub" }, true)] [TestCase(new[] { "linear" }, true)] [TestCase(new[] { "content" }, true)] [TestCase(new[] { "event" }, false)] [TestCase(new[] { "hub", "no_save" }, false)] [TestCase(new string[0], true)] public void SilentCoverageEvaluator_MatchesSavePointTagSemantics(string[] tags, bool expected) { var actual = SavePointEvaluator.EvaluateNodeTagsForAutoSaveSilently( "Node", tags, out _); Assert.That(actual, Is.EqualTo(expected)); } private static TestSaveRecordRequest CreateRequest( string sceneSoName, string yarnProject, string nodeName, string sceneName) { var key = TestSaveRecorder.BuildDedupeKey(sceneSoName, yarnProject, nodeName); return new TestSaveRecordRequest { Snapshot = new SaveSnapshot { gameVersion = "1.0", savedAt = "2026-01-01 00:00:00", scene = new SceneSnapshotDto { sceneName = sceneName }, anchor = new AnchorSnapshot { sceneSoName = sceneSoName, yarnProjectId = yarnProject, nodeName = nodeName } }, DedupeKey = key, EntryId = TestSaveRepository.StableHash(key), ChapterId = sceneSoName, ChapterTitle = sceneSoName, SceneSoName = sceneSoName, YarnProjectId = yarnProject, NodeName = nodeName, SceneName = sceneName, GameVersion = "1.0" }; } private static TestSaveMeta CreateMeta(TestSaveRecordRequest request, long order) { return new TestSaveMeta { entryId = request.EntryId, dedupeKey = request.DedupeKey, chapterId = request.ChapterId, chapterTitle = request.ChapterTitle, sceneSoName = request.SceneSoName, yarnProjectId = request.YarnProjectId, nodeName = request.NodeName, sceneName = request.SceneName, firstSeenOrder = order, firstRecordedAt = "2026-01-01 00:00:00", lastRecordedAt = "2026-01-01 00:00:00", snapshotSchemaVersion = SaveSnapshotSchema.CurrentVersion, gameVersion = request.GameVersion }; } } } #endif