afv-library/plugins/builder/salesforce-development/scripts/test/README.md

126 lines
6.4 KiB
Markdown

# salesforce-development runtime tests
Offline regression guards for the plugin's ported runtime (`../sf-deploy-gate`
and `../sf_context.py`, dispatched through `../sf-context`). No live org
required — each suite stubs `sf` output, mocks `subprocess`, or runs in a
throwaway cwd, so they all run locally without a connected org.
Run the whole set the way CI does:
```bash
npm run test:gates
```
`test:gates` globs every `*.test.sh` (bash) and `test_*.py` (Python `unittest`)
in this directory, so a new suite is picked up automatically once its filename
matches — no package.json edit needed. `python3` is a hard requirement of the
bash suites too (each one's `parse()` shells out to it), so the Python runner
adds no new dependency.
Run an individual suite:
```bash
DIR=plugins/builder/salesforce-development/scripts/test
bash "$DIR/classify.test.sh" # org-bucket classification
bash "$DIR/gate-decision.test.sh" # full allow/deny decision (stubbed sf)
bash "$DIR/win-shim-guard.test.sh" # Windows batch-shim metacharacter guard
bash "$DIR/detect-compact.test.sh" # SessionStart(compact) re-inject
bash "$DIR/feedback-nudge.test.sh" # feedback gate + one-nudge-per-session
bash "$DIR/post-deploy-failure.test.sh" # failed-deploy → owning-skill routing
bash "$DIR/skills-first-advisory.test.sh" # bypass-prone-op → owning-skill nudge
python3 "$DIR/test_sf_context.py" # cross-platform exec resolver + reporting
```
## Layout
Two suites live here: the deploy-gate tests and the org-context tests. In this
repo the runtime lives directly under `scripts/` (a sibling of this `test/`
dir), not under `bin/`, because this repo's `.gitignore` blocks `bin/`. There is
no LSP test suite here — this repo vendors the prebuilt LSP bundles rather than
the TypeScript source that such tests would exercise.
## Deploy gate — `sf-deploy-gate` (issue #259)
**`classify.test.sh`** — feeds `sf org display --json` fixtures into
`sf-deploy-gate classify` and asserts the bucket
(`production|sandbox|scratch|trial|devhub|unknown`). The headline cases are the
trial/dev orgs that were previously mis-classified as production:
- OrgFarm trials (`orgfarm-*`, `*.develop.my.salesforce.com`) with `isSandbox`/
`isScratch` returning `null`
- internal `*.pc-rnd.*` dev hosts
- any org carrying a `trialExpirationDate`
and the guard that genuine production (`*.my.salesforce.com`, classic
`*.salesforce.com`, no sandbox/scratch/trial markers) still classifies as
`production`.
**`gate-decision.test.sh`** — stubs `sf` on `PATH` and runs the full
`prod-check` path, asserting the hook's allow/deny JSON. Proves the #259 fix
end-to-end: a trial org **allows** the deploy, genuine production **denies** it,
the `CONFIRM_PROD=1` override still works, and destructive-changes deploys
(#407) are gated on prod even with the override.
**`win-shim-guard.test.sh`** — sources the gate (which defines its functions
without dispatching) and asserts the Windows batch-shim metacharacter guard:
`_has_cmd_metachars` flags every cmd.exe metacharacter, and `sf_cli` refuses a
metacharacter arg on a `.cmd` shim without invoking COMSPEC.
## Org-context runtime — `sf_context.py` / `sf-context`
**`test_sf_context.py`** (Python `unittest`, WIN-026/WIN-027) — the evidence for
the cross-platform executable resolver: `resolve_executable`/`build_command`/`run`
build a COMSPEC-wrapped **argv array** (never a shell string) for a Windows
`.cmd`/`.bat` shim, refuse metacharacter args on that reparse-prone path, and
spawn POSIX paths directly. Also covers deterministic setup/org reporting (a
missing tool is reported FAILED, never silently green) and secret-free
diagnostics. Stdlib-only (no pytest/PyYAML), mocks `subprocess`/`shutil.which`,
so it runs offline on the Python 3.9 baseline.
**`detect-compact.test.sh`** (#406) — asserts the SessionStart hook's
`source="compact"` re-fire re-injects **only** the lean skills-first reminder
(not the full catalog), shows no banner, stays silent outside a Salesforce
project, and never blocks.
**`feedback-nudge.test.sh`** (#277) — the feedback loop is default-**OFF** and
self-limiting: gate off → always silent; gate on + substantive work → exactly
one nudge per session; `record-feedback-decision` persists the opt-in. Every
response is non-blocking.
**`post-deploy-failure.test.sh`** (#405) — a failed `sf project deploy
start`/`validate`/`quick` routes to its owning skill
(`platform-metadata-deploy` / `platform-deploy-validate` / `platform-quick-deploy`);
non-deploy or garbled payloads stay silent (fail-open); never blocks. This suite
guards the skill-name references that a skill rename must keep in lockstep.
**`skills-first-advisory.test.sh`** (#286/#413/#415/#445) — a PreToolUse advisory
nudges toward the owning skill on bypass-prone ops (raw `.cls`/`-meta.xml` edits,
raw `sf apex/retrieve/data` calls), stays silent on owner-less metadata and
unrelated ops, and applies turn-aware suppression once the owning skill has
dispatched. WARN-ONLY — every response carries `continue: true`.
## Port adaptations (source ⇄ this repo)
These suites were translated from the source repo, not copied verbatim. Two
kinds of change were required, both mechanical:
1. **Path:** the runtime lives at `../sf-deploy-gate` / `../sf_context.py`
(sibling of `test/`), so `$ROOT/bin/…` references became `$ROOT/…` and the
Python test's `_MODULE_PATH` drops the `bin/` segment.
2. **Skill names + markers:** the advisory/routing suites assert the owning skill
by name, and this repo renamed the source's gerund skills to the
`<domain>-<verb>` taxonomy (e.g. `generating-apex``platform-apex-generate`,
`deploying-metadata``platform-metadata-deploy`, `quick-deploying-to-prod`
`platform-quick-deploy`). Compaction re-inject markers were rebranded
`sfdx-core``salesforce-development`. Expected values track the strings
`sf_context.py` actually emits in this repo.
## Hook matcher note
Unlike the source suite — which relied on prefix-anchored `matcher` strings like
`Bash(sf project deploy start*)` — the `salesforce-development` plugin wires the
gate through the **`if` field** on each hook entry (`if: "Bash(sf project deploy
start *)"`). Claude Code's PreToolUse/PostToolUse `matcher` filters on the tool
*name* only; command-content matching belongs in `if` (permission-rule syntax).
See the plugin's `.claude-plugin/plugin.json`.