namespace SomhairlesDream.Server.Tests open System open Xunit open SomhairlesDream.Shared open SomhairlesDream.Server module StoreTests = let private at hour = DateTimeOffset(2026, 9, 20, hour, 0, 0, 0, TimeSpan.Zero) let private artifactIds : RunIds = { ProjectId = "heritage-001" RunId = "run-20260920-001" BaseId = "base-000" TargetId = "target-003" } let private mesh version label = { Version = version Label = label Vertices = [| { X = 0.; Y = 0.; Z = 0. } { X = float version; Y = 1.; Z = 0. } |] Faces = [||] } [] let ``store publishes the latest state atomically`` () = let store = RunStore("heritage-001", "run-001", TimeSpan.FromMinutes 2.) let running = store.Start(at 9) let published = store.Publish(at 9, mesh running.TargetVersion "主承力骨架") Assert.Equal(RunStatus.Succeeded, published.Status) Assert.Equal(published, store.Snapshot()) Assert.Equal(1, published.CurrentVersion) [] let ``store marks an unresponsive run stale when observed`` () = let store = RunStore("heritage-001", "run-001", TimeSpan.FromMinutes 2.) store.Start(at 9) |> ignore let observed = store.Observe(at 10) Assert.Equal(RunStatus.Stale, observed.Status) Assert.Equal("心跳超时,等待重连", observed.Message) [] let ``store sends an initial snapshot and every transition to subscribers`` () = let store = RunStore("heritage-001", "run-001", TimeSpan.FromMinutes 2.) let reader = store.Subscribe() let initial = reader.ReadAsync().AsTask().GetAwaiter().GetResult() let running = store.Start(at 9) let update = reader.ReadAsync().AsTask().GetAwaiter().GetResult() Assert.Equal(RunStatus.Idle, initial.Status) Assert.Equal(running, update) [] let ``artifact store applies lifecycle events to a run snapshot`` () = let store = ArtifactRunStore(artifactIds, TimeSpan.FromSeconds 15., at 9) let start = RunEvent.Start { Ids = artifactIds At = at 9 TotalSteps = 3 } let checkpoint = RunEvent.Checkpoint { Ids = artifactIds At = at 9 StepId = "01-foundation" StepIndex = 0 TotalSteps = 3 Message = "artifact verified" ArtifactPath = "steps/01-foundation.glb" } Assert.True(store.Apply(start) |> Result.isOk) Assert.True(store.Apply(checkpoint) |> Result.isOk) let snapshot = store.Snapshot() Assert.Equal("running", snapshot.Status) Assert.Equal(artifactIds.BaseId, snapshot.BaseId) Assert.Equal(artifactIds.TargetId, snapshot.TargetId) Assert.Equal(Some "01-foundation", snapshot.CurrentStepId) Assert.Equal(0, snapshot.CurrentStepIndex) Assert.Equal(3, snapshot.TotalSteps) Assert.True(snapshot.ManifestPath.IsNone) Assert.False(snapshot.IsStale) [] let ``artifact store accumulates completed step artifact paths`` () = let store = ArtifactRunStore(artifactIds, TimeSpan.FromSeconds 15., at 9) store.Apply( RunEvent.Start { Ids = artifactIds At = at 9 TotalSteps = 2 } ) |> ignore Assert.Empty(store.Snapshot().CompletedSteps) for index, stepId in [| 0, "01-foundation"; 1, "02-frame" |] do store.Apply( RunEvent.Checkpoint { Ids = artifactIds At = at 9 StepId = stepId StepIndex = index TotalSteps = 2 Message = "artifact verified" ArtifactPath = $"steps/{stepId}.glb" } ) |> ignore Assert.Equal( [| "steps/01-foundation.glb"; "steps/02-frame.glb" |], store.Snapshot().CompletedSteps ) [] let ``artifact store derives staleness without changing lifecycle status`` () = let store = ArtifactRunStore(artifactIds, TimeSpan.FromSeconds 15., at 9) store.Apply( RunEvent.Start { Ids = artifactIds At = at 9 TotalSteps = 3 } ) |> ignore let snapshot = store.Observe(at 10) Assert.Equal("running", snapshot.Status) Assert.True(snapshot.IsStale) [] let ``artifact store rejects lifecycle events after completion`` () = let store = ArtifactRunStore(artifactIds, TimeSpan.FromSeconds 15., at 9) store.Apply( RunEvent.Start { Ids = artifactIds At = at 9 TotalSteps = 3 } ) |> ignore for index, stepId in [| 0, "01-foundation"; 1, "02-frame"; 2, "03-cabin" |] do store.Apply( RunEvent.Checkpoint { Ids = artifactIds At = at 9 StepId = stepId StepIndex = index TotalSteps = 3 Message = "artifact verified" ArtifactPath = $"steps/{stepId}.glb" } ) |> ignore store.Apply( RunEvent.Complete { Ids = artifactIds At = at 9 ManifestPath = "manifest.json" } ) |> ignore let result = store.Apply( RunEvent.Heartbeat { Ids = artifactIds At = at 9 Message = "late heartbeat" } ) Assert.True(result |> Result.isError) [] let ``artifact store rejects completion before every checkpoint`` () = let store = ArtifactRunStore(artifactIds, TimeSpan.FromSeconds 15., at 9) store.Apply( RunEvent.Start { Ids = artifactIds At = at 9 TotalSteps = 3 } ) |> ignore let result = store.Apply( RunEvent.Complete { Ids = artifactIds At = at 9 ManifestPath = "manifest.json" } ) Assert.True(result |> Result.isError) Assert.Equal("running", store.Snapshot().Status) [] let ``artifact store records a pipeline failure before start`` () = let store = ArtifactRunStore(artifactIds, TimeSpan.FromSeconds 15., at 9) let result = store.Apply( RunEvent.Fail { Ids = artifactIds At = at 9 StepId = None Message = "run directory already exists" } ) Assert.True(result |> Result.isOk) Assert.Equal("failed", store.Snapshot().Status) Assert.Equal(Some "run directory already exists", store.Snapshot().Error) [] let ``artifact store accepts completion after ordered checkpoints`` () = let store = ArtifactRunStore(artifactIds, TimeSpan.FromSeconds 15., at 9) store.Apply( RunEvent.Start { Ids = artifactIds At = at 9 TotalSteps = 3 } ) |> ignore for index, stepId in [| 0, "01-foundation"; 1, "02-frame"; 2, "03-cabin" |] do store.Apply( RunEvent.Checkpoint { Ids = artifactIds At = at 9 StepId = stepId StepIndex = index TotalSteps = 3 Message = "artifact verified" ArtifactPath = $"steps/{stepId}.glb" } ) |> ignore let result = store.Apply( RunEvent.Complete { Ids = artifactIds At = at 9 ManifestPath = "manifest.json" } ) Assert.True(result |> Result.isOk) Assert.Equal("complete", store.Snapshot().Status) [] let ``artifact registry atomically creates only one store for a run key`` () = let registry = ArtifactRunRegistry(TimeSpan.FromSeconds 15., fun () -> at 9) let first = registry.TryCreate(artifactIds) let duplicate = registry.TryCreate(artifactIds) Assert.True(first.IsSome) Assert.True(duplicate.IsNone)