Add source-controlled Puppet/Hiera contracts for edge2 Divoom-as-DM-device without replacing the live flowercore-divoom systemd deployment. Add Divoom TV Pi HDMI systemd/Puppet deployment artifacts, LF shell-script guardrails, and focused lint coverage for the additive non-K8s deploy shape. Co-Authored-By: Codex <codex@openai.com>
207 lines
8.3 KiB
C#
207 lines
8.3 KiB
C#
using FluentAssertions;
|
|
using Xunit;
|
|
|
|
namespace BluejayInfraLint.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class DivoomPiDeployArtifactTests
|
|
{
|
|
private static readonly string Root = FindRepoRoot();
|
|
private static readonly string DmRoot = Path.Combine(Root, "apps", "fc-divoom-dm-pi-device");
|
|
private static readonly string TvRoot = Path.Combine(Root, "apps", "fc-divoom-tv-pi");
|
|
|
|
public static TheoryData<string> DmRequiredArtifacts => new()
|
|
{
|
|
"README.md",
|
|
"hiera/edge2-divoom-dm-device.overlay.yaml",
|
|
"puppet/profile/pi/service/divoom_dm_device.pp",
|
|
"puppet/templates/divoom-device-registration.json.epp",
|
|
"puppet/templates/flowercore-divoom-dm-agent.service.epp",
|
|
};
|
|
|
|
public static TheoryData<string> TvRequiredArtifacts => new()
|
|
{
|
|
"README.md",
|
|
"hiera/example-divoom-tv-pi.iamworkin.lan.yaml",
|
|
"puppet/profile/pi/service/divoom_tv.pp",
|
|
"systemd/flowercore-divoom-tv.service",
|
|
"systemd/flowercore-divoom-tv-hdmi.service",
|
|
"systemd/99-flowercore-divoom-tv-hdmi.rules",
|
|
"scripts/flowercore-divoom-tv-prelaunch.sh",
|
|
"scripts/flowercore-divoom-tv-launch.sh",
|
|
"scripts/flowercore-divoom-tv-hdmi-respond.sh",
|
|
};
|
|
|
|
[Theory]
|
|
[MemberData(nameof(DmRequiredArtifacts))]
|
|
public void DmDeviceArtifacts_ArePresent(string relativePath)
|
|
{
|
|
File.Exists(Path.Combine(DmRoot, relativePath.Replace('/', Path.DirectorySeparatorChar))).Should().BeTrue(relativePath);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(TvRequiredArtifacts))]
|
|
public void TvPiArtifacts_ArePresent(string relativePath)
|
|
{
|
|
File.Exists(Path.Combine(TvRoot, relativePath.Replace('/', Path.DirectorySeparatorChar))).Should().BeTrue(relativePath);
|
|
}
|
|
|
|
[Fact]
|
|
public void DmDeviceReadme_DeclaresPuppetSystemdNotKubernetes()
|
|
{
|
|
var readme = ReadDm("README.md");
|
|
|
|
readme.Should().Contain("not a Kubernetes application");
|
|
readme.Should().Contain("profile::pi::service::divoom");
|
|
readme.Should().Contain("no K8s surface");
|
|
}
|
|
|
|
[Fact]
|
|
public void DmHieraOverlay_PreservesExistingEdge2DivoomService()
|
|
{
|
|
var hiera = ReadDm("hiera/edge2-divoom-dm-device.overlay.yaml");
|
|
|
|
hiera.Should().Contain("fc-pimanager:");
|
|
hiera.Should().Contain("fc-divoom:");
|
|
hiera.Should().Contain("enabled: true");
|
|
hiera.Should().Contain("profile::pi::service::divoom_dm_device::service_enabled: false");
|
|
hiera.Should().Contain("profile::pi::service::divoom_dm_device::service_ensure: 'stopped'");
|
|
}
|
|
|
|
[Fact]
|
|
public void DmPuppetProfile_DefaultsToStoppedDisabledService()
|
|
{
|
|
var profile = ReadDm("puppet/profile/pi/service/divoom_dm_device.pp");
|
|
|
|
profile.Should().Contain("Boolean $service_enabled = false");
|
|
profile.Should().Contain("Enum['running', 'stopped'] $service_ensure = 'stopped'");
|
|
profile.Should().Contain("service { $service_name:");
|
|
profile.Should().Contain("ensure => $service_ensure");
|
|
profile.Should().Contain("enable => $service_enabled");
|
|
}
|
|
|
|
[Fact]
|
|
public void DmPuppetProfile_DoesNotManageLiveDivoomWebUnit()
|
|
{
|
|
var profile = ReadDm("puppet/profile/pi/service/divoom_dm_device.pp");
|
|
|
|
profile.Should().NotContain("Service['flowercore-divoom.service']");
|
|
profile.Should().NotContain("service { 'flowercore-divoom.service'");
|
|
profile.Should().NotContain("notify => Service");
|
|
}
|
|
|
|
[Fact]
|
|
public void DmAgentUnit_IsSeparateAndGatedByExistingWrappers()
|
|
{
|
|
var unit = ReadDm("puppet/templates/flowercore-divoom-dm-agent.service.epp");
|
|
|
|
unit.Should().Contain("ConditionPathExists=<%= $divoom_install_dir %>/bt-link.sh");
|
|
unit.Should().Contain("ConditionPathExists=<%= $divoom_install_dir %>/bt-reset.sh");
|
|
unit.Should().Contain("ConditionPathExists=<%= $divoom_install_dir %>/audio-link.sh");
|
|
unit.Should().Contain("ExecStart=<%= $agent_binary_path %> --mode=Pi");
|
|
unit.Should().NotContain("flowercore-divoom.service");
|
|
}
|
|
|
|
[Fact]
|
|
public void DmRegistration_CarriesRenderProofAndSafetyPolicy()
|
|
{
|
|
var registration = ReadDm("puppet/templates/divoom-device-registration.json.epp");
|
|
|
|
registration.Should().Contain("\"candidateChannels\": <%= $bt_channels_json %>");
|
|
registration.Should().Contain("\"deviceInfoIsRenderProof\": false");
|
|
registration.Should().Contain("\"visibleRenderProofRequired\": <%= $visible_render_proof_required %>");
|
|
registration.Should().Contain("\"preserveExistingService\": \"flowercore-divoom.service\"");
|
|
registration.Should().Contain("\"doNotEnableFmRadio\": true");
|
|
}
|
|
|
|
[Fact]
|
|
public void TvService_UsesAvaloniaHdmiSafetyGates()
|
|
{
|
|
var unit = ReadTv("systemd/flowercore-divoom-tv.service");
|
|
|
|
unit.Should().Contain("ConditionPathExists=/opt/flowercore/divoom-tv/FlowerCore.Divoom.Tv");
|
|
unit.Should().Contain("Environment=XDG_RUNTIME_DIR=/run/fc-divoom-tv");
|
|
unit.Should().Contain("RuntimeDirectoryMode=0700");
|
|
unit.Should().Contain("ExecStartPre=/usr/local/bin/flowercore-divoom-tv-prelaunch.sh");
|
|
unit.Should().Contain("ExecStart=/usr/local/bin/flowercore-divoom-tv-launch.sh");
|
|
unit.Should().Contain("MemoryMax=2G");
|
|
unit.Should().Contain("PrivateTmp=true");
|
|
unit.Should().NotContain("/tmp");
|
|
}
|
|
|
|
[Fact]
|
|
public void TvLauncher_PrefersCageAndFallsBackToDirectLaunch()
|
|
{
|
|
var script = ReadTv("scripts/flowercore-divoom-tv-launch.sh");
|
|
|
|
script.Should().Contain("command -v cage");
|
|
script.Should().Contain("exec cage --");
|
|
script.Should().Contain("launching FlowerCore.Divoom.Tv directly");
|
|
script.Should().Contain("--target=hdmi");
|
|
script.Should().Contain("--presentation-mode=${PRESENTATION_MODE}");
|
|
}
|
|
|
|
[Fact]
|
|
public void TvHotplugRule_SettlesAndRestartsRenderer()
|
|
{
|
|
var rule = ReadTv("systemd/99-flowercore-divoom-tv-hdmi.rules");
|
|
var responder = ReadTv("scripts/flowercore-divoom-tv-hdmi-respond.sh");
|
|
|
|
rule.Should().Contain("KERNEL==\"card?-HDMI-A-?\"");
|
|
rule.Should().Contain("start flowercore-divoom-tv-hdmi.service");
|
|
responder.Should().Contain("sleep 2");
|
|
responder.Should().Contain("systemctl restart flowercore-divoom-tv.service");
|
|
}
|
|
|
|
[Fact]
|
|
public void TvPuppetProfile_InstallsCageAndStaticArtifacts()
|
|
{
|
|
var profile = ReadTv("puppet/profile/pi/service/divoom_tv.pp");
|
|
|
|
profile.Should().Contain("package { ['cage', 'libgbm1', 'libdrm2', 'libxkbcommon0', 'fonts-dejavu-core']");
|
|
profile.Should().Contain("'profile/pi/fc_divoom_tv/flowercore-divoom-tv.service'");
|
|
profile.Should().Contain("'profile/pi/fc_divoom_tv/flowercore-divoom-tv-launch.sh'");
|
|
profile.Should().Contain("profile/pi/fc_divoom_tv/99-flowercore-divoom-tv-hdmi.rules");
|
|
profile.Should().Contain("Boolean $service_enabled = false");
|
|
}
|
|
|
|
[Fact]
|
|
public void DivoomArtifacts_DoNotAddKubernetesWorkloads()
|
|
{
|
|
var allText = Directory.GetFiles(DmRoot, "*", SearchOption.AllDirectories)
|
|
.Concat(Directory.GetFiles(TvRoot, "*", SearchOption.AllDirectories))
|
|
.Select(File.ReadAllText);
|
|
|
|
foreach (var text in allText)
|
|
{
|
|
text.Should().NotContain("kind: Deployment");
|
|
text.Should().NotContain("kind: IngressRoute");
|
|
text.Should().NotContain("kind: Certificate");
|
|
text.Should().NotContain("kind: OnePasswordItem");
|
|
}
|
|
}
|
|
|
|
private static string ReadDm(string relativePath)
|
|
=> File.ReadAllText(Path.Combine(DmRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
|
|
|
private static string ReadTv(string relativePath)
|
|
=> File.ReadAllText(Path.Combine(TvRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
|
|
|
private static string FindRepoRoot()
|
|
{
|
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (current is not null)
|
|
{
|
|
if (Directory.Exists(Path.Combine(current.FullName, "apps"))
|
|
&& File.Exists(Path.Combine(current.FullName, "README.md")))
|
|
{
|
|
return current.FullName;
|
|
}
|
|
|
|
current = current.Parent;
|
|
}
|
|
|
|
throw new DirectoryNotFoundException("Could not find bluejay-infra root.");
|
|
}
|
|
}
|