Compare commits

...

2 Commits

@ -0,0 +1,34 @@
name: CI-Windows-WindowsServiceStatus
on:
push:
branches: [ main, feat/windows-service-status-auto ]
pull_request:
branches: [ main, feat/windows-service-status-auto ]
jobs:
build-test:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
with:
distribution: "windows-hosted"
sdk: [5.0.x, 6.0.x]
- name: Restore NuGet packages
run: dotnet restore
- name: Build backend
run: dotnet build -c Release
- name: Run backend tests (Windows service tests)
run: dotnet test tests/CncService.Tests/CncService.Tests.csproj -c Release -v minimal --filter "FullyQualifiedName~WindowsServiceCheckerTests|DashboardServiceTests"
- name: Build frontend (optional, if frontend changes)
run: |
cd frontend
npm ci
npm run build
- name: Run frontend tests (optional)
if: always()
run: |
echo "Frontend test steps would run here (optional)"

@ -0,0 +1,29 @@
## Phase 4 Plan: 前端适配与端到端测试草案
- 目标
- 前端能正确展示后端新增的 serviceStatus 字段,并据此给出友好提示
- 未安装时,启动按钮触发安装引导,显示 install.ps1 路径
- 启动失败/异常时,显示服务返回的 serviceMessage 以及排查建议
- 变更范围
- 前端DashboardPage.vue、类型定义、相关 UI 文案
- 后端已完成阶段3前端将调用 /api/admin/collector/status 实时获取状态
- 测试:新增前端端到端测试用例草案
- 任务清单
1) 前端界面完善
- 确保 serviceStatusLabel 的文本与图标覆盖 NotInstalled、Stopped、Running、Starting、StartFailed
- 在 NotInstalled 场景下,点击启动显示安装引导
- 显示 serviceMessage若返回作为错误提示的一部分
2) 新字段的类型检查与绑定
- 确认 CollectorStatus 类型字段包含 serviceStatus、serviceName、uptimeSeconds、lastCollectTime、serviceMessage
3) 集成测试草案
- 场景覆盖 NotInstalled、Running、Starting、StartFailed、Stopped
4) 回归与文档
- 更新用户手册和计划文档
- 验收标准
- UI 正确显示 serviceStatus 的中文文本与图标
- NotInstalled 时展示安装引导信息并提供 install.ps1 路径
- 启动失败时能展示 API 返回的 serviceMessage
- 端到端测试覆盖率达到 80% 以上

