From 2579046680a8f0a2d9767f0e917e475dd234a77f Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:12:13 +0000 Subject: [PATCH] fix: complete valid magic arguments and artifact path grammar --- .../magic/command/ArtifactPathCompletion.java | 90 +++++++++++++++++ .../magic/command/MagicCommand.java | 27 +++++- .../magic/command/ResonanceCommand.java | 9 +- .../magic/command/WeaponGiveCommand.java | 18 +++- .../magic/CommandEdgeCoverageTest.java | 2 +- .../net/tfminecraft/magic/CommandTest.java | 2 +- .../magic/CompletionRegressionTest.java | 96 +++++++++++++++++++ .../net/tfminecraft/magic/WeaponGiveTest.java | 29 ++++++ .../command/ArtifactPathCompletionTest.java | 45 +++++++++ 9 files changed, 309 insertions(+), 9 deletions(-) create mode 100644 src/main/java/net/tfminecraft/magic/command/ArtifactPathCompletion.java create mode 100644 src/test/java/net/tfminecraft/magic/CompletionRegressionTest.java create mode 100644 src/test/java/net/tfminecraft/magic/command/ArtifactPathCompletionTest.java diff --git a/src/main/java/net/tfminecraft/magic/command/ArtifactPathCompletion.java b/src/main/java/net/tfminecraft/magic/command/ArtifactPathCompletion.java new file mode 100644 index 0000000..6f56fb2 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/command/ArtifactPathCompletion.java @@ -0,0 +1,90 @@ +package net.tfminecraft.magic.command; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import net.tfminecraft.magic.artifact.config.ArtifactRarityRegistry; +import net.tfminecraft.magic.artifact.config.ArtifactTypeDef; +import net.tfminecraft.magic.artifact.config.ArtifactTypeRegistry; + +/** Completes the finite portions of the diagnostic's single-token path grammar. */ +final class ArtifactPathCompletion { + private ArtifactPathCompletion() {} + + static List complete(String token) { + String lower = token.toLowerCase(Locale.ROOT); + if (!lower.startsWith("magic.(")) { + return extend("", token, List.of("magic.artifact", "magic.(")); + } + if (token.indexOf(')') >= 0 || token.chars().anyMatch(Character::isWhitespace)) return List.of(); + int start = token.lastIndexOf(';') + 1; + if (start == 0) start = "magic.(".length(); + String head = token.substring(0, start); + String current = token.substring(start); + Set used = new HashSet<>(); + String primary = null; + for (String previous : token.substring("magic.(".length(), start).split(";")) { + if (previous.isEmpty()) continue; + String[] pair = previous.split("=", 2); + String key = pair[0].toLowerCase(Locale.ROOT); + if (key.equals("primary") || key.equals("element")) { + if (pair.length != 2 || type(pair[1]) == null) return List.of(); + primary = pair[1]; + used.add("primary"); used.add("element"); + } else if (key.equals("rarity")) { + if (pair.length != 2 || ArtifactRarityRegistry.getAll().stream() + .noneMatch(rarity -> rarity.getId().equalsIgnoreCase(pair[1]))) return List.of(); + used.add(key); + } else { + if (type(key) == null) return List.of(); + if (pair.length == 2) { + try { Double.parseDouble(pair[1]); } + catch (NumberFormatException ex) { return List.of(); } + } + used.add(key); + } + } + int equals = current.indexOf('='); + if (equals >= 0) { + String key = current.substring(0, equals).toLowerCase(Locale.ROOT); + if (used.contains(key)) return List.of(); + List values = new ArrayList<>(); + if (key.equals("primary") || key.equals("element")) { + ArtifactTypeRegistry.getAll().forEach(type -> values.add(type.getElementId())); + } else if (key.equals("rarity")) { + ArtifactTypeDef selected = type(primary); + ArtifactRarityRegistry.getAll().stream() + .filter(rarity -> selected == null || selected.hasRarity(rarity.getId())) + .forEach(rarity -> values.add(rarity.getId())); + } else { + return List.of(); // Numeric caps stay manual. + } + List endings = new ArrayList<>(); + values.forEach(value -> { endings.add(value + ")"); endings.add(value + ";"); }); + return extend(head + current.substring(0, equals + 1), current.substring(equals + 1), endings); + } + List keys = new ArrayList<>(); + for (String key : List.of("primary", "element", "rarity")) { + if (!used.contains(key)) keys.add(key + "="); + } + for (String id : ArtifactTypeRegistry.getAllIds()) { + if (!used.contains(id.toLowerCase(Locale.ROOT))) { + keys.add(id + ")"); keys.add(id + ";"); keys.add(id + "="); + } + } + return extend(head, current, keys); + } + + private static ArtifactTypeDef type(String id) { + if (id == null) return null; + return ArtifactTypeRegistry.getAll().stream() + .filter(type -> type.getElementId().equalsIgnoreCase(id)).findFirst().orElse(null); + } + + private static List extend(String head, String typed, List candidates) { + return candidates.stream().filter(candidate -> candidate.regionMatches(true, 0, typed, 0, typed.length())) + .map(candidate -> head + typed + candidate.substring(typed.length())).distinct().toList(); + } +} diff --git a/src/main/java/net/tfminecraft/magic/command/MagicCommand.java b/src/main/java/net/tfminecraft/magic/command/MagicCommand.java index a3bfa66..81e80b5 100644 --- a/src/main/java/net/tfminecraft/magic/command/MagicCommand.java +++ b/src/main/java/net/tfminecraft/magic/command/MagicCommand.java @@ -669,7 +669,8 @@ public List onTabComplete(CommandSender sender, Command command, String } boolean give = WeaponGiveCommand.allowed(sender); boolean admin = hasAdmin(sender); - boolean rune = hasRuneKeybind(sender); + boolean isPlayer = sender instanceof Player; + boolean rune = isPlayer && hasRuneKeybind(sender); if (!admin && !rune && !give) { return Collections.emptyList(); } @@ -682,7 +683,7 @@ public List onTabComplete(CommandSender sender, Command command, String options.add("rune"); } if (admin) { - options.addAll(SUBCOMMANDS); + options.addAll(isPlayer ? SUBCOMMANDS : List.of("reload", "artifact", "resonance")); } return filterPrefix(options, args[0]); } @@ -707,6 +708,9 @@ public List onTabComplete(CommandSender sender, Command command, String if (!admin) { return Collections.emptyList(); } + if (!isPlayer && List.of("open", "refresh", "rune", "fillchest", "shrine").contains(args[0].toLowerCase(Locale.ROOT))) { + return Collections.emptyList(); + } if ("fillchest".equalsIgnoreCase(args[0])) { if (args.length == 2) { List options = new ArrayList<>(); @@ -753,9 +757,16 @@ public List onTabComplete(CommandSender sender, Command command, String } if (args.length == 2) { return filterPrefix( - List.of("roll", "give", "create", "path", "setfill", "setmuffle", "unmuffle"), + isPlayer ? List.of("roll", "give", "create", "path", "setfill", "setmuffle", "unmuffle") + : List.of("roll", "give", "path"), args[1]); } + if (!isPlayer && !List.of("roll", "give", "path").contains(args[1].toLowerCase(Locale.ROOT))) { + return Collections.emptyList(); + } + if ("path".equalsIgnoreCase(args[1])) { + return args.length == 3 ? ArtifactPathCompletion.complete(args[2]) : Collections.emptyList(); + } if ("setfill".equalsIgnoreCase(args[1])) { if (args.length == 3) { return filterPrefix(ElementRegistry.getAllIds(), args[2]); @@ -791,9 +802,17 @@ private static List completeRollArgs(String[] args, int elementIndex) { } if (args.length == elementIndex + 1) { List options = new ArrayList<>(); + String primary = args[elementIndex - 1]; + boolean random = "random".equalsIgnoreCase(primary); + List types = ArtifactTypeRegistry.getAll().stream() + .filter(ArtifactTypeDef::isEnabled) + .filter(type -> random ? type.getWeight() > 0 && Double.isFinite(type.getWeight()) + && net.tfminecraft.magic.artifact.config.ArtifactGeneratorCache.inRandomPool(type.getElementId()) + : type.getElementId().equalsIgnoreCase(primary)).toList(); + if (types.isEmpty()) return Collections.emptyList(); options.add("roll"); for (ArtifactRarityDef rarity : ArtifactRarityRegistry.getAll()) { - options.add(rarity.getId()); + if (types.stream().anyMatch(type -> type.hasRarity(rarity.getId()))) options.add(rarity.getId()); } return filterPrefix(options, args[elementIndex]); } diff --git a/src/main/java/net/tfminecraft/magic/command/ResonanceCommand.java b/src/main/java/net/tfminecraft/magic/command/ResonanceCommand.java index d6b0ba6..7907c01 100644 --- a/src/main/java/net/tfminecraft/magic/command/ResonanceCommand.java +++ b/src/main/java/net/tfminecraft/magic/command/ResonanceCommand.java @@ -1,14 +1,21 @@ package net.tfminecraft.magic.command; +import java.util.List; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; import net.tfminecraft.magic.Magic; import net.tfminecraft.magic.Messages; -public final class ResonanceCommand implements CommandExecutor { +public final class ResonanceCommand implements CommandExecutor, TabCompleter { + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + return List.of(); + } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { diff --git a/src/main/java/net/tfminecraft/magic/command/WeaponGiveCommand.java b/src/main/java/net/tfminecraft/magic/command/WeaponGiveCommand.java index edb183b..53f235e 100644 --- a/src/main/java/net/tfminecraft/magic/command/WeaponGiveCommand.java +++ b/src/main/java/net/tfminecraft/magic/command/WeaponGiveCommand.java @@ -99,10 +99,24 @@ static List complete(CommandSender sender, String[] args) { else if (!"give".equalsIgnoreCase(args[1])) return List.of(); else if (args.length == 3) Bukkit.getOnlinePlayers().forEach(p -> options.add(p.getName())); else if (args.length == 4) ArchetypeRegistry.getAll().keySet().forEach(t -> options.add(t.name().toLowerCase(Locale.ROOT))); - else if (args.length == 5) options.addAll(ElementRegistry.getAllIds()); + else if (args.length == 5) ElementRegistry.getAllIds().stream().filter(ElementVisibility::shownOnCharge).forEach(options::add); else if (args.length >= 7) { GearType type = GearType.fromId(args[3]); - PartRegistry.matching(null, type).forEach(p -> options.add(p.getId())); + ArchetypeDef archetype = ArchetypeRegistry.get(type); + if (archetype == null) return List.of(); + Map selected = new LinkedHashMap<>(); + for (int i = 6; i < args.length - 1; i++) { + PartDef part = PartRegistry.get(args[i]); + if (part == null || part.isDisabled() || !part.supports(type) + || selected.putIfAbsent(part.getPartType(), part) != null) return List.of(); + } + List open = PartSlots.open(archetype, selected.get(PartSlots.CORE)); + if (!open.containsAll(selected.keySet())) return List.of(); + PartRegistry.matching(null, type).stream() + .filter(part -> open.contains(part.getPartType()) && !selected.containsKey(part.getPartType())) + .filter(part -> !PartSlots.CORE.equals(part.getPartType()) + || PartSlots.open(archetype, part).containsAll(selected.keySet())) + .forEach(part -> options.add(part.getId())); } String prefix = args[args.length - 1].toLowerCase(Locale.ROOT); return options.stream().filter(s -> s.toLowerCase(Locale.ROOT).startsWith(prefix)).sorted().toList(); diff --git a/src/test/java/net/tfminecraft/magic/CommandEdgeCoverageTest.java b/src/test/java/net/tfminecraft/magic/CommandEdgeCoverageTest.java index 6f936f3..d18a615 100644 --- a/src/test/java/net/tfminecraft/magic/CommandEdgeCoverageTest.java +++ b/src/test/java/net/tfminecraft/magic/CommandEdgeCoverageTest.java @@ -70,7 +70,7 @@ void reloadPermissionOffersAdminTabsWithoutRuneAccess() { assertTrue(f.tab("resonance", "set", "Mage", "fire", "").isEmpty()); assertTrue(f.tab("shrine", "fill", "fire", "").isEmpty()); ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("rare", 1, 1, 1)); - assertTrue(f.tab("artifact", "roll", "fire", "ra").contains("rare")); + assertTrue(f.tab("artifact", "roll", "fire", "ra").isEmpty()); } @Test diff --git a/src/test/java/net/tfminecraft/magic/CommandTest.java b/src/test/java/net/tfminecraft/magic/CommandTest.java index 68b9b18..47c4e32 100644 --- a/src/test/java/net/tfminecraft/magic/CommandTest.java +++ b/src/test/java/net/tfminecraft/magic/CommandTest.java @@ -342,7 +342,7 @@ void chestModesAndTabSuggestionsRespectPermissionsEnabledTypesAndPositions() { assertEquals(List.of("Mage"), tab("artifact", "give", "")); assertEquals(List.of("random", "fire"), tab("artifact", "give", "Mage", "")); assertEquals(List.of("roll"), tab("artifact", "give", "Mage", "fire", "")); - assertTrue(tab("artifact", "path", "").isEmpty()); + assertEquals(List.of("magic.artifact", "magic.("), tab("artifact", "path", "")); assertTrue(tab("unknown", "").isEmpty()); } diff --git a/src/test/java/net/tfminecraft/magic/CompletionRegressionTest.java b/src/test/java/net/tfminecraft/magic/CompletionRegressionTest.java new file mode 100644 index 0000000..654987c --- /dev/null +++ b/src/test/java/net/tfminecraft/magic/CompletionRegressionTest.java @@ -0,0 +1,96 @@ +package net.tfminecraft.magic; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Map; +import net.tfminecraft.magic.artifact.config.*; +import net.tfminecraft.magic.artifact.path.ArtifactPathParser; +import net.tfminecraft.magic.command.ResonanceCommand; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.junit.jupiter.api.*; + +class CompletionRegressionTest { + CommandTest fixture = new CommandTest(); + @BeforeEach void setup() { fixture.setup(); ArtifactGeneratorCache.reset(); } + @AfterEach void cleanup() { fixture.cleanup(); ArtifactGeneratorCache.reset(); } + + List console(String... args) { + var sender = mock(CommandSender.class); + when(sender.hasPermission(anyString())).thenReturn(true); + return fixture.command.onTabComplete(sender, null, "magic", args); + } + + @Test void consoleOnlySeesExecutableRoutesAtEveryDepth() { + assertEquals(List.of("weapon", "reload", "artifact", "resonance"), console("")); + assertEquals(List.of("reload", "resonance"), console("RE")); + assertEquals(List.of("roll", "give", "path"), console("artifact", "")); + for (String root : List.of("open", "refresh", "rune", "fillchest", "shrine")) { + assertTrue(console(root, "").isEmpty()); + assertTrue(console(root, "fill", "").isEmpty()); + } + for (String action : List.of("create", "setfill", "setmuffle", "unmuffle")) { + assertTrue(console("artifact", action, "").isEmpty()); + } + assertEquals(List.of("random", "fire"), console("artifact", "roll", "")); + assertEquals(List.of("Mage"), console("artifact", "give", "")); + assertEquals(List.of("magic.artifact"), console("artifact", "path", "magic.a")); + assertEquals(List.of("give"), console("weapon", "")); + assertEquals(List.of("get", "set", "add", "reset"), console("resonance", "")); + assertTrue(fixture.tab("artifact", "").contains("create")); + } + + @Test void raritiesFollowSelectedBracketsAndRandomPool() { + ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("common", 1, 1, 1)); + ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("rare", 1, 1, 1)); + ArtifactTypeRegistry.register(new ArtifactTypeDef("ice", "names", "models", true, 1, + Map.of("rare", new CapRange(5, 10)), Map.of())); + ArtifactTypeRegistry.register(new ArtifactTypeDef("zero", "names", "models", true, 0, + Map.of("rare", new CapRange(5, 10)), Map.of())); + ArtifactTypeRegistry.register(new ArtifactTypeDef("infinite", "names", "models", true, Double.POSITIVE_INFINITY, + Map.of("rare", new CapRange(5, 10)), Map.of())); + assertEquals(List.of("roll", "common"), fixture.tab("artifact", "roll", "FiRe", "")); + assertEquals(List.of("common"), fixture.tab("artifact", "give", "Mage", "fire", "CO")); + assertTrue(fixture.tab("artifact", "roll", "water", "").isEmpty()); + assertTrue(fixture.tab("artifact", "roll", "missing", "").isEmpty()); + ArtifactGeneratorCache.setRandomPool(List.of("fire", "zero", "infinite")); + assertEquals(List.of("roll", "common"), fixture.tab("artifact", "roll", "random", "")); + ArtifactGeneratorCache.reset(); + assertEquals(List.of("roll", "common", "rare"), fixture.tab("artifact", "roll", "random", "")); + } + + @Test void pathCompletesFiniteGrammarAndPreservesEarlierTokenText() { + ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("common", 1, 1, 1)); + ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("rare", 1, 1, 1)); + assertEquals(List.of("magic.artifact", "magic.("), fixture.tab("artifact", "path", "")); + assertEquals(List.of("magic.artifact"), fixture.tab("artifact", "path", "magic.a")); + assertTrue(fixture.tab("artifact", "path", "Magic.(Pri").contains("Magic.(Primary=")); + String typed = "Magic.(primary=Fi"; + assertEquals(List.of(typed + "re)", typed + "re;"), fixture.tab("artifact", "path", typed)); + String withCap = "magic.(primary=fire;fire=12.5;rarity=CO"; + assertEquals(List.of(withCap + "mmon)", withCap + "mmon;"), fixture.tab("artifact", "path", withCap)); + for (String candidate : fixture.tab("artifact", "path", withCap)) { + if (candidate.endsWith(")")) assertNotNull(ArtifactPathParser.parse(candidate)); + } + assertTrue(fixture.tab("artifact", "path", "magic.(primary=fire;rarity=ra").isEmpty()); + assertTrue(fixture.tab("artifact", "path", "magic.(fire=").isEmpty()); + assertTrue(fixture.tab("artifact", "path", "magic.(fire=12").isEmpty()); + assertTrue(fixture.tab("artifact", "path", "magic.(primary=fire)").isEmpty()); + assertFalse(fixture.tab("artifact", "path", "magic.(primary=fire;").contains("magic.(primary=fire;element=")); + assertTrue(fixture.tab("artifact", "path", "magic.artifact", "").isEmpty()); + assertTrue(fixture.tab("artifact", "unknown", "").isEmpty()); + } + + @Test void resonanceExecutorSuppressesDefaultNamesIncludingAlias() throws Exception { + var ctor = PluginCommand.class.getDeclaredConstructor(String.class, org.bukkit.plugin.Plugin.class); + ctor.setAccessible(true); + var registered = ctor.newInstance("resonance", Magic.plugin); + registered.setExecutor(new ResonanceCommand()); + for (String alias : List.of("resonance", "res")) { + assertTrue(registered.tabComplete(fixture.player, alias, new String[]{""}).isEmpty()); + assertTrue(registered.tabComplete(fixture.player, alias, new String[]{"Ma", ""}).isEmpty()); + } + } +} diff --git a/src/test/java/net/tfminecraft/magic/WeaponGiveTest.java b/src/test/java/net/tfminecraft/magic/WeaponGiveTest.java index ed81fd5..2e48d96 100644 --- a/src/test/java/net/tfminecraft/magic/WeaponGiveTest.java +++ b/src/test/java/net/tfminecraft/magic/WeaponGiveTest.java @@ -100,4 +100,33 @@ void reject(int index, String value, String message) { when(sender.hasPermission("custom.staff")).thenReturn(false); assertTrue(tab("weapon","give","").isEmpty()); assertTrue(tab("").isEmpty()); } + @Test void elementsAndPartSlotsMatchAssemblyRules() { + ElementRegistry.register(DomainTest.element("shadow")); + try (var visibility = mockStatic(ElementVisibility.class)) { + visibility.when(() -> ElementVisibility.shownOnCharge("fire")).thenReturn(true); + assertEquals(List.of("fire"), tab("weapon", "give", "Mage", "staff", "")); + assertTrue(tab("weapon", "give", "Mage", "staff", "Sh").isEmpty()); + } + ArchetypeRegistry.register(new ArchetypeDef(GearType.STAFF, "Staff", "v.STICK", "v.STICK", false, + List.of("core", "handle", "tome"), Map.of())); + PartRegistry.register(new PartDef("limited", "Core", "core", 1, Set.of(GearType.STAFF), "v.STICK", + Map.of(), List.of("handle"), Map.of(), Map.of(), List.of(), "", 1, false)); + PartRegistry.register(part("handle", "handle", false, GearType.STAFF)); + PartRegistry.register(part("tome", "tome", false, GearType.STAFF)); + PartRegistry.register(part("extra", "blade", false, GearType.STAFF)); + PartRegistry.register(part("disabled", "handle", true, GearType.STAFF)); + PartRegistry.register(part("wand", "handle", false, GearType.WAND)); + assertEquals(List.of("core", "handle", "limited", "tome"), tab("weapon", "give", "Mage", "staff", "fire", "10", "")); + assertEquals(List.of("limited"), tab("weapon", "give", "Mage", "STAFF", "fire", "10", "LIM")); + assertEquals(List.of("handle"), tab("weapon", "give", "Mage", "staff", "fire", "10", "limited", "H")); + assertTrue(tab("weapon", "give", "Mage", "staff", "fire", "10", "limited", "handle", "").isEmpty()); + assertEquals(List.of("core"), tab("weapon", "give", "Mage", "staff", "fire", "10", "tome", "c")); + assertTrue(tab("weapon", "give", "Mage", "staff", "fire", "10", "tome", "l").isEmpty()); + for (String previous : List.of("missing", "disabled", "wand", "extra")) { + assertTrue(tab("weapon", "give", "Mage", "staff", "fire", "10", previous, "").isEmpty()); + } + assertTrue(tab("weapon", "give", "Mage", "staff", "fire", "10", "handle", "handle", "").isEmpty()); + assertTrue(tab("weapon", "give", "Mage", "unknown", "fire", "10", "").isEmpty()); + } + } diff --git a/src/test/java/net/tfminecraft/magic/command/ArtifactPathCompletionTest.java b/src/test/java/net/tfminecraft/magic/command/ArtifactPathCompletionTest.java new file mode 100644 index 0000000..49b22c0 --- /dev/null +++ b/src/test/java/net/tfminecraft/magic/command/ArtifactPathCompletionTest.java @@ -0,0 +1,45 @@ +package net.tfminecraft.magic.command; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import java.util.Map; +import net.tfminecraft.magic.artifact.config.*; +import net.tfminecraft.magic.artifact.path.ArtifactPathParser; +import org.junit.jupiter.api.*; + +class ArtifactPathCompletionTest { + @BeforeEach void setup() { + ArtifactTypeRegistry.clear(); ArtifactRarityRegistry.clear(); + ArtifactTypeRegistry.register(new ArtifactTypeDef("fire", "names", "models", true, 1, + Map.of("common", new CapRange(1, 2)), Map.of())); + ArtifactRarityRegistry.register(new ArtifactRarityDef("common", "Common", "#ffffff", 1, 1, 1, new CapRange(1, 2))); + } + @AfterEach void cleanup() { ArtifactTypeRegistry.clear(); ArtifactRarityRegistry.clear(); } + + @Test void finiteKeysAndValuesCanBeContinuedOrClosedWithoutChangingExistingText() throws Exception { + var ctor = ArtifactPathCompletion.class.getDeclaredConstructor(); ctor.setAccessible(true); ctor.newInstance(); + assertEquals(List.of("magic.(element="), ArtifactPathCompletion.complete("magic.(el")); + assertEquals(List.of("magic.(fire)", "magic.(fire;", "magic.(fire="), ArtifactPathCompletion.complete("magic.(fi")); + assertEquals(List.of("magic.(element=fire)", "magic.(element=fire;"), ArtifactPathCompletion.complete("magic.(element=f")); + assertEquals(List.of("magic.(rarity=common)", "magic.(rarity=common;"), ArtifactPathCompletion.complete("magic.(rarity=c")); + assertEquals(List.of("magic.(rarity=common;primary=fire)", "magic.(rarity=common;primary=fire;"), + ArtifactPathCompletion.complete("magic.(rarity=common;primary=f")); + assertFalse(ArtifactPathCompletion.complete("magic.(element=fire;").stream().anyMatch(s -> s.contains(";primary="))); + for (String head : List.of("magic.(fire;", "magic.(fire=2;", "magic.(rarity=common;fire=2;")) { + assertTrue(ArtifactPathCompletion.complete(head).stream().noneMatch(s -> s.startsWith(head + "fire"))); + for (String completed : ArtifactPathCompletion.complete(head + "primary=f")) { + if (completed.endsWith(")")) assertNotNull(ArtifactPathParser.parse(completed)); + } + } + assertTrue(ArtifactPathCompletion.complete("magic.(primary=fire;rarity=c").stream().allMatch(s -> s.contains("common"))); + assertTrue(ArtifactPathCompletion.complete("magic.(primary=fire;rarity=r").isEmpty()); + } + + @Test void malformedOrManualInputDoesNotInventExecutableData() { + for (String token : List.of("other", "magic.(unknown;", "magic.(primary;", "magic.(primary=unknown;", + "magic.(rarity;", "magic.(rarity=unknown;", "magic.(fire=bad;", "magic.(fire=", "magic.(fire=20", + "magic.(unknown=", "magic.(primary=fire;primary=", "magic.(primary=fire)", "magic.( primary=")) { + assertTrue(ArtifactPathCompletion.complete(token).isEmpty(), token); + } + } +}