diff --git a/.github/skills/code-standards/SKILL.md b/.github/skills/code-standards/SKILL.md index c81db641..c24d0ca2 100644 --- a/.github/skills/code-standards/SKILL.md +++ b/.github/skills/code-standards/SKILL.md @@ -98,6 +98,15 @@ from db_test_helpers import make_db, DummyDB, insert_device, minutes_ago If a helper you need doesn't exist yet, add it to `db_test_helpers.py` — not locally in the test file. +## Stubbing Modules in Standalone-Capable Tests + +If a test stubs NetAlertX modules into `sys.modules` so a script can be imported +outside the container (see `test/plugins/test_ntfy_custom_headers.py`), pop each +stubbed name back out of `sys.modules` right after the one-time import that needed +it. Otherwise the fake module leaks into every other test file collected in the +same pytest session and shadows the real module (see `testing-workflow` skill for +the full pattern and reproduction steps). + ## MAC Literals in Tests — ALWAYS Lowercase **MANDATORY:** Every MAC address literal used in test fixtures, parametrize decorators, assertions, or comments must be lowercase hex: diff --git a/.github/skills/testing-workflow/SKILL.md b/.github/skills/testing-workflow/SKILL.md index e369021d..bb2e942e 100644 --- a/.github/skills/testing-workflow/SKILL.md +++ b/.github/skills/testing-workflow/SKILL.md @@ -59,3 +59,49 @@ docker buildx build -t netalertx-test . ``` This takes ~30 seconds unless venv stage changes (~90s). + +## Pitfall: `sys.modules` Stubbing Leaks Across Test Files + +Some plugin tests (e.g. `test/plugins/test_ntfy_custom_headers.py`) stub NetAlertX +modules (`conf`, `helper`, `models.notification_instance`, etc.) via +`sys.modules[name] = fake_module` so the plugin script can be imported standalone, +outside the container. Because `sys.modules` is a single process-wide cache shared +by the whole pytest session, a fake module inserted by one test file silently +shadows the real module for every other test file collected afterwards — pytest +imports all test files during collection, before any test runs, so this can happen +regardless of alphabetical/directory order. + +Symptom: `AttributeError: does not have +the attribute 'get_setting_value'` (or similar) in an unrelated test file, where +the module repr has no `from ''` suffix — a giveaway that a stub, not the +real module, was resolved. + +Fix pattern: track which module names your stub actually inserted, and pop them +back out of `sys.modules` immediately after the one-time import that needed them +(the already-imported script keeps its bound names regardless): + +```python +_stubbed_module_names = [] + +def _stub(name, **attrs): + if name not in sys.modules: + mod = types.ModuleType(name) + for k, v in attrs.items(): + setattr(mod, k, v) + sys.modules[name] = mod + _stubbed_module_names.append(name) + +# ... _stub(...) calls, then the one-time import ... +import ntfy + +for _name in _stubbed_module_names: + sys.modules.pop(_name, None) +``` + +Reproduce cross-file pollution locally by running the suspect file together with +the affected one in a single pytest invocation (order matters less than you'd +think — collection happens for all files first): + +```bash +pytest test/plugins/test_ntfy_custom_headers.py test/backend/test_notification_templates.py -v +```