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; } }