* docs: 6.0.0 changelog, README, FAQ and contributor docs Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * Release prep: version 6.0.0 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ci): keep the leaked test pools reachable for LSan; skip the tiny-file floor under coverage - ava1-ctest diag/mgmt_fs: the &'static Pool each test leaks for AvaTransport::with_pool is now held in a static list, so LeakSanitizer sees it (and the sessions on its threads) as reachable instead of reporting ~140 KB of indirect leaks the frame-name suppression could never match. Verified under clang ASan+LSan in Linux Docker: diag 13/13, mgmt_fs 40/40. - download.rs tiny-file throughput floor is skipped when cargo-llvm-cov runs (CARGO_LLVM_COV set): instrumentation slows every call several times over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): data_rust servers use the clients' fast timing Five folder-host tests gave the client fast timing (dead after 500 ms) but left the server on the default (ping every 2 s). On a slow runner a half-second gap made the client drop healthy lanes and the job stalled until the 36 s progress limit (both CI runs on #374/#377). The ordered-download tests already set both ends; these now do too. The app uses one timing on both ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): the C lane socket-buffer test accepts Linux's wmem_max cap Same cap as the Rust lane test: Linux CI gives the send side 416 KiB, so the 1 MiB floor failed under ASan in CI. 128 KiB is still far above a default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ci): gcc unused-result in the FTP selftest, serial ctest in make test, sender state on failure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ava1): flush a partial bundle after any reader message, not only a record The last small-file record could arrive while a large file's chunk or root was still queued behind it: the flush saw a non-empty channel, the next message was not a record, and the records sat unsent until the receiver cut the job as stalled (CI-only flake; 7/36 under local parallel load, 0/48 after). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(validate): run the C interop tests serially in make ci too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ava1): a resumed file goes in one-group pieces so a flapping link still makes progress With kills every 2 s, the 4 MiB tail piece was cut before it was durable on every attempt and the upload retried the same 28 MiB mark until it timed out (1 in 3 locally, CI coverage). Resumed files now send 1 MiB pieces: 5/5 in ~6 s. Also: make docker-engine uses the repo root as context (as CI); governor classification asserts are skipped under the sanitizers, where the C is the bottleneck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): single-instance gate tests hold the C-server lock; relay test cap leaves margin Coverage runs the C tests in parallel; the two gate tests stop the one global C server. The relay order test's 4 MiB/s cap finished in 5.9 s on CI against its >6 s precondition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): periodic-kill bound widened under coverage; tiny-file floor relaxed on CI runners Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): lane-kill test kills until a resend; periodic-kill liveness race skipped under coverage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): a hung relay fails its test at the bound instead of hanging the CI job The dest-ends relay test ran the relay under spawn_blocking: on a hang the timeout panicked but the runtime then waited forever for the blocking thread (27 min on CI). It now runs on a plain thread. The engine test steps get a 25-minute bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): dest-ends relay test in its own binary on small files; relay logs its silent retries The test hung a CI job twice inside the shared relay binary (behind the 384 MiB relays' lock) and never reproduced locally or in Linux Docker. It now runs alone, on 4x32 MiB (still twice the relay buffer), ~3 s. The relay's open-dropped and retry paths now say so on stderr instead of looping silently. Governor classification asserts also skip under coverage, where the instrumented sender is the bottleneck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): the partly-durable relay test caps its first attempt so it can be interrupted On a fast runner the uncapped relay finished all 160 MiB before the first durable report was observed. The first attempt now reads A through a 32 MiB/s proxy (~5 s); the resume runs uncapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * feat: one-time 6.0 upgrade clean-up that keeps every setting Host (engine, once per data dir): pre-5.x files no 5.41/6.0 code reads (api_token, ps5upload.ini, ps5upload_profiles.ini/.json, app-config.json) move to ~/.ps5upload/legacy-5x/. App (desktop, once): the 1.x/2.x config.json/profiles.json move to legacy-5x/; the cached ezremote-dpi.elf nothing loads is deleted. Client: the retired upload-streams key is dropped. Console (once per console, on its first 6.0 helper answer): the FTX2 /data/ps5upload/tx and /spool folders are deleted; a failure retries later. The health check no longer lists those folders (its fix would have recreated them). Nothing current is moved or reset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): the download periodic-kill race is skipped under coverage, like its upload twin Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): the tiny-zip floor only catches a stall on CI runners Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): 'other calls still answer during launches' is a state check, not a wall-clock bound Both mgmt_t6 tests asserted a sub-300 ms answer while a slow Sony call ran; on a loaded runner that separates nothing. They now assert the answer arrived while the slow call was still in flight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ci): reword the 6.0 migration comments for check-no-ftx2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
8.9 KiB
Testing
Four layers, cheapest first. Push a check down to the cheapest layer that can hold it — a host self-test runs in milliseconds and cannot wedge a console, which a hardware test very much can.
| Layer | Command | Needs |
|---|---|---|
| 1. Host gate | npm run validate |
nothing |
| 2. Payload build + self-tests | npm run validate:full |
PS5_PAYLOAD_SDK |
| 3. Live console | npm run validate:hardware |
a PS5 with the payload loaded |
| 4. Fresh-install matrix | manual, per release | clean VMs |
Layer 1 — the host gate
npm run validate # or: make quality
Runs, and fails on any of: version drift against VERSION; script syntax
(Node, Bash, Python, and PowerShell when pwsh is present); the script
inventory audit; git whitespace (git diff --check); engine fmt,
engine clippy (-D warnings) and engine tests; desktop cargo
check, desktop clippy and desktop tests; client typecheck,
client lint, client tests and client vite build; and i18n
coverage.
Two of these catch things the others structurally cannot, and both have caught real regressions:
engine fmtfails on formatting no test exercises. Runcargo fmt --allinengine/before assuming a failure is real.client typecheckcovers test files too. Vitest does not typecheck, so a test can pass while calling a function with the wrong argument types.
Running a narrower check and assuming the wider one follows is the most common way to burn a cycle here.
npm run validate is not the whole gate. Before a PR also run:
( cd client/src-tauri && cargo fmt --all -- --check ) # validate only checks fmt in engine/
( cd engine && cargo test -p ava1-ctest -- --test-threads=1 )
make test-payload # needs PS5_PAYLOAD_SDK
make check-no-ftx2 # the retired protocol must stay gone
Layer 2 — payload build and self-tests
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
npm run validate:full # or: make quality-full / make test-payload
Builds payload/ps5upload.elf and the DPI daemon, checks both are PS5
ELFs with valid gzip resources, then runs every host self-test with
-Wall -Wextra -Werror:
Each self-test lives in payload/tests/*_selftest.c (hardware guards, ptrace recovery,
app.db reading, FTP, SDK param rewriting, installer, cheats, wake watchdog and more). The
AVA1 C itself is tested by ava1-ctest, which compiles it on the host and checks it against the
Rust side (see the full gate below).
The SDK version is pinned in scripts/ps5-sdk.env (currently v0.43)
and verified by checksum. Local installers and CI read the same file.
Adding payload logic? If it can be separated from the console, put it
header-only in payload/include/ and give it a self-test here. That is
the established convention and the reason the list above exists.
Layer 3 — live console
Nothing here is tied to one network. Every entry point takes an address:
export PS5_HOST=192.168.1.50 # console IP (loader + payload)
export PS5_ADDR=192.168.1.50 # console IP; the engine owns the port (9120)
make send-payload PS5_HOST=$PS5_HOST
npm run smoke:hardware # or: node tests/smoke-hardware.mjs
npm run validate:hardware # host gate + make validate
Keep your own addresses in a gitignored local file rather than editing committed defaults — those stay generic on purpose.
| Command | Does |
|---|---|
make validate |
Build, send, wait for :9120, smoke, sweep, write bench/reports/<ts>-sweep.{json,md} |
make validate-xl |
Adds the 200k-file stress profile |
node tests/smoke-hardware.mjs --no-spawn-engine |
Against an engine already running |
Clean up test uploads afterwards:
curl -X POST http://127.0.0.1:19113/api/ps5/cleanup \
-H 'content-type: application/json' \
-d '{"addr":"'"$PS5_HOST"'","path":"/data/ps5upload/tests/manual"}'
Use a live console when touching: the payload C runtime, AVA1
(payload/ava1/), transfer/reconcile/resume, storage, mount, cleanup, the file
browser, volume commands, or anything performance-sensitive.
Hardware testing has teeth
A payload bug can leave a console needing a power cycle — ports stay open but every connection resets, so payload takeover cannot recover it. That has happened, from a stack overflow in a file-transfer handler. Before testing a payload change:
- Prefer a read-only probe first; confirm the payload still answers
/api/ps5/statusbetween steps. - Some reads (temperatures, power telemetry) ptrace-pause the system UI. They are safe on demand and unsafe on a loop.
- Expect blanks that are not bugs: SoC clock, SoC power, CPU usage, fan duty, per-drive sensors and the console date/time are not exposed by retail firmware.
Layer 4 — fresh-install matrix, per release
CI proves the binaries build. It does not prove they launch on a machine that has never been a dev box. Every artifact has a historical "ships green, fails on fresh install" failure:
| Platform | Failure caught before | Guard now in tree |
|---|---|---|
| Windows x64 / arm64 | VCRUNTIME140.dll not found |
.cargo/config.toml sets +crt-static |
| Windows x64 / arm64 | Explorer "Extract All" rejects the zip | publish.yml packs via pwsh Compress-Archive |
| Linux x64 / arm64 | AppImage needs FUSE | ships PS5Upload.sh with APPIMAGE_EXTRACT_AND_RUN=1 |
| Linux (any) | glibc too new | built on ubuntu-24.04 (glibc 2.39) |
| macOS x64 / arm64 | Gatekeeper on first run | documented; right-click → Open |
Before tagging: unzip with the OS's own tool and launch on a clean Windows x64 VM, Windows arm64, Ubuntu 24.04, arm64 Linux, macOS arm64 and macOS x64. Sideload the APK on a real Android device.
See v2.5.1 and v2.5.2 for the "noticed after release, patched same
day" playbook.
Coverage reports
npm run coverage # or: make coverage
make coverage-client # one side only
make coverage-engine
Outputs client/coverage/index.html, coverage/engine/html/index.html
and coverage/engine/lcov.info. Rust coverage needs cargo-llvm-cov
(npm run coverage -- --install-tools).
Cross-platform
CI checks the core crate for every shipped target
(x86_64/aarch64 × linux-gnu, apple-darwin, pc-windows-msvc) and runs
the desktop Rust suite natively on ubuntu-24.04, macos-14 and
windows-2022 — that native matrix is what exercises the
#[cfg(target_os = ...)] paths (keep-awake, update/download handling,
launcher, sidecar extraction). A macOS dev box generally cannot build the
Windows/Linux targets, so CI is the source of truth there.
Android
make android-deps # verify SDK / JDK 17 / NDK / rustup target
make android-build # debug APK, no device needed
make android-deploy # build + install on connected devices
make run-android # run on a device with live reload
Android links the engine in-process rather than spawning a sidecar,
so engine startup failures surface differently — check the in-app log.
.rar is desktop-only (the UnRAR C dependency is excluded).
Self-hosted engine
make docker-engine # build the image
make docker-engine-run # run it; exposes :19113
CI publishes the same image to GHCR on every release tag. The API is unauthenticated and can read, write and delete files on the console — LAN only, never the internet.
i18n
npm run i18n:check # gate: fails on any non-allowlisted miss
npm run i18n:report # same, always prints the per-language summary
client/src/i18n/locales/en.ts is the source of truth. A new tr() key
must exist there or the gate fails. Then either translate it into the 18
locales or add it to scripts/i18n-known-missing.json as a deliberate,
scoped deferral.
Do not run i18n:bootstrap. It rewrites the allowlist to the current
state, silencing every gap at once — including whatever you were about to
forget.
Script hygiene
npm run scripts:check # syntax
npm run scripts:audit # inventory
A lab or debug script with no caller is intentional, not dead code. Only remove generated artifacts, or scripts whose replacement is documented.
Multi-volume .rar
Multi-part archives work in both debug and release builds.
This needed a fix: upstream unrar 0.5.8 reads ~8 KB out of a much
smaller buffer every time it crosses a volume boundary, which aborted any
debug build that touched a multi-part .rar and was a latent
out-of-bounds read in release. 0.5.8 is the latest release, so the crate
is vendored at third_party/unrar with the fix and both workspaces
redirect to it via [patch.crates-io].
See third_party/unrar/README.md for the patch. If you bump
the crate, re-apply the hunk or drop the vendoring once upstream ships the
fix.
The shipped .rar fixtures are all single-volume, so they never exercise
this — verifying it needs a real multi-part archive.