feat: daily node restart controlled from the admin, connect timings as a default line task [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

Agents and Probes exit in order at a configured time (default 04:30 UTC+8, switch and time zone in the admin settings) and the service manager starts them again; a local restart_at or MOSHIRO_RESTART_AT wins. The connect kind (TCP connect and TLS handshake timings) is now a default continuous line template, with a section on the line page.
This commit is contained in:
justme committed 2026-10-07 14:56:50 +08:00
1 parent 388619b8b6
commit 9bcef56497
37 files changed
+928 -101

No files matched your search

@@ -24,8 +24,8 @@ public sealed class EnrollmentAndTaskSetTests
Assert.Equal("active", agent.EnrollmentStatus);
Assert.Equal("active", agent.AuthStatus);
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == 2);
Assert.Equal(["tcp_echo", "udp_echo"], set.Tasks.Select(t => t.Kind).Order().ToArray());
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
Assert.Equal(["connect", "tcp_echo", "udp_echo"], set.Tasks.Select(t => t.Kind).Order().ToArray());
Assert.All(set.Tasks, t =>
{
Assert.Equal("line", t.Scope);
@@ -56,7 +56,7 @@ public sealed class EnrollmentAndTaskSetTests
public async Task Reconnecting_AlwaysGetsTheCurrentSnapshotAgain()
{
AgentSimulator first = await _backend.ConnectAgentAsync();
TaskSet before = await first.WaitForTaskSetAsync(s => s.Tasks.Count == 2);
TaskSet before = await first.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
await first.DisposeAsync();
// 同一个身份重新连接:新连接的服务端状态为空,所以会重新推送当前版本
@@ -157,7 +157,7 @@ public sealed class EnrollmentAndTaskSetTests
using HttpResponseMessage approve = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.NodeId.Value}/approve", null);
Assert.Equal(HttpStatusCode.OK, approve.StatusCode);
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == 2);
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
Assert.NotNull(set);
}
@@ -165,7 +165,7 @@ public sealed class EnrollmentAndTaskSetTests
public async Task DisablingAConnectedAgent_RevokesItAndRefusesFutureConnections()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
await agent.WaitForTaskSetAsync(s => s.Tasks.Count == 2);
await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
using HttpResponseMessage disable = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.NodeId.Value}/disable", null);
Assert.Equal(HttpStatusCode.OK, disable.StatusCode);
@@ -239,7 +239,7 @@ public sealed class EnrollmentAndTaskSetTests
public async Task TaskOverride_ChangesTheDesiredStateAndBumpsTheVersion()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
TaskSet initial = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == 2);
TaskSet initial = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
TaskSpec udp = initial.Tasks.Single(t => t.Kind == "udp_echo");
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
@@ -254,7 +254,7 @@ public sealed class EnrollmentAndTaskSetTests
Assert.StartsWith("/api/admin/tasks/", created.Headers.Location!.ToString());
TaskSet updated = await agent.WaitForTaskSetAsync(s => s.Version > initial.Version && s.Tasks.Any(t => t.Kind == "udp_echo" && t.Params["rate_hz"].GetInt64() == 20));
Assert.Equal(2, updated.Tasks.Count);
Assert.Equal(Defaults.TaskCount, updated.Tasks.Count);
Assert.Equal(7443, updated.Tasks.Single(t => t.Kind == "udp_echo").Params["port"].GetInt64());
// 停用覆盖 → 该 Agent 不再运行这条任务(模板对其他 Agent 不受影响)
@@ -262,7 +262,7 @@ public sealed class EnrollmentAndTaskSetTests
using HttpResponseMessage disable = await _backend.Admin.PatchAsync("/api/admin/tasks/" + overrideId, BackendFixture.Json(new JsonObject { ["enabled"] = false }, "application/merge-patch+json"));
Assert.Equal(HttpStatusCode.OK, disable.StatusCode);
TaskSet disabled = await agent.WaitForTaskSetAsync(s => s.Version > updated.Version && s.Tasks.All(t => t.Kind != "udp_echo"));
Assert.Single(disabled.Tasks);
Assert.Equal(Defaults.TaskCount - 1, disabled.Tasks.Count);
}
[Fact]
@@ -314,7 +314,7 @@ public sealed class EnrollmentAndTaskSetTests
public async Task TaskSet_RejectedItemsReportedByTheAgent_AreVisibleToTheAdmin()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
await agent.WaitForTaskSetAsync(s => s.Tasks.Count == 2);
await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
agent.RejectNextState = [new Fleet.Rpc.RejectedItem { Id = "tsk_x", Reason = "policy_denied" }];
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
@@ -330,7 +330,7 @@ public sealed class EnrollmentAndTaskSetTests
public async Task ProbeLifecycle_CreateRotateRevokeDelete_IsReflectedInEveryAgentsPeerList()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
await agent.WaitForTaskSetAsync(s => s.Tasks.Count == 2);
await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/probes", BackendFixture.Json(new JsonObject { ["name"] = _backend.UniqueName("home") }));
Assert.Equal(HttpStatusCode.Created, created.StatusCode);