Skip to content

Development

yuriy edited this page Oct 19, 2026 · 5 revisions

Development

This page is for people who change Sextant. To just build it, see Building from source.

The internal project notes (AGENTS.md, CONTEXT.md, docs/architecture.md and the decision records in docs/adr/) are written in English. They are the detailed source for architecture and conventions; this page summarizes what you need to get going.

Layout

Path What
crates/sextant-core Everything about clusters (kube-rs): kubeconfig, connection, discovery, watches, actions, logs, exec, port forwarding, metrics, Helm, Argo CD, settings, the agent access policy. No Tauri dependency, so it can back another front end later.
src-tauri The desktop application (crate sextant): Tauri commands, events, the local terminal, window handling, and the MCP server (src-tauri/src/mcp/, src-tauri/src/mcp_proxy.rs).
src The interface: React + TypeScript + Vite. src/api is the only bridge to the backend.
src/features/<name> Vertical features, picked up automatically from their index.ts.
src/locales/<lang>/<namespace>.json Interface texts: English (en/) is the source, every other language has the same keys. A new language is a new directory. Rules: src/locales/TRANSLATING.md.
scripts/ The test cluster, fixtures, and checks used by CI.
e2e/ End-to-end tests of the whole window (WebdriverIO + tauri-driver).
docs/ Architecture, decision records, the README screenshot.

Dependencies point one way: src/ (React) → src-tauri/ (the sextant crate) → crates/sextant-core/.

