Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
codeplain-ai avatar

Implement Unit Testing Script

  • 39 installs
  • 54 repo stars
  • Updated July 22, 2026
  • codeplain-ai/plain-forge

Generate a Bash or PowerShell unit-test runner script for a new language in a ***plain project, following the reference pattern.

About

Generates a language-agnostic unit-test runner script (Bash or PowerShell) for a ***plain build folder. A developer uses it to add a unit-testing script for a new language to a ***plain project.

  • Generates a Bash or PowerShell unit-test runner per language
  • Follows the same seven-step pattern as the bundled reference scripts

Implement Unit Testing Script by the numbers

  • 39 all-time installs (skills.sh)
  • Ranked #1,290 of 2,153 Testing & QA skills by installs in the Skillselion catalog
  • Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/codeplain-ai/plain-forge --skill implement-unit-testing-script

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs39
repo stars54
Last updatedJuly 22, 2026
Repositorycodeplain-ai/plain-forge

What it does

Generate a Bash or PowerShell unit-test runner script for a new language in a ***plain project, following the reference pattern.

Files

SKILL.mdMarkdownGitHub ↗

Implement Unit Testing Script

This skill produces a single executable script that runs the unit tests for a generated build folder, following a consistent, language-agnostic pattern.

The reference implementation is assets/run_unittests_java.sh. Read it first — every script you produce must be a faithful translation of that pattern into the target language's tooling and the user's shell environment. There are also Windows PowerShell equivalents of these scripts in assets/run_unittests_*.ps1.

Pick the Shell First

Before writing anything, decide which shell flavor the script must target — it depends on the user's environment, not on the language:

  • Bash (`.sh`) — macOS, Linux, WSL, CI runners on Linux. Default unless the user is on native Windows.
  • PowerShell (`.ps1`) — native Windows / PowerShell-only environments.

If you can't tell from the project (no obvious OS hints, no existing scripts), ask the user.

The same seven-step pattern applies to both. Only the syntax changes.

The Pattern

Every testing script must implement these steps in this order:

1. Toolchain check. Verify that the required language runtime / build tool (and the required version, if any) is installed. If not, print an error and exit with code 69. 2. Argument validation. Require exactly one positional argument: the source build folder name. If missing, print usage and exit with code 1. 3. Working directory setup. Define a working folder at .tmp/<lang>_<arg>. If it exists, wipe its contents; otherwise create it. This folder — and only this folder — is where every subsequent write must land. 4. Copy the build. Recursively copy everything from the source folder into the working folder. After this step the source folder ($1) is treated as read-only for the rest of the script. 5. Enter the working directory. cd / Set-Location into .tmp/<lang>_<arg>. If that fails, exit with code 2. All remaining steps run from inside the working folder; they must never write back to the source build folder. 6. Install dependencies into an isolated environment inside `.tmp/<lang>_<arg>`. Set up a per-working-folder dependency location (a Python venv at ./.venv, a local ./node_modules, a project-scoped Maven repo at ./.m2, etc.) and install/resolve all dependencies into it. Never install into the source build folder, the user's global cache (~/.m2, system-wide pip, ~/.cargo, ~/.npm, ...), or anywhere outside .tmp/<lang>_<arg>. If the install command fails, propagate its exit code immediately and do not proceed to step 7. See Dependency isolation for per-language specifics. 7. Run the tests. Invoke the language's standard test command (e.g. mvn test, pytest, npm test, go test ./..., cargo test), pointed at the same isolated environment from step 6. The script's final exit code is whatever the test command returns.

The build folder is read-only — hard rule

The source build folder passed in as $1 is input only. The script must never:

  • install dependencies into it (no pip install inside $1, no npm install inside $1, no mvn install writing into $1, no Cargo build artifacts ending up under $1),
  • write a virtualenv / node_modules / .m2 / .gocache / .cargo directory inside it,
  • run the test command from inside it (every test command runs from inside .tmp/<lang>_<arg> after the cd in step 5),
  • create logs, caches, build outputs, or temp files inside it.

The build folder is shared with the renderer (plain_modules/... by default) and downstream tooling. Writing into it corrupts the renderer's view of "what was generated" and breaks subsequent renders. Every write must go into .tmp/<lang>_<arg> — the whole point of staging via .tmp is so the source build folder stays a clean, reproducible artifact of the render.

If you find yourself about to issue any command whose cwd is the source folder, or whose target path starts with $1/, stop. Either move the operation into .tmp/<lang>_<arg>, or you're doing something the script must not do.

Conventions

