diff --git a/require-ess/tools/driver.makefile b/require-ess/tools/driver.makefile
index c439a9b1760de3461db0707b263dbd8ea6b4f906..eb811fa269c9dc2e392ccd7f6af75037c85e4910 100644
--- a/require-ess/tools/driver.makefile
+++ b/require-ess/tools/driver.makefile
@@ -401,13 +401,14 @@ else
 
 ifeq ($(shell echo "${LIBVERSION}" | grep -v -E "^$(VERSIONREGEX)\$$"),)
 install:: build
-	@test ! -d ${MODULE_LOCATION}/lib/${T_A} || \
-        (echo -e "Error: ${MODULE_LOCATION}/lib/${T_A} already exists.\nNote: If you really want to overwrite then uninstall first."; false)
+	$(if $(wildcard ${MODULE_LOCATION}/lib/${T_A}),$(error ${MODULE_LOCATION}/lib/${T_A} already exists. If you really want to overwrite then uninstall first.))
+
 else
 install:: build
-	@test ! -d ${MODULE_LOCATION}/lib/${T_A} || \
-        (echo -e "Warning: Re-installing ${MODULE_LOCATION}/lib/${T_A}"; \
-        $(RMDIR) ${MODULE_LOCATION}/lib/${T_A})
+	$(if $(wildcard ${MODULE_LOCATION}/lib/${T_A}),\
+      $(warning Re-installing ${MODULE_LOCATION}/lib/${T_A})\
+      $(RMDIR) ${MODULE_LOCATION}/lib/${T_A}\
+    )
 endif
 
 install build debug:: O.${EPICSVERSION}_${T_A}
diff --git a/tests/conftest.py b/tests/conftest.py
index 7fdf70eba5fdda19367bfd27456624e5dca2b40c..4ff2cb9a5e54a8a6cc5894c115f41864a8fa7b9d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,11 +10,27 @@ from git import Repo
 
 class Wrapper:
     def __init__(self, root_path: Path, name=None, include_dbd=True, **kwargs):
+        test_env = os.environ.copy()
+
+        assert "EPICS_BASE" in test_env
+        assert "E3_REQUIRE_VERSION" in test_env
+
+        e3_require_config = (
+            Path(os.environ.get("EPICS_BASE"))
+            / "require"
+            / os.environ.get("E3_REQUIRE_VERSION")
+            / "configure"
+        )
+
+        assert e3_require_config.is_dir()
+
         if name is None:
             name = "test_mod_" + "".join(choice(ascii_lowercase) for _ in range(16))
         self.path = root_path / f"e3-{name}"
         self.name = name
 
+        self.version = "0.0.0+0"
+
         module_path = (
             name if "E3_MODULE_SRC_PATH" not in kwargs else kwargs["E3_MODULE_SRC_PATH"]
         )
@@ -24,33 +40,36 @@ class Wrapper:
         self.config_dir = self.path / "configure"
         self.config_dir.mkdir()
 
-        self.config_module = self.path / "CONFIG_MODULE"
+        self.config_module = self.config_dir / "CONFIG_MODULE"
         self.config_module.touch()
 
-        self.makefile = self.path / f"{name}.Makefile"
-
+        self.makefile = self.path / "Makefile"
         makefile_contents = f"""
 TOP:=$(CURDIR)
 
 E3_MODULE_NAME:={name}
-E3_MODULE_VERSION?=0.0.0+0
+E3_MODULE_VERSION:={self.version}
 E3_MODULE_SRC_PATH:={module_path}
 E3_MODULE_MAKEFILE:={name}.Makefile
 
-include $(TOP)/CONFIG_MODULE
+include $(TOP)/configure/CONFIG_MODULE
+-include $(TOP)/configure/CONFIG_MODULE.local
+
+REQUIRE_CONFIG:={e3_require_config}
 
 include $(REQUIRE_CONFIG)/CONFIG
 include $(REQUIRE_CONFIG)/RULES_SITEMODS
 """
