Development
Two test suites
They cover different things and neither substitutes for the other.
Python — pytest
# everything, including real pinets-cli subprocess runs
.\.venv\Scripts\python.exe -m pytest -q
# the fast offline-only subset (what the installer runs as a gate)
.\.venv\Scripts\python.exe -m pytest -q \
tests/test_dataset.py \
tests/test_orchestrator_ranking.py \
tests/test_pine_lookahead.py \
tests/test_license_hygiene.pyConfig lives in pyproject.toml: testpaths = ["tests"], python_files = ["test_*.py"],
addopts = "-ra -q".
| File | Covers |
|---|---|
test_dataset.py | Dataset loading, required fields, seeded determinism |
test_orchestrator_ranking.py | Ranking behaviour |
test_pine_lookahead.py | Look-ahead bias detection |
test_license_hygiene.py | docs/licenses.md completeness; no AGPL source vendored |
test_pinets_cli_runs.py | Real pinets-cli execution; bytewise determinism |
test_hyperopt.py | Hyperopt config, search space, objective |
The pinets-cli integration tests spawn real subprocesses against real Node, so
they are meaningfully slower than the offline subset. If the CLI is not on your
PATH, those tests skip or fail for environment reasons rather than logic
reasons — check the path before believing a red suite.
Why the gate is four files when the table lists six
The split is whether a test needs a subprocess. The four gate files run in-process:
dataset loading, ranking, look-ahead detection and licence hygiene.
test_pinets_cli_runs.py is the other kind — real pinets-cli execution and bytewise
determinism, which means spawning real Node, which is why it is the slow half and why it
reports environment problems as test results. test_hyperopt.py is in the table and not
in the gate command.
So a green gate is a fast, environment-independent signal, not a summary of the suite. It is the right thing to run while you are editing; it is not the thing to quote when you claim the tests pass.
JavaScript — browser smoke suites
There are ~40 __smoke_*.cjs files in app/. They run against a served workspace in a
pinned Chrome profile.
npm run dev
# then, in another shell:
node app/__smoke_phase_f.cjs
node app/__smoke_optimizer_workspace.cjsThey accept a base URL so a harness can point them elsewhere:
SMOKE_BASE=http://127.0.0.2:8765/ node app/__smoke_optimizer_workspace.cjs
TRAD_BASE=http://localhost:8080/ node app/__smoke_phase_f.cjsBrowser suites must use a disposable profile, never your normal one. Several write to
IndexedDB and localStorage, and a careless run can disturb a real workspace.
What the pinned profile is protecting
The suites drive a real browser against a real served origin, so a run writes into whatever
profile that browser was launched with. Several suites write to IndexedDB and
localStorage, and both are persistent and per-origin — a normal profile carries a real
workspace into the run and takes the run’s writes back out.
Pinning is what makes two runs comparable. Making that pinned profile disposable is what
keeps the comparison from costing you a workspace. The base URL is settable through
SMOKE_BASE and TRAD_BASE, so a harness can aim the same suite at a different origin
instead of at the one you work in.
npm scripts
| Script | What it does |
|---|---|
npm run vendor | Copy the vendor bundles into app/vendor/ |
npm run dev | Vendor, then serve app/ on port 8080 |
npm run test:smoke | pinets-cli --version |
npm run auth:configure | Generate app/auth-config.json from SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY |
npm run test:auth | Node test runner: auth config, service, profile and navigation |
npm run test:auth:browser | Playwright over the real auth flows, against mocked Supabase HTTP |
npm run test:propfirm | The isolated prop-firm core test |
npm run verify | A dry-run sweep — config parsing without executing |
run-smokes.cjs — the aggregate runner, and what it adds
node scripts/run-smokes.cjs discovers every _smoke*.cjs in
app/ and runs the lot with a per-file timeout, so one hanging smoke cannot
block the sweep.
What it does with a hang is the point: it records TIMEOUT rather than
skipping the file quietly. A suite that cannot finish and a suite that was never run are very
different results, and collapsing them hides exactly the failure you are looking for.
Set SMOKE_TIMEOUT_MS to change the per-file budget; it defaults to 180000.
Conventions
Browser
- No framework. Plain JavaScript, UMD-style module wrappers, a concatenation build.
- Bounded containers. Every panel has a hard height or width and
overflow: hidden. Modules are never allowed to superimpose — overlap bugs are the recurring failure mode here, and the fix is always a bound, not az-index. - Hairline chrome. 1 px borders, compact density, square corners.
- Lucide-style inline SVG for icons. No emoji as UI glyphs.
- The optimizer bundle is generated. Edit the sources under
app/js/optimizer/, notapp/js/optimizer.bundle.js.
Pine
- Positional defaults on every
input.*— that is what makes a parameter sweepable. - Hidden plots carry the machine-readable signal alongside any visible
plotshape. strategy()scripts declare their costs. See Architecture decisions.
Python
- Config paths resolve against the repo root, not the CWD.
- Failures are recorded, never papered over: a failed cell gets
ok=Falseand an error string, and no metrics are invented for it. - AGPL code stays behind a process boundary.
How to read a failure you did not cause
The rule that a failed cell gets ok=False and an error string, with no metrics invented
for it, is the same rule that applies to a red test. Classify before you edit. The offline
gate exists to separate an environment problem from a logic one, and config paths that
resolve against the repo root mean a run is reproducible wherever you invoke it from — so a
failure that survives both is a real behaviour change.
When it is a real behaviour change, the answer is still not to edit the test. A test rewritten until it passes is indistinguishable from one that was right all along, and the change that broke it stays unexplained. If the change was deliberate, say so in the same commit — which is exactly what the optimizer baseline asks for.
Adding things
| To add | Do this |
|---|---|
| A strategy | Write app/strategies/<name>.pine, verify it with pinets-cli run, then point a config at it |
| A sweep config | Copy an examples/*.yaml and edit — the schema is documented in CLI reference |
| An optimizer algorithm | Implement the five-method interface and self-register in the algorithm registry |
| A prop-firm challenge | No code needed — it comes from the live directory; pass --firm and --challenge |
Seeding a regression
Optimizer changes are guarded by a recorded baseline. A fixed-seed TPE run must reproduce exactly:
tests/fixtures/optimizer-ui-baseline.jsonholds the five-trial, seed-42 expectations for parameters, PnL, PnL %, drawdown %, trade count and win rate.
If a change to the surrounding UI breaks that, the bug is in the UI. If you change the optimizer deliberately, update the baseline in the same commit and say why.
What a seeded baseline proves, and what it does not
tests/fixtures/optimizer-ui-baseline.json records one configuration — five trials, seed
42, TPE — across every quantity in the bullet above. Reproducing it exactly proves that this
path did not move. It says nothing about whether the optimizer searches well: a fixed seed
at a fixed budget is a determinism check, not a quality measurement, and a poor search
reproduces just as reliably as a good one.
It also covers the UI, because the UI feeds the run. One file of recorded numbers holds both halves, so a mismatch tells you something changed and not which half changed. That split is the judgement the two sentences above ask you to make, and it is why the baseline is edited in the same commit as the change rather than after it.
Editing this documentation
The docs live in docs-site/ and build to a static export.
cd docs-site
npm install
npm run dev # local preview
npm run build # static output in out/Screenshots are managed through a registry rather than being embedded in pages. To add a
capture, drop the file into public/screenshots/ and add one entry to
lib/screenshots.ts — pages reference figures by id, so no page needs rewriting. Until a
file exists, <Shot> renders a labelled placeholder rather than a broken image.