Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
519b8f6
fix(merge): the same gloss on a re-add is not a new form
Oct 6, 2026
1a28dd6
test(helpers): load extension libraries without wrapper text
Oct 6, 2026
9640162
test(dom): cover the privacy page, the theme script and the connect page
Oct 6, 2026
bf17086
test: property-based tests with fast-check, and an assertion mode
Oct 6, 2026
0f4bb9c
fix(server): words with Å or Ņ no longer break a lookup
Oct 6, 2026
eba1343
fix(server): a repeated query parameter is a 400, not a 500
Oct 6, 2026
bd016da
test(server): 90 % line coverage, enforced, and more properties
Oct 6, 2026
aa8bca6
ci: gate coverage, and run every test before a release
Oct 6, 2026
77a414a
docs: coverage gates, property tests and assertion mode
Oct 6, 2026
cc6d039
Merge remote-tracking branch 'origin/main' into silver/gold-tests
Oct 6, 2026
d799359
Merge remote-tracking branch 'origin/chore/maintainers-like-trinity' …
Oct 6, 2026
84cd651
Merge remote-tracking branch 'origin/fix/site-test-build-race' into s…
Oct 6, 2026
5feac34
Merge remote-tracking branch 'origin/silver/server-ops' into silver/g…
Oct 6, 2026
2c08296
Merge remote-tracking branch 'origin/silver/sast' into silver/gold-tests
Oct 6, 2026
143d434
Merge remote-tracking branch 'origin/main' into silver/gold-tests
Oct 6, 2026
d01c8db
docs(best-practices): the coverage gates and property tests earn thei…
Oct 6, 2026
86be7c1
test(server): a fake key that looks fake, and the old one recorded fo…
Oct 6, 2026
f02766b
ci: gitleaks scans the history being merged, not every branch
Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ jobs:
echo "${GITLEAKS_SHA256} $RUNNER_TEMP/gitleaks.tar.gz" | sha256sum -c -
tar -xzf "$RUNNER_TEMP/gitleaks.tar.gz" -C "$RUNNER_TEMP" gitleaks
- name: Scan the full history
run: '"$RUNNER_TEMP/gitleaks" git --log-opts="--all" --redact --config .gitleaks.toml --exit-code 1 .'
# HEAD's full history (all of main plus this pull request's commits). Not --all: the
# checkout fetches every branch, and another branch's finding would fail this one.
run: '"$RUNNER_TEMP/gitleaks" git --log-opts="HEAD" --redact --config .gitleaks.toml --exit-code 1 .'
- name: No secret, database or oversized files
env:
BASE: ${{ github.event.pull_request.base.sha }}
Expand Down Expand Up @@ -90,6 +92,11 @@ jobs:
if: matrix.current
- run: mix deps.unlock --check-unused
- run: mix test --warnings-as-errors
if: ${{ !matrix.current }}
# The coverage gate (90 % of lines, mix.exs) on one compiler only: line counts differ
# a little between Elixir versions, and cover makes the run slower.
- run: mix test --warnings-as-errors --cover
if: matrix.current

extension:
runs-on: ubuntu-24.04
Expand All @@ -101,7 +108,9 @@ jobs:
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test
# Every unit, DOM and background test (property tests and assertion mode included),
# failing below 90 % line or 80 % branch coverage of extension/.
- run: npm run coverage
- run: npm run perf

# Slices 08 and 09: the shared spec. The extension's copy is current, the CLDR-derived
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,18 @@ jobs:
echo "Building $v with Node $(node --version)"
- run: node scripts/check-versions.mjs
- run: npm ci
# The same checks as CI, on the tree being released.
# The same checks as CI, on the tree being released: the extension's and the server's
# tests with their coverage gates. The property tests run in both, so the dynamic
# analysis runs before every release.
- run: npm run lint
- run: npm test
- run: npm run coverage
- name: Server tests
working-directory: server
env:
MIX_ENV: test
run: |
mix deps.get
mix test --cover
- name: Build the zips twice and compare
env:
VERSION: ${{ steps.version.outputs.version }}
Expand Down
9 changes: 9 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2026 ScriptKittyOS and the Kotiko contributors
# SPDX-License-Identifier: Apache-2.0
#
# Findings gitleaks may skip. Each line is a fingerprint (commit:file:rule:line) of a value
# that is not a secret, with the reason above it. New test keys follow CONTRIBUTING.md
# ("0123456789" in every fake key), so this list should stay short.

# A fake Whisper key in a test, written before it followed that rule; renamed since.
bd016da4200b0d28093d8360304e46d844ef8f23:server/test/kotiko/voice_test.exs:generic-api-key:75
50 changes: 47 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ You need Elixir 1.15 or newer and Node 22.
# server
cd server
mix deps.get
mix test
mix test # or mix test --cover, with the coverage gate

# extension and end-to-end tests (from the repository root)
npm ci
npm test
npm test # or npm run coverage, with the coverage gate
npx playwright install chromium
npm run e2e
```
Expand Down Expand Up @@ -61,7 +61,51 @@ site in `site/` (its own `package.json`, build and tests), in
practical, say why in the pull request; a maintainer has to agree.
- Tests never use the network: the server stubs HTTP with `Req.Test`, and the extension
tests serve their own pages.
- How to run them is under [Setup](#setup); CI runs them on every pull request.
- How to run them is under [Setup](#setup); CI runs them on every pull request, and the
release workflow runs them again on the tree it releases.

### Coverage gates

CI fails when coverage drops below these numbers, and prints them on every run:

| Part | Command | Gate |
|---|---|---|
| Extension | `npm run coverage` | 90 % of lines and 80 % of branches in `extension/` (Node's built-in coverage) |
| Server | `cd server && mix test --cover` | 90 % of lines (`test_coverage` in `server/mix.exs`; the helpers in `test/support` don't count) |

Elixir's cover tool counts lines, not branches, so the server has no branch number. The
server gate runs on the newest Elixir in the CI matrix only, because line counts differ a
little between compiler versions. A gate is never lowered; raise it when coverage grows.
Coverage comes from tests of behaviour: a test that only runs code without checking what
it does is not accepted.

### Property tests

Besides examples, both halves have property-based tests, which check a rule on many
generated inputs:

- Extension: [fast-check](https://fast-check.dev/) (a dev dependency) in
`test/unit/properties/`, for the matcher, server address checks, the bulk list reader,
the backup, casing, the word-list check and the policy page's Markdown.
- Server: StreamData in `server/test/**/*_property_test.exs`, for word validation and
input preparation, the v1 routes, `.env` parsing, language tags, text cleaning, UUIDv7,
log redaction and backups; shared generators are in `server/test/support/gen.ex`.

They run as part of `npm test` and `mix test`, so in CI and before every release. Each run
uses a new random seed. When one fails, it prints the seed and the smallest input it found;
replay it with `FC_SEED=<seed> FC_PATH=<path> npm test` (extension; `FC_NUM_RUNS=<n>`
tries more inputs) or `mix test --seed <seed>` (server). Fix the bug and add a plain
regression test with that input, so the case is checked on every run.

### Assertion mode

`npm test` and `npm run coverage` load `test/helpers/assert-mode.mjs`, which turns on the
extension's invariant checks in every test process, jsdom window and vm context: the
matcher checks each scan's matches, the backup each restore plan, and the word-list check
each filtered list, and they throw when an invariant breaks. The shipped extension never
turns them on, and `npm run perf` measures the code as it ships. Add a check to this mode
when a function makes a promise its callers rely on. The end-to-end tests run the real
extension, which has no way to turn the mode on.

## Commits and pull requests

Expand Down
Loading