Skip to Content
Development

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.py

Config lives in pyproject.toml: testpaths = ["tests"], python_files = ["test_*.py"], addopts = "-ra -q".

FileCovers
test_dataset.pyDataset loading, required fields, seeded determinism
test_orchestrator_ranking.pyRanking behaviour
test_pine_lookahead.pyLook-ahead bias detection
test_license_hygiene.pydocs/licenses.md completeness; no AGPL source vendored
test_pinets_cli_runs.pyReal pinets-cli execution; bytewise determinism
test_hyperopt.pyHyperopt 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.cjs

They 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.cjs

Browser 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

ScriptWhat it does
npm run vendorCopy the vendor bundles into app/vendor/
npm run devVendor, then serve app/ on port 8080
npm run test:smokepinets-cli --version
npm run auth:configureGenerate app/auth-config.json from SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY
npm run test:authNode test runner: auth config, service, profile and navigation
npm run test:auth:browserPlaywright over the real auth flows, against mocked Supabase HTTP
npm run test:propfirmThe isolated prop-firm core test
npm run verifyA 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 a z-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/, not app/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=False and 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 addDo this
A strategyWrite app/strategies/<name>.pine, verify it with pinets-cli run, then point a config at it
A sweep configCopy an examples/*.yaml and edit — the schema is documented in CLI reference
An optimizer algorithmImplement the five-method interface and self-register in the algorithm registry
A prop-firm challengeNo 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.json holds 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.

Last updated on