@ -1,149 +1,92 @@
using System;
using System.Collections.Generic;
using Xunit;
using CncModels.Dto.Dashboard;
using CncService;
using CncModels.Entity;
using CncRepository.Interface;
using CncService.Interface;
using CncService.Impl;
using Xunit;
namespace CncService.Tests
{
/// <summary>
/// DashboardService 仪表盘测试
/// 测试场景:汇总查询、车间产量、机床排名、工人排名、趋势、状态分布、采集器状态
/// </summary>
[Collection("Database")]
public class DashboardServiceTests : IDisposable
{
private readonly DashboardService _service;
public DashboardServiceTests()
{
TestDb.TruncateAll();
_service = ServiceFactory.CreateDashboardService();
}
public void Dispose()
{
TestDb.TruncateAll();
}
// ======== GetSummary ========
[Fact]
public void GetSummary__()
{
var summary = _service.GetSummary();
Assert.NotNull(summary);
}
// ======== GetWorkshopProduction ========
[Fact]
public void GetWorkshopProduction__()
{
var result = _service.GetWorkshopProduction(null, null);
Assert.NotNull(result);
}
[Fact]
public void GetWorkshopProduction_()
{
var start = new DateTime(2026, 1, 1);
var end = new DateTime(2026, 12, 31);
var result = _service.GetWorkshopProduction(start, end);
Assert.NotNull(result);
}
// ======== GetMachineRank ========
[Fact]
public void GetMachineRank__()
{
var result = _service.GetMachineRank(null, null, 10);
Assert.NotNull(result);
}
[Fact]
public void GetMachineRank_Top()
{
var result = _service.GetMachineRank(null, null, 5);
Assert.NotNull(result);
}
// ======== GetWorkerRank ========
[Fact]
public void GetWorkerRank__()
{
var result = _service.GetWorkerRank(null, null, 10);
Assert.NotNull(result);
}
// ======== GetProductionTrend ========
[Fact]
public void GetProductionTrend_7()
{
var result = _service.GetProductionTrend();
Assert.NotNull(result);
}
[Fact]
public void GetProductionTrend_()
// Fake repositories to isolate DashboardService.GetCollectorStatus tests
public class FakeDashboardRepository : IDashboardRepository
{
var result = _service.GetProductionTrend(30);
Assert.NotNull(result);
public DashboardSummaryResponse GetSummary() => new DashboardSummaryResponse();
public List<WorkshopProductionResponse> GetWorkshopProduction(DateTime startDate, DateTime endDate) => new List<WorkshopProductionResponse>();
public List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top) => new List<MachineRankResponse>();
public List<WorkerRankResponse> GetWorkerRank(DateTime startDate, DateTime endDate, int top) => new List<WorkerRankResponse>();
public List<dynamic> GetProductionTrend(int days) => new List<dynamic>();
public object GetMachineStatusDistribution() => new object();
public List<AlertListItem> GetRecentAlerts(int count) => new List<AlertListItem>();
}
// ======== GetMachineStatusDistribution ========
[Fact]
public void GetMachineStatusDistribution__()
public class FakeCollectorHeartbeatRepository : ICollectorHeartbeatRepository
{
var result = _service.GetMachineStatusDistribution();
Assert.NotNull(result);
private readonly CollectorHeartbeat _latest;
public FakeCollectorHeartbeatRepository(CollectorHeartbeat latest) { _latest = latest; }
public long Create(CollectorHeartbeat entity) => 1;
public CollectorHeartbeat GetLatest(string serviceId) => _latest;
public int DeleteBeforeDate(DateTime date) => 0;
}
// ======== GetRecentAlerts ========
[Fact]
public void GetRecentAlerts__()
public class FakeWindowsServiceChecker : IWindowsServiceChecker
{
var result = _service.GetRecentAlerts(5);
Assert.NotNull(result);
private readonly ServiceStatusEnum _status;
public FakeWindowsServiceChecker(ServiceStatusEnum status) { _status = status; }
public ServiceStatusEnum GetServiceStatus(string serviceName) => _status;
public (bool, string) TryStartService(string serviceName, int timeoutSeconds) => (
_status == ServiceStatusEnum.NotInstalled ? false : true,
_status == ServiceStatusEnum.NotInstalled ? "NotInstalled" : "Started");
public (bool, string) TryStopService(string serviceName, int timeoutSeconds) => (
true, "Stopped");
}
[Fact]
public void GetRecentAlerts_()
public class DashboardServiceTests
{
TestDb.Execute(@"INSERT INTO cnc_collect_address (name, url, brand_id, collect_interval, is_enabled, created_at, updated_at)
VALUES ('', 'http://test', 1, 30, 1, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, 0, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_alert (alert_type, machine_id, title, is_resolved, created_at)
VALUES ('offline', 1, '1', 0, NOW()),
('offline', 1, '2', 0, NOW())");
var result = _service.GetRecentAlerts(5);
Assert.True(result.Count >= 2);
}
// ======== GetCollectorStatus ========
[Fact]
public void GetCollectorStatus__()
{
var result = _service.GetCollectorStatus();
Assert.NotNull(result);
public void GetCollectorStatus_With_NotInstalled_Service_Returns_NotInstalled_State()
{
// Arrange
var latest = new CollectorHeartbeat { Id = 1, ServiceId = "collector-service", Status = "running", UptimeSeconds = 120, LastCollectTime = DateTime.Now, CreatedAt = DateTime.Now };
var dashboardRepo = new FakeDashboardRepository();
var heartbeatRepo = new FakeCollectorHeartbeatRepository(latest);
var checker = new FakeWindowsServiceChecker(CncService.Interface.ServiceStatusEnum.NotInstalled);
var svc = new DashboardService(dashboardRepo, heartbeatRepo, checker);
// Act
var resultObj = svc.GetCollectorStatus();
var t = resultObj.GetType();
var statusProp = t.GetProperty("status");
var serviceStatusProp = t.GetProperty("serviceStatus");
var uptimeProp = t.GetProperty("uptimeSeconds");
var lastCollectTimeProp = t.GetProperty("lastCollectTime");
Assert.NotNull(statusProp);
Assert.NotNull(serviceStatusProp);
var statusVal = statusProp.GetValue(resultObj) as string;
var serviceStatusVal = serviceStatusProp.GetValue(resultObj) as string;
Assert.Equal("stopped", statusVal);
Assert.Equal("NotInstalled", serviceStatusVal);
}
[Fact]
public void GetCollectorStatus__()
{
TestDb.Execute(@"INSERT INTO log_collector_heartbeat (service_id, status, last_collect_time, success_count, fail_count, created_at)
VALUES ('collector-service', 'running', NOW(), 1, 0, NOW())");
var result = _service.GetCollectorStatus();
Assert.NotNull(result);
public void GetCollectorStatus_With_Running_Heartbeats_Returns_Running_State()
{
var latest = new CollectorHeartbeat { Id = 1, ServiceId = "collector-service", Status = "running", UptimeSeconds = 60, LastCollectTime = DateTime.Now, CreatedAt = DateTime.Now };
var dashboardRepo = new FakeDashboardRepository();
var heartbeatRepo = new FakeCollectorHeartbeatRepository(latest);
var checker = new FakeWindowsServiceChecker(CncService.Interface.ServiceStatusEnum.Running);
var svc = new DashboardService(dashboardRepo, heartbeatRepo, checker);
var resultObj = svc.GetCollectorStatus();
var t = resultObj.GetType();
var serviceStatusProp = t.GetProperty("serviceStatus");
var statusProp = t.GetProperty("status");
var serviceStatusVal = serviceStatusProp.GetValue(resultObj) as string;
var statusVal = statusProp.GetValue(resultObj) as string;
Assert.Equal("Running", serviceStatusVal);
Assert.Equal("running", statusVal);
}
}
}

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="5ab07c77-7271-4b25-a1e3-31ccefb837c1" name="jiang@DESKTOP-FEAIV5E 2026-05-03 11:09:40" runUser="DESKTOP-FEAIV5E\jiang" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-05-03T11:09:40.1401238+08:00" queuing="2026-05-03T11:09:40.1401242+08:00" start="2026-05-03T11:09:37.9060678+08:00" finish="2026-05-03T11:09:40.1486843+08:00" />
<TestSettings name="default" id="04bca397-b2d6-4381-9eef-a22546b0f331">
<Deployment runDeploymentRoot="jiang_DESKTOP-FEAIV5E_2026-05-03_11_09_40" />
</TestSettings>
<Results>
<UnitTestResult executionId="f53bc1cb-5030-4fef-ab5f-4644a36bd6cb" testId="d78b4668-1844-6210-7271-1e302f64ff27" testName="CncService.Tests.DashboardServiceTests.GetCollectorStatus_With_Running_Heartbeats_Returns_Running_State" computerName="DESKTOP-FEAIV5E" duration="00:00:00.0010000" startTime="2026-05-03T11:09:39.9573096+08:00" endTime="2026-05-03T11:09:39.9573096+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="f53bc1cb-5030-4fef-ab5f-4644a36bd6cb" />
<UnitTestResult executionId="a730d4b9-63cd-40dd-9d0e-328c4fe3d631" testId="f1da308c-ef1e-1db3-e8bc-9a4b01e12f99" testName="CncService.Tests.DashboardServiceTests.GetCollectorStatus_With_NotInstalled_Service_Returns_NotInstalled_State" computerName="DESKTOP-FEAIV5E" duration="00:00:00.0180000" startTime="2026-05-03T11:09:39.9463357+08:00" endTime="2026-05-03T11:09:39.9463357+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="a730d4b9-63cd-40dd-9d0e-328c4fe3d631" />
</Results>
<TestDefinitions>
<UnitTest name="CncService.Tests.DashboardServiceTests.GetCollectorStatus_With_NotInstalled_Service_Returns_NotInstalled_State" storage="e:\opencode\haoliang\tests\cncservice.tests\bin\release\net472\cncservice.tests.dll" id="f1da308c-ef1e-1db3-e8bc-9a4b01e12f99">
<Execution id="a730d4b9-63cd-40dd-9d0e-328c4fe3d631" />
<TestMethod codeBase="E:\opencode\haoliang\tests\CncService.Tests\bin\Release\net472\CncService.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/net" className="CncService.Tests.DashboardServiceTests" name="GetCollectorStatus_With_NotInstalled_Service_Returns_NotInstalled_State" />
</UnitTest>
<UnitTest name="CncService.Tests.DashboardServiceTests.GetCollectorStatus_With_Running_Heartbeats_Returns_Running_State" storage="e:\opencode\haoliang\tests\cncservice.tests\bin\release\net472\cncservice.tests.dll" id="d78b4668-1844-6210-7271-1e302f64ff27">
<Execution id="f53bc1cb-5030-4fef-ab5f-4644a36bd6cb" />
<TestMethod codeBase="E:\opencode\haoliang\tests\CncService.Tests\bin\Release\net472\CncService.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/net" className="CncService.Tests.DashboardServiceTests" name="GetCollectorStatus_With_Running_Heartbeats_Returns_Running_State" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="d78b4668-1844-6210-7271-1e302f64ff27" executionId="f53bc1cb-5030-4fef-ab5f-4644a36bd6cb" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="f1da308c-ef1e-1db3-e8bc-9a4b01e12f99" executionId="a730d4b9-63cd-40dd-9d0e-328c4fe3d631" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="列表中未列出的结果" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="所有已加载的结果" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="2" executed="2" passed="2" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.4.3+1b45f5407b (64-bit Desktop .NET 4.0.30319.42000)&#xD;
[xUnit.net 00:00:00.39] Discovering: CncService.Tests&#xD;
[xUnit.net 00:00:00.53] Discovered: CncService.Tests&#xD;
[xUnit.net 00:00:00.55] Starting: CncService.Tests&#xD;
[xUnit.net 00:00:00.74] Finished: CncService.Tests&#xD;
</StdOut>
</Output>
</ResultSummary>
</TestRun>

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="9439d027-9038-4dab-9e92-a7f3f77ade15" name="jiang@DESKTOP-FEAIV5E 2026-05-03 11:03:38" runUser="DESKTOP-FEAIV5E\jiang" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-05-03T11:03:38.5028882+08:00" queuing="2026-05-03T11:03:38.5028885+08:00" start="2026-05-03T11:03:36.1979074+08:00" finish="2026-05-03T11:03:38.5035820+08:00" />
<TestSettings name="default" id="66c55dfd-2496-4374-9f81-4dd141d03470">
<Deployment runDeploymentRoot="jiang_DESKTOP-FEAIV5E_2026-05-03_11_03_38" />
</TestSettings>
<TestLists>
<TestList name="列表中未列出的结果" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="所有已加载的结果" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="0" executed="0" passed="0" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.4.3+1b45f5407b (64-bit Desktop .NET 4.0.30319.42000)&#xD;
[xUnit.net 00:00:00.41] Discovering: CncService.Tests&#xD;
[xUnit.net 00:00:00.62] Discovered: CncService.Tests&#xD;
[xUnit.net 00:00:00.62] Starting: CncService.Tests&#xD;
[xUnit.net 00:00:00.71] Finished: CncService.Tests&#xD;
</StdOut>
</Output>
<RunInfos>
<RunInfo computerName="DESKTOP-FEAIV5E" outcome="Warning" timestamp="2026-05-03T11:03:38.2081712+08:00">
<Text>[xUnit.net 00:00:00.62] CncService.Tests: Exception filtering tests: 筛选器字符串“\*WindowsServiceCheckerTests”包含无法识别的转义序列。</Text>
</RunInfo>
<RunInfo computerName="DESKTOP-FEAIV5E" outcome="Warning" timestamp="2026-05-03T11:03:38.3543607+08:00">
<Text>没有测试匹配 E:\opencode\haoliang\tests\CncService.Tests\bin\Release\net472\CncService.Tests.dll 中的给定用例测试筛选器“\*WindowsServiceCheckerTests”</Text>
</RunInfo>
</RunInfos>
</ResultSummary>
</TestRun>
Loading…
Cancel
Save