summaryrefslogtreecommitdiff
path: root/tests/SomhairlesDream.Shared.Tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests/SomhairlesDream.Shared.Tests')
-rw-r--r--tests/SomhairlesDream.Shared.Tests/ArtifactContractTests.fs73
-rw-r--r--tests/SomhairlesDream.Shared.Tests/FrontendContractTests.fs36
-rw-r--r--tests/SomhairlesDream.Shared.Tests/ReplayTests.fs26
-rw-r--r--tests/SomhairlesDream.Shared.Tests/RunStateTests.fs62
-rw-r--r--tests/SomhairlesDream.Shared.Tests/SomhairlesDream.Shared.Tests.fsproj24
5 files changed, 221 insertions, 0 deletions
diff --git a/tests/SomhairlesDream.Shared.Tests/ArtifactContractTests.fs b/tests/SomhairlesDream.Shared.Tests/ArtifactContractTests.fs
new file mode 100644
index 0000000..e4f0c86
--- /dev/null
+++ b/tests/SomhairlesDream.Shared.Tests/ArtifactContractTests.fs
@@ -0,0 +1,73 @@
+module SomhairlesDream.Shared.Tests.ArtifactContractTests
+
+open System
+open Xunit
+open SomhairlesDream.Shared
+
+let private ids : RunIds =
+ { ProjectId = "heritage-001"
+ RunId = "run-20260920-001"
+ BaseId = "heritage"
+ TargetId = "heritage-001" }
+
+[<Fact>]
+let ``resource ids reject path traversal and separators`` () =
+ Assert.True(Ids.validateResourceId "heritage-001" |> Result.isOk)
+ Assert.True(Ids.validateResourceId "../outside" |> Result.isError)
+ Assert.True(Ids.validateResourceId "heritage/001" |> Result.isError)
+ Assert.True(Ids.validateResourceId "heritage_001" |> Result.isError)
+
+[<Fact>]
+let ``object ids use the stable dotted namespace`` () =
+ Assert.True(Ids.validateObjectId "heritage.foundation" |> Result.isOk)
+ Assert.True(Ids.validateObjectId "heritage/frame.left" |> Result.isError)
+ Assert.True(Ids.validateObjectId "heritage.foundation/../../outside" |> Result.isError)
+
+[<Fact>]
+let ``start event serializes as flat camel-case json`` () =
+ let event =
+ RunEvent.Start
+ { Ids = ids
+ At = DateTimeOffset.Parse("2026-09-20T12:00:00Z")
+ TotalSteps = 3 }
+
+ let json = RunEvent.toJson event
+
+ Assert.Contains("\"kind\":\"start\"", json)
+ Assert.Contains("\"projectId\":\"heritage-001\"", json)
+ Assert.Contains("\"runId\":\"run-20260920-001\"", json)
+ Assert.Contains("\"totalSteps\":3", json)
+ Assert.DoesNotContain("\"stepId\":null", json)
+
+[<Fact>]
+let ``checkpoint event includes its step and artifact path`` () =
+ let event =
+ RunEvent.Checkpoint
+ { Ids = ids
+ At = DateTimeOffset.Parse("2026-09-20T12:00:05Z")
+ StepId = "01-foundation"
+ StepIndex = 1
+ TotalSteps = 3
+ Message = "foundation exported"
+ ArtifactPath = "steps/01-foundation.glb" }
+
+ let wire = RunEvent.toWire event
+
+ Assert.Equal("checkpoint", wire.Kind)
+ Assert.Equal(Some "01-foundation", wire.StepId)
+ Assert.Equal(Some 3, wire.TotalSteps)
+ Assert.Equal(Some "steps/01-foundation.glb", wire.ArtifactPath)
+
+[<Fact>]
+let ``checkpoint event serializes total steps`` () =
+ let event =
+ RunEvent.Checkpoint
+ { Ids = ids
+ At = DateTimeOffset.Parse("2026-09-20T12:00:05Z")
+ StepId = "01-foundation"
+ StepIndex = 0
+ TotalSteps = 3
+ Message = "foundation exported"
+ ArtifactPath = "steps/01-foundation.glb" }
+
+ Assert.Contains("\"totalSteps\":3", RunEvent.toJson event)
diff --git a/tests/SomhairlesDream.Shared.Tests/FrontendContractTests.fs b/tests/SomhairlesDream.Shared.Tests/FrontendContractTests.fs
new file mode 100644
index 0000000..d961d8b
--- /dev/null
+++ b/tests/SomhairlesDream.Shared.Tests/FrontendContractTests.fs
@@ -0,0 +1,36 @@
+module SomhairlesDream.Shared.Tests.FrontendContractTests
+
+open System.IO
+open Xunit
+
+let private root = Path.GetFullPath(Path.Combine(__SOURCE_DIRECTORY__, "..", ".."))
+
+let private readRelative path = File.ReadAllText(Path.Combine(root, path))
+
+let private assertBrowserContract contents =
+ Assert.Contains("/api/runs/start", contents)
+ Assert.Contains("/api/runs/events?projectId=", contents)
+ Assert.Contains("artifact-manifest-link", contents)
+ Assert.DoesNotContain("/api/events", contents)
+
+[<Fact>]
+let ``frontend source and bundle use the selected artifact run lifecycle`` () =
+ readRelative "src/SomhairlesDream.Frontend/App.fs"
+ |> assertBrowserContract
+
+ readRelative "public/App.js"
+ |> assertBrowserContract
+
+ let html = readRelative "public/index.html"
+ Assert.Contains("id=\"run-start-button\"", html)
+ Assert.Contains("id=\"artifact-manifest-link\"", html)
+
+[<Fact>]
+let ``solution contains every project in the artifact pipeline`` () =
+ let solution = readRelative "SomhairlesDream.sln"
+
+ [ "SomhairlesDream.Modeling"
+ "SomhairlesDream.Cli"
+ "SomhairlesDream.Modeling.Tests"
+ "SomhairlesDream.Cli.Tests" ]
+ |> List.iter (fun project -> Assert.Contains(project, solution))
diff --git a/tests/SomhairlesDream.Shared.Tests/ReplayTests.fs b/tests/SomhairlesDream.Shared.Tests/ReplayTests.fs
new file mode 100644
index 0000000..317fabe
--- /dev/null
+++ b/tests/SomhairlesDream.Shared.Tests/ReplayTests.fs
@@ -0,0 +1,26 @@
+namespace SomhairlesDream.Shared.Tests
+
+open Xunit
+open SomhairlesDream.Shared
+
+module ReplayTests =
+ [<Fact>]
+ let ``reconstruction contains three visibly different geometry frames`` () =
+ let frames = Replay.frames ()
+
+ Assert.Equal(3, frames.Length)
+ Assert.True([| 1; 2; 3 |] = (frames |> Array.map (fun frame -> frame.Version)))
+ Assert.Equal("重建起点", frames[0].Label)
+ Assert.Equal("主承力骨架", frames[1].Label)
+ Assert.Equal("舱段重构", frames[2].Label)
+ Assert.False(frames[0].Mesh.Vertices = frames[1].Mesh.Vertices)
+ Assert.False(frames[1].Mesh.Vertices = frames[2].Mesh.Vertices)
+
+ [<Fact>]
+ let ``mesh snapshots expose indexed triangle buffers`` () =
+ let mesh = (Replay.frames ()).[2].Mesh
+ let positions = MeshData.positionData mesh
+ let indices = MeshData.triangleIndices mesh
+
+ Assert.Equal(mesh.Vertices.Length * 3, positions.Length)
+ Assert.Equal(45, indices.Length)
diff --git a/tests/SomhairlesDream.Shared.Tests/RunStateTests.fs b/tests/SomhairlesDream.Shared.Tests/RunStateTests.fs
new file mode 100644
index 0000000..08e4e89
--- /dev/null
+++ b/tests/SomhairlesDream.Shared.Tests/RunStateTests.fs
@@ -0,0 +1,62 @@
+namespace SomhairlesDream.Shared.Tests
+
+open System
+open Xunit
+open SomhairlesDream.Shared
+
+module RunStateTests =
+ let private at hour = DateTimeOffset(2026, 9, 20, hour, 0, 0, 0, TimeSpan.Zero)
+
+ [<Fact>]
+ let ``a new run is idle with no design work scheduled`` () =
+ let snapshot = RunState.initial "heritage-001" "run-001" (at 8)
+
+ Assert.Equal("heritage-001", snapshot.ProjectId)
+ Assert.Equal("run-001", snapshot.RunId)
+ Assert.Equal(RunStatus.Idle, snapshot.Status)
+ Assert.Equal(0, snapshot.CurrentVersion)
+ Assert.Equal(0, snapshot.TargetVersion)
+ Assert.False(snapshot.NextDesignRunScheduled)
+ Assert.Equal("等待设计运行", snapshot.Message)
+
+ [<Fact>]
+ let ``starting a run increments the target version and heartbeat keeps it running`` () =
+ let started =
+ RunState.initial "heritage-001" "run-001" (at 8)
+ |> RunState.start (at 9)
+
+ let alive = RunState.heartbeat (at 9) started
+
+ Assert.Equal(RunStatus.Running, alive.Status)
+ Assert.Equal(1, alive.TargetVersion)
+ Assert.Equal(Some(at 9), alive.HeartbeatAt)
+
+ [<Fact>]
+ let ``a running run becomes stale after the heartbeat window`` () =
+ let started =
+ RunState.initial "heritage-001" "run-001" (at 8)
+ |> RunState.start (at 9)
+
+ let stale = RunState.statusAt (TimeSpan.FromMinutes 2.) (at 10) started
+
+ Assert.Equal(RunStatus.Stale, stale.Status)
+ Assert.Equal("心跳超时,等待重连", stale.Message)
+
+ [<Fact>]
+ let ``publishing a mesh records the current version and makes the run successful`` () =
+ let started =
+ RunState.initial "heritage-001" "run-001" (at 8)
+ |> RunState.start (at 9)
+
+ let mesh =
+ { Version = 1
+ Label = "基础结构"
+ Vertices = [| { X = 0.; Y = 0.; Z = 0. } |]
+ Faces = [||] }
+
+ let published = RunState.publish (at 9) mesh started
+
+ Assert.Equal(RunStatus.Succeeded, published.Status)
+ Assert.Equal(1, published.CurrentVersion)
+ Assert.Equal(Some mesh, published.Mesh)
+ Assert.Equal("已发布基础结构", published.Message)
diff --git a/tests/SomhairlesDream.Shared.Tests/SomhairlesDream.Shared.Tests.fsproj b/tests/SomhairlesDream.Shared.Tests/SomhairlesDream.Shared.Tests.fsproj
new file mode 100644
index 0000000..d0e6130
--- /dev/null
+++ b/tests/SomhairlesDream.Shared.Tests/SomhairlesDream.Shared.Tests.fsproj
@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <IsTestProject>true</IsTestProject>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
+ <PackageReference Include="xunit" Version="2.9.2" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="../../src/SomhairlesDream.Shared/SomhairlesDream.Shared.fsproj" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="RunStateTests.fs" />
+ <Compile Include="ReplayTests.fs" />
+ <Compile Include="ArtifactContractTests.fs" />
+ <Compile Include="FrontendContractTests.fs" />
+ </ItemGroup>
+</Project>