summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel.Tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Kernel.Tests')
-rw-r--r--src/LivingVillage.Kernel.Tests/DailyTaskTests.fs109
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 ())