| name | test-file-split |
| audience | swarm-plugin |
| description | Protocol for splitting test files that exceed the FR-006 500-line hard limit. Covers describe-block extraction, shared helper management, pure-function extraction, mock isolation verification, and cascading-split detection. Load when a test file approaches or exceeds 500 lines. |
Test File Split Protocol (FR-006)
CI enforces a hard 500-line limit per test file (FR-006 / SC-006.1). Files exceeding this limit fail the quality gate and block PR merge. This skill covers the complete splitting protocol.
Read first: .opencode/skills/writing-tests/SKILL.md (or .claude/skills/writing-tests/SKILL.md) for bun:test framework rules, mock isolation patterns, and file placement conventions.
When to use this skill
- A test file exceeds or approaches 500 lines
- CI fails with an FR-006 file-size violation
- You are adding tests to a file that is already above 400 lines (proactive split)
Step 1 — Measure and identify split boundaries
wc -l tests/unit/scripts/my-module.test.ts
find tests/ -name "*.test.ts" -exec wc -l {} \; | sort -rn | awk '$1 > 400'
Identify natural describe() block boundaries. Group blocks by functional area:
- Each
describe() block should belong to exactly one split file
- Shared
beforeEach/afterEach hooks determine which blocks must stay together
Step 2 — Choose a suffix for the new file
| Pattern | Example | When to use |
|---|
<module>-<area>.test.ts | release-notes-fragments-sha.test.ts | Split by functional area (SHA resolution, validation, merge logic) |
<module>-<area>.adversarial.test.ts | auth-login.adversarial.test.ts | Split adversarial tests into their own file |
Step 3 — Manage shared imports and helpers
Three options, in order of preference:
-
Extract to shared utility (preferred for complex shared setup):
Create tests/helpers/<module>-shared.ts with shared fixtures, mock factories, and setup functions. Import from both split files.
-
Duplicate simple imports (for small overlap):
If only bun:test imports and 1-2 source imports are shared, duplicate them in both files. Simpler than a utility module for trivial cases.
-
Extract pure functions from source (for testability):
If the source module has inline validation logic, extract them as exported pure functions (e.g., isValidPrNumber, resolveAllCandidates) so both test files can target them independently. See the PR #1762 example below. See .opencode/skills/generated/safe-extraction/SKILL.md for the source extraction pattern.
Step 4 — Extract and move describe blocks
- Cut the selected
describe() blocks from the original file.
- Paste them into the new file.
- Add all necessary imports to the new file.
- Remove now-unused imports from the original file.
Step 5 — Verify both files
Line count check
wc -l tests/unit/scripts/my-module.test.ts tests/unit/scripts/my-module-sha.test.ts
Isolated run
bun --smol test tests/unit/scripts/my-module.test.ts --timeout 60000
bun --smol test tests/unit/scripts/my-module-sha.test.ts --timeout 60000
Co-run (mock isolation verification)
bun --smol test tests/unit/scripts/my-module*.test.ts --timeout 60000
If the co-run fails but isolated runs pass, check for mock.module() leakage. See .opencode/skills/writing-tests/SKILL.md → "Mock Isolation Rules" and the _internals DI seam pattern.
Step 6 — Evaluate _test_exports opportunity
After splitting, evaluate whether internal utility functions in the source module can be exported via _test_exports for zero-mock testing. This is a natural cleanup moment — the split already forces you to review test coverage boundaries.
Cascading split warning
If a previously split file exceeds 500 lines again, the test suite is structurally too large for a single module. Do not split a third time — reorganize the tests by source module boundaries instead. Repeated splitting produces fragmented test suites that are hard to navigate and maintain.
Real-world example (PR #1762 — pending merge)
Note: This example references functions and files from PR #1762's branch. These do not exist on main until the PR is merged. The pattern is still valid as an illustration of the splitting protocol.
tests/unit/scripts/release-notes-fragments.test.ts exceeded 500 lines. It was split into:
| File | Lines | Content |
|---|
release-notes-fragments.test.ts | 379 | Fragment collection, deduplication, output formatting |
release-notes-fragments-sha.test.ts | 261 | extractCommitShasFromBody, mergeCandidateLists, resolveAllCandidates, isValidPrNumber, stripCustomReleaseNotesBlock |
The split also extracted isValidPrNumber and resolveAllCandidates as pure exported functions from the source module, enabling independent testing.