diff --git a/src/test/java/com/mythlane/gravityflip/command/DefineValidationTest.java b/src/test/java/com/mythlane/gravityflip/command/DefineValidationTest.java
new file mode 100644
index 0000000..d39128b
--- /dev/null
+++ b/src/test/java/com/mythlane/gravityflip/command/DefineValidationTest.java
@@ -0,0 +1,93 @@
+package com.mythlane.gravityflip.command;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Pure-data tests for {@link DefineValidation} — no Hytale runtime dependency.
+ *
+ *
Covers :
+ *
+ * - Name regex accepts alnum + underscore + dash, 1..32 chars.
+ * - Name regex rejects blank, spaces, special chars, non-ASCII, over-length.
+ * - Componentwise min/max return smallest/largest per axis.
+ * - Inflate-max-by-1 convention (max block is INSIDE the AABB only if we add 1 per axis).
+ *
+ */
+class DefineValidationTest {
+
+ // ---------- name regex ----------
+
+ @Test
+ void validName_acceptsAlnumUnderscoreDash() {
+ assertTrue(DefineValidation.isValidName("abc"));
+ assertTrue(DefineValidation.isValidName("my-zone_1"));
+ assertTrue(DefineValidation.isValidName("Z"));
+ assertTrue(DefineValidation.isValidName("a".repeat(32)));
+ assertTrue(DefineValidation.isValidName("ABC_123-xyz"));
+ }
+
+ @Test
+ void validName_rejectsNullEmptyBlankAndInvalidChars() {
+ assertFalse(DefineValidation.isValidName(null));
+ assertFalse(DefineValidation.isValidName(""));
+ assertFalse(DefineValidation.isValidName(" "));
+ assertFalse(DefineValidation.isValidName("my zone"));
+ assertFalse(DefineValidation.isValidName("a".repeat(33)));
+ assertFalse(DefineValidation.isValidName("name!"));
+ assertFalse(DefineValidation.isValidName("名前"));
+ assertFalse(DefineValidation.isValidName("../etc/passwd"));
+ assertFalse(DefineValidation.isValidName("has.dot"));
+ }
+
+ // ---------- componentwise min/max ----------
+
+ @Test
+ void componentwiseMin_returnsSmallestPerAxis() {
+ int[] a = {5, 10, -3};
+ int[] b = {1, 20, -7};
+ assertArrayEquals(new int[]{1, 10, -7}, DefineValidation.componentwiseMin(a, b));
+ }
+
+ @Test
+ void componentwiseMax_returnsLargestPerAxis() {
+ int[] a = {5, 10, -3};
+ int[] b = {1, 20, -7};
+ assertArrayEquals(new int[]{5, 20, -3}, DefineValidation.componentwiseMax(a, b));
+ }
+
+ @Test
+ void componentwise_orderIndependent() {
+ int[] a = {1, 2, 3};
+ int[] b = {4, 5, 6};
+ assertArrayEquals(
+ DefineValidation.componentwiseMin(a, b),
+ DefineValidation.componentwiseMin(b, a));
+ assertArrayEquals(
+ DefineValidation.componentwiseMax(a, b),
+ DefineValidation.componentwiseMax(b, a));
+ }
+
+ // ---------- inflate-max convention (block inclusion) ----------
+
+ /**
+ * A block occupies the cube between (x,y,z) and (x+1,y+1,z+1). If an AABB's max is the
+ * raw block coord (maxBlockX, maxBlockY, maxBlockZ), a player standing on top of that
+ * block is OUTSIDE the AABB. We therefore inflate max by +1 per axis.
+ */
+ @Test
+ void boxFromCorners_inflateMax_includesMaxBlock() {
+ int[] mn = {0, 64, 0};
+ int[] mx = {10, 70, 10};
+ // Player feet at (10.5, 70.5, 10.5) — standing in the maxBlock cube.
+ double px = 10.5, py = 70.5, pz = 10.5;
+ // Without inflate: max = (10,70,10) — player OUT.
+ assertFalse(px < mx[0] && py < mx[1] && pz < mx[2]);
+ // With inflate: max = (11,71,11) — player IN.
+ int[] mxInflated = {mx[0] + 1, mx[1] + 1, mx[2] + 1};
+ assertTrue(px < mxInflated[0] && py < mxInflated[1] && pz < mxInflated[2]);
+ }
+}