From d554e9e659a32c92e680c59550b6e9a59b2a3a91 Mon Sep 17 00:00:00 2001 From: Tyrone Date: Mon, 6 Apr 2026 22:38:23 +0800 Subject: [PATCH] xigau --- .../Production/IRunLayerSessionRepository.cs | 86 +++++++ .../Production/IRunPartJudgmentRepository.cs | 102 ++++++++ .../IRunStateTransitionLogRepository.cs | 104 ++++++++ .../Production/RunLayerSessionModel.cs | 174 +++++++++++++ .../Production/RunPartJudgmentModel.cs | 229 +++++++++++++++++ .../Production/RunStateTransitionLogModel.cs | 133 ++++++++++ .../OrpaonVision.SiteApp.Tests.csproj | 29 +++ .../RuntimeStateMachineServiceTests.cs | 128 ++++++++++ .../ModelPackageImportActivationTests.cs | 238 ++++++++++++++++++ .../Contracts/IConsistencyCheckService.cs | 2 + 10 files changed, 1225 insertions(+) create mode 100644 OrpaonVision.Core/Production/IRunLayerSessionRepository.cs create mode 100644 OrpaonVision.Core/Production/IRunPartJudgmentRepository.cs create mode 100644 OrpaonVision.Core/Production/IRunStateTransitionLogRepository.cs create mode 100644 OrpaonVision.Model/Production/RunLayerSessionModel.cs create mode 100644 OrpaonVision.Model/Production/RunPartJudgmentModel.cs create mode 100644 OrpaonVision.Model/Production/RunStateTransitionLogModel.cs create mode 100644 OrpaonVision.SiteApp.Tests/OrpaonVision.SiteApp.Tests.csproj create mode 100644 OrpaonVision.SiteApp.Tests/Runtime/Services/RuntimeStateMachineServiceTests.cs create mode 100644 OrpaonVision.SiteApp.Tests/Training/ModelPackageImportActivationTests.cs diff --git a/OrpaonVision.Core/Production/IRunLayerSessionRepository.cs b/OrpaonVision.Core/Production/IRunLayerSessionRepository.cs new file mode 100644 index 0000000..785b605 --- /dev/null +++ b/OrpaonVision.Core/Production/IRunLayerSessionRepository.cs @@ -0,0 +1,86 @@ +using OrpaonVision.Core.Results; +using OrpaonVision.Model.Production; + +namespace OrpaonVision.Core.Production; + +/// +/// 运行层级会话仓储接口。 +/// +public interface IRunLayerSessionRepository +{ + /// + /// 创建层级会话。 + /// + /// 层级会话模型。 + /// 取消令牌。 + /// 创建结果。 + Task> CreateAsync(RunLayerSessionModel layerSession, CancellationToken cancellationToken = default); + + /// + /// 更新层级会话。 + /// + /// 层级会话模型。 + /// 取消令牌。 + /// 更新结果。 + Task> UpdateAsync(RunLayerSessionModel layerSession, CancellationToken cancellationToken = default); + + /// + /// 根据ID获取层级会话。 + /// + /// 层级会话ID。 + /// 取消令牌。 + /// 层级会话模型。 + Task> GetByIdAsync(Guid layerSessionId, CancellationToken cancellationToken = default); + + /// + /// 根据产品会话ID获取层级会话列表。 + /// + /// 产品会话ID。 + /// 取消令牌。 + /// 层级会话列表。 + Task>> GetBySessionIdAsync(Guid sessionId, CancellationToken cancellationToken = default); + + /// + /// 根据状态获取层级会话列表。 + /// + /// 状态。 + /// 取消令牌。 + /// 层级会话列表。 + Task>> GetByStatusAsync(RunLayerSessionStatus status, CancellationToken cancellationToken = default); + + /// + /// 分页查询层级会话。 + /// + /// 页索引。 + /// 页大小。 + /// 产品类型编码(可选)。 + /// 状态(可选)。 + /// 开始时间(可选)。 + /// 结束时间(可选)。 + /// 取消令牌。 + /// 分页结果。 + Task>> GetPagedAsync( + int pageIndex, + int pageSize, + string? productTypeCode = null, + RunLayerSessionStatus? status = null, + DateTime? startTime = null, + DateTime? endTime = null, + CancellationToken cancellationToken = default); + + /// + /// 删除层级会话。 + /// + /// 层级会话ID。 + /// 取消令牌。 + /// 删除结果。 + Task> DeleteAsync(Guid layerSessionId, CancellationToken cancellationToken = default); + + /// + /// 批量删除层级会话。 + /// + /// 产品会话ID列表。 + /// 取消令牌。 + /// 删除结果。 + Task> DeleteBySessionIdsAsync(List sessionIds, CancellationToken cancellationToken = default); +} diff --git a/OrpaonVision.Core/Production/IRunPartJudgmentRepository.cs b/OrpaonVision.Core/Production/IRunPartJudgmentRepository.cs new file mode 100644 index 0000000..7cc96a6 --- /dev/null +++ b/OrpaonVision.Core/Production/IRunPartJudgmentRepository.cs @@ -0,0 +1,102 @@ +using OrpaonVision.Core.Results; +using OrpaonVision.Model.Production; + +namespace OrpaonVision.Core.Production; + +/// +/// 运行部件判定仓储接口。 +/// +public interface IRunPartJudgmentRepository +{ + /// + /// 创建部件判定。 + /// + /// 部件判定模型。 + /// 取消令牌。 + /// 创建结果。 + Task> CreateAsync(RunPartJudgmentModel judgment, CancellationToken cancellationToken = default); + + /// + /// 更新部件判定。 + /// + /// 部件判定模型。 + /// 取消令牌。 + /// 更新结果。 + Task> UpdateAsync(RunPartJudgmentModel judgment, CancellationToken cancellationToken = default); + + /// + /// 根据ID获取部件判定。 + /// + /// 判定ID。 + /// 取消令牌。 + /// 部件判定模型。 + Task> GetByIdAsync(Guid judgmentId, CancellationToken cancellationToken = default); + + /// + /// 根据层级会话ID获取部件判定列表。 + /// + /// 层级会话ID。 + /// 取消令牌。 + /// 部件判定列表。 + Task>> GetByLayerSessionIdAsync(Guid layerSessionId, CancellationToken cancellationToken = default); + + /// + /// 根据产品会话ID获取部件判定列表。 + /// + /// 产品会话ID。 + /// 取消令牌。 + /// 部件判定列表。 + Task>> GetBySessionIdAsync(Guid sessionId, CancellationToken cancellationToken = default); + + /// + /// 根据结果获取部件判定列表。 + /// + /// 判定结果。 + /// 取消令牌。 + /// 部件判定列表。 + Task>> GetByResultAsync(RunPartJudgmentResult result, CancellationToken cancellationToken = default); + + /// + /// 分页查询部件判定。 + /// + /// 页索引。 + /// 页大小。 + /// 产品类型编码(可选)。 + /// 判定结果(可选)。 + /// 开始时间(可选)。 + /// 结束时间(可选)。 + /// 取消令牌。 + /// 分页结果。 + Task>> GetPagedAsync( + int pageIndex, + int pageSize, + string? productTypeCode = null, + RunPartJudgmentResult? result = null, + DateTime? startTime = null, + DateTime? endTime = null, + CancellationToken cancellationToken = default); + + /// + /// 删除部件判定。 + /// + /// 判定ID。 + /// 取消令牌。 + /// 删除结果。 + Task> DeleteAsync(Guid judgmentId, CancellationToken cancellationToken = default); + + /// + /// 批量删除部件判定。 + /// + /// 产品会话ID列表。 + /// 取消令牌。 + /// 删除结果。 + Task> DeleteBySessionIdsAsync(List sessionIds, CancellationToken cancellationToken = default); + + /// + /// 批量删除层级会话的部件判定。 + /// + /// 层级会话ID列表。 + /// 取消令牌。 + /// 删除结果。 + Task> DeleteByLayerSessionIdsAsync(List layerSessionIds, CancellationToken cancellationToken = default); +} diff --git a/OrpaonVision.Core/Production/IRunStateTransitionLogRepository.cs b/OrpaonVision.Core/Production/IRunStateTransitionLogRepository.cs new file mode 100644 index 0000000..6cc28da --- /dev/null +++ b/OrpaonVision.Core/Production/IRunStateTransitionLogRepository.cs @@ -0,0 +1,104 @@ +using OrpaonVision.Core.Results; +using OrpaonVision.Model.Production; + +namespace OrpaonVision.Core.Production; + +/// +/// 运行状态转换日志仓储接口。 +/// +public interface IRunStateTransitionLogRepository +{ + /// + /// 创建状态转换日志。 + /// + /// 状态转换日志模型。 + /// 取消令牌。 + /// 创建结果。 + Task> CreateAsync(RunStateTransitionLogModel log, CancellationToken cancellationToken = default); + + /// + /// 根据ID获取状态转换日志。 + /// + /// 日志ID。 + /// 取消令牌。 + /// 状态转换日志模型。 + Task> GetByIdAsync(Guid logId, CancellationToken cancellationToken = default); + + /// + /// 根据产品会话ID获取状态转换日志列表。 + /// + /// 产品会话ID。 + /// 取消令牌。 + /// 状态转换日志列表。 + Task>> GetBySessionIdAsync(Guid sessionId, CancellationToken cancellationToken = default); + + /// + /// 根据层级会话ID获取状态转换日志列表。 + /// + /// 层级会话ID。 + /// 取消令牌。 + /// 状态转换日志列表。 + Task>> GetByLayerSessionIdAsync(Guid layerSessionId, CancellationToken cancellationToken = default); + + /// + /// 根据转换类型获取状态转换日志列表。 + /// + /// 转换类型。 + /// 取消令牌。 + /// 状态转换日志列表。 + Task>> GetByTransitionTypeAsync(RunStateTransitionType transitionType, CancellationToken cancellationToken = default); + + /// + /// 根据人工干预标志获取状态转换日志列表。 + /// + /// 是否人工干预。 + /// 取消令牌。 + /// 状态转换日志列表。 + Task>> GetByManualInterventionAsync(bool isManualIntervention, CancellationToken cancellationToken = default); + + /// + /// 分页查询状态转换日志。 + /// + /// 页索引。 + /// 页大小。 + /// 产品类型编码(可选)。 + /// 转换类型(可选)。 + /// 是否人工干预(可选)。 + /// 开始时间(可选)。 + /// 结束时间(可选)。 + /// 取消令牌。 + /// 分页结果。 + Task>> GetPagedAsync( + int pageIndex, + int pageSize, + string? productTypeCode = null, + RunStateTransitionType? transitionType = null, + bool? isManualIntervention = null, + DateTime? startTime = null, + DateTime? endTime = null, + CancellationToken cancellationToken = default); + + /// + /// 删除状态转换日志。 + /// + /// 日志ID。 + /// 取消令牌。 + /// 删除结果。 + Task> DeleteAsync(Guid logId, CancellationToken cancellationToken = default); + + /// + /// 批量删除状态转换日志。 + /// + /// 产品会话ID列表。 + /// 取消令牌。 + /// 删除结果。 + Task> DeleteBySessionIdsAsync(List sessionIds, CancellationToken cancellationToken = default); + + /// + /// 清理指定日期之前的日志。 + /// + /// 指定日期。 + /// 取消令牌。 + /// 删除的记录数。 + Task> CleanupLogsBeforeAsync(DateTime beforeDate, CancellationToken cancellationToken = default); +} diff --git a/OrpaonVision.Model/Production/RunLayerSessionModel.cs b/OrpaonVision.Model/Production/RunLayerSessionModel.cs new file mode 100644 index 0000000..6713f16 --- /dev/null +++ b/OrpaonVision.Model/Production/RunLayerSessionModel.cs @@ -0,0 +1,174 @@ +namespace OrpaonVision.Model.Production; + +/// +/// 运行层级会话模型。 +/// +public sealed class RunLayerSessionModel +{ + /// + /// 层级会话ID。 + /// + public Guid LayerSessionId { get; set; } + + /// + /// 产品会话ID。 + /// + public Guid SessionId { get; set; } + + /// + /// 层级序号。 + /// + public int LayerNumber { get; set; } + + /// + /// 层级名称。 + /// + public string LayerName { get; set; } = string.Empty; + + /// + /// 层级编码。 + /// + public string LayerCode { get; set; } = string.Empty; + + /// + /// 开始时间(UTC)。 + /// + public DateTime StartedAtUtc { get; set; } + + /// + /// 结束时间(UTC)。 + /// + public DateTime? EndedAtUtc { get; set; } + + /// + /// 层级状态。 + /// + public RunLayerSessionStatus Status { get; set; } + + /// + /// 检测到的部件数量。 + /// + public int DetectedPartsCount { get; set; } + + /// + /// 合格部件数量。 + /// + public int QualifiedPartsCount { get; set; } + + /// + /// NG部件数量。 + /// + public int NgPartsCount { get; set; } + + /// + /// 处理的图像数量。 + /// + public int ProcessedImageCount { get; set; } + + /// + /// 首张截图路径。 + /// + public string? FirstScreenshotPath { get; set; } + + /// + /// 末张截图路径。 + /// + public string? LastScreenshotPath { get; set; } + + /// + /// 层级结果。 + /// + public RunLayerSessionResult Result { get; set; } + + /// + /// NG原因描述。 + /// + public string? NgReason { get; set; } + + /// + /// 处理耗时(毫秒)。 + /// + public long ProcessingTimeMs { get; set; } + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// 更新时间(UTC)。 + /// + public DateTime UpdatedAtUtc { get; set; } + + /// + /// 创建人。 + /// + public string CreatedBy { get; set; } = string.Empty; + + /// + /// 更新人。 + /// + public string UpdatedBy { get; set; } = string.Empty; +} + +/// +/// 运行层级会话状态。 +/// +public enum RunLayerSessionStatus +{ + /// + /// 未开始。 + /// + NotStarted = 0, + + /// + /// 进行中。 + /// + InProgress = 1, + + /// + /// 已完成(OK)。 + /// + CompletedOk = 2, + + /// + /// 已完成(NG)。 + /// + CompletedNg = 3, + + /// + /// 已跳过。 + /// + Skipped = 4, + + /// + /// 异常终止。 + /// + Error = 5 +} + +/// +/// 运行层级会话结果。 +/// +public enum RunLayerSessionResult +{ + /// + /// 待定。 + /// + Pending = 0, + + /// + /// 合格。 + /// + Ok = 1, + + /// + /// 不合格。 + /// + Ng = 2, + + /// + /// 跳过。 + /// + Skipped = 3 +} diff --git a/OrpaonVision.Model/Production/RunPartJudgmentModel.cs b/OrpaonVision.Model/Production/RunPartJudgmentModel.cs new file mode 100644 index 0000000..0f87e88 --- /dev/null +++ b/OrpaonVision.Model/Production/RunPartJudgmentModel.cs @@ -0,0 +1,229 @@ +namespace OrpaonVision.Model.Production; + +/// +/// 运行部件判定模型。 +/// +public sealed class RunPartJudgmentModel +{ + /// + /// 判定ID。 + /// + public Guid JudgmentId { get; set; } + + /// + /// 产品会话ID。 + /// + public Guid SessionId { get; set; } + + /// + /// 层级会话ID。 + /// + public Guid LayerSessionId { get; set; } + + /// + /// 部件序号(在层级中的序号)。 + /// + public int PartSequence { get; set; } + + /// + /// 部件编码。 + /// + public string PartCode { get; set; } = string.Empty; + + /// + /// 部件名称。 + /// + public string PartName { get; set; } = string.Empty; + + /// + /// 检测到的类别。 + /// + public string DetectedClass { get; set; } = string.Empty; + + /// + /// 置信度。 + /// + public decimal Confidence { get; set; } + + /// + /// 边界框X坐标。 + /// + public float BoundingBoxX { get; set; } + + /// + /// 边界框Y坐标。 + /// + public float BoundingBoxY { get; set; } + + /// + /// 边界框宽度。 + /// + public float BoundingBoxWidth { get; set; } + + /// + /// 边界框高度。 + /// + public float BoundingBoxHeight { get; set; } + + /// + /// 实际中心点X坐标。 + /// + public float ActualCenterX { get; set; } + + /// + /// 实际中心点Y坐标。 + /// + public float ActualCenterY { get; set; } + + /// + /// 期望中心点X坐标。 + /// + public float ExpectedCenterX { get; set; } + + /// + /// 期望中心点Y坐标。 + /// + public float ExpectedCenterY { get; set; } + + /// + /// 位置偏差(像素)。 + /// + public float PositionDeviation { get; set; } + + /// + /// 判定状态。 + /// + public RunPartJudgmentStatus Status { get; set; } + + /// + /// 判定结果。 + /// + public RunPartJudgmentResult Result { get; set; } + + /// + /// 判定规则ID。 + /// + public Guid? RuleId { get; set; } + + /// + /// 判定规则名称。 + /// + public string? RuleName { get; set; } + + /// + /// NG原因。 + /// + public string? NgReason { get; set; } + + /// + /// 截图路径。 + /// + public string? ScreenshotPath { get; set; } + + /// + /// 判定时间(UTC)。 + /// + public DateTime JudgedAtUtc { get; set; } + + /// + /// 推理耗时(毫秒)。 + /// + public long InferenceTimeMs { get; set; } + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// 更新时间(UTC)。 + /// + public DateTime UpdatedAtUtc { get; set; } + + /// + /// 创建人。 + /// + public string CreatedBy { get; set; } = string.Empty; + + /// + /// 更新人。 + /// + public string UpdatedBy { get; set; } = string.Empty; +} + +/// +/// 运行部件判定状态。 +/// +public enum RunPartJudgmentStatus +{ + /// + /// 待检测。 + /// + Pending = 0, + + /// + /// 检测中。 + /// + Detecting = 1, + + /// + /// 已检测。 + /// + Detected = 2, + + /// + /// 判定中。 + /// + Judging = 3, + + /// + /// 已判定。 + /// + Judged = 4, + + /// + /// 异常。 + /// + Error = 5 +} + +/// +/// 违行部件判定结果。 +/// +public enum RunPartJudgmentResult +{ + /// + /// 待定。 + /// + Pending = 0, + + /// + /// 合格。 + /// + Ok = 1, + + /// + /// 不合格。 + /// + Ng = 2, + + /// + /// 缺失。 + /// + Missing = 3, + + /// + /// 位置错误。 + /// + PositionError = 4, + + /// + /// 禁装。 + /// + Forbidden = 5, + + /// + /// 重复。 + /// + Duplicate = 6 +} diff --git a/OrpaonVision.Model/Production/RunStateTransitionLogModel.cs b/OrpaonVision.Model/Production/RunStateTransitionLogModel.cs new file mode 100644 index 0000000..62a8d3a --- /dev/null +++ b/OrpaonVision.Model/Production/RunStateTransitionLogModel.cs @@ -0,0 +1,133 @@ +namespace OrpaonVision.Model.Production; + +/// +/// 运行状态转换日志模型。 +/// +public sealed class RunStateTransitionLogModel +{ + /// + /// 日志ID。 + /// + public Guid LogId { get; set; } + + /// + /// 产品会话ID。 + /// + public Guid SessionId { get; set; } + + /// + /// 层级会话ID(可选)。 + /// + public Guid? LayerSessionId { get; set; } + + /// + /// 转换前状态。 + /// + public string FromState { get; set; } = string.Empty; + + /// + /// 转换后状态。 + /// + public string ToState { get; set; } = string.Empty; + + /// + /// 触发事件。 + /// + public string TriggerEvent { get; set; } = string.Empty; + + /// + /// 转换原因。 + /// + public string Reason { get; set; } = string.Empty; + + /// + /// 转换类型。 + /// + public RunStateTransitionType TransitionType { get; set; } + + /// + /// 操作者ID。 + /// + public string? OperatorId { get; set; } + + /// + /// 操作者姓名。 + /// + public string? OperatorName { get; set; } + + /// + /// 是否人工干预。 + /// + public bool IsManualIntervention { get; set; } + + /// + /// 关联数据(JSON格式)。 + /// + public string? ContextJson { get; set; } + + /// + /// 截图路径(可选)。 + /// + public string? ScreenshotPath { get; set; } + + /// + /// 转换时间(UTC)。 + /// + public DateTime TransitionAtUtc { get; set; } + + /// + /// 处理耗时(毫秒)。 + /// + public long ProcessingTimeMs { get; set; } + + /// + /// 错误信息(如果有)。 + /// + public string? ErrorMessage { get; set; } + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// 创建人。 + /// + public string CreatedBy { get; set; } = string.Empty; +} + +/// +/// 运行状态转换类型。 +/// +public enum RunStateTransitionType +{ + /// + /// 自动转换。 + /// + Automatic = 0, + + /// + /// 人工干预。 + /// + Manual = 1, + + /// + /// 系统触发。 + /// + System = 2, + + /// + /// 错误恢复。 + /// + ErrorRecovery = 3, + + /// + /// 超时处理。 + /// + Timeout = 4, + + /// + /// 异常终止。 + /// + Exception = 5 +} diff --git a/OrpaonVision.SiteApp.Tests/OrpaonVision.SiteApp.Tests.csproj b/OrpaonVision.SiteApp.Tests/OrpaonVision.SiteApp.Tests.csproj new file mode 100644 index 0000000..ca8dbf8 --- /dev/null +++ b/OrpaonVision.SiteApp.Tests/OrpaonVision.SiteApp.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0-windows + enable + enable + false + true + true + + + + + + + + + + + + + + + + + + + + diff --git a/OrpaonVision.SiteApp.Tests/Runtime/Services/RuntimeStateMachineServiceTests.cs b/OrpaonVision.SiteApp.Tests/Runtime/Services/RuntimeStateMachineServiceTests.cs new file mode 100644 index 0000000..78f399d --- /dev/null +++ b/OrpaonVision.SiteApp.Tests/Runtime/Services/RuntimeStateMachineServiceTests.cs @@ -0,0 +1,128 @@ +using Microsoft.Extensions.Logging; +using Moq; +using OrpaonVision.Core.Results; +using OrpaonVision.SiteApp.Runtime.Contracts; +using OrpaonVision.SiteApp.Runtime.Services; + +namespace OrpaonVision.SiteApp.Tests.Runtime.Services; + +/// +/// 运行时状态机服务单元测试。 +/// +[TestClass] +public class RuntimeStateMachineServiceTests +{ + private IRuntimeStateMachineService _stateMachine = null!; + private Mock> _loggerMock = null!; + + [TestInitialize] + public void TestInitialize() + { + _loggerMock = new Mock>(); + + // 使用简单状态机实现进行测试 + _stateMachine = new SimpleRuntimeStateMachineService(_loggerMock.Object); + } + + [TestMethod] + public void GetSnapshot_InitialState_ReturnsCorrectSnapshot() + { + // Act + var snapshot = _stateMachine.GetSnapshot(); + + // Assert + Assert.IsNotNull(snapshot); + Assert.AreEqual(0, snapshot.CurrentLayer); + Assert.AreEqual(0, snapshot.TotalLayers); + Assert.IsTrue(snapshot.StateText.Contains("Idle") || snapshot.StateText.Contains("初始")); + } + + [TestMethod] + public void MoveToNextLayer_FromInitialState_ReturnsSuccess() + { + // Act + var result = _stateMachine.MoveToNextLayer(); + + // Assert + Assert.IsTrue(result.Succeeded); + var snapshot = _stateMachine.GetSnapshot(); + Assert.AreEqual(1, snapshot.CurrentLayer); + } + + [TestMethod] + public void MoveToNextLayer_SequentialCalls_ReturnsSuccess() + { + // Act & Assert - 第一层 + var result1 = _stateMachine.MoveToNextLayer(); + Assert.IsTrue(result1.Succeeded); + var snapshot1 = _stateMachine.GetSnapshot(); + Assert.AreEqual(1, snapshot1.CurrentLayer); + + // Act & Assert - 第二层 + var result2 = _stateMachine.MoveToNextLayer(); + Assert.IsTrue(result2.Succeeded); + var snapshot2 = _stateMachine.GetSnapshot(); + Assert.AreEqual(2, snapshot2.CurrentLayer); + } + + [TestMethod] + public void Reset_AfterProgress_ReturnsToInitialState() + { + // Arrange - 推进几层 + _stateMachine.MoveToNextLayer(); + _stateMachine.MoveToNextLayer(); + var snapshotBefore = _stateMachine.GetSnapshot(); + Assert.AreEqual(2, snapshotBefore.CurrentLayer); + + // Act + _stateMachine.Reset(); + + // Assert + var snapshotAfter = _stateMachine.GetSnapshot(); + Assert.AreEqual(0, snapshotAfter.CurrentLayer); + Assert.AreEqual(0, snapshotAfter.TotalLayers); + } + + [TestMethod] + public void StateTransitionFlow_CompleteWorkflow_ReturnsExpectedStates() + { + // Arrange & Act - 初始状态 + var initialSnapshot = _stateMachine.GetSnapshot(); + Assert.AreEqual(0, initialSnapshot.CurrentLayer); + + // Act & Assert - 第一层 + var result1 = _stateMachine.MoveToNextLayer(); + Assert.IsTrue(result1.Succeeded); + var snapshot1 = _stateMachine.GetSnapshot(); + Assert.AreEqual(1, snapshot1.CurrentLayer); + + // Act & Assert - 第二层 + var result2 = _stateMachine.MoveToNextLayer(); + Assert.IsTrue(result2.Succeeded); + var snapshot2 = _stateMachine.GetSnapshot(); + Assert.AreEqual(2, snapshot2.CurrentLayer); + + // Act & Assert - 重置 + _stateMachine.Reset(); + var finalSnapshot = _stateMachine.GetSnapshot(); + Assert.AreEqual(0, finalSnapshot.CurrentLayer); + } + + [TestMethod] + public void GetSnapshot_AfterMultipleTransitions_ReturnsCorrectCurrentState() + { + // Arrange + for (int i = 0; i < 5; i++) + { + _stateMachine.MoveToNextLayer(); + } + + // Act + var snapshot = _stateMachine.GetSnapshot(); + + // Assert + Assert.AreEqual(5, snapshot.CurrentLayer); + Assert.IsNotNull(snapshot.StateText); + Assert.IsFalse(string.IsNullOrEmpty(snapshot.StateText)); + } +} diff --git a/OrpaonVision.SiteApp.Tests/Training/ModelPackageImportActivationTests.cs b/OrpaonVision.SiteApp.Tests/Training/ModelPackageImportActivationTests.cs new file mode 100644 index 0000000..255a062 --- /dev/null +++ b/OrpaonVision.SiteApp.Tests/Training/ModelPackageImportActivationTests.cs @@ -0,0 +1,238 @@ +using Microsoft.Extensions.Logging; +using Moq; +using OrpaonVision.Core.Results; +using OrpaonVision.Core.Training; +using OrpaonVision.Core.Training.Contracts; +using OrpaonVision.Core.Training.Contracts.Commands; + +namespace OrpaonVision.SiteApp.Tests.Training; + +/// +/// 模型包导入激活单元测试。 +/// +[TestClass] +public class ModelPackageImportActivationTests +{ + private Mock _modelPackageServiceMock = null!; + private Mock> _loggerMock = null!; + + [TestInitialize] + public void TestInitialize() + { + _modelPackageServiceMock = new Mock(); + _loggerMock = new Mock>(); + } + + [TestMethod] + public async Task ImportPackageAsync_ValidPackage_ReturnsSuccess() + { + // Arrange + var packagePath = "test_package.ovpkg"; + var expectedResult = new ModelPackageImportResultDto + { + ModelPackageId = Guid.NewGuid(), + Status = ModelPackageImportStatus.Imported, + Message = "导入成功", + ImportedAtUtc = DateTime.UtcNow, + ImportedBy = "TestUser" + }; + + _modelPackageServiceMock + .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny())) + .ReturnsAsync(Result.Success(expectedResult)); + + // Act + var result = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath); + + // Assert + Assert.IsTrue(result.Succeeded); + Assert.IsNotNull(result.Data); + Assert.AreEqual(ModelPackageImportStatus.Imported, result.Data.Status); + Assert.AreEqual("导入成功", result.Data.Message); + Assert.AreEqual("TestUser", result.Data.ImportedBy); + } + + [TestMethod] + public async Task ImportPackageAsync_InvalidPackage_ReturnsFailure() + { + // Arrange + var packagePath = "invalid_package.ovpkg"; + var expectedError = Result.Fail("INVALID_PACKAGE", "模型包格式无效"); + + _modelPackageServiceMock + .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny())) + .ReturnsAsync(expectedError); + + // Act + var result = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath); + + // Assert + Assert.IsFalse(result.Succeeded); + Assert.AreEqual("INVALID_PACKAGE", result.Code); + Assert.AreEqual("模型包格式无效", result.Message); + } + + [TestMethod] + public async Task ImportPackageAsync_MissingFile_ReturnsFailure() + { + // Arrange + var packagePath = "missing_package.ovpkg"; + var expectedError = Result.Fail("FILE_NOT_FOUND", "模型包文件不存在"); + + _modelPackageServiceMock + .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny())) + .ReturnsAsync(expectedError); + + // Act + var result = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath); + + // Assert + Assert.IsFalse(result.Succeeded); + Assert.AreEqual("FILE_NOT_FOUND", result.Code); + Assert.AreEqual("模型包文件不存在", result.Message); + } + + [TestMethod] + public async Task ActivatePackageAsync_ValidPackage_ReturnsSuccess() + { + // Arrange + var packageId = Guid.NewGuid(); + var expectedResult = new ModelPackageActivationResultDto + { + ModelPackageId = packageId, + Status = ModelPackageActivationStatus.Activated, + Message = "激活成功", + ActivatedAtUtc = DateTime.UtcNow, + ActivatedBy = "TestUser" + }; + + _modelPackageServiceMock + .Setup(x => x.ActivatePackageAsync(packageId, It.IsAny())) + .ReturnsAsync(Result.Success(expectedResult)); + + // Act + var result = await _modelPackageServiceMock.Object.ActivatePackageAsync(packageId); + + // Assert + Assert.IsTrue(result.Succeeded); + Assert.IsNotNull(result.Data); + Assert.AreEqual(ModelPackageActivationStatus.Activated, result.Data.Status); + Assert.AreEqual("激活成功", result.Data.Message); + Assert.AreEqual("TestUser", result.Data.ActivatedBy); + } + + [TestMethod] + public async Task ActivatePackageAsync_PackageNotFound_ReturnsFailure() + { + // Arrange + var packageId = Guid.NewGuid(); + var expectedError = Result.Fail("PACKAGE_NOT_FOUND", "模型包不存在"); + + _modelPackageServiceMock + .Setup(x => x.ActivatePackageAsync(packageId, It.IsAny())) + .ReturnsAsync(expectedError); + + // Act + var result = await _modelPackageServiceMock.Object.ActivatePackageAsync(packageId); + + // Assert + Assert.IsFalse(result.Succeeded); + Assert.AreEqual("PACKAGE_NOT_FOUND", result.Code); + Assert.AreEqual("模型包不存在", result.Message); + } + + [TestMethod] + public async Task ActivatePackageAsync_AlreadyActivated_ReturnsFailure() + { + // Arrange + var packageId = Guid.NewGuid(); + var expectedError = Result.Fail("ALREADY_ACTIVATED", "模型包已激活"); + + _modelPackageServiceMock + .Setup(x => x.ActivatePackageAsync(packageId, It.IsAny())) + .ReturnsAsync(expectedError); + + // Act + var result = await _modelPackageServiceMock.Object.ActivatePackageAsync(packageId); + + // Assert + Assert.IsFalse(result.Succeeded); + Assert.AreEqual("ALREADY_ACTIVATED", result.Code); + Assert.AreEqual("模型包已激活", result.Message); + } + + [TestMethod] + public async Task ImportAndActivate_CompleteWorkflow_ReturnsSuccess() + { + // Arrange + var packagePath = "test_package.ovpkg"; + var packageId = Guid.NewGuid(); + + var importResult = new ModelPackageImportResultDto + { + ModelPackageId = packageId, + Status = ModelPackageImportStatus.Imported, + Message = "导入成功", + ImportedAtUtc = DateTime.UtcNow, + ImportedBy = "TestUser" + }; + + var activationResult = new ModelPackageActivationResultDto + { + ModelPackageId = packageId, + Status = ModelPackageActivationStatus.Activated, + Message = "激活成功", + ActivatedAtUtc = DateTime.UtcNow, + ActivatedBy = "TestUser" + }; + + _modelPackageServiceMock + .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny())) + .ReturnsAsync(Result.Success(importResult)); + + _modelPackageServiceMock + .Setup(x => x.ActivatePackageAsync(packageId, It.IsAny())) + .ReturnsAsync(Result.Success(activationResult)); + + // Act - Import + var importStep = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath); + + // Assert - Import + Assert.IsTrue(importStep.Succeeded); + Assert.AreEqual(packageId, importStep.Data.ModelPackageId); + + // Act - Activate + var activationStep = await _modelPackageServiceMock.Object.ActivatePackageAsync(packageId); + + // Assert - Activate + Assert.IsTrue(activationStep.Succeeded); + Assert.AreEqual(packageId, activationStep.Data.ModelPackageId); + Assert.AreEqual(ModelPackageActivationStatus.Activated, activationStep.Data.Status); + } + + [TestMethod] + public async Task ImportAndActivate_ImportFailure_StopsWorkflow() + { + // Arrange + var packagePath = "invalid_package.ovpkg"; + var packageId = Guid.NewGuid(); + + var importError = Result.Fail("INVALID_FORMAT", "模型包格式无效"); + + _modelPackageServiceMock + .Setup(x => x.ImportPackageAsync(packagePath, It.IsAny())) + .ReturnsAsync(importError); + + // Act + var importStep = await _modelPackageServiceMock.Object.ImportPackageAsync(packagePath); + + // Assert + Assert.IsFalse(importStep.Succeeded); + Assert.AreEqual("INVALID_FORMAT", importStep.Code); + + // Verify that activation was not attempted + _modelPackageServiceMock.Verify( + x => x.ActivatePackageAsync(It.IsAny(), It.IsAny()), + Times.Never); + } +} diff --git a/OrpaonVision.SiteApp/Runtime/Contracts/IConsistencyCheckService.cs b/OrpaonVision.SiteApp/Runtime/Contracts/IConsistencyCheckService.cs index 59b4e1e..effcd57 100644 --- a/OrpaonVision.SiteApp/Runtime/Contracts/IConsistencyCheckService.cs +++ b/OrpaonVision.SiteApp/Runtime/Contracts/IConsistencyCheckService.cs @@ -1,3 +1,5 @@ +using OrpaonVision.Core.Results; + namespace OrpaonVision.SiteApp.Runtime.Contracts; ///