-        with open(self.path / "Makefile", "w") as f:
-            f.write(makefile_contents)
+        (self.makefile).write_text(makefile_contents)
 
+        self.module_makefile = self.path / f"{name}.Makefile"
         module_makefile_contents = """
 where_am_I := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
 include $(E3_REQUIRE_TOOLS)/driver.makefile
+
+EXCLUDE_ARCHS+=debug
 """
-        with open(self.path / f"{name}.Makefile", "w") as f:
-            f.write(module_makefile_contents)
+        (self.module_makefile).write_text(module_makefile_contents)
 
         if include_dbd:
             self.add_file("test.dbd")
@@ -71,8 +90,8 @@ include $(E3_REQUIRE_TOOLS)/driver.makefile
         with open(self.config_module, "a") as f:
             f.write(f"export {makefile_var} {modifier}= {value}\n")
 
-    def add_var_to_makefile(self, makefile_var: str, value: str, modifier="+"):
-        with open(self.makefile, "a") as f:
+    def add_var_to_module_makefile(self, makefile_var: str, value: str, modifier="+"):
+        with open(self.module_makefile, "a") as f:
             f.write(f"{makefile_var} {modifier}= {value}\n")
 
     def get_env_var(self, var: str):
@@ -88,42 +107,28 @@ include $(E3_REQUIRE_TOOLS)/driver.makefile
         self,
         target: str,
         *args,
-        module_name: str = "",
         module_version: str = "",
         cell_path: str = "",
     ):
         """Attempt to run `make <target> <args>` for the current wrapper."""
 
-        test_env = os.environ.copy()
-
-        assert "EPICS_BASE" in test_env
-        assert Path(test_env["EPICS_BASE"]).is_dir
-        assert "E3_REQUIRE_VERSION" in test_env
-        assert test_env["E3_REQUIRE_VERSION"]
-
-        e3_require_location = (
-            Path(os.environ.get("EPICS_BASE"))
-            / "require"
-            / os.environ.get("E3_REQUIRE_VERSION")
-        )
-        assert e3_require_location.is_dir()
-        test_env["E3_REQUIRE_LOCATION"] = e3_require_location
+        # We should not install in the global environment during tests
+        if target == "install":
+            target = "cellinstall"
 
-        require_config = f"{e3_require_location}/configure"
-        test_env["REQUIRE_CONFIG"] = require_config
+        if target == "uninstall":
+            target = "celluninstall"
 
-        if module_name:
-            test_env["E3_MODULE_NAME"] = module_name
+        args = list(args)
         if module_version:
-            test_env["E3_MODULE_VERSION"] = module_version
+            args.append(f"E3_MODULE_VERSION={module_version}")
         if cell_path:
             self.write_dot_local_data("CONFIG_CELL", {"E3_CELL_PATH": cell_path})
-        make_cmd = ["make", "-C", self.path, target] + list(args)
+        make_cmd = ["make", "-C", self.path, target] + args
         p = subprocess.Popen(
             make_cmd,
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
-            env=test_env,
             encoding="utf-8",
         )
         outs, errs = p.communicate()
diff --git a/tests/test_build.py b/tests/test_build.py
index fa12115a7b65f6d66359dbc6459b3a3d6dc2b400..8f8f774ff54933965aaca4f123a0156cd5452416 100644
--- a/tests/test_build.py
+++ b/tests/test_build.py
@@ -4,13 +4,12 @@ from pathlib import Path
 
 import pytest
 
+from .conftest import Wrapper
 from .utils import run_ioc_get_output
 
-MODULE_VERSION = "0.0.0+0"
-MODULE_VERSION_NO_BUILD = "0.0.0"
-
 RE_MISSING_FILE = "No rule to make target [`']{filename}'"
 RE_MISSING_VERSION = "Module '{module}' version '{version}' does not exist."
+RE_MODULE_VERSION_EXISTS = "Error .*{module}/{version}.* already exists"
 
 
 def create_patch_file(path, desc):
