Skip to content

Testing and CI

Claude edited this page Oct 27, 2026 · 11 revisions

Testing and CI

English · Español

What is tested, how, and which gate catches what: Vitest for the frontend and Node scripts, cargo test for Rust (including image-comparison golden tests for the card designer and the game engine crate mesa), the room server's own tests and the contract between app and server, the custom check scripts, Knip, the git hook, and the GitHub Actions workflows.

The local gates and CI run the same commands, so a green pnpm check plus the three Rust commands predicts a green CI. Setup is in Development Setup; conventions that the gates enforce are in Conventions.

The gates at a glance

flowchart TD
    C["pnpm check"] --> T["check:tipos: tsc --noEmit"]
    C --> V["test: vitest run"]
    C --> B["check:biome: biome check ."]
    C --> TI["check:title: sin-title-nativo.mjs"]
    C --> I["idiomas: idiomas.mjs"]
    C --> K["check:knip: knip"]
    R1["check:rust-fmt: cargo fmt --check"]
    R2["check:clippy: clippy -D warnings"]
    R3["test:rust: cargo test --workspace"]
    O["no OpenSSL: cargo tree -i openssl is empty"]
    CI["CI job 'checks'"] --> C
    CI --> R1
    CI --> R2
    CI --> R3
    CI --> O
Loading
Gate Command Fails when
Types pnpm check:tipos (tsc --noEmit) TypeScript errors, unused locals/parameters (strict, noUnusedLocals, noUnusedParameters in tsconfig.json)
Unit tests pnpm test (vitest run) A test fails
Lint + format pnpm check:biome (biome check .) Lint error or wrong formatting (tabs, quotes, line width 100; see biome.json)
Native tooltips pnpm check:title A .tsx puts title= on an HTML element or a pass-through component
Translations pnpm idiomas A language of src/lib/idiomas.ts has no dictionary, a string passed to t() is missing in one, one string is in two area files, placeholders or plural forms do not match (see below)
Dead code pnpm check:knip (knip) Unused files or dependencies
Rust format pnpm check:rust-fmt cargo fmt --check reports differences
Rust lints pnpm check:clippy Any clippy warning (-D warnings, all targets, whole workspace), including unwrap/expect outside tests
Rust tests pnpm test:rust Any test fails (this also runs the ts-rs export tests, see Development Setup)
No OpenSSL CI step only cargo tree -i openssl finds the crate

pnpm check runs its parts in the order tipos, test, biome, title, idiomas, knip (package.json scripts.check); title and idiomas run with --silent so only their own messages show.

Frontend tests (Vitest)

Config: vitest.config.ts uses the node environment (no DOM), alias @ to src, restoreMocks: true, and picks up src/**/*.test.ts, src/**/*.test.tsx and scripts/**/*.test.mjs. happy-dom is a dev dependency but the default environment is node.

Philosophy: React components are not rendered in tests. The tested code is the logic that was deliberately pulled out of components into plain modules and stores, which are given a mocked @/lib/ipc. Typical pattern (see src/stores/__tests__/actualizacion-store.test.ts): vi.mock("@/lib/ipc", ...) fakes call and on with a switch on the command name and throws on an unexpected command, then asserts the sequence of commands the store issued.

