# Development

Each folder is one tool with its own tests. A change touches one folder, its page under `docs/`, and its line in the README when the tool's one-line summary changes.

## Run the tests

```
python3 -m pip install requests pytest
python3 .github/scripts/run_tests.py              # every package
python3 .github/scripts/run_tests.py --only apply # one package (comma-separate several)
(cd spanish-poller && node --test)
```

`run_tests.py` finds every top-level folder with Python tests (a `test_*.py` file or a `tests/` folder), runs `python -m pytest -q` inside it, then the folder's `ci-extra.sh` when there is one (`check/ci-extra.sh` runs the `gwt-check` self-test), and prints one `ok` or `FAIL` line per package. A new tool needs no CI edit: give its folder tests and CI runs them. A folder that ships a tool (a `gwt-*` wrapper or a `pyproject.toml`) with no tests fails the run, unless `UNTESTED` in the script names it with the reason.

CI runs the same script on every pull request ([ci.yml](<https://github.com/GeiserX/gramps-web-toolkit/blob/main/.github/workflows/ci.yml>)), and `node --test` for `spanish-poller` in its own job.

CI also builds the package in a `wheel` job: `.github/scripts/check_wheel.sh` installs the wheel into a clean virtualenv outside the clone and runs `gwt --version`, `gwt <tool> --help` and `gwt-<tool> --help` for every tool, then `gwt check selftest`. A new Python tool goes in three places: its module in `[tool.hatch.build.targets.wheel.force-include]` and its `gwt-<tool>` line in `[project.scripts]` of the root `pyproject.toml`, and a row in `TOOLS` in `core/gwt_core/cli.py`. Miss the first and the `wheel` job fails.

No test needs a Gramps Web server or a FamilySearch or MyHeritage account. The `apply` tests run against an in-process fake of the Gramps Web API, and the `gwt-mh` tests read invented fixture pages.

## Mutants: prove a test can fail

A test that passes whether or not its guard is there protects nothing. `<pkg>/mutants.txt` lists guards with the mutation that removes each one:

```
gwt_apply/ops.py | if name == "merge_person" and set(op.get("use_loser") or []) - set(USE_LOSER): | if False: | validate refuses an unknown use_loser vital
```

The four fields are the file (relative to the package), the exact text to replace (it must occur exactly once), the replacement, and what the guard protects. `python3 .github/scripts/run_tests.py --mutants [--only PKG]` copies the package to a scratch folder once per row, applies the replacement, and runs the package's tests there. The run fails when a mutant survives (the tests still pass), or when a row's text is not found.

Rows can also go in `<pkg>/mutants.d/*.txt`, same format, read after `mutants.txt` in name order; a new gwt-read source puts its rows in `read/mutants.d/<id>.txt`, so changes made at the same time never edit one file. When you add a guard, add its row and a test that fails with the row applied. CI runs the mutants of every package a pull request touches, and all of them on a push to `main` or when a pull request changes `.github/`.

## Releases and the changelog

The version comes from git tags (hatch-vcs): a `v0.2.0` tag builds 0.2.0, and a commit after it builds `0.2.1.devN+g<sha>`. `gwt --version` in a clone prints the same shape, read from git. Pushing a `v*` tag runs [release.yml](<https://github.com/GeiserX/gramps-web-toolkit/blob/main/.github/workflows/release.yml>): it builds the sdist and wheel, publishes them to PyPI through trusted publishing, and creates the GitHub release from the tag's section of [CHANGELOG.md](<https://github.com/GeiserX/gramps-web-toolkit/blob/main/CHANGELOG.md>). It stops before publishing when that section is missing or has no `### Change lists` heading. A manual run of the workflow is a dry run that publishes a dev build to TestPyPI only. To release, rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`, merge that, then tag the merge commit and push the tag.

Every pull request that changes how an existing change list behaves (an op newly refused, a newly required field, a different skip or duplicate rule, a different default, in `gwt-apply` or in what `gwt-import-excel` writes) adds a line under `### Change lists` in the Unreleased section of `CHANGELOG.md`. Other user-facing changes go under Added, Changed or Fixed.

## Build this site

```
python3 -m pip install -r docs/requirements-docs.txt
python3 -m zensical serve                          # http://127.0.0.1:8000, rebuilds on save
python3 -m zensical build --clean --strict         # the build CI runs
python3 -I .github/scripts/site-check.py site      # the check CI runs after it
```

The site is built with [Zensical](<https://zensical.org/>) from `docs/` and `zensical.toml`. The strict build fails on a broken link, a link to a missing heading, a link that leaves `docs/`, or a link to an excluded file (a capitalised `.md`); link a file outside `docs/` by its GitHub URL. `site-check.py` fails on what the build lets through: a page left out of the nav, a nav entry for a missing file, a missing image, a link rooted at `/`. A push to `main` that changes the docs publishes the site.

## Rules for a change

- Every change reaches `main` through a pull request with green CI. Commits follow [Conventional Commits](<https://www.conventionalcommits.org>).
- No private data. Examples and test data are invented: no real people's names from anyone's tree, no real case numbers, tree ids, hostnames, IP addresses, accounts or email addresses. Public records, such as a Gaceta page or a published parish entry, are fine with their source.
- Never commit a secret, a certificate or a configuration file with credentials.
- A new trap goes into [Hard-won lessons](<https://geiserx.github.io/gramps-web-toolkit/hard-won/index.md>): what happened, why, and the rule.

Security problems go through the [security policy](<https://github.com/GeiserX/gramps-web-toolkit/blob/main/SECURITY.md>), never a public issue. The toolkit is licensed [GPL-3.0-or-later](<https://github.com/GeiserX/gramps-web-toolkit/blob/main/LICENSE>).
