diff options
Diffstat (limited to 'src/LivingVillage.Kernel.Tests')
| -rw-r--r-- | src/LivingVillage.Kernel.Tests/DailyTaskTests.fs | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/src/LivingVillage.Kernel.Tests/DailyTaskTests.fs b/src/LivingVillage.Kernel.Tests/DailyTaskTests.fs index 12bdf68..e784d83 100644 --- a/src/LivingVillage.Kernel.Tests/DailyTaskTests.fs +++ b/src/LivingVillage.Kernel.Tests/DailyTaskTests.fs @@ -19,6 +19,23 @@ module private DailyTaskHarness = let accepted (template: Occupation.TaskTemplateId) : Occupation.DailyTask = Occupation.acceptTask (taskOf template Occupation.Offered) + let worldWithAnnal () : World = + let world = Sim.initialWorldN 7UL 2 + match Sim.trade { Buyer = NpcId 0; Seller = NpcId 1; Item = Food; Quantity = 1 } world with + | TradeSucceeded next -> next + | TradeRejected (failure, _) -> Assert.Fail($"trade rejected: {failure}"); Unchecked.defaultof<World> + + let taskForDay (day: int64) : Occupation.DailyTask = + (Occupation.dailyTaskOf 42UL 7UL day Occupation.Fisher).Value |> Occupation.acceptTask + + let withTempSave (action: string -> unit) = + let id = Guid.NewGuid().ToString("N") + let path = Path.Combine(Path.GetTempPath(), $"living-village-p11-{id}.save") + try + action path + finally + if File.Exists path then File.Delete path + [<TestClass>] type DailyTaskTests () = @@ -189,3 +206,95 @@ type DailyTaskTests () = Assert.AreEqual<Occupation.TaskState>(Occupation.Failed, (Occupation.expireAt (offered.DueTick + 1L) offered).State) let finished = { active with State = Occupation.Done } Assert.AreEqual<Occupation.TaskState>(Occupation.Done, (Occupation.expireAt (finished.DueTick + 1L) finished).State) + + // ---- §2-A 存档 v3:annals 之后追加 today task 尾段 ---- + + [<TestMethod>] + member _.SaveV3AppendsTaskTailAfterAnnalsAndRoundTrips () = + let world = DailyTaskHarness.worldWithAnnal () + let state = + { (Occupation.stateOf Occupation.Fisher) with + TaskToken = 5 + StoryStage = 2 + Today = Some(DailyTaskHarness.taskForDay 2L) } + DailyTaskHarness.withTempSave (fun path -> + WorldSave.saveToFileWith (Some state) path world + let text = File.ReadAllText path + Assert.IsTrue(text.StartsWith("LV_WORLD_SAVE_V3"), "v3 header expected") + let tokens = text.Split('|') + Assert.IsTrue(tokens |> Array.exists (fun token -> token = "task-today"), "task tail marker expected") + Assert.AreEqual<string>("tile-none", Array.last tokens) + let markerIndex = tokens |> Array.findIndex (fun token -> token = "task-today") + let lastAnnalKindIndex = + tokens |> Array.findIndexBack (fun token -> token = "trade") + Assert.IsTrue(markerIndex > lastAnnalKindIndex, "task tail must follow the annals segment") + match WorldSave.loadFromFileWith path with + | Ok (restored, Some restoredState) -> + Assert.AreEqual<Occupation.State>(state, restoredState) + Assert.AreEqual<string>(WorldSave.save world, WorldSave.save restored) + Assert.AreEqual<string>(text, WorldSave.saveWith (Some restoredState) restored) + | Ok (_, None) -> Assert.Fail("v3 with task tail must restore the occupation") + | Error message -> Assert.Fail($"v3 task round trip failed: {message}")) + + [<TestMethod>] + member _.SaveV3WithoutTaskKeepsOldTailFreeLayout () = + let world = DailyTaskHarness.worldWithAnnal () + let state = Occupation.stateOf Occupation.Peddler + DailyTaskHarness.withTempSave (fun path -> + WorldSave.saveToFileWith (Some state) path world + let text = File.ReadAllText path + Assert.IsTrue(text.StartsWith("LV_WORLD_SAVE_V3"), "v3 header expected") + Assert.IsFalse(text.Contains("task-today"), "no-task v3 must not carry a task tail") + match WorldSave.loadFromFileWith path with + | Ok (restored, Some restoredState) -> + Assert.AreEqual<Occupation.DailyTask option>(None, restoredState.Today) + Assert.AreEqual<string>(text, WorldSave.saveWith (Some restoredState) restored) + | Ok (_, None) -> Assert.Fail("v3 with kind must restore the occupation") + | Error message -> Assert.Fail($"v3 no-task read failed: {message}")) + + [<TestMethod>] + member _.V1AndV2SavesCarryNoTaskTail () = + let world = Sim.initialWorld 42UL + DailyTaskHarness.withTempSave (fun path -> + WorldSave.saveToFile path world + let v2Text = File.ReadAllText path + Assert.IsTrue(v2Text.StartsWith("LV_WORLD_SAVE_V2"), "default save must stay v2") + Assert.IsFalse(v2Text.Contains("task-today")) + match WorldSave.loadFromFileWith path with + | Ok (restored, occupation) -> + Assert.AreEqual<Occupation.State option>(None, occupation) + Assert.AreEqual<string>(v2Text, WorldSave.save restored) + | Error message -> Assert.Fail($"v2 read failed: {message}") + + let firstPipe = v2Text.IndexOf('|') + let secondPipe = v2Text.IndexOf('|', firstPipe + 1) + let thirdPipe = v2Text.IndexOf('|', secondPipe + 1) + let v1Text = "LV_WORLD_SAVE_V1" + v2Text.Substring(thirdPipe) + File.WriteAllText(path, v1Text) + match WorldSave.loadFromFileWith path with + | Ok (_, occupation) -> Assert.AreEqual<Occupation.State option>(None, occupation) + | Error message -> Assert.Fail($"v1 read failed: {message}") + + File.WriteAllText(path, WorldSave.saveWith (Some (Occupation.stateOf Occupation.Fisher)) world + "|task-today|bogus|offered|0|0|npc-none|tile-none") + match WorldSave.loadFromFile path with + | Ok _ -> Assert.Fail("unknown task template must be rejected") + | Error _ -> ()) + + [<TestMethod>] + member _.TrailingGarbageAfterTaskTailIsRejected () = + let world = DailyTaskHarness.worldWithAnnal () + let state = { (Occupation.stateOf Occupation.Farmer) with Today = Some(DailyTaskHarness.taskForDay 4L) } + DailyTaskHarness.withTempSave (fun path -> + let text = WorldSave.saveWith (Some state) world + File.WriteAllText(path, text + "|junk") + match WorldSave.loadFromFile path with + | Ok _ -> Assert.Fail("trailing garbage after the task tail must be rejected") + | Error _ -> ()) + + [<TestMethod>] + member _.TaskCarryingSavesAreByteIdenticalAcrossRuns () = + let build () = + let world = DailyTaskHarness.worldWithAnnal () + let state = { (Occupation.stateOf Occupation.Farmer) with Today = Some(DailyTaskHarness.taskForDay 9L) } + WorldSave.saveWith (Some state) world + Assert.AreEqual<string>(build (), build ()) |
