248 lines
13 KiB
Markdown
248 lines
13 KiB
Markdown
# Testing strategy
|
||
|
||
Tests run locally in containers and never inspect or mutate live x1, x2,
|
||
lithium, or Titan services. Host tooling follows `playground/ENV.md`: Docker and
|
||
Compose provide runtimes/build tools and write generated files as UID/GID
|
||
`1001:1001`.
|
||
|
||
## Current happy-path coverage
|
||
|
||
The initial happy-path pass is tracked by executable capability rather than by
|
||
declaring an unfinished phase complete:
|
||
|
||
As of 2026-07-23, the full control/bot suite passes 103 tests, the full client
|
||
suite passes 60 tests, and a clean five-project Compose run passes automatic
|
||
2×2 route creation plus archive → covered eviction → unarchive with SHA-256
|
||
verification at both materialized targets.
|
||
|
||
| Capability | Unit/contract | Component/E2E |
|
||
| --- | --- | --- |
|
||
| Registration, heartbeat, replay, snapshots | Covered | Control/client integration covered |
|
||
| On-demand inventory and content trees | Covered | WebSocket integration covered |
|
||
| Automatic pairwise route provisioning | Covered | Local 2×2 topology covered |
|
||
| Manifest/ready-marker publication | Covered | Receiver filesystem round trip covered |
|
||
| Hardlink, reflink, sparse-copy fallback | Covered | Local filesystem round trip covered |
|
||
| Existing same-size target reuse | Covered | Local filesystem round trip covered |
|
||
| qB stopped add, selection, guarded recheck, start/resume, entry-only delete | Covered for 4.x/5.x semantics | Current pinned service covered |
|
||
| Syncthing scoped rescan and completion proof | Covered | Real route transfer covered |
|
||
| Transfer command state machine and qB verification | Covered | Archive and unarchive covered |
|
||
| Archive/unarchive placement commit | Covered | Both directions covered |
|
||
| Cache eviction | Coverage union, inode/path safety, idempotency covered | Real retained-source eviction covered |
|
||
| Telegram archive-control flows | Persistence, nonce, stale callback, stalled rendering, dummy controls/trace covered | Real handler with isolated dummy backend covered; live Telegram remains release smoke |
|
||
|
||
Cancellation, rollback provenance, shared/unknown eviction paths,
|
||
case-collisions, and stale preview guards now have focused tests. The broader
|
||
chaos matrix remains a release gate rather than part of the initial happy-path
|
||
run.
|
||
|
||
The executable complex E2E matrix additionally covers complementary partial
|
||
archive placements, selective cache-placement merging, repeated placement
|
||
generation after eviction, two-archive coverage union,
|
||
uncovered/no-op/stale-preview rejection, exact-path sharing and unknown file
|
||
retention, queued record-only cancellation, precommit rollback after target
|
||
materialization, control restart after source staging, durable command replay
|
||
after a client disconnect, and a no-downloaded-bytes assertion at the target.
|
||
It also mutates the source selection and creates an absent target after job
|
||
confirmation; both hostile races must fail before commit with the precise
|
||
precondition reason while retaining the source. A wrong-size pre-existing
|
||
target and an unwritable target root likewise fail without commit; the latter
|
||
is wrapped in fixture cleanup that restores the original directory mode.
|
||
|
||
The second adversarial matrix also passes from clean state. It covers pure-v2
|
||
and hybrid archive/evict/unarchive round trips, including qBittorrent's
|
||
truncated-v2 primary key and hybrid v1/v2 aliases; a disconnected target that
|
||
becomes `STALLED` at 20% and resumes successfully; bounded disk exhaustion with
|
||
an exact precommit failure and intact source; and duplicate/reordered protocol
|
||
delivery from a separate WebSocket probe process.
|
||
|
||
The remaining Compose expansion includes supported
|
||
partfiles, sparse-capability mismatch, case/type collisions, broader
|
||
service-level network partitions, and database backup/restore during active
|
||
recovery. Focused unit/component tests already cover several of these
|
||
invariants, but they remain unchecked in the full five-project topology.
|
||
|
||
## Test layers
|
||
|
||
### Proto contract
|
||
|
||
- Buf format, lint, descriptor build, and breaking checks.
|
||
- Decode every canonical protobuf-JSON fixture through the descriptor.
|
||
- Golden round trips for envelopes and on-disk manifests.
|
||
- Reject duplicate JSON keys, unknown major versions, invalid hashes/ranges,
|
||
malformed UUIDs/timestamps, over-limit frames, token outside registration,
|
||
and unknown required commands.
|
||
- Consumer tests assert the generated binding header records the exact proto
|
||
release/commit.
|
||
|
||
### Unit and property tests
|
||
|
||
Control tests cover state transitions, hybrid-identity alias reservations,
|
||
FIFO scheduling,
|
||
placement unions, coverage-set calculation, command retries, event gap
|
||
reconciliation, UI nonce idempotency, retention, and backup selection.
|
||
|
||
Client tests cover path normalization/confinement, file-index range algebra,
|
||
tree expansion, root mapping, capability probes, version adapters, manifest
|
||
digesting, journal replay, progress aggregation, and safe directory removal.
|
||
Property/fuzz inputs emphasize path traversal, Unicode/case collisions,
|
||
overlapping selections, reordered/duplicated events, and partial journals.
|
||
|
||
### Adapter contract tests
|
||
|
||
Pinned qBittorrent 4.5–4.6 and 5.x and Syncthing container versions cover every
|
||
supported API family. Assertions include stopped add, exact selection, export, full recheck,
|
||
download-attempt detection, entry-only delete, folder/device idempotency,
|
||
events fallback, need/completion proof, and redacted errors. Partfile adapters
|
||
are tested only against explicitly supported qBittorrent/libtorrent fixtures.
|
||
|
||
### Component integration tests
|
||
|
||
- Control plus fake clients: reconnect, duplicate ID replacement, lost ack,
|
||
replay, sequence gaps, old connection fencing, queue leases, and restart.
|
||
- One client plus qB/Syncthing: inventory, staging, sparse/copy fallbacks,
|
||
service restart, permission failure, and database recovery.
|
||
- Two clients plus two Syncthing services: automatic device/folder creation,
|
||
bidirectional nonce verification, timeout, and existing-route discovery.
|
||
|
||
## Cross-project E2E topology
|
||
|
||
The coordination repository owns uniquely named Compose projects:
|
||
|
||
```text
|
||
e2e/
|
||
├── control/ # mogic archive-control core + test HTTP adapter
|
||
├── cache-1/ # qBittorrent + Syncthing + cache client
|
||
├── cache-2/ # qBittorrent + Syncthing + cache client
|
||
├── archive-1/ # qBittorrent + Syncthing + archive client
|
||
├── archive-2/ # qBittorrent + Syncthing + archive client
|
||
├── fixtures/
|
||
├── scenarios/
|
||
└── scripts/
|
||
```
|
||
|
||
Each data-node project has isolated config, state, backup, qB data, sync data,
|
||
and service volumes. All join one dedicated, uniquely labeled test network.
|
||
Syncthing advertises static Compose service addresses in E2E; production
|
||
defaults remain `dynamic`. The topology deliberately starts without explicit
|
||
route folders so tests exercise automatic 2×2 pairing.
|
||
|
||
Teardown resolves exact project names/labels and stops only those resources.
|
||
Volume deletion is a separate explicit test action, never an unscoped prune.
|
||
|
||
## Thin test HTTP adapter
|
||
|
||
The adapter is enabled only in the control E2E configuration and delegates to
|
||
`ArchiveControlService`; it contains no orchestration logic. Initial endpoints:
|
||
|
||
| Method/path | Purpose |
|
||
| --- | --- |
|
||
| `GET /test/v1/clients` | connection/health/capability view |
|
||
| `GET /test/v1/routes` | route discovery/provisioning state |
|
||
| `GET /test/v1/resources?operation=...` | on-demand eligible source listing |
|
||
| `GET /test/v1/resources/{id}/tree?...` | scoped complete tree |
|
||
| `POST /test/v1/jobs/preview` | validate and calculate exact delta/coverage |
|
||
| `POST /test/v1/jobs` | create using preview revision and idempotency key |
|
||
| `GET /test/v1/jobs/{id}` | projected state and progress |
|
||
| `POST /test/v1/jobs/{id}/cancel` | queued removal or active compensation |
|
||
| `POST /test/v1/jobs/{id}/hide` | UI/history-only removal |
|
||
| `POST /test/v1/scheduler/pause` | stop automatic advancement at a durable boundary |
|
||
| `POST /test/v1/scheduler/advance` | dispatch exactly the next eligible command |
|
||
| `POST /test/v1/scheduler/resume` | restore automatic advancement |
|
||
| `POST /test/v1/protocol/order-probe` | create and lease one synthetic ordering-test job |
|
||
| `POST /test/v1/maintenance/backup` | request and observe a test backup |
|
||
|
||
JSON objects are adapter DTOs, not a second daemon protocol. Test scripts use
|
||
curl, poll bounded state conditions, and dump control/client/service logs on
|
||
failure. Scheduler gating is available only through the loopback, opt-in test
|
||
adapter and is absent from normal daemon and Telegram workflows.
|
||
|
||
## Interactive Telegram UX harness
|
||
|
||
The integrated bot has an opt-in `Archive Control (Test)` entry backed by a
|
||
scripted in-process service. It runs the production conversation handler and
|
||
planner, but never starts a control listener and never contacts a client
|
||
daemon, qBittorrent, or Syncthing. Its UI sessions, scenario state, synthetic
|
||
jobs, and append-only trace share one dedicated SQLite test database that
|
||
configuration validation forbids from being the production control database.
|
||
|
||
The `comprehensive` scenario provides enough resources, targets, and retained
|
||
jobs to exercise resource/job pagination, filtering, Unicode and long names,
|
||
full and partial placements, selective entry previews, archive, unarchive,
|
||
covered eviction, job removal, every significant displayed job state, and
|
||
single-use confirmations. `empty` covers empty lists. `route_slow` keeps a
|
||
newly requested route pending until an administrator explicitly marks it ready.
|
||
|
||
The existing Telegram manager allowlist and optional numeric-ID pin apply.
|
||
Private-chat controls are:
|
||
|
||
```text
|
||
/actest status
|
||
/actest reset [comprehensive|empty|route_slow]
|
||
/actest advance <job-id> <state>
|
||
/actest fail-next <backend-method> [message]
|
||
/actest route-ready
|
||
/actest trace [limit]
|
||
/actest export
|
||
```
|
||
|
||
`advance` accepts queued, preparing, running, waiting, stalled, cancelling,
|
||
cleanup_required, succeeded, failed, and cancelled. `fail-next` injects one
|
||
deterministic planner-style backend failure. Reset clears only the test
|
||
sessions, jobs, scenario state, and trace. State otherwise survives bot
|
||
restarts.
|
||
|
||
The SQLite trace records callback receipt and validated callback state before
|
||
and after handling, text filters, complete rendered text and inline keyboards,
|
||
backend calls/results/errors, test-control commands, and job transitions.
|
||
`trace` shows a bounded recent view in chat; `export` sends the complete trace
|
||
as JSON Lines for offline diagnosis. See the
|
||
[`/actest` operator guide](telegram-ux.md#actest-operator-guide) for complete
|
||
syntax, valid states/failure points, and a recommended review sequence.
|
||
|
||
## Fixture matrix
|
||
|
||
Fixtures include small deterministic v1, v2, and hybrid torrents with:
|
||
|
||
- full, complementary, overlapping, and one-directory selections;
|
||
- same-size corrupt pre-existing targets and type/size collisions;
|
||
- nested shared directories and exact file paths shared by multiple torrents;
|
||
- unknown files/directories inside a resource-owned directory;
|
||
- supported, unsupported, and ambiguous partfiles;
|
||
- sparse files plus a simulated non-sparse target capability;
|
||
- canonical and renamed/noncanonical qBittorrent paths;
|
||
- hardlink, reflink, and copy-only filesystem conditions;
|
||
- permissions and space-reserve failures.
|
||
|
||
## Acceptance scenarios
|
||
|
||
| Area | Required scenarios |
|
||
| --- | --- |
|
||
| Archive | new target, selective target, source retained, second archive replica |
|
||
| Merge | complementary sources build union, overlap transfers delta only, no-op rejected, selected-incomplete target rejected |
|
||
| Unarchive | new cache, merge existing cache, archive always retained, repeat after prior eviction |
|
||
| Eviction | one-archive coverage, union coverage, missing coverage disabled, shared/unknown content retained |
|
||
| Routes | on-demand 2×2 creation, eager mesh, existing match, slow success, timeout, restart during provisioning |
|
||
| Recovery | restart every step on source/target/control, lost DB with proof, ambiguous loss fails closed |
|
||
| Messaging | duplicate command/event, lost ack, sequence gap, stale revision, duplicate client ID |
|
||
| Safety | attempted download, corrupt same-size file, symlink/path escape, special file, partfile mismatch |
|
||
| Operations | stall retains percent, offline wait, cancellation each phase, postcommit cleanup retry, disconnect/reconnect during every transfer step with exactly-once replay |
|
||
| Database | online backup under load, pre/post migration, retention, corrupt backup rejection, offline restore |
|
||
| UI | pagination/filter chain, stale/double callback, persisted session, clear vs evict separation |
|
||
|
||
Every scenario asserts final qB selections/states, placement generations,
|
||
manifests/journals, filesystem ownership/provenance, job event order, absence of
|
||
unexpected received bytes, and preservation of unrelated files.
|
||
|
||
## Fault injection and pass criteria
|
||
|
||
Scenario hooks pause or restart each container, disconnect network edges,
|
||
duplicate/reorder protocol frames, fill a bounded test filesystem, change an
|
||
external qB selection, and kill processes between intent and completion journal
|
||
writes. Tests use eventual assertions with explicit deadlines; long production
|
||
waits are shortened by test-only configuration without altering state logic.
|
||
|
||
A release candidate passes only when the whole matrix succeeds repeatedly from
|
||
clean volumes and from retained/restarted volumes, existing mogic-bot regression
|
||
tests remain green, Buf reports no unintended break, and both amd64 and arm64
|
||
client images start and pass smoke/config checks.
|