feat(vt): Profiles tab Delete action, bound through the markup contract

Campaign VT slice 1 Part A round 2 step 5: the Profiles tab's action set
(Create/Select/Save/Delete/name field/mine-only) was missing Delete
entirely — every profile family only ever had Create/Copy/Clear. Adds
MossTankProfileStore.Delete (removes the selected named profile's real
.usd file and its side-car, falls back to "By char"; refuses for "By
char" itself, which has nothing to delete — see ClearCurrent for that
case) and wires it through MossTankPanel.DeleteProfile to a new "Delete"
button in mosstank.xml, next to "Clear profile!".

Create/Select/the name field/the mine-only toggle already bind to the
directory-backed store from steps 1-4; this closes the one missing verb.

Mutation shown to fail: Delete short-circuited to always refuse made
DeleteProfileRemovesTheRealFileAndFallsBackToByCharacter fail (selection
stayed on the named file instead of falling back); restored, it passes
along with the By-char refusal companion test. The markup contract's
interactive-control count was updated for the new button (190 -> 191).

595 MossTank tests passing (was 593).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 00:34:34 +02:00
parent e5c4912b6d
commit 8a146aa8c4
5 changed files with 79 additions and 1 deletions

View file

@ -850,6 +850,7 @@ internal sealed partial class MossTankPanel
public Action CreateProfile => () => CreateProfileCore(copyCurrent: false);
public Action CopyProfile => () => CreateProfileCore(copyCurrent: true);
public Action ClearProfile => ClearProfileCore;
public Action DeleteProfile => DeleteProfileCore;
public Action ToggleMineOnly => () =>
{
string before = _profiles.Selected;
@ -3465,6 +3466,17 @@ internal sealed partial class MossTankPanel
ResetProfileConsumers();
}
private void DeleteProfileCore()
{
if (!_profiles.Delete(out string notice))
{
_profileLifecycleNotice = notice;
return;
}
LoadSelectedProfile();
_profileLifecycleNotice = notice;
}
private void LoadSelectedProfile()
{
_profiles.LoadCurrent(_allSettings, _noBuffItemNames);

View file

@ -217,6 +217,33 @@ internal sealed class MossTankProfileStore
return true;
}
/// <summary>
/// Deletes the currently selected named profile's <c>.usd</c> file and
/// its side-car, then falls back to <see cref="ByCharacter"/>. Refuses
/// for <see cref="ByCharacter"/> itself — there is no file to delete,
/// only a reset (see <see cref="ClearCurrent"/>).
/// </summary>
public bool Delete(out string notice)
{
if (_selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
{
notice = "'By char' is the built-in character profile and cannot be deleted.";
return false;
}
string fileName = _selected;
if (VtankStorage.IsAvailable)
VtankStorage.Delete(fileName);
if (_host.Storage.IsAvailable)
_host.Storage.Delete(SideCarKey(fileName));
_selected = ByCharacter;
_pendingLegacyBareName = null;
_currentDatabase = null;
_currentDatabaseFileName = null;
WriteBinding();
notice = $"Deleted profile {fileName}.";
return true;
}
public void LoadCurrent(
VtankSettingsProfileSerializer.AllSettings settings,
ISet<string> noBuffItemNames)

View file

@ -136,6 +136,9 @@
onclick="{CreateProfile}" />
<button x="448" y="36" w="112" h="23" text="Clear profile!"
onclick="{ClearProfile}" />
<button x="568" y="36" w="76" h="23" text="Delete"
onclick="{DeleteProfile}"
tooltip="Delete the selected named profile's real .usd file. Not available for By char." />
<toggle x="580" y="4" w="120" h="20" text="Mine only"
checked="{MineOnlyEnabled}" onclick="{ToggleMineOnly}" />

View file

@ -88,7 +88,7 @@ public sealed class MossTankMarkupContractTests
XElement[] controls = root.Descendants()
.Where(element => interactive.Contains(element.Name.LocalName))
.ToArray();
Assert.Equal(190, controls.Length);
Assert.Equal(191, controls.Length);
foreach (XElement control in controls)
{

View file

@ -846,6 +846,42 @@ public sealed class MossTankPanelTests
Assert.Equal(0.88f, panel.NormalHealthValue, precision: 2);
}
// Campaign VT slice 1 Part A round 2 step 5: the Profiles tab's new
// Delete button (mosstank.xml), bound through DeleteProfile exactly
// like every other markup action.
[Fact]
public void DeleteProfileRemovesTheRealFileAndFallsBackToByCharacter()
{
var storage = new MemoryStorage();
var automation = new FakeAutomation { Name = "Moss Wart" };
var panel = new MossTankPanel(new FakeHost(automation, storage));
panel.SetNormalHealth(0.42f);
panel.SetProfileNameDraft("Fellowship");
panel.CopyProfile();
const string fellowshipFile = "--Moss Wart__Fellowship.usd";
Assert.Equal(fellowshipFile, panel.SelectedMacroProfile);
Assert.Contains(fellowshipFile, panel.MacroProfileNames);
panel.DeleteProfile();
Assert.Equal(MossTankProfileStore.ByCharacter, panel.SelectedMacroProfile);
Assert.DoesNotContain(fellowshipFile, panel.MacroProfileNames);
Assert.False(storage.Text.ContainsKey(fellowshipFile));
Assert.Contains("Deleted", panel.ProfileLifecycleNotice, StringComparison.Ordinal);
}
[Fact]
public void DeleteProfileRefusesToRemoveByCharacter()
{
var storage = new MemoryStorage();
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
panel.DeleteProfile();
Assert.Equal(MossTankProfileStore.ByCharacter, panel.SelectedMacroProfile);
Assert.Contains("cannot be deleted", panel.ProfileLifecycleNotice, StringComparison.Ordinal);
}
// Item J (slice-1 fix round): renamed from
// NavCommandsImportAndExportExactVtankNavFiles — .af is a real writer
// output now (item E's header/fold-marker emission), not a byte-exact