--- name: polylith-migrate-refactor-tests description: "[Internal sub-skill of `polylith-migrate-orchestrator`. Do not load directly — load `polylith-migrate-orchestrator` first, which drives all phases.] Restructure unit tests to align with the workspace's Polylith theme." --- # Skill: polylith-migrate-refactor-tests ## Goal Restructure unit tests so they live next to the brick they test, following the workspace's Polylith theme (`loose` or `tdd`). ## Scope - **Unit tests only.** Integration tests typically stay in a shared location (e.g., `test/integration/` or `test//integration/`). Do not redistribute them across bricks. - **Structure only.** Reorganize test files and update imports/mock patch strings. Do not rewrite test logic or fixtures unless an import/path change makes it strictly necessary. ## Shared test helpers & the namespace-merge hazard (read before moving) Two real traps when moving unit tests next to their bricks: 1. **Shared test-support packages stop resolving.** If tests import shared helpers/fixtures by package (e.g. `from _service.helpers import …`, `from _service.fixtures import …`), those resolve today only because the service test dir sits on `sys.path` (pytest inserts the topmost non-package dir). Once a test moves to `test/components///` (no `__init__.py`), pytest inserts the **leaf** test dir instead, and the shared import breaks. 2. **Do NOT "fix" it by adding `test/` to `pythonpath`.** That makes `test/<…>///` directories **merge into the real `` namespace** and collide with `components//` / `bases//` — an ambiguous, hard-to-debug namespace package. ### Two sanctioned layouts — pick one - **Per-brick (the target table below).** Use it **after** making shared helpers reachable *without* putting `test/` on the path: convert them to `conftest.py` **fixtures** (pytest injects fixtures with no import), or move pure helper functions to an on-path support location. Unit tests then live under `test////`. - **Workspace-level service dir.** Keep the project's tests under a single `test/_service/` directory (the importable, valid-package name that `polylith-migrate-prepare-project` created). Shared helpers stay co-located and importable. Choose this when converting helpers to fixtures isn't worth it; record the choice in `state.md`. This still satisfies "tests at workspace level". If unsure, the **workspace-level service dir is the lower-risk default**. ## Inputs From `migration//state.md`: - `TARGET_TOP_NS` - `RUN_TEST_CMD` (will be updated by this skill) - `RUN_LINT_CMD`, `RUN_TYPECHECK_CMD` (optional) From `migration//manifest.md`: - List of all bricks (bases and components) with their module maps. From `workspace.toml`: - `[tool.polylith.structure].theme` (`loose` or `tdd`). From `test/`: - Current test directory structure. > All inputs from `state.md` are assumed to satisfy the validation rules in `polylith-migrate-discover` (`### Validation rules`). Validate before proceeding. ## Target layout | Theme | Test path for a base | Test path for a component | |----------|------------------------|-----------------------------| | `loose` | `test/bases///test_*.py` | `test/components///test_*.py` | | `tdd` | `bases//test///test_*.py` | `components//test///test_*.py` | ## Steps ### 1. Classify each unit test file For each `test_*.py` under the current test root: 1. Read its `import` statements and `mock.patch("...")` strings. 2. Identify the *primary* brick under test — usually the one most imported or the one mock-patched. Tie-break by file name (`test_user_handler.py` → ``). 3. Record the classification in a table you keep in scratch (do **not** commit a separate file for this — it's transient): | Test file | Primary brick | Target path | |-----------|---------------|-------------| If a test exercises 2+ bricks at integration level, classify it as integration and move it to `test/integration/` instead. ### 2. Move test files - Create the target directories per the theme matrix above. - Move each `test_*.py` to its brick's test directory. - For each `conftest.py`: - **Brick-scoped fixtures** (referenced only by tests under one brick) → move to that brick's test directory. - **Workspace-shared fixtures** (cross-brick) → keep one `test//conftest.py` or `test/conftest.py`. ### 3. Update imports and mock patch strings - Update any `from tests.` imports that survived to the new layout. - Update `mock.patch("")` strings. Brick reshuffling earlier in the migration may have changed where the patched symbol now lives — use `grep` to locate the target symbol's new path and align the patch string. Patching a wrong path silently succeeds and the test will pass for the wrong reason — verify by intentionally breaking the target function and checking that the test fails. ### 4. Update `RUN_TEST_CMD` - Set `RUN_TEST_CMD` in `state.md` to a command that collects from the **new** test root (typically ` pytest test/` for `loose`, or per-brick collection for `tdd`). - Verify the test count after the move equals the baseline recorded in `polylith-migrate-discover`. **A drop in collected tests means files were lost or pytest discovery is misconfigured.** ## Verify - `RUN_TEST_CMD` succeeds and **collects the same number of tests** as the baseline from `polylith-migrate-discover`. - If set, `RUN_LINT_CMD` succeeds. - `POLY_CMD_PREFIX check` is still green. ## Common failure modes | Symptom | Likely cause | Remediation | |---------|--------------|-------------| | `pytest` collects fewer tests than before | The old test root is no longer on `pytest`'s path; or duplicate `conftest.py` files silently shadow each other. | Update `[tool.pytest.ini_options].testpaths` (or pass paths explicitly in `RUN_TEST_CMD`). Run `pytest --collect-only` and diff against baseline collection. | | `fixture '' not found` | The fixture was in a `conftest.py` you moved into a brick's test dir; tests in another brick can no longer see it. | Either move the fixture up to a higher-scope `conftest.py`, or duplicate it (only if cheap). Don't import fixtures across `conftest.py` files. | | Tests pass but assert nothing useful (`mock.patch` no longer hits anything) | Patch string still points at the pre-migration module path. | Re-derive the patch path: `...`. Validate by deliberately breaking the patched function and confirming the test fails. | | `ImportError: cannot import name 'fixture_'` from `conftest.py` | A `conftest.py` `import`s a moved test helper module that didn't follow it. | Move the helper next to the new `conftest.py`, or import it from its new brick path. | | Tests for moved code suddenly find themselves under a brick name that doesn't match their content | Misclassification in step 1. | Re-read the test's imports — the brick most imported is the one that owns the test. Move and update. | | `ModuleNotFoundError: No module named '_service'` after moving a test to `test////` | Shared test-support package is no longer on `sys.path`. | Use a sanctioned layout (above): convert the helpers to conftest fixtures, or keep tests in the workspace-level `_service/` dir. Do **not** add `test/` to `pythonpath` (namespace merge with the real bricks). | | Verification fails and you can't quickly diagnose | Phase commit not yet made. | `git reset --hard HEAD` to roll back to the previous phase's commit and consult the user. | ## Commit After verification passes, commit this phase to the migration branch: ```bash git add -A && git commit -m "migrate(): phase — refactor-tests" ``` Substitute ``, ``, and `` from `state.md` and the orchestrator's phase table. Do not proceed to the next phase without a clean commit — the per-phase commit is the rollback point for the next phase's failure-mode tables.