Folder Tests (files)
src/stores/__tests__/ deck-store, deck-store-abrir, ui-store, plan-store, trabajo-store, actualizacion-store, originales-store (conOriginales), vivo-store (cursors and ghosts of other players), mesa-store (applies the patches written by crates/mesa, see below)
src/lib/__tests__/ cartas, deck-ops, dialogos, errores, idiomas (the language table), image-url, imagen-viva, tema-inicial, temas-instalados, trabajos (the listen-before-launch job helper), cierre (the close guard: pendientes / hayPendientes with each reason, adding and removing dialogs from the registry, several dialogs at once, a deck that failed to save)
src/i18n/__tests__/ traducir (fallback language, English, Spanish; Russian plural forms by n; non-number n uses other; numbers formatted in the UI language; cargarDiccionario)
src/components/__tests__/ emoji (every language's flag has an image; rendered to a string with react-dom/server, no DOM)
src/features/decklist/__tests__/ acciones
src/features/export/__tests__/ acciones
src/features/library/__tests__/ organizar (filter, sort, group, selection), partidas-deck (win percentage)
src/features/mesa/__tests__/, mesa/dados/__tests__/, mesa/listas/__tests__/ The game table screen logic (drag, shortcuts, search, chat, counters, commander damage, teams, layout, sounds, dice physics and geometry, list choice...); see Game Table
src/features/salas/__tests__/ enlaces-entrantes (an incoming invitation link is kept for "Play with friends", or refused during a game)
src/features/preview/__tests__/ acciones-carta, arrastrar-ficheros
src/features/variants/__tests__/ vincular
src/features/card-editor/__tests__/ propagar (apply to card / copies / all)
src/features/disenar/__tests__/ estilos, encuadre, grupos, por-cara (per-face style and settings), renumerar, textos
src/features/stats/__tests__/ textos
scripts/__tests__/ release-notas.test.mjs (argument parsing, release notes template and cleaning, latest.json notes rewriting), idiomas.test.mjs (comprobar: missing keys, changed placeholders, missing or foreign plural categories, plural without {n}, warnings, a key repeated in one file, a missing area file)

Gotcha: the scroll and animation behaviour of the real UI is not covered by any automated test; it has to be tried by hand in WebKitGTK (see Development Setup).

Rust tests

pnpm test:rust runs cargo test --workspace for three crates (members in src-tauri/Cargo.toml): the app (proxy-builder, library proxy_builder_lib), the pure core proxy-core and the pure game engine mesa. As of this version there are about 750 #[test]/#[tokio::test] functions spread over some 140 files (grep the tree for exact numbers). Dev-dependencies of the app: tempfile, lopdf (inspects generated PDFs), and tokio with test-util; proxy-core has lopdf.

Rules visible in the code:

  • No network in the default run. Anything that needs the live internet (Scryfall, GitHub, Google Fonts, MTGJSON, Cockatrice, mtg.io) is #[ignore] and meant for manual runs. Parsers are tested against saved fixtures: src-tauri/src/fixtures/ (archidekt-1.json, cockatrice-tokens.xml, mtgio-muro-de-algas.json, tweakcn-amethyst-haze.json) and src-tauri/src/scryfall/fixtures/.
  • Temp state. Tests that need an AppState build one over a tempfile::tempdir() (data and cache paths), never the real app folders.
  • Tests may unwrap. #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] only applies outside tests; integration test files add #![allow(clippy::unwrap_used, clippy::expect_used)].
  • Where the volume is. Most tests sit in proxy-core: the designer (diseno/render.rs, componer.rs, cuerpos.rs, texto.rs, pack.rs, pintar.rs, estructura.rs), layout (layout/plan.rs, grid.rs), decklist parsing and export (decklist/), imaging (imaging/frame.rs, corners.rs, adjust.rs) and stats. In the app crate: scryfall/resolve.rs, commands/cards.rs, commands/stats.rs, render.rs, store/decks.rs, package.rs, commands/export.rs, commands/identify.rs.

Updater configuration guard

src-tauri/src/actualizar.rs has a test, la_configuracion_del_actualizador_es_valida, that reads tauri.conf.json and asserts createUpdaterArtifacts is true, requireSignedVersion is true, the endpoint is a GitHub releases/latest/download/latest.json URL, and the public key decodes to a minisign key with the expected key id. Reason: breaking any of these silently leaves every installed copy without updates. See Releases and Auto-Updates.

Golden tests (designer rendering)

Location: src-tauri/crates/proxy-core/tests/.

File What
dorados.rs Base cases per style, each rendered with the real engine and compared to a stored PNG in tests/dorados/; also dorados_en_otros_idiomas (idioma-fr.png, idioma-ru.png, idioma-ro.png: Latin with diacritics, Cyrillic and a language without printed cards, asserting no FaltanLetras and SinTextoOficial only for Romanian)
layouts.rs One golden per special Magic layout in tests/dorados/layouts/ (planeswalker, saga, class, case, split, flip, transform, MDFC, battle, meld, dungeon, plane, vanguard...) using real card data saved in tests/dorados/layouts/datos/*.json
comun/mod.rs Shared helpers: synthetic art (arte(seed)), emblem, symbols drawn locally, and diferencia()
compatibilidad/diseno-2.5.9.json A design as saved by version 2.5.9, used by the compatibility tests below

Design points:

  • Test assets are generated in code (gradient-and-circles art, a blue oval emblem, coloured-circle symbols) so no copyrighted art is committed. Fonts and the house frame pack come from the real assets/ (the engine constructed by Motor::de_fabrica()).
  • Comparison: diferencia(a, b) returns the percentage of pixels where any channel differs by more than 8/255; sizes that differ count as 100 %. A case fails at 0.5 % or more (dorados_de_los_casos_base). On failure the new render is written to the OS temp directory as <case>-nuevo.png (nuevo-<name> in layouts.rs) for inspection, and the test lists all failing cases together.
  • First run creates, later runs compare. If a golden file does not exist it is written and the case passes.
  • Updating goldens: PB_ACTUALIZAR_DORADOS=1 cargo test -p proxy-core --test dorados (or --test layouts) rewrites them. Only do this when the visual change is intended and approved; review the diff in the PNGs before committing.
  • Determinism tests: dos_renders_iguales_dan_los_mismos_bytes asserts two renders of one case are byte-identical (the PDF and preview rely on determinism); cambiar_un_color_del_pack_cambia_el_dorado proves a pack palette edit changes the output (guards against the pack colours being ignored).
  • las_cartas_grandes_salen_a_su_medida checks large cards (planes, schemes, Vanguard) render at their own size.

Compatibility with older versions (design languages)

Adding languages must not change what Spanish and English designs already are, on disk or on screen. Three tests pin that down:

  • diseno::efectivo::tests::las_huellas_en_espanol_e_ingles_son_las_de_antes: fixed render hashes (huella) taken from 2.5.9 for Spanish and English. If they changed, every existing designed card would be re-rendered.
  • diseno::modelo::tests::un_diseno_de_antes_se_reescribe_igual: tests/compatibilidad/diseno-2.5.9.json is read and written back byte-identical (so saving a deck in this version does not rewrite older designs).
  • In the same module, otro_idioma_se_guarda_tras_es_y_en_y_lo_lee_la_version_de_antes: a design with another language keeps es and en first, an older two-field struct still reads it, and an unknown language key is ignored.

See Designer Engine for ByLang and the fingerprint.

Ignored (manual) tests and their environment variables

Run with cargo test <path> -- --ignored --nocapture. They need local data or the network and are never part of CI.

Variable Used by Meaning
PB_ACTUALIZAR_DORADOS tests/dorados.rs, tests/layouts.rs Rewrite goldens instead of comparing
PB_CJK dorados::dorado_en_japones Path of a Noto Sans JP font file (the app downloads it apart, so it is not in the repo); compares with idioma-ja.png. Without the font the render carries FaltanLetras.
PB_BALATRO tests/comun pack_balatro(), layouts::balatro_matriz, dorados::balatro_* Folder of the Balatro frame pack (the separate packs repository)
PB_SALIDA same Output folder for contact sheets (defaults to a temp dir)
PB_ORIGINAL dorados::original_con_sus_imagenes, original_colores Folder with the downloaded "Original" frame images
PB_GF dorados::letras_libres_a_ojo, diseno/letras_libres.rs Folder of the free Google Fonts files
PB_AUTOR dorados::balatro_contra_el_autor The pack author's reference renders to compare against
PB_OCR_DATOS, PB_OCR_IMG ocr.rs, originales.rs OCR model folder and a test image
PB_ID_DIR commands/identify.rs Folder of images for batch identification
PB_IMG imaging/corners.rs en_vivo An image to measure the corner fill
PB_DB, PB_DECK scryfall/fichas_auto.rs en_vivo A copy of a library DB and a deck name for token detection
PB_PDF_DE_PRUEBA commands/export.rs de_punta_a_punta Where to leave the end-to-end PDF
PB_PACKS_LOCAL diseno/descargables.rs Debug builds only: load downloadable packs from a local folder instead of GitHub (not just a test variable)
PB_DATOS, PB_CACHE mesa/ejemplo.rs deck_de_ejemplo, deck_grande_de_ejemplo The app's data and cache folders: writes a sample deck of designed cards (no Scryfall needed) into that library, to try the game table
PB_PRUEBA_SERVIDOR, PB_PRUEBA_RELE red/servidor/cliente.rs A deployed room server (wss://…/v1/ws) and its relay (see Online play tests)

Two more variables are not for ignored tests but rewrite saved data during a normal run: PB_FIXTURES=1 (the protocol and server fixtures of src-tauri/src/red/ and the patches of crates/mesa) and PB_CATALOGO_ESCRIBIR=1 (the catalogue snapshot of crates/mesa). Both are covered below.

Other #[ignore] examples: live downloads (diseno/original.rs en_vivo, descargables.rs, fuentes/mtgio.rs, fuentes/cockatrice.rs, fuentes/mtgjson.rs, scryfall/client.rs, scryfall/nombres.rs, temas.rs, diseno/traduccion.rs, diseno/borrador.rs).

src-tauri/crates/proxy-core/examples/ (disenar.rs, prototipo.rs) are runnable viewers, not tests: cargo run --release -p proxy-core --example disenar -- <symbols> <arts> <out> [width].

Game engine tests (crates/mesa)

Location: src-tauri/crates/mesa/tests/, run with cargo test -p mesa (also part of pnpm test:rust). The crate is compiled with opt-level = 2 in dev so the property tests take seconds.

File What
acciones.rs Every action of the catalogue (before → event → after). An exhaustive match (nombre) plus the TODAS list fail when a new Accion variant has no test
propiedad.rs 1,000 random intentions (same seed → same fingerprint, rejected ones change nothing), visibility for every viewer after every action with patches equal to the full view, 2,000 arbitrary events that never panic
registro.rs Undo, redo, undo up to a line, online undo permissions, late guests, replay at any seq, mulligan, fingerprint; also greps the sources for HashMap<, SystemTime, Instant::, rand::
preparar.rs Deck sections for the table and dealing
turnos.rs Turn order by seat from the first player, eliminated players skipped, the round goes up when passing the first player
equipos.rs Teams: who counts as an opponent, shared life
formatos.rs Multiplayer formats: factory values, Archenemy, Planechase, commander damage
muerte.rs Who is out of the game (life, poison, commander damage, with team shared life) and that the view says so
encuentro.rs Matches: the score and swapping cards with the sideboard
resumen.rs The end-of-game summary: places by elimination order, ties, concessions, teams
sparring.rs The sparring partner over simulated games (a scripted player: one land a round, the spells its mana allows, pass) with every intensity: each falls in its band, off never reacts, same seed gives the same reactions, resolving then undoing restores everything, basic lands are never removed
catalogo.rs The catalogue snapshots (see below)
comun/mod.rs Sample decks and games, a random intention generator and sin_fugas (the visibility rules written again by hand)
fixtures.rs Plays a sample game, checks in Rust that the patches applied to the first view give the final view and, with PB_FIXTURES=1 cargo test -p mesa --test fixtures, writes src/stores/__tests__/fixtures/mesa-parches.json (a first view, 60 patches, the final view). src/stores/__tests__/mesa-store.test.ts applies them in TypeScript and must reach the final view, so Rust and the screen store agree on patches. Nothing compares the saved file with the current Rust output, so rewrite it after changing the view or the patches

Some engine modules also have unit tests in src/ (azar.rs, lista.rs, formato.rs, ids.rs, sparring/amenaza.rs, sparring/cartas.rs).

The catalogue snapshots (VERSION_CATALOGO)

Events (Evento, with its Cambio) are what the game log stores, sends to the other players and replays, so a saved replay or game must still read in later versions. The rule, written in crates/mesa/src/accion.rs: variants of Accion and Cambio are never changed or removed, and a new or changed one bumps VERSION_CATALOGO (currently 1), because a serde enum rejects a tag it does not know and the network and saved replays compare the version.

tests/catalogo.rs (los_eventos_de_cada_version_se_siguen_leyendo) reads every tests/catalogo/v1.json … v<VERSION_CATALOGO>.json and fails if one is missing or no longer parses. So, after bumping the version, write the new snapshot once with PB_CATALOGO_ESCRIBIR=1 cargo test -p mesa --test catalogo (about 120 random events of a two-player game plus a chat line, without the full-game restores) and never touch it again. Without that file the test fails, which is the reminder.

tests/catalogo/ is excluded from Biome (biome.json), like proxy-core/tests/compatibilidad, and so is src/stores/__tests__/fixtures.

The game table app layer and screen have their own tests (src-tauri/src/mesa/, protocol.rs::las_rutas_de_la_mesa, src/features/mesa/__tests__/): see Game Table.

Online play tests

  • src-tauri/src/red/pruebas.rs: in-process games over iroh without relays (local addresses), each player with an in-memory session: three players and 200 random plays end with the same hash; a cut guest comes back with the same game; a wrong code, another version or (as a player) a started game is refused; when the host drops, the next player by seat carries on by itself and the others find it, with two players the one left waits without playing, a host that loses its network stops and then joins the new host as a guest, and a spectator keeps watching with the new host (host handover without a server, red/sucesion.rs, with the time unit at 40 ms); spectators never see hidden identities, and only get the movement of what they can see; own images reach a guest through the host and a wrong hash is dropped; each player knows its latency and whether the path is direct; the host clock is worked out even if the local one is behind; a guest's ghost reaches the host and the others with its name; a player who leaves shows as absent and stops being so on return; the host keeps the hand of an absent player who does not decide; a kicked player is eliminated, cut off and cannot come back.
  • red/protocolo.rs: frame limits, the Hola first, every message round-trips, broken JSON and decks, oversized or broken datagrams, and the examples for other clients in src-tauri/src/red/fixtures/ (al-host.json, del-host.json, datagramas.json), which PB_FIXTURES=1 cargo test --lib escribir_fixtures rewrites.

The contract between app and room server

The room server (playgroups, invitations, rooms, host election; the game itself stays P2P) is a separate repository, cloned inside the app checkout at server/ and ignored by the app's git (.gitignore), so the app's Biome skips it too (vcs.useIgnoreFile). Its protocol v1 lives in server/docs/protocolo.md. Both sides keep example messages and each one checks the other's:

flowchart LR
    A["app: red/servidor/mensajes.rs"] -->|"writes (PB_FIXTURES=1)"| F["src-tauri/src/red/servidor/fixtures/al-servidor.json"]
    F -->|"server/test/contrato.test.ts"| S["server zod schemas (src/protocolo.ts)"]
    E["server/test/ejemplos/al-servidor.json, del-servidor.json"] -->|"contrato.test.ts"| S
    E -->|"los_ejemplos_del_servidor_se_entienden"| A
Loading
Side Test Checks
App red/servidor/mensajes.rs los_mensajes_de_la_app_son_los_de_los_fixtures One example of every message the app sends equals fixtures/al-servidor.json and reads back the same. After changing a message, rewrite it with PB_FIXTURES=1 cargo test --lib los_mensajes_de_la_app
App los_ejemplos_del_servidor_se_entienden Every message of server/test/ejemplos/del-servidor.json parses, writes back identical (no field lost), and there are 11 message types. Without server/ it prints a warning and passes
App red/servidor/cliente.rs crea_un_playgroup_invita_y_otra_identidad_entra Starts the real server with server/node_modules/.bin/tsx src/index.ts on a free port (PB_PUERTO) with an in-memory database (PB_DB=:memory:); two identities connect, one creates a playgroup and a one-use invitation, the other joins (a second use fails with caducada), a room is created and joined, and the playgroup notice arrives. Without server/node_modules it is skipped
App, ignored el_rele_solo_deja_pasar_a_quien_esta_en_el_servidor Against a deployment (PB_PRUEBA_SERVIDOR, PB_PRUEBA_RELE): wss:// through nginx, and the own relay only lets in a key connected to the server
Server test/contrato.test.ts The examples of test/ejemplos/ pass the zod schemas and there is one of every type; the app's al-servidor.json also passes and covers every type (skipped when the server is cloned on its own). pnpm protocolo:comprobar runs only this file

Because CI checks out only the app repository, server/ is not there and the app's server tests pass by skipping: the contract is only really checked on a machine with the server cloned (and pnpm install run in it, for cliente.rs).

Server tests

The server has its own scripts and no CI workflow: pnpm check is tsc --noEmit && vitest run && biome check ., pnpm test is vitest run (test/**/*.test.ts and src/**/*.test.ts, Node environment). It needs Node 22.13 or later (node:sqlite). Files:

