agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry. Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
This commit is contained in:
1 parent
d5ebf29e57
commit
ad5efc62ff
206 files changed
+4202
-4964
No files matched your search
@@ -4,7 +4,7 @@ using System.Text.Json.Nodes;
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 一行安装:Agent 和 Probe 共用同一份<b>静态</b>脚本(所有人拿到的字节都一样,所以哈希固定、可以先核对再执行),
|
||||
/// 一行安装:监听侧和拨号侧 Agent 共用同一份<b>静态</b>脚本(所有人拿到的字节都一样,所以哈希固定、可以先核对再执行),
|
||||
/// 面板地址、角色、一次性代码都走命令里的环境变量;脚本再向面板取 CI 签名的发布清单来验证镜像。
|
||||
/// </summary>
|
||||
public sealed class InstallTests : IClassFixture<BackendFixture>
|
||||
@@ -37,11 +37,10 @@ public sealed class InstallTests : IClassFixture<BackendFixture>
|
||||
Assert.Equal("", agent["install_commands"]!["openwrt"]!.GetValue<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("agent")]
|
||||
[InlineData("probe")]
|
||||
public async Task Commands_VerifyTheScriptHash_ThenRunItWithThePublicValues(string role)
|
||||
[Fact]
|
||||
public async Task Commands_VerifyTheScriptHash_ThenRunItWithThePublicValues()
|
||||
{
|
||||
const string role = "agent";
|
||||
JsonNode created = await CreateAsync(role);
|
||||
string code = created["plaintext"]!.GetValue<string>();
|
||||
JsonNode commands = created["install_commands"]!;
|
||||
@@ -62,11 +61,10 @@ public sealed class InstallTests : IClassFixture<BackendFixture>
|
||||
Assert.DoesNotContain("<", linux + macos + windows, StringComparison.Ordinal); // 没有要用户自己替换的占位符
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("agent")]
|
||||
[InlineData("probe")]
|
||||
public async Task ComposeInstall_WritesThePublicValuesToAnEnvFile_AndChecksTheComposeHash(string role)
|
||||
[Fact]
|
||||
public async Task ComposeInstall_WritesThePublicValuesToAnEnvFile_AndChecksTheComposeHash()
|
||||
{
|
||||
const string role = "agent";
|
||||
JsonNode created = await CreateAsync(role);
|
||||
string code = created["plaintext"]!.GetValue<string>();
|
||||
JsonNode compose = created["compose"]!;
|
||||
@@ -91,28 +89,14 @@ public sealed class InstallTests : IClassFixture<BackendFixture>
|
||||
byte[] body = await served.Content.ReadAsByteArrayAsync();
|
||||
Assert.Equal(compose["sha256"]!.GetValue<string>(), Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(body)));
|
||||
|
||||
if (role == "probe")
|
||||
{
|
||||
// Docker Desktop 没有宿主网络:macOS 和 Windows 的 .env 里改成 bridge
|
||||
string macos = compose["macos"]!.GetValue<string>();
|
||||
string windows = compose["windows"]!.GetValue<string>();
|
||||
Assert.Contains("MOSHIRO_NETWORK_MODE=bridge", macos + windows, StringComparison.Ordinal);
|
||||
Assert.Contains("docker compose up -d", macos, StringComparison.Ordinal);
|
||||
Assert.Contains("Get-FileHash compose.yaml", windows, StringComparison.Ordinal);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Agent 需要 Linux 的宿主网络
|
||||
Assert.Equal(string.Empty, compose["macos"]!.GetValue<string>());
|
||||
Assert.Equal(string.Empty, compose["windows"]!.GetValue<string>());
|
||||
}
|
||||
Assert.Equal(string.Empty, compose["macos"]!.GetValue<string>());
|
||||
Assert.Equal(string.Empty, compose["windows"]!.GetValue<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("agent")]
|
||||
[InlineData("probe")]
|
||||
public async Task QuadletInstall_ChecksTheUnitHash_PointsTheImageAtTheConfiguredRepository_AndLeavesAutoUpdateOn(string role)
|
||||
[Fact]
|
||||
public async Task QuadletInstall_ChecksTheUnitHash_PointsTheImageAtTheConfiguredRepository_AndLeavesAutoUpdateOn()
|
||||
{
|
||||
const string role = "agent";
|
||||
JsonNode created = await CreateAsync(role);
|
||||
JsonNode quadlet = created["quadlet"]!;
|
||||
string env = quadlet["env"]!.GetValue<string>();
|
||||
@@ -148,7 +132,7 @@ public sealed class InstallTests : IClassFixture<BackendFixture>
|
||||
[InlineData("evil.container")]
|
||||
[InlineData("backend.container")]
|
||||
[InlineData("agent")]
|
||||
public async Task OnlyTheTwoQuadletUnits_AreServed(string file)
|
||||
public async Task UnknownQuadletUnits_AreRejected(string file)
|
||||
{
|
||||
using HttpResponseMessage response = await _backend.Anonymous.GetAsync("/install/quadlet/" + file);
|
||||
|
||||
@@ -157,7 +141,6 @@ public sealed class InstallTests : IClassFixture<BackendFixture>
|
||||
|
||||
[Theory]
|
||||
[InlineData("agent.yaml")]
|
||||
[InlineData("probe.yaml")]
|
||||
[InlineData("backend.yaml")]
|
||||
public async Task TheComposeFiles_AreStatic_PinNothingToABuild_AndKeepTheNodeHardening(string file)
|
||||
{
|
||||
@@ -177,7 +160,7 @@ public sealed class InstallTests : IClassFixture<BackendFixture>
|
||||
[InlineData("evil.yaml")]
|
||||
[InlineData("agent")]
|
||||
[InlineData("..%2Finstall.sh")]
|
||||
public async Task OnlyTheThreeComposeFiles_AreServed(string file)
|
||||
public async Task UnknownComposeFiles_AreRejected(string file)
|
||||
{
|
||||
using HttpResponseMessage response = await _backend.Anonymous.GetAsync("/install/compose/" + file);
|
||||
|
||||
@@ -220,15 +203,15 @@ public sealed class InstallTests : IClassFixture<BackendFixture>
|
||||
[Fact]
|
||||
public async Task TheScript_GetsTheSignedManifestOfItsRole_ByteForByte()
|
||||
{
|
||||
string manifest = """{"schema":1,"component":"probe","channel":"dev","image":"registry.example/moshiro-dev/moshiro-probe","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","tag":"dev-1","version":"0.1.1","commit":"abc","built_at":"2026-10-06T00:00:00Z","seq":1}""";
|
||||
string manifest = """{"schema":1,"component":"agent","channel":"dev","image":"registry.example/moshiro-dev/moshiro-agent","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","tag":"dev-1","version":"0.1.1","commit":"abc","built_at":"2026-10-06T00:00:00Z","seq":1}""";
|
||||
using HttpResponseMessage published = await _backend.Anonymous.PostAsync("/api/release/offer", BackendFixture.Json(new JsonObject { ["manifest"] = manifest, ["signature"] = TestReleaseKey.Sign(manifest) }));
|
||||
Assert.True(published.StatusCode is HttpStatusCode.Created or HttpStatusCode.Conflict, await published.Content.ReadAsStringAsync());
|
||||
|
||||
string code = (await CreateAsync("probe"))["plaintext"]!.GetValue<string>();
|
||||
string code = (await CreateAsync("agent"))["plaintext"]!.GetValue<string>();
|
||||
using HttpResponseMessage served = await _backend.Anonymous.GetAsync("/install/manifest?code=" + code);
|
||||
Assert.Equal(HttpStatusCode.OK, served.StatusCode);
|
||||
Assert.Equal("no-store", served.Headers.CacheControl?.ToString());
|
||||
Assert.Contains("\"component\":\"probe\"", await served.Content.ReadAsStringAsync(), StringComparison.Ordinal); // 签名对象是原始字节,面板一个字节也不改
|
||||
Assert.Contains("\"component\":\"agent\"", await served.Content.ReadAsStringAsync(), StringComparison.Ordinal); // 签名对象是原始字节,面板一个字节也不改
|
||||
using HttpResponseMessage signature = await _backend.Anonymous.GetAsync("/install/manifest.sig?code=" + code);
|
||||
Assert.Equal(HttpStatusCode.OK, signature.StatusCode);
|
||||
Assert.False(string.IsNullOrWhiteSpace(await signature.Content.ReadAsStringAsync()));
|
||||
@@ -243,14 +226,14 @@ public sealed class InstallTests : IClassFixture<BackendFixture>
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
JsonNode revoked = await CreateAsync("probe");
|
||||
JsonNode revoked = await CreateAsync("agent");
|
||||
using HttpResponseMessage delete = await _backend.Admin.DeleteAsync("/api/admin/join-tokens/" + revoked["token"]!["id"]!.GetValue<string>());
|
||||
Assert.True(delete.IsSuccessStatusCode);
|
||||
using HttpResponseMessage gone = await _backend.Anonymous.GetAsync("/install/manifest?code=" + revoked["plaintext"]!.GetValue<string>());
|
||||
Assert.Equal(HttpStatusCode.NotFound, gone.StatusCode);
|
||||
|
||||
// 用完了(一次性令牌已经注册过一个节点):脚本在动手之前就能从这里知道,给出明确的提示
|
||||
JsonNode used = await CreateAsync("probe");
|
||||
JsonNode used = await CreateAsync("agent");
|
||||
string code = used["plaintext"]!.GetValue<string>();
|
||||
await using (await MoShiro.TestSupport.AgentSimulator.EnrollAsync(_backend.Anonymous, code, _backend.UniqueName("used"))) { }
|
||||
using HttpResponseMessage after = await _backend.Anonymous.GetAsync("/install/manifest?code=" + code);
|
||||
|
||||
Reference in new issue
Block a user