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" } [] 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) [] 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) [] 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) [] 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) [] 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)