diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 74464d6..b486c8f 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -321,6 +321,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.ThemeSwitchSelfTestStep(this), new SelfTests.FontManagerCtorSmokeStep(this), new SelfTests.FontPushSmokeStep(this), + new SelfTests.WizardStateSmokeStep(this), ]); if (!Config.FirstRunCompleted) diff --git a/HellionChat/SelfTests/WizardStateSmokeStep.cs b/HellionChat/SelfTests/WizardStateSmokeStep.cs new file mode 100644 index 0000000..3fc3d20 --- /dev/null +++ b/HellionChat/SelfTests/WizardStateSmokeStep.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.SelfTest; +using HellionChat.Code; +using HellionChat.Ui; + +namespace HellionChat.SelfTests; + +// Drives the FirstRunWizard state machine through every step and +// commits a no-op pending state (Variant 1), then re-runs picking +// Roleplay on Step 2 and skipping Step 3 (Variant 2). Verifies +// that the staged-commit path does not throw under any combination +// of Pending* values and that CommitPending leaves Config in a +// readable shape. Variant 2's Roleplay commit would normally +// mutate the six PrivacyFilter / Retention fields ApplyRoleplay +// touches, so the step snapshots them before Variant 2 runs and +// CleanUp() restores them — the self-test stays idempotent across +// repeated /xlperf runs and does not overwrite an active privacy +// profile. +internal sealed class WizardStateSmokeStep : ISelfTestStep +{ + private readonly Plugin plugin; + + // Snapshot slots for the six Configuration fields ApplyRoleplay + // writes in Variant 2. Populated right before Variant 2 mutates + // Config, consumed by CleanUp(). Reference-typed snapshots + // (HashSet, Dictionary) capture the existing slot by reference, + // which is safe because ApplyRoleplay reassigns the slot with + // a fresh instance instead of mutating in place. + private bool? snapshotPrivacyFilterEnabled; + private HashSet? snapshotPrivacyPersistChannels; + private bool? snapshotPrivacyPersistUnknownChannels; + private bool? snapshotRetentionEnabled; + private int? snapshotRetentionDefaultDays; + private Dictionary? snapshotRetentionPerChannelDays; + + public WizardStateSmokeStep(Plugin plugin) + { + this.plugin = plugin; + } + + public string Name => "Hellion Chat - FirstRunWizard state smoke"; + + public SelfTestStepResult RunStep() + { + var wizard = this.plugin.FirstRunWizard; + if (wizard is null) + { + ImGui.Text("Plugin.FirstRunWizard is null"); + return SelfTestStepResult.Fail; + } + + try + { + // Variant 1: no-op CommitPending. Walks the state machine and + // verifies the empty-pending write-back path does not throw. + wizard.TestOnly_AdvanceTo(1); + wizard.TestOnly_AdvanceTo(2); + wizard.TestOnly_AdvanceTo(3); + wizard.TestOnly_AdvanceTo(4); + wizard.CommitPending(); + + // Variant 2: skip Step 3 explicitly. Picks Roleplay on Step 2, + // jumps straight to Step 4 (no Step-3 entry → no seed for + // LoadPreviousSession / FilterIncludePreviousSessions), commits, + // and asserts the two coupled history toggles remained on their + // pre-test value. Pins the null-semantics from Spec Z.176 so a + // regression in CommitPending that started writing seeded + // recommendations unconditionally would surface here. + // CommitPending → ApplyRoleplay overwrites six privacy / + // retention fields, so snapshot them first and let CleanUp + // restore them after the assert. Keeps /xlperf idempotent. + this.snapshotPrivacyFilterEnabled = Plugin.Config.PrivacyFilterEnabled; + this.snapshotPrivacyPersistChannels = Plugin.Config.PrivacyPersistChannels; + this.snapshotPrivacyPersistUnknownChannels = Plugin.Config.PrivacyPersistUnknownChannels; + this.snapshotRetentionEnabled = Plugin.Config.RetentionEnabled; + this.snapshotRetentionDefaultDays = Plugin.Config.RetentionDefaultDays; + this.snapshotRetentionPerChannelDays = Plugin.Config.RetentionPerChannelDays; + + var loadPrevBefore = Plugin.Config.LoadPreviousSession; + var filterPrevBefore = Plugin.Config.FilterIncludePreviousSessions; + wizard.TestOnly_AdvanceTo(2); + wizard.TestOnly_SetPendingProfile(FirstRunWizard.PrivacyProfile.Roleplay); + wizard.TestOnly_AdvanceTo(4); + wizard.CommitPending(); + if (Plugin.Config.LoadPreviousSession != loadPrevBefore) + { + ImGui.Text("Skip-Step-3 path overwrote LoadPreviousSession"); + return SelfTestStepResult.Fail; + } + if (Plugin.Config.FilterIncludePreviousSessions != filterPrevBefore) + { + ImGui.Text("Skip-Step-3 path overwrote FilterIncludePreviousSessions"); + return SelfTestStepResult.Fail; + } + } + catch (Exception ex) + { + ImGui.Text($"Wizard state smoke threw: {ex.GetType().Name}: {ex.Message}"); + return SelfTestStepResult.Fail; + } + + return SelfTestStepResult.Pass; + } + + public void CleanUp() + { + // Restore the six Variant-2 snapshots so back-to-back /xlperf + // runs don't drift the active privacy profile. If Variant 2 + // never ran (Variant 1 threw early), the slots stay null and + // restore is a no-op. After restore the slots are nulled so a + // future RunStep starts fresh. + if (this.snapshotPrivacyFilterEnabled is { } privacyFilter) + Plugin.Config.PrivacyFilterEnabled = privacyFilter; + if (this.snapshotPrivacyPersistChannels is { } persistChannels) + Plugin.Config.PrivacyPersistChannels = persistChannels; + if (this.snapshotPrivacyPersistUnknownChannels is { } persistUnknown) + Plugin.Config.PrivacyPersistUnknownChannels = persistUnknown; + if (this.snapshotRetentionEnabled is { } retentionEnabled) + Plugin.Config.RetentionEnabled = retentionEnabled; + if (this.snapshotRetentionDefaultDays is { } retentionDays) + Plugin.Config.RetentionDefaultDays = retentionDays; + if (this.snapshotRetentionPerChannelDays is { } retentionPolicy) + Plugin.Config.RetentionPerChannelDays = retentionPolicy; + + this.snapshotPrivacyFilterEnabled = null; + this.snapshotPrivacyPersistChannels = null; + this.snapshotPrivacyPersistUnknownChannels = null; + this.snapshotRetentionEnabled = null; + this.snapshotRetentionDefaultDays = null; + this.snapshotRetentionPerChannelDays = null; + } +}