using System.Text.Json; using Flyshot.Core.Domain; namespace Flyshot.Core.Tests; /// /// Verifies the Task 2 domain contracts before planning and runtime code depend on them. /// public sealed class DomainModelTests { /// /// Ensures robot profiles keep a stable copy of joint limits and reject invalid dimensions. /// [Fact] public void RobotProfile_CopiesJointLimits_AndRejectsMismatchedDof() { var jointLimits = new[] { new JointLimit("J1", 7.85, 32.72, 272.7), new JointLimit("J2", 6.63, 27.63, 230.28) }; var profile = new RobotProfile( name: "LR_Mate_200iD_7L", modelPath: "Models/LR_Mate_200iD_7L.robot", degreesOfFreedom: 2, jointLimits: jointLimits, jointCouplings: Array.Empty(), servoPeriod: TimeSpan.FromMilliseconds(8), triggerPeriod: TimeSpan.FromMilliseconds(8)); Assert.Equal(2, profile.DegreesOfFreedom); Assert.NotSame(jointLimits, profile.JointLimits); // The planner must not accept a profile whose DOF and limits disagree. Assert.Throws(() => new RobotProfile( name: "InvalidRobot", modelPath: "Models/Invalid.robot", degreesOfFreedom: 3, jointLimits: jointLimits, jointCouplings: Array.Empty(), servoPeriod: TimeSpan.FromMilliseconds(8), triggerPeriod: TimeSpan.FromMilliseconds(8))); } /// /// Ensures uploaded flyshot programs keep their shot metadata aligned with teach waypoints. /// [Fact] public void FlyshotProgram_RejectsMisalignedShotMetadata() { var waypoints = new[] { new JointWaypoint(new[] { 0.0, 1.0 }), new JointWaypoint(new[] { 2.0, 3.0 }) }; var validProgram = new FlyshotProgram( name: "EOL10_EAU_0", waypoints: waypoints, shotFlags: new[] { true, false }, offsetValues: new[] { 0, 1 }, addressGroups: new[] { new IoAddressGroup(new[] { 100 }), new IoAddressGroup(Array.Empty()) }); Assert.Equal(1, validProgram.ShotWaypointCount); // The gateway cannot recover from count mismatches after this point, so fail fast here. Assert.Throws(() => new FlyshotProgram( name: "BrokenProgram", waypoints: waypoints, shotFlags: new[] { true }, offsetValues: new[] { 0, 1 }, addressGroups: new[] { new IoAddressGroup(new[] { 100 }), new IoAddressGroup(Array.Empty()) })); } /// /// Ensures execution requests start from predictable defaults for compatibility paths. /// [Fact] public void TrajectoryRequest_UsesExpectedDefaults() { var request = new TrajectoryRequest( robot: CreateRobotProfile(), program: CreateProgram(), method: PlanningMethod.Icsp); Assert.False(request.MoveToStart); Assert.False(request.SaveTrajectoryArtifacts); Assert.False(request.UseCache); Assert.Equal(PlanningMethod.Icsp, request.Method); } /// /// Ensures runtime snapshots expose safe empty collections before the controller connects. /// [Fact] public void ControllerStateSnapshot_InitializesEmptyRuntimeCollections() { var snapshot = new ControllerStateSnapshot( capturedAt: DateTimeOffset.Parse("2026-04-23T10:00:00+08:00"), connectionState: "Disconnected", isEnabled: false, isInMotion: false, speedRatio: 100.0); Assert.Empty(snapshot.JointPositions); Assert.Empty(snapshot.CartesianPose); Assert.Empty(snapshot.ActiveAlarms); Assert.Empty(snapshot.StateTailWords); } /// /// 验证控制器快照会保留 TCP 10010 尾部状态字作为诊断字段。 /// [Fact] public void ControllerStateSnapshot_CopiesStateTailWordsForDiagnostics() { var snapshot = new ControllerStateSnapshot( capturedAt: DateTimeOffset.Parse("2026-04-23T10:00:00+08:00"), connectionState: "Connected", isEnabled: true, isInMotion: false, speedRatio: 1.0, stateTailWords: [2u, 0u, 0u, 1u]); var json = JsonSerializer.Serialize(snapshot); Assert.Equal([2u, 0u, 0u, 1u], snapshot.StateTailWords); Assert.Contains("\"stateTailWords\":[2,0,0,1]", json); } /// /// Ensures JSON payloads keep stable enum and property names for downstream SDKs. /// [Fact] public void DomainObjects_SerializeStableContract() { var result = new TrajectoryResult( programName: "EOL10_EAU_0", method: PlanningMethod.SelfAdaptIcsp, isValid: true, duration: TimeSpan.FromSeconds(1.25), shotEvents: new[] { new ShotEvent( waypointIndex: 0, triggerTime: 0.5, sampleIndex: 62, sampleTime: 0.496, addressGroup: new IoAddressGroup(new[] { 100 })) }, triggerTimeline: new[] { new TrajectoryDoEvent( waypointIndex: 0, triggerTime: 0.5, offsetCycles: 0, holdCycles: 1, addressGroup: new IoAddressGroup(new[] { 100 })) }, artifacts: new[] { new TrajectoryArtifact( kind: TrajectoryArtifactKind.JointDenseTrajectory, logicalName: "JointDetialTraj.txt", relativePath: "artifacts/JointDetialTraj.txt") }, failureReason: null, usedCache: true, originalWaypointCount: 4, plannedWaypointCount: 5); var json = JsonSerializer.Serialize(result); Assert.Contains("\"programName\":\"EOL10_EAU_0\"", json); Assert.Contains("\"method\":\"SelfAdaptIcsp\"", json); Assert.Contains("\"kind\":\"JointDenseTrajectory\"", json); Assert.Contains("\"usedCache\":true", json); } /// /// Creates a representative robot profile for request-level domain tests. /// private static RobotProfile CreateRobotProfile() { return new RobotProfile( name: "LR_Mate_200iD_7L", modelPath: "Models/LR_Mate_200iD_7L.robot", degreesOfFreedom: 2, jointLimits: new[] { new JointLimit("J1", 7.85, 32.72, 272.7), new JointLimit("J2", 6.63, 27.63, 230.28) }, jointCouplings: Array.Empty(), servoPeriod: TimeSpan.FromMilliseconds(8), triggerPeriod: TimeSpan.FromMilliseconds(8)); } /// /// Creates a representative uploaded program for request-level domain tests. /// private static FlyshotProgram CreateProgram() { return new FlyshotProgram( name: "EOL10_EAU_0", waypoints: new[] { new JointWaypoint(new[] { 0.0, 1.0 }), new JointWaypoint(new[] { 2.0, 3.0 }) }, shotFlags: new[] { true, false }, offsetValues: new[] { 0, 1 }, addressGroups: new[] { new IoAddressGroup(new[] { 100 }), new IoAddressGroup(Array.Empty()) }); } }