Тестирование и инфраструктура
Обзор
Kontinuum использует четыре уровня тестирования:
| Уровень | Технология | Что тестирует | Команда |
|---|---|---|---|
| Backend unit | cargo test | Rust-модули (managers, crypto, DB) | nx run backend:test |
| Frontend unit | Vitest | Pinia stores, composables, утилиты | nx run frontend:test |
| E2E | Playwright | UI flows (headless backend + Vite) | nx run frontend:e2e |
| Integration | node:test + HTTP client | Backend API, P2P, S3, sync | nx run integration-tests:test |
Backend unit тесты
nx run backend:test # Unit тесты
nx run backend:test:all # Все тесты с features
nx run backend:test:coverage # CoverageProperty-Based Testing (proptest)
В дополнение к детерминированным unit-тестам, проект использует proptest для проверки инвариантов на случайных данных:
| Модуль | Тест | Свойство |
|---|---|---|
types/mod.rs | identity_id_roundtrip | IdentityId::new(s).to_string() == s |
types/mod.rs | device_id_roundtrip | DeviceId::new(s).to_string() == s |
types/mod.rs | space_id_serde_roundtrip | json(SpaceId) → parse → original |
sharing/types.rs | sharing_message_ping_roundtrip | json(Ping) → parse → json == original |
sharing/types.rs | sharing_message_ack_roundtrip | json(Ack) → parse → json == original |
pairing/types.rs | pairing_message_discovery_roundtrip | json(Discovery) → parse → json == original |
sharing/state_sync.rs | state_hash_order_independent | hash(a,b) == hash(b,a) (determinism) |
Цели покрытия
| Модуль | Цель |
|---|---|
| Crypto/Vault | 90%+ |
| Identity/Device managers | 80%+ |
| P2P network | 70%+ |
| Commands | 60%+ |
Frontend unit тесты
nx run frontend:test # Vitest
nx run frontend:test:ui # UI
nx run frontend:test:coverage # Coverage
nx run frontend:test:watch # Watch режимЦели покрытия
| Слой | Цель |
|---|---|
| Pinia stores | 90%+ |
| Composables | 80%+ |
| Components | 70%+ |
| Utils | 85%+ |
E2E тесты (Playwright)
nx run frontend:e2e # Headless (автозапуск backend)
nx run frontend:e2e:headed # С видимым браузером
nx run frontend:e2e:debug # Пошаговая отладка
nx run frontend:e2e -- e2e/app-load.spec.ts # Конкретный тест
nx run frontend:e2e -- --last-failed # Только упавшие тестыPlaywright автоматически собирает headless backend и запускает Vite в e2e-режиме.
Просмотр trace упавшего теста:
npx playwright show-trace test-results/<test-folder>/trace.zipИнтеграционные тесты
Самый развитый уровень тестирования. 22 сценария, 110+ тестов, ~18 секунд.
Архитектура
Headless бекенды запускаются на хосте как child processes (mDNS работает нативно), observability стек — в Docker:
Почему бекенды на хосте, а не в Docker?
- mDNS discovery требует нативной сети — в Docker bridge network mDNS не работает
- P2P pairing через libp2p использует mDNS для обнаружения пиров
- Headless binary собирается с
--features headless,test-loopback(см. примечание о test-loopback)
Quick Start
# Полный запуск: cargo build + observability + тесты
nx run integration-tests:test:full
# Без сборки (headless binary уже собран)
nx run integration-tests:test
# С очисткой Docker после тестов
nx run integration-tests:test:clean
# Без завершения бекендов (для инспекции после тестов, Ctrl+C для выхода)
nx run integration-tests:test:inspect
# Только observability стек
nx run integration-tests:observability:up
nx run integration-tests:observability:downРучной запуск
cd infra/testing
npx tsx src/runner.ts [флаги]
# Один сценарий с подробными логами (без Docker)
npx tsx src/runner.ts --no-build --no-observability --scenario pairing --verbose
# Только S3 и sync тесты
npx tsx src/runner.ts --no-build --scenario s3
npx tsx src/runner.ts --no-build --scenario sync
# Только бекенды без тестов (для ручной инспекции или Android pairing)
npx tsx src/runner.ts --no-build --no-observability --no-tests --verbose
# Инспекция бекендов после тестов (Ctrl+C для остановки)
npx tsx src/runner.ts --no-build --keep-alive --scenario sharing --verboseФлаги runner.ts
| Флаг | Описание |
|---|---|
--no-build | Пропустить cargo build (binary уже собран) |
--no-observability | Пропустить Docker observability стек |
--scenario <name> | Запустить один сценарий (например --scenario pairing) |
--clean | Остановить observability стек после тестов |
--verbose | Вывод логов бекендов в stdout |
--no-tests | Пропустить тесты (подразумевает --keep-alive) |
--keep-alive | Не останавливать бекенды после тестов (Ctrl+C для выхода) |
Тестовые сценарии
| # | Сценарий | Описание | Бекенды |
|---|---|---|---|
| 01 | identity | Identity CRUD (идемпотентный) | 1, 2, 3 |
| 02 | device | Device management | 1 |
| 03 | vault | Vault: PIN, lock/unlock, recovery phrase | 1 |
| 04 | s3 | S3 операции с MinIO | 1 |
| 05 | pairing | P2P pairing через mDNS (bidirectional exchange) | 1, 2 |
| 06 | sync | File sync через S3/MinIO (cross-backend) | 1, 2 |
| 07 | recovery-cross | Vault recovery на другом бекенде | 1, 2 |
| 08 | full-lifecycle | E2E: identity → vault → pairing → S3 → sync | 1, 2, 3 |
| 09 | stronghold | IOTA Stronghold: init, secrets, clear | 1 |
| 10 | signing | Ed25519 подпись и верификация | 1 |
| 11 | filesystem | Файловые операции: read-dir, file-info | 1 |
| 13 | vault-extended | PIN management, recovery, negative paths | 1 |
| 14 | debug | Debug endpoints: registry, logs, metrics | 1 |
| 15 | multi-pairing | Sequential pairing of 5 devices | 1–5 |
| 16 | spaces | Spaces CRUD: create, add files, list, delete | 1 |
| 17 | sharing | P2P sharing: publish, private share, mDNS | 1, 2 |
| 18 | publish | Space publishing to all peers | 1–5 |
| 19 | sharing-access | Encrypted sharing access control (3 identities) | 1–5 |
| 20 | large-file-sync | Sync больших файлов: 16/128/512/1024 MB | 1, 2 |
| 21 | remote-directory | Remote directory listing через P2P sharing | 1, 2 |
| 22 | peer-reconnect | PeerTracker: detect offline, reconnect, recover | 4, 5 |
Cargo feature flags
Headless binary собирается с feature flags, влияющими на поведение:
| Feature | Описание |
|---|---|
headless | Включает HTTP API (Axum) вместо Tauri GUI + fast-crypto |
fast-crypto | Минимальные KDF-параметры Argon2 (быстрые тесты, НЕ для production) |
test-loopback | mDNS peer connections через 127.0.0.1 (страховка для CI/нестандартных сред) |
# Сборка для интеграционных тестов (все тестовые features)
cargo build --features headless,test-loopback --bin kontinuum-headless
# Сборка без test-loopback (работает на обычной dev-машине с сетью)
cargo build --features headless --bin kontinuum-headless
# Сборка для реального headless деплоя (без test features)
cargo build --features headless --bin kontinuum-headless --releasetest-loopback — при обнаружении пиров через mDNS, вместо реального IP (например 192.168.1.10) используется 127.0.0.1. На обычной dev-машине с рабочей сетью (Wi-Fi/Ethernet) эта фича не обязательна — ОС маршрутизирует пакеты к собственному LAN IP локально, и несколько бекендов на одном хосте подключаются друг к другу по реальному IP. Фича полезна как страховка в средах, где реальный IP может быть недоступен: CI-раннеры, Docker-контейнеры, VM без LAN-интерфейса.
Компоненты
runner.ts
Единый entry point. Последовательность выполнения:
- Генерация
targets.jsonдля VMAgent (на основеINSTANCE_CONFIGS) cargo build --features headless,test-loopback(если не--no-build)docker compose upдля observability (если не--no-observability)- Запуск 5 headless backend процессов с уникальными портами и DB
- Health check — ожидание готовности всех бекендов
- Запуск тестов через
node:testrunner - Flush логов в Loki, остановка бекендов
- Опционально
docker compose down(если--clean)
process-manager.ts
Управляет child processes бекендов. Каждый бекенд получает env-переменные:
HTTP_PORT— порт HTTP APIMETRICS_PORT— порт Prometheus метрикDB_PATH— путь к SQLite базе (tmpdir)RUST_LOG— уровень логированияINSTANCE_ID— ID экземпляра
client.ts
Типизированный HTTP-клиент. Использует COMMAND_ROUTES из frontend transport layer для синхронности маршрутов с backend router. Экспортирует:
BackendClient— класс с методами для всех API endpointscreateClients()— создаёт клиенты изRUNTIME.configsgetDbPath(id)— путь к DB конкретного бекенда
loki-pusher.ts
Собирает stdout/stderr бекендов и пушит в Loki через POST /loki/api/v1/push. Батчинг: 50 строк или 1 секунда.
Написание нового сценария
Создай файл infra/testing/src/scenarios/NN-name.test.ts. Runner обнаружит его автоматически по паттерну *.test.ts.
import { describe, it, before } from 'node:test';
import assert from 'node:assert/strict';
import { createClients } from '../client';
const clients = createClients();
const [backend1] = clients;
describe('NN — My scenario', () => {
before(async () => {
assert.ok(await backend1.health(), 'backend-1 should be healthy');
// ВАЖНО: vault должен быть разблокирован ДО создания identity.
// При создании identity сохраняется Ed25519 signing key в vault.
// Если vault заблокирован, ключ держится в памяти до первого unlock.
const unlocked = await backend1.isUnlocked();
if (!unlocked) {
const phrase = await backend1.generateRecoveryPhrase();
await backend1.setupVaultWithRecovery(phrase.words, '1234');
}
});
it('does something', async () => {
// test logic
});
});Ключевые утилиты:
createClients()— создаёт типизированные HTTP-клиенты для всех 5 бекендовgetDbPath(id)— путь к временной SQLite БД бекендаwriteTestFile(dbPath, name, content)— создание тестовых файловsleep(ms)/retry(fn, opts)/poll(fn, opts)— утилиты изhelpers.ts
Паттерн: bidirectional pairing
submitPairingRequest() блокируется до получения ответа от host'а через P2P. Поэтому submit и approve нужно запускать конкурентно:
// submitPairingRequest блокируется до host approval — запускаем параллельно
const [submitResponse, approval] = await Promise.all([
joiner.submitPairingRequest(target, 'Device-joiner', 'desktop'),
sleep(1000).then(() => host.approvePairing(target.token)),
]);
assert.ok(approval.approved);
assert.ok(submitResponse.approved, 'joiner gets approved response via P2P');
assert.ok(submitResponse.identity, 'joiner receives identity data');
// Joiner applies identity received through P2P
if (submitResponse.identity) {
await joiner.applyPairedIdentity(submitResponse.identity);
}sleep(1000) даёт joiner'у время подключиться и отправить request до вызова approve.
Observability
Grafana (http://localhost:3000)
- Логин: admin / admin (или anonymous viewer)
- Datasources: VictoriaMetrics, Loki (провиженены автоматически)
- Dashboards: Overview, Backend Operations, Logs, P2P Network, Sync, Vault Security, Spaces & Sharing
Полное описание метрик и Grafana дашбордов: Internals: Observability.
Полезные запросы
# Loki: логи конкретного бекенда
{job="kontinuum-backend", instance="backend-1"}
# Loki: ошибки во всех бекендах
{job="kontinuum-backend"} |= "ERROR"
# VictoriaMetrics: метрики vault
kontinuum_vault_unlock_attempts_total
kontinuum_vault_unlock_duration_secondsКлючевые файлы
| Файл | Назначение |
|---|---|
infra/testing/src/runner.ts | CLI entry point интеграционных тестов |
infra/testing/src/client.ts | Типизированный HTTP-клиент для backend API |
infra/testing/src/process-manager.ts | Запуск/остановка headless backend процессов |
infra/testing/src/loki-pusher.ts | Пересылка логов в Loki HTTP API |
infra/testing/src/sse-listener.ts | SSE-клиент для backend event stream |
infra/testing/src/fs-helpers.ts | Файловые операции (замена docker exec) |
infra/testing/src/docker.ts | Docker Compose управление (observability) |
infra/testing/src/helpers.ts | Утилиты (sleep, retry, etc.) |
infra/testing/src/scenarios/ | 22 тестовых сценария |
infra/testing/docker-compose.yml | Observability stack + MinIO |
infra/testing/project.json | Nx targets для integration-tests |
infra/shared/grafana/ | Grafana dashboards, datasources |