@@ -30,7 +29,7 @@ index 1806ff6..8701832 100644
         f.write(patch_file_contents.format(desc=desc))
 
 
-def test_patch(wrapper):
+def test_patch(wrapper: Wrapper):
     db_path = wrapper.module_dir / "database.db"
     db_file_contents = """record(ai, "TEST") {
 
@@ -41,35 +40,32 @@ def test_patch(wrapper):
 
     patch_dir = wrapper.path / "patch"
     create_patch_file(patch_dir / "apply.p0.patch", "OK")
-    create_patch_file(patch_dir / MODULE_VERSION / "do-not-apply.p0.patch", "Bad")
+    create_patch_file(patch_dir / wrapper.version / "do-not-apply.p0.patch", "Bad")
 
-    rc, _, _ = wrapper.run_make("patch", module_version=MODULE_VERSION)
+    rc, _, _ = wrapper.run_make("patch")
     assert rc == 0
     with open(db_path, "r") as f:
         db_contents = f.read()
     assert 'field(DESC, "OK")' in db_contents
     assert "Bad" not in db_contents
 
-    rc, _, _ = wrapper.run_make("build", module_version=MODULE_VERSION)
+    rc, _, _ = wrapper.run_make("build")
     assert rc == 0
     assert any((wrapper.module_dir).glob("O.*"))
 
-    rc, _, _ = wrapper.run_make("cellinstall", module_version=MODULE_VERSION)
+    rc, _, _ = wrapper.run_make("cellinstall")
     assert rc == 0
     assert any((wrapper.path / "cellMods").glob("**/*.db"))
 
 
-def test_local_module(wrapper):
-    rc, outs, _ = wrapper.run_make(
-        "init",
-        module_version=MODULE_VERSION,
-    )
+def test_local_module(wrapper: Wrapper):
+    rc, outs, _ = wrapper.run_make("init")
     assert rc == 0
     assert "You are in the local source mode" in outs
 
 
-def test_missing_dbd_file(wrapper):
-    wrapper.add_var_to_makefile("DBDS", "nonexistent.dbd")
+def test_missing_dbd_file(wrapper: Wrapper):
+    wrapper.add_var_to_module_makefile("DBDS", "nonexistent.dbd")
     rc, _, errs = wrapper.run_make("build")
 
     assert rc == 2
@@ -79,8 +75,8 @@ def test_missing_dbd_file(wrapper):
     )
 
 
-def test_missing_source_file(wrapper):
-    wrapper.add_var_to_makefile("SOURCES", "nonexistent.c")
+def test_missing_source_file(wrapper: Wrapper):
+    wrapper.add_var_to_module_makefile("SOURCES", "nonexistent.c")
     rc, _, errs = wrapper.run_make("build")
 
     assert rc == 2
@@ -90,7 +86,7 @@ def test_missing_source_file(wrapper):
     )
 
 
-def test_missing_requirement(wrapper):
+def test_missing_requirement(wrapper: Wrapper):
     wrapper.add_var_to_config_module("FOO_DEP_VERSION", "bar")
 
     rc, _, errs = wrapper.run_make("build")
@@ -119,14 +115,14 @@ def test_missing_dependent_version(wrappers):
     )
 
 
-def test_header_install_location(wrapper):
+def test_header_install_location(wrapper: Wrapper):
     subdir = wrapper.module_dir / "db" / "subdir"
     subdir.mkdir(parents=True)
 
     extensions = ["h", "hpp", "hxx", "hh"]
     for ext in extensions:
-        wrapper.add_var_to_makefile("HEADERS", f"db/subdir/header.{ext}")
-    wrapper.add_var_to_makefile("KEEP_HEADER_SUBDIRS", "db")
+        wrapper.add_var_to_module_makefile("HEADERS", f"db/subdir/header.{ext}")
+    wrapper.add_var_to_module_makefile("KEEP_HEADER_SUBDIRS", "db")
 
     for ext in extensions:
         (subdir / f"header.{ext}").touch()
@@ -444,10 +440,10 @@ def test_recursive_header_include(wrappers):
     wrapper_b.add_var_to_config_module(f"{wrapper_c.name}_DEP_VERSION", module_version)
     wrapper_a.add_var_to_config_module(f"{wrapper_b.name}_DEP_VERSION", module_version)
 
-    wrapper_c.add_var_to_makefile("HEADERS", f"{wrapper_c.name}.h")
+    wrapper_c.add_var_to_module_makefile("HEADERS", f"{wrapper_c.name}.h")
     (wrapper_c.module_dir / f"{wrapper_c.name}.h").touch()
 
-    wrapper_a.add_var_to_makefile("SOURCES", f"{wrapper_a.name}.c")
+    wrapper_a.add_var_to_module_makefile("SOURCES", f"{wrapper_a.name}.c")
     with open(wrapper_a.module_dir / f"{wrapper_a.name}.c", "w") as f:
         f.write(f'#include "{wrapper_c.name}.h"')
 
@@ -471,8 +467,8 @@ def test_recursive_header_include(wrappers):
     assert f"Loaded {wrapper_c.name} version {module_version}" in outs
 
 
-def test_updated_template_files(wrapper):
-    wrapper.add_var_to_makefile("SUBS", "x.substitutions")
+def test_updated_template_files(wrapper: Wrapper):
+    wrapper.add_var_to_module_makefile("SUBS", "x.substitutions")
 
     substitution_file = wrapper.module_dir / "x.substitutions"
     substitution_file.write_text("file x.template {pattern {x} {y}}")
@@ -494,12 +490,12 @@ def test_updated_template_files(wrapper):
     assert db_file.read_text() == "record(ai, updated) {}"
 
 
-def test_expand_db_files(wrapper):
+def test_expand_db_files(wrapper: Wrapper):
     """Test that the automated template/substitution file expansion works."""
 
-    wrapper.add_var_to_makefile("TMPS", "templates/a.template")
-    wrapper.add_var_to_makefile("SUBS", "b.substitutions")
-    wrapper.add_var_to_makefile("USR_DBFLAGS", "-I templates")
+    wrapper.add_var_to_module_makefile("TMPS", "templates/a.template")
+    wrapper.add_var_to_module_makefile("SUBS", "b.substitutions")
+    wrapper.add_var_to_module_makefile("USR_DBFLAGS", "-I templates")
 
     template_dir = wrapper.module_dir / "templates"
     template_dir.mkdir()
@@ -540,10 +536,10 @@ def test_expand_db_files(wrapper):
         ("foo-bar foo-baz baz baz-qux", "EXCLUDE_ARCHS=foo", ["baz", "baz-qux"]),
     ],
 )
-def test_arch_filter(wrapper, installed_archs, param, expected):
+def test_arch_filter(wrapper: Wrapper, installed_archs, param, expected):
     arch_regex = re.compile(r"Pass 2: T_A =\s*([^\s]+)")
 
-    wrapper.add_var_to_makefile(
+    wrapper.add_var_to_module_makefile(
         "CROSS_COMPILER_TARGET_ARCHS", installed_archs, modifier=""
     )
 
@@ -558,12 +554,12 @@ def test_arch_filter(wrapper, installed_archs, param, expected):
 
 
 @pytest.mark.parametrize("archs,failing_arch", [("foo bar", "foo"), ("foo bar", "bar")])
-def test_build_fails_if_nth_architecture_fails(wrapper, archs, failing_arch):
+def test_build_fails_if_nth_architecture_fails(wrapper: Wrapper, archs, failing_arch):
     # LIBOBJS is determined in part based on configuration data coming from
     # $(CONFIG)/os/CONFIG.Common.$(T_A); since our architectures do not actually
     # exist, we need to manually define these /before/ driver.makefile is included.
-    makefile_content = wrapper.makefile.read_text()
-    with open(wrapper.makefile, "w") as f:
+    makefile_content = wrapper.module_makefile.read_text()
+    with open(wrapper.module_makefile, "w") as f:
         f.write(
             f"""ifeq ($(T_A),{failing_arch})
                 LIBOBJS = nonexistent_{failing_arch}.o
