Skip to content

Тестирование и инфраструктура

Обзор

Kontinuum использует четыре уровня тестирования:

УровеньТехнологияЧто тестируетКоманда
Backend unitcargo testRust-модули (managers, crypto, DB)nx run backend:test
Frontend unitVitestPinia stores, composables, утилитыnx run frontend:test
E2EPlaywrightUI flows (headless backend + Vite)nx run frontend:e2e
Integrationnode:test + HTTP clientBackend API, P2P, S3, syncnx run integration-tests:test

Backend unit тесты

bash
nx run backend:test                   # Unit тесты
nx run backend:test:all               # Все тесты с features
nx run backend:test:coverage          # Coverage

Property-Based Testing (proptest)

В дополнение к детерминированным unit-тестам, проект использует proptest для проверки инвариантов на случайных данных:

МодульТестСвойство
types/mod.rsidentity_id_roundtripIdentityId::new(s).to_string() == s
types/mod.rsdevice_id_roundtripDeviceId::new(s).to_string() == s
types/mod.rsspace_id_serde_roundtripjson(SpaceId) → parse → original
sharing/types.rssharing_message_ping_roundtripjson(Ping) → parse → json == original
sharing/types.rssharing_message_ack_roundtripjson(Ack) → parse → json == original
pairing/types.rspairing_message_discovery_roundtripjson(Discovery) → parse → json == original
sharing/state_sync.rsstate_hash_order_independenthash(a,b) == hash(b,a) (determinism)

Цели покрытия

МодульЦель
Crypto/Vault90%+
Identity/Device managers80%+
P2P network70%+
Commands60%+

Frontend unit тесты

bash
nx run frontend:test                  # Vitest
nx run frontend:test:ui               # UI
nx run frontend:test:coverage         # Coverage
nx run frontend:test:watch            # Watch режим

Цели покрытия

СлойЦель
Pinia stores90%+
Composables80%+
Components70%+
Utils85%+

E2E тесты (Playwright)

bash
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 упавшего теста:

bash
npx playwright show-trace test-results/<test-folder>/trace.zip

Интеграционные тесты

Самый развитый уровень тестирования. 22 сценария, 110+ тестов, ~18 секунд.

Архитектура

Headless бекенды запускаются на хосте как child processes (mDNS работает нативно), observability стек — в Docker:

plantuml Diagram

Почему бекенды на хосте, а не в Docker?

  • mDNS discovery требует нативной сети — в Docker bridge network mDNS не работает
  • P2P pairing через libp2p использует mDNS для обнаружения пиров
  • Headless binary собирается с --features headless,test-loopback (см. примечание о test-loopback)

Quick Start

bash
# Полный запуск: 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

Ручной запуск

bash
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 для выхода)

Тестовые сценарии

#СценарийОписаниеБекенды
01identityIdentity CRUD (идемпотентный)1, 2, 3
02deviceDevice management1
03vaultVault: PIN, lock/unlock, recovery phrase1
04s3S3 операции с MinIO1
05pairingP2P pairing через mDNS (bidirectional exchange)1, 2
06syncFile sync через S3/MinIO (cross-backend)1, 2
07recovery-crossVault recovery на другом бекенде1, 2
08full-lifecycleE2E: identity → vault → pairing → S3 → sync1, 2, 3
09strongholdIOTA Stronghold: init, secrets, clear1
10signingEd25519 подпись и верификация1
11filesystemФайловые операции: read-dir, file-info1
13vault-extendedPIN management, recovery, negative paths1
14debugDebug endpoints: registry, logs, metrics1
15multi-pairingSequential pairing of 5 devices1–5
16spacesSpaces CRUD: create, add files, list, delete1
17sharingP2P sharing: publish, private share, mDNS1, 2
18publishSpace publishing to all peers1–5
19sharing-accessEncrypted sharing access control (3 identities)1–5
20large-file-syncSync больших файлов: 16/128/512/1024 MB1, 2
21remote-directoryRemote directory listing через P2P sharing1, 2
22peer-reconnectPeerTracker: detect offline, reconnect, recover4, 5

Cargo feature flags

Headless binary собирается с feature flags, влияющими на поведение:

FeatureОписание
headlessВключает HTTP API (Axum) вместо Tauri GUI + fast-crypto
fast-cryptoМинимальные KDF-параметры Argon2 (быстрые тесты, НЕ для production)
test-loopbackmDNS peer connections через 127.0.0.1 (страховка для CI/нестандартных сред)
bash
# Сборка для интеграционных тестов (все тестовые 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 --release

test-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. Последовательность выполнения:

  1. Генерация targets.json для VMAgent (на основе INSTANCE_CONFIGS)
  2. cargo build --features headless,test-loopback (если не --no-build)
  3. docker compose up для observability (если не --no-observability)
  4. Запуск 5 headless backend процессов с уникальными портами и DB
  5. Health check — ожидание готовности всех бекендов
  6. Запуск тестов через node:test runner
  7. Flush логов в Loki, остановка бекендов
  8. Опционально docker compose down (если --clean)

process-manager.ts

Управляет child processes бекендов. Каждый бекенд получает env-переменные:

  • HTTP_PORT — порт HTTP API
  • METRICS_PORT — порт Prometheus метрик
  • DB_PATH — путь к SQLite базе (tmpdir)
  • RUST_LOG — уровень логирования
  • INSTANCE_ID — ID экземпляра

client.ts

Типизированный HTTP-клиент. Использует COMMAND_ROUTES из frontend transport layer для синхронности маршрутов с backend router. Экспортирует:

  • BackendClient — класс с методами для всех API endpoints
  • createClients() — создаёт клиенты из RUNTIME.configs
  • getDbPath(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.

typescript
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 нужно запускать конкурентно:

typescript
// 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.tsCLI 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.tsSSE-клиент для backend event stream
infra/testing/src/fs-helpers.tsФайловые операции (замена docker exec)
infra/testing/src/docker.tsDocker Compose управление (observability)
infra/testing/src/helpers.tsУтилиты (sleep, retry, etc.)
infra/testing/src/scenarios/22 тестовых сценария
infra/testing/docker-compose.ymlObservability stack + MinIO
infra/testing/project.jsonNx targets для integration-tests
infra/shared/grafana/Grafana dashboards, datasources