Lumi/companion/tests/Lumi.Companion.Core.Tests/Program.cs
2026-07-26 22:06:34 +02:00

151 lines
9.1 KiB
C#

using Lumi.Companion.Abstractions;
using Lumi.Companion.Core;
using Lumi.Companion.Overlay;
static void Assert(bool condition, string message)
{
if (!condition) throw new InvalidOperationException(message);
}
var operation = new SingleFlightOperation();
var calls = 0;
await operation.RunAsync(_ => { calls += 1; return Task.CompletedTask; });
Assert(calls == 1 && !operation.IsRunning, "A successful update-style operation did not reset.");
await operation.RunAsync(_ => { calls += 1; return Task.CompletedTask; });
Assert(calls == 2 && !operation.IsRunning, "A no-update-style repeat did not execute.");
try
{
await operation.RunAsync(_ => throw new InvalidOperationException("expected"));
}
catch (InvalidOperationException error) when (error.Message == "expected") { }
await operation.RunAsync(_ => { calls += 1; return Task.CompletedTask; });
Assert(calls == 3 && !operation.IsRunning, "A failed operation left stale single-flight state.");
using (var cancelled = new CancellationTokenSource())
{
cancelled.Cancel();
try { await operation.RunAsync(token => Task.Delay(1, token), cancelled.Token); }
catch (OperationCanceledException) { }
}
await operation.RunAsync(_ => { calls += 1; return Task.CompletedTask; });
Assert(calls == 4 && !operation.IsRunning, "A cancelled operation left stale single-flight state.");
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var concurrentCalls = 0;
var first = operation.RunAsync(async _ => { concurrentCalls += 1; await release.Task; });
var second = operation.RunAsync(async _ => { concurrentCalls += 1; await Task.Yield(); });
Assert(operation.IsRunning && concurrentCalls == 1, "Rapid update checks started more than one operation.");
release.SetResult();
await Task.WhenAll(first, second);
Assert(concurrentCalls == 1 && !operation.IsRunning, "Rapid update checks did not share and reset the active operation.");
var sanitized = CompanionLogSanitizer.Sanitize(
"LumiDevice device-id.private-device-secret Authorization=private-value https://lumi.test/?token=private-token Cookie: session=private-cookie"
);
Assert(!sanitized.Contains("private-value"), "Companion logs retained an authorization value.");
Assert(!sanitized.Contains("private-cookie"), "Companion logs retained a cookie.");
Assert(!sanitized.Contains("private-token"), "Companion logs retained a query token.");
Assert(!sanitized.Contains("private-device-secret"), "Companion logs retained a paired-device credential.");
Assert(CompanionLogSanitizer.NormalizeEvent("Update Failed!") == "update_failed", "Companion event normalization drifted.");
Assert(CompanionLogSanitizer.LevelForEvent("update_failed") == "error", "Companion failed events must use the error level.");
var anchored = new OverlayContainerSettings
{
Anchor = OverlayAnchor.Top,
PaddingTop = 20,
PaddingLeft = 40,
PaddingRight = 60,
PaddingBottom = 999,
Width = 5000,
Height = 300
};
var rect = AnchorLayout.Calculate(anchored, 1920, 1080);
Assert(anchored.PaddingBottom == 0, "Top-side anchors must zero disabled bottom padding.");
Assert(rect.Width == 1820 && rect.X == 40 && rect.Y == 20, "Physical-pixel layout did not clamp and center symmetrically inside valid padding.");
var now = DateTimeOffset.UnixEpoch;
var queueSettings = new OverlayContainerSettings { QueueLimit = 2, EntryDurationMs = 100, LifetimeMs = 200, ExitDurationMs = 100 };
var queue = new OverlayLifecycleQueue(() => queueSettings, () => now);
queue.Add("one", "chat", "one");
queue.Add("two", "chat", "two");
queue.Add("three", "chat", "three");
Assert(queue.Items[0].State == OverlayItemState.Exiting && queue.Items.Count == 3,
"Overflow must animate the oldest item out while keeping it rendered.");
now += TimeSpan.FromMilliseconds(100);
queue.Advance();
Assert(queue.Items.Count == 2 && queue.Items.All(item => item.State == OverlayItemState.Visible),
"Entry and exit lifecycle transitions did not advance deterministically.");
now += TimeSpan.FromMilliseconds(201);
queue.Advance();
Assert(queue.Items.All(item => item.State == OverlayItemState.Exiting), "Lifetime must begin only after entry completes.");
for (var index = 0; index < 20; index++) queue.Add($"burst-{index}", "chat", index);
Assert(queue.Items.Count <= queueSettings.QueueLimit * 2, "Burst handling must keep the rendered lifecycle queue strictly bounded.");
var settingsPath = Path.Combine(Path.GetTempPath(), $"lumi-overlay-test-{Guid.NewGuid():N}", "settings.json");
var overlayStore = new OverlaySettingsStore(settingsPath);
await overlayStore.LoadAsync();
overlayStore.Current.Sources.Add(new OverlaySourceSetting
{
Id = "twitch:renamed-channel",
Platform = "twitch",
Label = "Previously configured channel",
Selected = true
});
overlayStore.Current.Unified.Anchor = OverlayAnchor.BottomRight;
overlayStore.Current.Unified.PaddingTop = 42;
await overlayStore.SaveAsync(overlayStore.Current);
var restoredStore = new OverlaySettingsStore(settingsPath);
await restoredStore.LoadAsync();
Assert(restoredStore.Current.Sources.Single().Selected && restoredStore.Current.Sources.Single().Label == "Previously configured channel",
"Unavailable source selections were not retained by atomic local settings persistence.");
Assert(restoredStore.Current.Unified.PaddingTop == 0,
"Settings normalization must clear padding that is invalid for the selected anchor.");
var unsavedDraft = restoredStore.Current.CreateCopy();
unsavedDraft.ChatPercentage = 60;
unsavedDraft.Sources[0].Selected = false;
Assert(restoredStore.Current.ChatPercentage != unsavedDraft.ChatPercentage && restoredStore.Current.Sources[0].Selected,
"Editable overlay drafts must not mutate or get replaced by the last saved settings.");
Assert(!CompanionPluginNavigation.UsesNestedNavigation(
[new CompanionPluginPage("SongOverlay", "Overview & settings")]) &&
CompanionPluginNavigation.UsesNestedNavigation(
[new CompanionPluginPage("Transcription", "Capture"), new CompanionPluginPage("Test", "Test")]),
"Single-page plugins must be direct navigation entries while multi-page plugins remain grouped.");
Assert(new CaptureExclusionStatus(CaptureExclusionState.Unsupported, "unsupported").State == CaptureExclusionState.Unsupported,
"Capture-exclusion unsupported state must remain distinct from failure.");
var visibilitySettings = new LumiOverlaySettings
{
Visibility = OverlayVisibilityMode.WhenLive,
Sources = [new() { Id = "twitch:lumi", Platform = "twitch", Label = "Lumi", Selected = true }]
};
var liveSources = new[] { new OverlaySource("twitch:lumi", "twitch", "Lumi", true, true, "live") };
Assert(OverlayVisibilityPolicy.Automatic(visibilitySettings, liveSources), "A selected live Twitch source must activate When live.");
Assert(!OverlayVisibilityPolicy.Automatic(visibilitySettings,
[new OverlaySource("discord:guild:channel", "discord", "Discord", true, true, "connected")]),
"Discord must never activate When live by itself.");
Assert(OverlayVisibilityPolicy.Effective(visibilitySettings, [], true, null), "Configuration preview must override live-state visibility.");
visibilitySettings.Enabled = false;
Assert(OverlayVisibilityPolicy.Effective(visibilitySettings, [], true, null), "Configuration preview must remain available while the runtime is disabled.");
visibilitySettings.Enabled = true;
Assert(!OverlayVisibilityPolicy.Effective(visibilitySettings, liveSources, false, false), "A manual hidden override must win until automatic visibility is restored.");
var automaticPresentation = OverlayVisibilityPresenter.Describe(visibilitySettings, true, false, null);
Assert(automaticPresentation.Visible && automaticPresentation.Action == "Hide temporarily" &&
automaticPresentation.State.Contains("automatic", StringComparison.Ordinal),
"Visibility presentation must separate the current automatic state from the button action.");
var overridePresentation = OverlayVisibilityPresenter.Describe(visibilitySettings, true, false, false);
Assert(!overridePresentation.Visible && overridePresentation.Action == "Restore automatic visibility",
"Visibility presentation must identify a temporary override and explain how to leave it.");
var previewPresentation = OverlayVisibilityPresenter.Describe(visibilitySettings, false, true, null);
Assert(previewPresentation.Visible && !previewPresentation.CanToggle && previewPresentation.Action.Contains("End preview", StringComparison.Ordinal),
"Preview-controlled visibility must be explicit and must not offer an ineffective override action.");
var monitorFallback = OverlayMonitorPolicy.Resolve("missing", ["primary", "second"], "primary");
Assert(monitorFallback.MonitorId == "primary" && monitorFallback.UsedFallback && monitorFallback.Warning is not null,
"Monitor loss must fall back without erasing the saved monitor or hiding the warning.");
Assert(CompanionLiveReconnectPolicy.Delay(1) == TimeSpan.FromSeconds(2) &&
CompanionLiveReconnectPolicy.Delay(99) == TimeSpan.FromSeconds(30),
"Live feed reconnect backoff must be bounded and deterministic.");
Directory.Delete(Path.GetDirectoryName(settingsPath)!, true);
Console.WriteLine("Companion core, native overlay layout/lifecycle, settings persistence, and capture-status regression checks passed.");