@@ -576,10 +572,12 @@ def test_build_fails_if_nth_architecture_fails(wrapper, archs, failing_arch):
 
     # Skip the host architecture, we are not testing it.
     host_arch = os.getenv("EPICS_HOST_ARCH")
-    wrapper.add_var_to_makefile("EXCLUDE_ARCHS", host_arch)
+    wrapper.add_var_to_module_makefile("EXCLUDE_ARCHS", host_arch)
 
-    wrapper.add_var_to_makefile("CROSS_COMPILER_TARGET_ARCHS", archs, modifier="")
-    wrapper.add_var_to_makefile("SOURCES", "-none-")
+    wrapper.add_var_to_module_makefile(
+        "CROSS_COMPILER_TARGET_ARCHS", archs, modifier=""
+    )
+    wrapper.add_var_to_module_makefile("SOURCES", "-none-")
 
     rc, _, errs = wrapper.run_make("build")
     assert rc == 2
@@ -587,3 +585,52 @@ def test_build_fails_if_nth_architecture_fails(wrapper, archs, failing_arch):
         RE_MISSING_FILE.format(filename=re.escape(f"nonexistent_{failing_arch}.o")),
         errs,
     )
+
+
+def test_double_install_fails(wrapper: Wrapper):
+    RE_ERR_MOD_VER_EXISTS = ".*{module}/{version}.* already exists"
+
+    rc, *_ = wrapper.run_make("install")
+    assert rc == 0
+
+    rc, _, errs = wrapper.run_make("install")
+    assert rc == 2
+    assert re.search(
+        RE_ERR_MOD_VER_EXISTS.format(
+            module=re.escape(wrapper.name), version=re.escape(wrapper.version)
+        ),
+        errs,
+    )
+
+
+def test_double_install_test_version_succeeds(wrapper: Wrapper):
+    RE_WARN_MOD_VER_EXISTS = "Re-installing .*{module}/{version}.*"
+
+    test_version = "test"
+    wrapper.write_dot_local_data("CONFIG_MODULE", {"E3_MODULE_VERSION": test_version})
+    cell_path = wrapper.get_env_var("E3_MODULES_INSTALL_LOCATION")
+
+    wrapper.add_file("header.h")
+
+    rc, _, errs = wrapper.run_make("install")
+    assert rc == 0
+    assert not re.search(
+        RE_WARN_MOD_VER_EXISTS.format(
+            module=re.escape(wrapper.name), version=test_version
+        ),
+        errs,
+    )
+
+    assert not (Path(cell_path) / "include" / "header.h").is_file()
+
+    wrapper.add_var_to_module_makefile("HEADERS", "header.h")
+
+    rc, _, errs = wrapper.run_make("install")
+    assert rc == 0
+    assert re.search(
+        RE_WARN_MOD_VER_EXISTS.format(
+            module=re.escape(wrapper.name), version=test_version
+        ),
+        errs,
+    )
+    assert (Path(cell_path) / "include" / "header.h").is_file()
diff --git a/tests/test_e3.py b/tests/test_e3.py
index 4918681dd553c2a434711cadcc7c2e57027d7d40..b14024309428de1c59568b67a0de119e77f6866c 100644
--- a/tests/test_e3.py
+++ b/tests/test_e3.py
@@ -1,3 +1,6 @@
+from .conftest import Wrapper
+
+
 def test_loc(wrappers):
     wrapper = wrappers.get(E3_MODULE_SRC_PATH="test-loc")
     rc, _, errs = wrapper.run_make("build")