Conventions worth knowing

  • Names in code are English.
  • Every interface string is a key in src/locales/<lang>/<namespace>.json; string literals in JSX are forbidden by eslint. A new or changed string goes into every language in the same change: src/locales/locales.test.ts fails otherwise (see Translations).
  • @tauri-apps/* may be imported only inside src/api/.
  • Types that cross the boundary are #[serde(rename_all = "camelCase")] plus ts-rs; the generated TypeScript in src/api/generated/ is never edited by hand: run npm run gen:types and commit the result. Command and event names are written by hand in src-tauri/src/lib.rs, src/api/commands.ts and src-tauri/tests/commands.rs.
  • Anything long-lived (a subscription, a stream, a session, a port forward) must live in AppState so that disconnecting a cluster and closing the window stop it.
  • All kube errors go through error::kube_error, which removes exec-plugin secrets; Secret values are removed in the core and sent to the interface only on explicit request.
  • sextant-core must not depend on tauri or portable-pty (checked by scripts/check-core-deps.mjs).
  • Code must work on Linux, macOS and Windows; CI runs on all three.

Translations

The interface is translated into 13 languages besides English (see Translating for the list and how to report a bad translation). For code changes this means:

  • Add the English text to src/locales/en/<namespace>.json and the same key to every other src/locales/<lang>/ directory. npx vitest run src/locales/locales.test.ts checks the files, keys, {{placeholders}}, $t(…) nesting, tag-like text, empty texts and plural forms of every language against English.
  • Counts use plural keys: English _one and _other; every other language exactly its CLDR forms (_one/_few/_many/_other for ru, uk, pl; _one/_many/_other for fr, es, it, pt-BR; _other for zh-CN, zh-TW, ja, ko). Every form keeps {{count}}.
  • One key per whole sentence: never glue keys or count + " pods" together; a list goes into one placeholder, joined by formatList from src/i18n/format.ts, which also formats dates and numbers in the interface language.
  • Kubernetes kinds and API terms stay English in every language; the glossary in src/locales/TRANSLATING.md fixes the common words.

Corrections from native speakers are welcome as issues or pull requests that change the JSON files.

Checks

Run from the repository root:

export PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH"
npm ci
npm run build          # a clean clone needs the frontend built once: the crate embeds dist/
npm run check          # everything CI runs

npm run check runs: a case-collision check of file names, the version check, rustfmt, clippy -D warnings, the core-dependency check, the Rust tests, the generated-types check, tsc (including e2e/), eslint and vitest. To keep target/ small: CARGO_PROFILE_DEV_DEBUG=0 CARGO_INCREMENTAL=0 npm run check.

Smaller steps:

cargo test -p sextant-core <name>      # one Rust test (or `-p sextant`)
npx vitest run <file>                  # one frontend test file, e.g. src/layout
npm run gen:types                      # regenerate src/api/generated from the Rust types
npm run check:version                  # the version is the same everywhere

How the code is tested

  1. Core, without a cluster: unit tests next to the code; tests that need HTTP use a fake API server (crates/sextant-core/src/fake_api.rs, test builds only).
  2. Core, against a real cluster: crates/sextant-core/tests/<feature>.rs against the kind test cluster below.
  3. Commands: src-tauri/tests/commands.rs calls every Tauri command on a mock runtime, isolated from your KUBECONFIG and ~/.kube/config.
  4. Interface: Vitest with Testing Library, with the backend replaced by mockBackend from src/api. Accessibility is part of it: src/test/a11y.test.tsx and src/test/a11y-pages.test.tsx run axe over the main screens and every registered page (axeViolations from src/test/axe.ts; add a check for a new screen), src/test/keyboard.test.tsx covers the keyboard model, and src/styles/contrast.test.ts computes the WCAG contrast of the theme colours (a new colour is a token in src/styles/global.css, in both themes). Measurements for very large clusters: see Large clusters.
  5. The whole window: e2e/.

Integration tests that need a cluster are skipped without SEXTANT_TEST_KUBECONFIG and then count as passed: a green cargo test --workspace without a cluster says nothing about the cluster.

The test cluster

A local kind cluster named sextant-test with fixtures. Needs kind, kubectl and Docker.

scripts/test-cluster.sh up                 # cluster + metrics-server + fixtures
scripts/test-cluster.sh up --prometheus    # also Prometheus in `monitoring`
scripts/test-cluster.sh up --gitops        # also Flux and Argo CD with fixtures (flags combine)
scripts/test-cluster.sh up --gateway       # also the Gateway API CRDs and fixtures (see Gateway API)
scripts/test-cluster.sh kubeconfig         # path to its admin kubeconfig
scripts/test-cluster.sh kubeconfig limited # kubeconfig of a ServiceAccount that cannot read Secrets
scripts/test-cluster.sh down               # delete the cluster

up --gateway needs openssl for a self-signed certificate; what it sets up is described in Gateway API. The script never touches your ~/.kube/config; its files live in ~/.cache/sextant-test (override with SEXTANT_TEST_DIR).

Integration tests of the core against that cluster:

SEXTANT_TEST_KUBECONFIG=$(scripts/test-cluster.sh kubeconfig) \
  cargo test -p sextant-core --tests -- --test-threads=1

The GitOps tests (Flux, Argo CD) also need SEXTANT_TEST_GITOPS=1 and a cluster started with up --gitops:

SEXTANT_TEST_GITOPS=1 SEXTANT_TEST_KUBECONFIG=$(scripts/test-cluster.sh kubeconfig) \
  cargo test -p sextant-core --test gitops --test gitops_flux --test gitops_argocd --test gitops_argocd_diff -- --test-threads=1

A load fixture (150 CRDs and 500 pods in the namespace sextant-load) backs one timing test and is skipped without it: scripts/fixtures/load/load.sh apply, and scripts/fixtures/load/load.sh delete afterwards. Tests that stop the kind container run only with SEXTANT_TEST_DOCKER_RESTART=1.

End-to-end tests

npm run e2e starts the debug app through tauri-driver and walks through the features against sextant-test: connecting, the pod table, details, logs, a shell, YAML editing, scaling, port forwarding, a Helm release, the overview, Settings and the narrow layout. It also takes the README screenshot (docs/screenshot.png).

cargo install tauri-driver --locked
sudo apt install webkit2gtk-driver        # Linux: WebKitWebDriver
scripts/test-cluster.sh up --prometheus
npm run e2e

The app runs with throwaway config and data folders and only the test cluster's kubeconfig. Everything the tests create lives in the namespace sextant-e2e, deleted afterwards. A failed scenario leaves a screenshot in target/e2e/. SEXTANT_E2E_SKIP_BUILD=1 reuses the last build (target/debug/sextant).

Known quirks: the config fixes tauri-driver's port at 4444 (the native port defaults to 4445), and the driver fails to start if they are busy; tauri-driver --port 4447 --native-port 4448 helps. WebKitWebDriver drops repeated keys and spaces in browser.keys and mistypes / and :; use the typeText helper in e2e/app.ts.

Environment variables

Variable Used for
SEXTANT_TEST_KUBECONFIG Admin kubeconfig of the test cluster; turns on the integration tests and e2e
SEXTANT_TEST_KUBECONFIG_LIMITED Kubeconfig of the limited ServiceAccount (default: kubeconfig-limited next to the admin one)
SEXTANT_TEST_DIR Folder of the test cluster files (default ~/.cache/sextant-test)
SEXTANT_TEST_DOCKER_RESTART Lets tests stop and start the kind container
SEXTANT_TEST_GITOPS 1 turns on the GitOps integration tests
SEXTANT_E2E_SKIP_BUILD e2e reuses the existing debug binary
CARGO_PROFILE_DEV_DEBUG, CARGO_INCREMENTAL Turn off debug info and incremental builds to keep target/ small

The MCP server in the code

For the user's view see MCP server. Where things are:

What Where
The access policy: types, decide, the sets (preset_rules), with no I/O crates/sextant-core/src/access.rs
Operations used by the tools: resources::list, resources::events, logs::tail, exec::run_command crates/sextant-core/src/
The server (Streamable HTTP on 127.0.0.1, rmcp and axum), the HTTP checks, the token, the guard that applies rules and asks for confirmation, the audit log, the tools src-tauri/src/mcp/
sextant mcp (the stdio bridge) src-tauri/src/mcp_proxy.rs, started from src-tauri/src/main.rs
Commands for the interface src-tauri/src/commands/mcp.rs, src-tauri/src/commands/access.rs
Settings section, confirmation dialog, activity log src/features/mcp/
AI agent access in the cluster settings src/features/agent-access/

The access rules are the single source of truth in the core (the interface asks the backend for the rules of each set instead of copying them). A new tool must check its rights through the call's Guard right before it acts, and must not put Secret values, YAML or command output into the journal.

Versions and releases

The application version is written in five places: Cargo.toml ([workspace.package]), Cargo.lock, src-tauri/tauri.conf.json, package.json and package-lock.json. Bump all of them together and run npm run check:version; node scripts/check-version.mjs --tag vX.Y.Z also checks it against a release tag.

Pushing a tag v* equal to that version (for example v0.2.0) runs release.yml. It refuses a tag that does not match the version, builds installers for Linux (.deb, .AppImage), macOS (universal .dmg) and Windows (.msi), signs the update bundles, builds latest.json for the in-app updater and publishes a GitHub Release only if every platform is there. The release notes list the closed issues of the milestone named like the tag. To rebuild an existing tag without rewriting it: gh workflow run release.yml -f tag=vX.Y.Z (Actions → Release → Run workflow); a rebuild of an older tag does not become the latest release.

Signing needs the repository secrets TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD, the private half of the key whose public half is plugins.updater.pubkey. Losing the private key means installed copies will not accept updates. Installers are not signed with operating-system certificates.

CI

ci.yml runs npm run check on Linux, macOS and Windows for every push and pull request to main (and manually). Things that break only on macOS or Windows: file names that differ only in case, GNU-only tools in scripts (stat -c), /proc, path rules of the host, and IPC test URLs (tauri://localhost versus http://tauri.localhost).

Plans live in GitHub Issues and milestones.

Clone this wiki locally