File What
identidad.test.ts Sign-in by signature: good signature welcomed; another challenge's signature, a malformed key or a long nickname refused; another protocol version refused and three bad messages close; at most three connections per key (the fourth closes the oldest)
playgroups.test.ts Owner invites, members join by code, permissions; invitations expire and have a use cap (fake clock); limits of 20 live invitations and 100 members; codes read regardless of case, dashes, I/L and O
salas.test.ts Over real sockets: three join by a code and see the addresses, the eleventh does not fit; after a server restart the room comes back with the epoch the clients bring; the relay callout only lets in a connected key or one in a room; playgroups announce their rooms and who is online; the room creator kicks someone, who leaves and cannot return
presencia.test.ts Host migration (fake clock): a cut host is replaced with epoch 2 within 10 s; a short cut within the grace period changes nothing; one complaint about a live host changes nothing; a host that does not answer the probe is replaced; two complaints for 10 s replace it
eleccion.test.ts Host election: lowest worst RTT wins, a relayed path counts ×1.5, a phone with 30 ms loses to a desktop with 60 ms, missing rows count 1,000 ms, ties broken by mean then order
contrato.test.ts The contract above

The check scripts

scripts/idiomas.mjs (pnpm idiomas)

Guards the i18n contract: UI text is written in Spanish inside t("..."), and every dictionary (src/i18n/<code>/<area>.json, merged by cargar.ts) is keyed by the Spanish source string. Because t() silently falls back (language, then English, then the Spanish source), a missing translation compiles and renders fine and would only be visible with the app in that language; this script is the way to notice. It:

  1. Collects the requested strings: every double-quoted literal in t("...") and paraTraducir("...") under src/**/*.ts(x) (skipping bindings, node_modules, dot folders, *.test.ts(x) and src/i18n/ itself).
  2. Reads the language table from src/lib/idiomas.ts, which is code and so is transpiled with typescript. Then it loads each dictionary folder by parsing its .json files with JSON.parse (plural objects come through as real data), and scans their raw text for a key repeated in the same file, which JSON.parse would otherwise hide. Folders starting with _ (such as __tests__) are skipped.
  3. For every language except Spanish, fails when: its folder is missing; a requested string has no entry; a key is in two areas (the merge would let the last win silently) or twice in the same file; an area file of the English dictionary is missing; the {x} placeholders differ from the Spanish key (also in each plural form); a plural object sits on a key without {n}; or a plural lacks, or adds, a category of that language's Intl.PluralRules(locale). A folder that is not a language of the table is also an error. Spanish (the source, es/) is checked by its own rule: it has the same areas and keys as English, and each value must equal its key.
  4. Warns (without failing) about entries nobody requests and values identical to English (ignoring brand names in the script's IDENTICAS_PERMITIDAS and the keys listed in an optional src/i18n/<code>/identicas.json). --detalle prints each warning; by default only the count shows.

Its pure parts (leerPedidas, leerTabla, leerDiccionarios, comprobar) are exported and tested in scripts/__tests__/idiomas.test.mjs.

Gotchas: only double-quoted literals directly inside t(...) are seen; a t( with a template literal or a variable is not checked. paraTraducir("...") (identity function, src/components/language-provider.tsx) exists to mark texts that live in data tables and are translated elsewhere (for example TEXTOS in src/lib/errores.ts). See Internationalization.

scripts/sin-title-nativo.mjs (pnpm check:title)

Fails if a .tsx under src/ sets title= on a lowercase HTML tag (except iframe/frame, where it is the accessible name) or on a component that forwards its props to the DOM (PASAN_AL_DOM: Table*, Badge, Label, Button). Reason: on Linux the native title tooltip is drawn by GTK with system colours and looks like a foreign window; swept clean by hand it came back within a day. The right tool is Tooltip (src/components/ui/tooltip.tsx) or WithTooltip. A runtime backstop, TooltipsNativos (src/components/tooltips-nativos.tsx), converts any that slip through. Own components with a title or titulo prop (for example SettingCard) are not flagged because they do not render a native attribute.

Knip (pnpm check:knip)

Config knip.json: entries are scripts/**/*.{ts,mjs,js} and src/**/*.test.{ts,tsx}; project files src/**/*.{ts,tsx} and scripts/**/*.{ts,mjs,js}. Unused exports, types, enum members and duplicates are off (rules), so Knip is about unused files and dependencies. src/bindings/** is ignored; tw-animate-css and tailwindcss are whitelisted dependencies (consumed from CSS), and the rustfmt binary (lint-staged config) and inkscape (scripts/emoji-noto.mjs) are whitelisted binaries.

Git hook (local safety net)

.husky/pre-commit runs lint-staged (auto-fix staged files), then, depending on what is staged, tsc --noEmit + idiomas + check:title for TypeScript and check:clippy for Rust. It is intentionally fast; vitest, knip, cargo test and check:biome over the whole repo are left to pnpm check and CI. Details in Development Setup.

GitHub Actions

ci.yml (name ci)

Triggers: push to main and every pull_request. One job, checks, on ubuntu-22.04:

  1. actions/checkout@v4.
  2. Install Tauri's Linux libraries (libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf) with awalsh128/cache-apt-pkgs-action@v1, which caches the .deb files: the Ubuntu mirror sometimes drops to about 50 kB/s and those 55 MB took up to 20 minutes, while the rest of CI takes about 3. (release.yml still uses a plain apt-get install.)
  3. pnpm/action-setup@v4 (version from packageManager), actions/setup-node@v4 (Node 22, pnpm cache).
  4. dtolnay/rust-toolchain@stable with clippy and rustfmt, and swatinem/rust-cache@v2 (workspace src-tauri -> target).
  5. pnpm install --frozen-lockfile.
  6. pnpm check, pnpm check:rust-fmt, pnpm check:clippy, pnpm test:rust.
  7. Step "Sin OpenSSL en el árbol" (the workflow text is Spanish): ! cargo tree --manifest-path src-tauri/Cargo.toml -i openssl 2>/dev/null passes only when the crate is absent from the tree. Why: networking is reqwest with rustls (default-features = false); OpenSSL would complicate cross-platform builds and installers.

Because the toolchain is latest stable (not pinned), a new Rust release can introduce new clippy warnings and fail CI without any code change; the maintainer runs the latest-stable clippy before sending changes.

release.yml (name release)

Manual only (workflow_dispatch), deliberately not triggered by tags: publishing the draft creates the tag, and a tag trigger rebuilt everything again. Permissions contents: write. A matrix with fail-fast: false:

Platform args
ubuntu-22.04 none
windows-latest none
macos-latest --target aarch64-apple-darwin
macos-latest --target x86_64-apple-darwin

Each leg: checkout, Linux libs (Linux only), pnpm, Node 22, Rust stable (with both Apple targets on macOS), cache, pnpm install --frozen-lockfile, then tauri-apps/tauri-action@v0 with tagName: v__VERSION__, releaseDraft: true, and the signing secrets. It does not run the test gates: scripts/release.mjs waits for the CI run of the release commit before launching it. Full detail in Releases and Auto-Updates.

Where to add tests

Changing Add a test in
Pure logic (parsing, geometry, imaging, stats, designer) src-tauri/crates/proxy-core/src/<module>.rs (#[cfg(test)]), or a golden if it is visual
A visual change in card rendering Update goldens deliberately (see above)
A Tauri command or store/DB behaviour The module's #[cfg(test)] in src-tauri/src/..., using a temp AppState
Network parsers A saved fixture next to the module, plus an #[ignore] live test
A game table action (Accion/Cambio variant) Its case in crates/mesa/tests/acciones.rs (the exhaustive match will not compile without it), plus a VERSION_CATALOGO bump and its tests/catalogo/v<N>.json
A room server message The app's ejemplos() in red/servidor/mensajes.rs and PB_FIXTURES=1 to rewrite al-servidor.json; on the server side, test/ejemplos/ and its schema (contrato.test.ts checks there is one example of every type)
Frontend store/lib logic __tests__/ next to the code, mocking @/lib/ipc
A new UI string An entry in the matching src/i18n/<code>/<area>.json of every language (the gate will tell you)

See also

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally