diff --git a/tests/unit/test_ksparser_types.py b/tests/unit/test_ksparser_types.py
new file mode 100644
index 000000000000..49ac38ba5397
--- /dev/null
+++ b/tests/unit/test_ksparser_types.py
@@ -0,0 +1,226 @@
+"""
+Unit tests for wic/ksparser.py's custom argparse types: sizetype,
+overheadtype, and systemidtype. These are wic's own input validators for
+kickstart/command-line values, so the tests pin the accepted forms, the
+rejected forms, and the boundaries between them.
+"""
+import sys
+from pathlib import Path
+
+import pytest
+
+_SRC = Path(__file__).resolve().parent.parent.parent / "src"
+if str(_SRC) not in sys.path:
+    sys.path.insert(0, str(_SRC))
+
+from argparse import ArgumentTypeError
+
+from wic.ksparser import sizetype, overheadtype, systemidtype
+
+
+class TestSizetype:
+    """sizetype(default, size_in_bytes=False) returns f(arg) that turns a
+    "<num>[S|s|K|k|M|G]" string into an integer. With size_in_bytes=False
+    the multiplier is 1 (K->1, M->1024, G->1024*1024, i.e. KiB counts);
+    with size_in_bytes=True the multiplier is 1024 and S/s means sectors of
+    512 bytes. A bare number takes the default suffix."""
+
+    @pytest.mark.parametrize("arg,expected", [
+        ("0", 0),
+        ("0M", 0),
+        ("1K", 1),
+        ("1k", 1),
+        ("3K", 3),
+        ("7K", 7),
+        ("512k", 512),
+        ("999k", 999),
+        ("1024K", 1024),
+        ("1000K", 1000),
+        ("1M", 1 * 1024),
+        ("10M", 10 * 1024),
+        ("13M", 13 * 1024),
+        ("100M", 100 * 1024),
+        ("512M", 512 * 1024),
+        ("1G", 1 * 1024 * 1024),
+        ("2G", 2 * 1024 * 1024),
+        ("3G", 3 * 1024 * 1024),
+        # a large magnitude past 2**31, to catch any 32-bit truncation
+        ("5000000K", 5000000),
+    ])
+    def test_kib_units_with_M_default(self, arg, expected):
+        assert sizetype("M")(arg) == expected
+
+    def test_bare_number_uses_M_default(self):
+        assert sizetype("M")("100") == 100 * 1024
+
+    def test_bare_number_uses_K_default(self):
+        assert sizetype("K")("512") == 512
+
+    @pytest.mark.parametrize("arg,expected", [
+        ("1S", 512),
+        ("1s", 512),
+        ("2S", 2 * 512),
+        ("7S", 7 * 512),
+        ("1K", 1024),
+        ("3K", 3 * 1024),
+        ("17M", 17 * 1024 * 1024),
+        ("1M", 1024 * 1024),
+        ("1G", 1024 ** 3),
+        # past 2**63 to confirm Python's arbitrary-precision ints hold
+        ("10000000G", 10000000 * 1024 ** 3),
+    ])
+    def test_size_in_bytes_units(self, arg, expected):
+        assert sizetype("K", size_in_bytes=True)(arg) == expected
+
+    def test_sectors_only_when_size_in_bytes(self):
+        # 'S' is a valid suffix only in byte mode; without it the suffix is
+        # unknown and rejected.
+        with pytest.raises(ArgumentTypeError):
+            sizetype("M")("1S")
+
+    # --- suffix case sensitivity -------------------------------------------
+
+    @pytest.mark.parametrize("arg", ["1m", "1g"])
+    def test_lowercase_m_and_g_rejected(self, arg):
+        # Only k/K are case-insensitive; m and g are not accepted.
+        with pytest.raises(ArgumentTypeError):
+            sizetype("M")(arg)
+
+    # --- negative sizes -----------------------------------------------------
+
+    @pytest.mark.parametrize("arg", ["-1M", "-5", "-1K", "-1024K", "-1G"])
+    def test_negative_size_rejected(self, arg):
+        with pytest.raises(ArgumentTypeError):
+            sizetype("M")(arg)
+
+    # --- invalid formats ----------------------------------------------------
+
+    @pytest.mark.parametrize("bad", [
+        "abc",       # no digits
+        "M",         # suffix only
+        "1X",        # unsupported suffix
+        "1.5M",      # float not accepted
+        "1M ",       # trailing space becomes the suffix
+        "1e3M",      # scientific notation
+        "",          # empty
+        "   ",       # whitespace only
+        "1;M",       # shell metacharacters
+        "1|M",
+        "$(1)M",
+        "1`M",
+    ])
+    def test_invalid_format_rejected(self, bad):
+        with pytest.raises(ArgumentTypeError):
+            sizetype("M")(bad)
+
+    def test_none_rejected(self):
+        with pytest.raises((ArgumentTypeError, TypeError)):
+            sizetype("M")(None)
+
+
+class TestOverheadtype:
+    """overheadtype(arg) -> float, and the factor must be >= 1.0. Anything
+    below 1.0, non-numeric, or non-finite is an ArgumentTypeError."""
+
+    @pytest.mark.parametrize("arg,expected", [
+        ("1.0", 1.0),
+        ("1.001", 1.001),
+        ("1.1", 1.1),
+        ("1.3", 1.3),
+        ("1.37", 1.37),
+        ("3.14159", 3.14159),
+        ("7", 7.0),
+        ("2", 2.0),
+        ("2.0", 2.0),
+        ("100.0", 100.0),
+        ("1000000.0", 1000000.0),
+    ])
+    def test_valid_factor(self, arg, expected):
+        assert overheadtype(arg) == pytest.approx(expected)
+
+    def test_leading_whitespace_accepted(self):
+        # float() strips surrounding whitespace.
+        assert overheadtype(" 1.1") == pytest.approx(1.1)
+
+    def test_trailing_whitespace_accepted(self):
+        assert overheadtype("1.1 ") == pytest.approx(1.1)
+
+    # --- below 1.0: a clean ArgumentTypeError, not a TypeError crash --------
+
+    @pytest.mark.parametrize("arg", ["0.0", "0.5", "0.999", "-1.0", "-0.5"])
+    def test_below_one_rejected(self, arg):
+        with pytest.raises(ArgumentTypeError):
+            overheadtype(arg)
+
+    # --- non-finite ---------------------------------------------------------
+
+    @pytest.mark.parametrize("arg", ["nan", "NaN", "inf", "Infinity", "-inf"])
+    def test_non_finite_rejected(self, arg):
+        with pytest.raises(ArgumentTypeError):
+            overheadtype(arg)
+
+    # --- non-numeric --------------------------------------------------------
+
+    @pytest.mark.parametrize("bad", [
+        "abc", "1.1.1", "M", "1,3", "1 1", "--1.1", "",
+    ])
+    def test_non_numeric_rejected(self, bad):
+        with pytest.raises(ArgumentTypeError):
+            overheadtype(bad)
+
+    def test_none_rejected(self):
+        with pytest.raises((ArgumentTypeError, TypeError)):
+            overheadtype(None)
+
+
+class TestSystemidtype:
+    """systemidtype(arg) validates an MBR partition type id as an explicit
+    0x-prefixed hex byte in 0x1..0xFF and returns the original string
+    unchanged."""
+
+    @pytest.mark.parametrize("arg", [
+        "0x1", "0x01", "0x0b", "0x0B", "0x82", "0x83", "0xFE", "0xFF",
+        "0xff", "0xFf", "0X82",
+        # odd, non-round bytes across the range
+        "0x7", "0x2a", "0x5b", "0x93", "0xa7", "0xd3",
+    ])
+    def test_valid_returned_unchanged(self, arg):
+        assert systemidtype(arg) == arg
+
+    # --- out of range -------------------------------------------------------
+
+    @pytest.mark.parametrize("arg", ["0x0", "0x00", "0x100", "0xFFFF"])
+    def test_out_of_range_rejected(self, arg):
+        with pytest.raises(ArgumentTypeError):
+            systemidtype(arg)
+
+    # --- must be an explicit 0x-prefixed hex string -------------------------
+
+    @pytest.mark.parametrize("arg", [
+        "82",        # no 0x prefix (would parse as hex 130)
+        "130",       # decimal that names a valid value
+        "+0x82",     # leading sign
+        "-0x82",
+        "0b1010",    # binary prefix
+    ])
+    def test_non_prefixed_forms_rejected(self, arg):
+        with pytest.raises(ArgumentTypeError):
+            systemidtype(arg)
+
+    @pytest.mark.parametrize("bad", [
+        "0x",              # prefix with no digit
+        "0xGG",            # invalid hex digit
+        "abc",             # not hex
+        "0x 82",           # embedded whitespace
+        " 0x82",           # leading whitespace
+        "",                # empty
+        "   ",             # whitespace only
+        "0x" + "F" * 10000,  # very long
+    ])
+    def test_invalid_format_rejected(self, bad):
+        with pytest.raises(ArgumentTypeError):
+            systemidtype(bad)
+
+    def test_none_rejected(self):
+        with pytest.raises((ArgumentTypeError, TypeError)):
+            systemidtype(None)