@@ -5,28 +8,16 @@ def test_loc(wrappers):
     assert 'Local source mode "-loc" has been deprecated' in errs
 
 
-def test_sitelibs(wrappers):
-    # Overwrite the default makefile
-    wrapper = wrappers.get()
-    with open(wrapper.path / "Makefile", "w") as f:
-        f.write(
-            f"""
-TOP:=$(CURDIR)
-
-E3_MODULE_NAME:={wrapper.name}
-
-include $(REQUIRE_CONFIG)/RULES_E3
-include $(REQUIRE_CONFIG)/RULES_CELL
-include $(REQUIRE_CONFIG)/DEFINES_FT
-include $(REQUIRE_CONFIG)/RULES_PATCH
-include $(REQUIRE_CONFIG)/RULES_TEST
-
-include $(REQUIRE_CONFIG)/RULES_DKMS
-include $(REQUIRE_CONFIG)/RULES_VARS
+def test_sitelibs(wrapper: Wrapper):
+    makefile_contents = wrapper.makefile.read_text()
 
-include $(REQUIRE_CONFIG)/RULES_DEV
-"""
+    wrapper.makefile.write_text(
+        makefile_contents.replace(
+            "include $(REQUIRE_CONFIG)/RULES_SITEMODS",
+            "include $(REQUIRE_CONFIG)/RULES_E3",
         )
+    )
+
     rc, _, errs = wrapper.run_make("build")
     assert rc == 2
     assert "RULES_E3 should only be loaded from RULES_SITEMODS" in errs
@@ -36,6 +27,15 @@ def test_incorrect_module_name(wrappers):
     module_name = "ADCore"
 
     wrapper = wrappers.get(name=module_name)
-    rc, _, errs = wrapper.run_make("build", module_name=module_name)
+    rc, _, errs = wrapper.run_make("build")
     assert rc == 2
     assert f"E3_MODULE_NAME '{module_name}' is not valid" in errs
+
+
+def test_add_missing_build_number(wrapper: Wrapper):
+    version = "1.2.3"
+    wrapper.write_dot_local_data("CONFIG_MODULE", {"E3_MODULE_VERSION": version})
+
+    rc, outs, _ = wrapper.run_make("vars")
+    assert rc == 0
+    assert f"E3_MODULE_VERSION = {version}+0" in outs.splitlines()
diff --git a/tests/test_expand_dbd.py b/tests/test_expand_dbd.py
new file mode 100644
index 0000000000000000000000000000000000000000..eae44dee4933da57f30e07c7f2130ff15f553cc5
--- /dev/null
+++ b/tests/test_expand_dbd.py
@@ -0,0 +1,60 @@
+import os
+import subprocess
+from pathlib import Path
+
+import pytest
+
+
+@pytest.fixture
+def expanddbdtcl():
+    require_path = Path(os.environ.get("E3_REQUIRE_LOCATION"))
+    expanddbdtcl = require_path / "tools" / "expandDBD.tcl"
+    assert expanddbdtcl.is_file() and os.access(expanddbdtcl, os.X_OK)
+    return expanddbdtcl
+
+
+def test_missing_dbd_file(tmpdir, expanddbdtcl):
+    dbd_file = Path(tmpdir) / "tmp.dbd"
+    dbd_file.write_text("include not_a_file.dbd")
+    result = subprocess.run(
+        [expanddbdtcl, dbd_file],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        encoding="utf-8",
+    )
+    assert result.returncode == 1
+    assert "file not_a_file.dbd not found" in result.stderr
+
+
+def test_include_dbd_file(tmpdir, expanddbdtcl):
+    dbd_a = Path(tmpdir) / "a.dbd"
+    dbd_a.write_text("include b.dbd")
+
+    dbd_b = Path(tmpdir) / "b.dbd"
+    dbd_b.write_text("content")
+
+    result = subprocess.run(
+        [expanddbdtcl, "-I", str(tmpdir), dbd_a],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        encoding="utf-8",
+    )
+    assert result.returncode == 0
+    assert "content" == result.stdout.strip("\n")
+
+
+def test_skip_repeated_includes(tmpdir, expanddbdtcl):
+    dbd_a = Path(tmpdir) / "a.dbd"
+    dbd_a.write_text("include b.dbd\ninclude b.dbd")
+
+    dbd_b = Path(tmpdir) / "b.dbd"
+    dbd_b.touch()
+
+    result = subprocess.run(
+        [expanddbdtcl, "-I", str(tmpdir), dbd_a],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        encoding="utf-8",
+    )
+    assert result.returncode == 0
+    assert "Info: skipping duplicate file b.dbd included from" in result.stderr
diff --git a/tests/test_fixture.py b/tests/test_fixture.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e61921b3e3c4772f1a4184a2d52ec48f4be77cb
--- /dev/null
+++ b/tests/test_fixture.py
@@ -0,0 +1,8 @@
+import subprocess
+
+from .conftest import Wrapper
+
+
+def test_can_run_make_in_wrapper_directory(wrapper: Wrapper):
+    results = subprocess.run(["make", "-C", wrapper.path, "vars"])
+    assert results.returncode == 0
diff --git a/tests/test_versions.py b/tests/test_versions.py
index 22522b62944c9ccb0429732440684c1e603fb81c..dcec1f4e7f2b638b0767f8dfd6639033dcaeb819 100644
--- a/tests/test_versions.py
+++ b/tests/test_versions.py
@@ -2,6 +2,7 @@ import re
 
 import pytest
 
+from .conftest import Wrapper
 from .utils import run_ioc_get_output
 
 RE_MODULE_LOADED = "Loaded {module} version {version}"
@@ -14,27 +15,27 @@ RE_MODULE_NOT_LOADED = "Module {module} (not available|version {required} not av
         # If nothing is installed, nothing should be loaded
         ("", "", []),
         # Test versions can be loaded
-        ("test", "test", ["test", "0.0.1"]),
+        ("test", "test", ["test", "0.0.1+0"]),
         # Numeric versions should be prioritized over test versions
-        ("", "0.0.1+0", ["test", "0.0.1"]),
-        ("", "0.0.1+0", ["0.0.1", "test"]),
+        ("", "0.0.1+0", ["test", "0.0.1+0"]),
+        ("", "0.0.1+0", ["0.0.1+0", "test"]),
         # Highest build number should be loaded if version is unspecified
-        ("", "0.0.1+7", ["0.0.1", "0.0.1+7", "0.0.1+3"]),
+        ("", "0.0.1+7", ["0.0.1+0", "0.0.1+7", "0.0.1+3"]),
         # Only load the given build number if it is specified
         ("0.0.1+0", "", ["0.0.1+1"]),
         # If no build number is specified, load the highest build number
-        ("0.0.1", "0.0.1+4", ["0.1.0", "0.0.1+4", "0.0.1"]),
+        ("0.0.1", "0.0.1+4", ["0.1.0+0", "0.0.1+4", "0.0.1+0"]),
         # Build number 0 means load that version exactly
         ("0.0.1+0", "0.0.1+0", ["0.0.1+0"]),
-        ("0.0.1+0", "0.0.1+0", ["0.0.1", "0.0.1+1", "1.0.0"]),
+        ("0.0.1+0", "0.0.1+0", ["0.0.1+0", "0.0.1+1", "1.0.0+0"]),
         # 1-test counts as a test version, as does 1.0
-        ("", "0.0.1+0", ["0.0.1", "1-test"]),
-        ("", "0.0.1+0", ["0.0.1", "1.0"]),
+        ("", "0.0.1+0", ["0.0.1+0", "1-test"]),
+        ("", "0.0.1+0", ["0.0.1+0", "1.0"]),
         # Numeric version should be prioritised over "higher" test version
-        ("", "0.1.0+0", ["0.1.0", "1.0.0-rc1"]),
+        ("", "0.1.0+0", ["0.1.0+0", "1.0.0-rc1"]),
     ],
 )
-def test_version(wrapper, requested, expected, installed):
+def test_version(wrapper: Wrapper, requested, expected, installed):
     for version in installed:
         returncode, _, _ = wrapper.run_make(
             "clean",