Shared across both shell flavors:

  • Exit codes:
  • 1 — bad usage (missing argument).
  • 2 — filesystem problem (couldn't enter the working folder).
  • 69 — required toolchain / runtime is not installed.
  • Any other non-zero code — propagated from the underlying test command.
  • Working folder naming: .tmp/<lang>_<arg> where <lang> is a short identifier for the language (java, python, node, go, rust, ...). All dependency installs, build outputs, caches, and the test run itself live inside this folder. Nothing the script does should touch the source build folder after step 4.
  • Logging: print short progress lines ("Copied from ... to ...", "Installing dependencies into ...", "Running <lang> unittests in ...") so failures are easy to triage.

Dependency isolation

The dependency environment must live inside $WORKING_FOLDER so the test run can't be polluted by — or pollute — the user's global caches. Pick the most idiomatic isolation mechanism for the language:

LanguageIsolation mechanismInstall command (run inside $WORKING_FOLDER)Test command
Pythonvenv at ./.venvpython3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt (or pyproject.toml / uv sync / poetry install)./.venv/bin/pytest (or ./.venv/bin/python -m pytest)
Node.jslocal ./node_modules (default)npm ci (preferred) or npm installnpm test
Javaproject-scoped Maven repo at ./.m2mvn -Dmaven.repo.local=./.m2 dependency:resolve (optional pre-warm)mvn -Dmaven.repo.local=./.m2 test
Gomodule cache at ./.gocacheGOMODCACHE="$PWD/.gocache" go mod download (optional pre-warm)GOMODCACHE="$PWD/.gocache" go test ./...
Rustcargo home at ./.cargoCARGO_HOME="$PWD/.cargo" cargo fetch (optional pre-warm)CARGO_HOME="$PWD/.cargo" cargo test

Notes:

  • Every path in the install command and test command is relative to `.tmp/<lang>_<arg>`. That's why the script cds into the working folder in step 5 — from that point on, ./.venv, ./node_modules, ./.m2, etc. all resolve under .tmp/<lang>_<arg>, never under the source build folder.
  • Always pass the isolation flag/env var to both the install command and the test command — they must agree on where deps live, otherwise the test command will silently fall back to the global cache or (worse) the source build folder.
  • Python is the only ecosystem where the venv is mandatory to satisfy "into a virtual environment" literally. The others use language-native equivalents that achieve the same isolation.
  • Pre-warming is optional for Java/Go/Rust — their test commands will fetch deps on demand. Doing it as a separate step makes failures easier to diagnose and gives a clean "install failed vs test failed" signal.
  • Don't activate the venv in Bash via source .venv/bin/activate — call ./.venv/bin/<tool> directly. It's more portable and avoids subshell weirdness. In PowerShell, use & .\.venv\Scripts\<tool>.exe similarly.
  • Propagate the install exit code immediately. In Bash: <install cmd> || exit $?. In PowerShell: check $LASTEXITCODE and exit $LASTEXITCODE if non-zero.

Bash specifics

  • Shebang: #!/bin/bash.
  • File naming: run_unittests_<lang>.sh, placed in assets/.
  • Argument: $1.
  • Make it executable: chmod +x assets/run_unittests_<lang>.sh.

PowerShell specifics

  • No shebang. Use a param([Parameter(Mandatory=$true)][string]$Subfolder) block at the top instead.
  • File naming: run_unittests_<lang>.ps1, placed in assets/.
  • Exit codes: use exit 69 etc. (PowerShell honors them just like Bash).
  • Toolchain check: prefer Get-Command <tool> -ErrorAction SilentlyContinue and, where a specific version is needed, parse the tool's --version output.
  • Filesystem: use Test-Path, Remove-Item -Recurse -Force, New-Item -ItemType Directory, Copy-Item -Recurse, Set-Location. Quote paths to handle spaces.
  • No `chmod` step needed. If execution policy is likely to block the script, mention Set-ExecutionPolicy -Scope CurrentUser RemoteSigned to the user — don't bake it into the script.

Workflow

1. Confirm the target language, shell flavor (Bash or PowerShell), and dependency manifest (pom.xml, requirements.txt / pyproject.toml, package.json, go.mod, Cargo.toml, ...). Ask if any is unclear. 2. Read assets/run_unittests_java.sh to refresh the exact structure. 3. Translate each of the seven steps above into the equivalent commands for the target language and shell. The toolchain check, dependency install, and test invocation are the language-specific parts; the rest is mechanical translation between Bash and PowerShell syntax. 4. Pick the dependency-isolation mechanism from the Dependency isolation table and use it consistently in both step 6 and step 7. 5. Save the new script to assets/run_unittests_<lang>.sh or assets/run_unittests_<lang>.ps1. For Bash, chmod +x it.

Anti-Patterns

  • (Hard mistake) Don't install into, build into, or otherwise write to the source build folder. The build folder passed as $1 is read-only input. Every install, cache, build artifact, log, and temp file must land in .tmp/<lang>_<arg>. This includes never running pip install, npm install, mvn install, or cargo build with the source folder as their cwd or target, never letting a venv / node_modules / .m2 / .gocache / .cargo directory appear inside the source folder, and never running the test command from inside it. The whole point of staging the build into .tmp/ is so the source folder remains a clean, reproducible artifact of the render — writing to it corrupts the renderer's view and breaks subsequent renders.
  • Don't skip the toolchain check, even when "everyone has it installed" — exit code 69 is what the calling system relies on to detect a missing runtime.
  • Don't reuse the source folder in place. Always copy into .tmp/<lang>_<arg> first; the renderer relies on this isolation.
  • Don't change the exit-code contract. Other parts of the system branch on 1, 2, and 69 specifically — and these codes must be identical between the Bash and PowerShell variants.
  • Don't write a cross-shell hybrid (e.g. a .sh that detects PowerShell, or vice versa). Ship one script per shell, named with the appropriate extension.
  • Don't install dependencies into the user's global location (~/.m2, system-wide pip, ~/.cargo, etc.). Always isolate inside $WORKING_FOLDER so concurrent runs and other projects can't interfere.
  • Don't run the test command without first verifying the install step succeeded. A failed install followed by a "test" run produces misleading errors that look like test failures.

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.