From 016880cdde8953ed07a760f98585734b5b6b5e0e Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:06:52 +0000 Subject: [PATCH] fix: filter vehicle completions and expose valid track actions --- .../vehicleframework/util/TabCompletion.java | 19 ++++++++- .../CommandAndOwnershipCoverageTest.java | 42 ++++++++++++++++++- .../util/RuntimeUtilitiesCoverageTest.java | 5 +-- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/tfminecraft/vehicleframework/util/TabCompletion.java b/src/main/java/net/tfminecraft/vehicleframework/util/TabCompletion.java index cd19c3f..ec64605 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/util/TabCompletion.java +++ b/src/main/java/net/tfminecraft/vehicleframework/util/TabCompletion.java @@ -15,7 +15,21 @@ public class TabCompletion implements TabCompleter { @Override public List onTabComplete(CommandSender sender, Command cmd, String label, String[] args) { - if (!(sender instanceof Player)) return null; + if (args.length == 0) return List.of(); + List options = candidates(sender, cmd, args); + if (options == null) return null; + String prefix = args[args.length - 1]; + return options.stream().filter(option -> option.regionMatches(true, 0, prefix, 0, prefix.length())).toList(); + } + + private List candidates(CommandSender sender, Command cmd, String[] args) { + if (!(sender instanceof Player)) { + if (!cmd.getName().equalsIgnoreCase("vf") || !Permissions.isAdmin(sender)) return List.of(); + if (args.length == 1) return List.of("reload", "admin", "track"); + if (args.length == 2 && args[0].equalsIgnoreCase("admin")) return List.of("quarantine"); + if (args.length == 2 && args[0].equalsIgnoreCase("track")) return List.of("resync"); + return List.of(); + } List completions = new ArrayList<>(); if (cmd.getName().equalsIgnoreCase("vf")) { @@ -70,7 +84,8 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe return completions; } if (args.length == 3 && sender instanceof Player player - && (args[1].equalsIgnoreCase("delete") || args[1].equalsIgnoreCase("clearance"))) { + && List.of("delete", "clearance", "info", "particles", "bind") + .contains(args[1].toLowerCase(java.util.Locale.ROOT))) { var registry = net.tfminecraft.vehicleframework.VehicleFramework.getTrackRegistry(); if (registry != null) { for (net.tfminecraft.vehicleframework.tracks.TrackSpline spline diff --git a/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java b/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java index 1204bdd..168a198 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java @@ -194,9 +194,47 @@ void ownershipMenuReflectsWhitelistTicketsAndLinkedTrainOwnership(boolean enable assertEquals(Material.GRAY_STAINED_GLASS_PANE, menu.getItem(0).getType()); verify(player).openInventory(menu); } + @Test void completionFiltersEveryCurrentTokenAndConsoleOnlySeesUsableActions() { + TabCompletion tabs = new TabCompletion(); + when(player.hasPermission("vf.admin")).thenReturn(true); + assertEquals(List.of("spawn"), tabs.onTabComplete(player, command, "vf", new String[]{"SP"})); + assertEquals(List.of(), tabs.onTabComplete(player, command, "vf", new String[]{"unmatched"})); + assertEquals(List.of(), tabs.onTabComplete(player, command, "vf", new String[0])); + assertEquals(List.of(), tabs.onTabComplete(player, command, "vf", new String[]{"admin", "x"})); + assertEquals(List.of("quarantine"), tabs.onTabComplete(player, command, "vf", new String[]{"admin", "QU"})); + assertEquals(List.of("bind"), tabs.onTabComplete(player, command, "vf", new String[]{"track", "bi"})); + assertEquals(List.of("10"), tabs.onTabComplete(player, command, "vf", new String[]{"kill", "1"})); + Vehicle wagon = mock(Vehicle.class); when(wagon.getId()).thenReturn("Wagon"); + Vehicle cart = mock(Vehicle.class); when(cart.getId()).thenReturn("cart"); + VehicleLoader.get().put("wagon", wagon); VehicleLoader.get().put("cart", cart); + assertEquals(List.of("Wagon"), tabs.onTabComplete(player, command, "vf", new String[]{"spawn", "WA"})); + TrackRegistry registry = mock(TrackRegistry.class); + TrackSpline track = mock(TrackSpline.class); + UUID id = UUID.fromString("abcdef00-0000-0000-0000-000000000001"); when(track.getId()).thenReturn(id); + when(registry.inWorld("world")).thenReturn(List.of(track)); + framework.when(VehicleFramework::getTrackRegistry).thenReturn(registry); + for (String action : List.of("delete", "clearance", "info", "particles", "bind")) { + assertEquals(List.of(id.toString()), tabs.onTabComplete(player, command, "vf", new String[]{"track", action, "AB"})); + assertEquals(List.of(), tabs.onTabComplete(player, command, "vf", new String[]{"track", action, "deadbeef"})); + } + var console = mock(CommandSender.class); + when(console.hasPermission("vf.admin")).thenReturn(true); + assertEquals(List.of("reload", "admin", "track"), tabs.onTabComplete(console, command, "vf", new String[]{""})); + assertEquals(List.of("reload"), tabs.onTabComplete(console, command, "vf", new String[]{"RE"})); + assertEquals(List.of("quarantine"), tabs.onTabComplete(console, command, "vf", new String[]{"admin", "Q"})); + assertEquals(List.of("resync"), tabs.onTabComplete(console, command, "vf", new String[]{"track", "R"})); + for (String[] args : new String[][]{{"spawn", ""}, {"track", "info", ""}, {"reload", ""}}) { + assertEquals(List.of(), tabs.onTabComplete(console, command, "vf", args)); + } + when(console.hasPermission("vf.admin")).thenReturn(false); + assertEquals(List.of(), tabs.onTabComplete(console, command, "vf", new String[]{""})); + when(command.getName()).thenReturn("other"); + assertEquals(List.of(), tabs.onTabComplete(console, command, "other", new String[]{""})); + } + @Test void completionsFollowPermissionsAndOfferKnownVehiclesAndWorldTracks() { TabCompletion completion = new TabCompletion(); - assertNull(completion.onTabComplete(mock(CommandSender.class), command, "vf", new String[]{""})); + assertEquals(List.of(), completion.onTabComplete(mock(CommandSender.class), command, "vf", new String[]{""})); assertEquals(List.of("findvehicles", "keybinds"), completion.onTabComplete(player, command, "vf", new String[]{""})); assertTrue(completion.onTabComplete(player, command, "vf", new String[]{"spawn", ""}).isEmpty()); when(player.hasPermission("vehicleframework.spawn")).thenReturn(true); @@ -211,7 +249,7 @@ void ownershipMenuReflectsWhitelistTicketsAndLinkedTrainOwnership(boolean enable assertTrue(completion.onTabComplete(player, command, "vf", new String[]{"track", "delete", ""}).isEmpty()); TrackRegistry registry = mock(TrackRegistry.class); TrackSpline track = mock(TrackSpline.class); UUID id = UUID.randomUUID(); when(track.getId()).thenReturn(id); when(registry.inWorld("world")).thenReturn(List.of(track)); framework.when(VehicleFramework::getTrackRegistry).thenReturn(registry); - for (String verb : List.of("delete", "clearance")) assertEquals(List.of(id.toString()), completion.onTabComplete(player, command, "vf", new String[]{"track", verb, ""})); + for (String verb : List.of("delete", "clearance", "info", "particles", "bind")) assertEquals(List.of(id.toString()), completion.onTabComplete(player, command, "vf", new String[]{"track", verb, ""})); assertNull(completion.onTabComplete(player, command, "vf", new String[]{"track", "unknown", ""})); assertNull(completion.onTabComplete(player, command, "vf", new String[]{"unknown", ""})); when(command.getName()).thenReturn("other"); assertNull(completion.onTabComplete(player, command, "other", new String[]{""})); diff --git a/src/test/java/net/tfminecraft/vehicleframework/util/RuntimeUtilitiesCoverageTest.java b/src/test/java/net/tfminecraft/vehicleframework/util/RuntimeUtilitiesCoverageTest.java index 943270f..cae7ff4 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/util/RuntimeUtilitiesCoverageTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/util/RuntimeUtilitiesCoverageTest.java @@ -907,7 +907,7 @@ void tabCompletionReflectsPermissionsVehiclesAndCurrentWorldTracks( Command command = mock(Command.class); when(command.getName()).thenReturn("vf"); Player player = r.entity(Player.class, new Location(r.world, 0, 64, 0)); - assertNull(tabs.onTabComplete(mock(CommandSender.class), command, "vf", new String[] {""})); + assertEquals(List.of(), tabs.onTabComplete(mock(CommandSender.class), command, "vf", new String[] {""})); assertEquals( List.of("findvehicles", "keybinds"), tabs.onTabComplete(player, command, "vf", new String[] {""})); @@ -960,7 +960,7 @@ void tabCompletionReflectsPermissionsVehiclesAndCurrentWorldTracks( registry.replace(elsewhere); r.framework.when(VehicleFramework::getTrackRegistry).thenReturn(registry); try { - for (String sub : List.of("delete", "clearance")) + for (String sub : List.of("delete", "clearance", "info", "particles", "bind")) assertEquals( List.of(local.getId().toString()), tabs.onTabComplete(player, command, "vf", new String[] {"track", sub, ""})); @@ -970,7 +970,6 @@ void tabCompletionReflectsPermissionsVehiclesAndCurrentWorldTracks( assertEquals( List.of("10", "20", "50"), tabs.onTabComplete(player, command, "vf", new String[] {"kill", ""})); - assertNull(tabs.onTabComplete(player, command, "vf", new String[] {"track", "info", ""})); assertNull(tabs.onTabComplete(player, command, "vf", new String[] {"unknown", ""})); when(command.getName()).thenReturn("other"); assertNull(tabs.onTabComplete(player, command, "other", new String[] {""}));