Volumes end-to-end test plan
Volumes end-to-end test plan
This is the release test for both Platinum storage products. Run it against an
isolated control plane, database, object prefix, and designated KVM host. Never
point it at the CAS namespace or a production bucket. Record real identifiers,
timestamps, and failures in TESTED.md; a source-text assertion, compile, or
green CI job is not end-to-end evidence.
Required topology
- Local control plane and disposable database with migrations 0052-0059 applied.
- One real Linux/KVM host running the candidate host-agent and Cloud Hypervisor.
rclone,fuse3, andvirtiofsdinstalled byinfra/scripts/host-install.sh.- A private disposable S3-compatible bucket or isolated non-CAS prefix.
volumes_enabled,shared_volumes_enabled, and the test org'ssharedVolumesgrant enabled only for that org.- A test API token, the CLI, TypeScript SDK, Python SDK, MCP server, and local dashboard pointed at the same control plane.
Before testing, prove that PT_VOLUME_S3_BUCKET and PT_VOLUME_S3_PREFIX do
not equal, contain, or sit beneath the CAS bucket/prefix. Run
cd apps/api && bun run env:check. Do not print credentials.
Evidence rules
For each case capture the API status/body, volume and sandbox ids, guest command output, attachment row state, and relevant host-agent log line. After cleanup, prove that no test VM, FUSE mount, rclone/virtiofsd process, cache loop device, credential file, or object prefix remains. Measure lifecycle latency where the case waits for host work; do not replace measurements with estimates.
1. Static and component gates
| Surface | Command | Pass condition |
|---|---|---|
| API | cd apps/api && bun test | All non-environment-gated tests pass, including volume storage, attachment dispatch, snapshots, reconciliation, OpenAPI, and SDK parity guards. |
| API types | cd apps/api && bunx tsc --noEmit | Exit 0. |
| Host | cd hosts/host-agent && go test ./... | All host volume tests execute on Linux and pass. |
| Host build | cd hosts/host-agent && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go vet ./... && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ./... | Both exit 0. A Darwin Stat_t.Atim failure is not authoritative. |
| CLI | cd packages/cli && bun test | Volume parsing, streaming, mixed sandbox descriptors, and error rendering pass. |
| SDK TS | cd packages/sdk-ts && bun test | Volume, attachment, file, and snapshot surfaces pass. |
| SDK Python | cd packages/sdk-py && python -m pytest | Synchronous and async volume surfaces pass. |
| MCP | cd packages/mcp && bun test | Permission and volume tools pass. |
| Web | build the dashboard package | Build exits 0; this is followed by the manual UI cases below. |
| Migration | run the repository migration tripwire against a copy of the pre-0052 schema | Old code can insert during 0052; 0053-0059 apply; duplicate/live-name indexes and attachment fences exist. |
2. Feature gate and tenant boundary
- With
volumes_enabled=false, all volume routes and UI entry points are absent or denied as designed. - With base volumes enabled but Shared disabled, Local operations work and Shared create/attach/file/snapshot operations fail closed.
- Grant Shared to org A only. Org A can use it; org B receives no volume data, attachment data, object metadata, presigned URL, or existence oracle.
- Attempt every GET and mutation using another org's volume id, attachment id, snapshot id, and file cursor. Expect 404/denial, never cross-org content.
- Verify admin access resolves storage under the volume owner's org, not the admin's current org.
- Submit bucket names, endpoints, credentials, raw object keys,
.., encoded traversal, invalid Unicode, control characters, and reserved marker names in public payloads. Expect rejection and no host command containing credentials.
3. Common volume resource API
Run each applicable operation through raw HTTP, CLI, TypeScript SDK, Python SDK, MCP, and dashboard. The clients must agree on snake_case public models.
| Case | Pass condition |
|---|---|
| Create Local and Shared | Optional legacy Local name is generated; Shared name is required; size/type are correct; no backing path or credentials leak. |
| Duplicate and Unicode names | Trimmed NFC names work; case-insensitive duplicate returns 409; empty, control, malformed, or oversized names return 4xx. |
| List/get | Live rows appear once; attachment count/state and usage fields are accurate; org isolation holds. |
| Rename | New valid name appears across every client; collision is 409; old name is gone. |
| Delete while attached/busy | Returns 409 and preserves data and attachment state. |
| Delete | Local deletion removes the host backing file; Shared deletion tombstones immediately and the bounded reaper removes data, staging objects, and generations. |
| Deleted listing | Default list hides deleted rows; include_deleted shows the tombstone without exposing provider errors or private paths. |
| Idempotency/races | Concurrent create-name, attach, detach, snapshot, restore, and delete requests produce one owner/winner and a stable retryable result, not split state. |
4. Local Volume matrix
- Create an unattached 1 GiB Local Volume. Prove
available, selected host, sparse ext4 backing, and no sandbox attachment. - Create with
sandbox_id. Prove placement on that sandbox's host and mounted state when the sandbox is running. - Create a sandbox using legacy
volume_ids, then using the explicitvolumes[]descriptor. Write a random marker and checksum it in the guest. - Hot-attach at a valid custom path. Verify the block device, ext4 mount, read/write, reported device path, and exactly one attachment.
- Attempt attach to a sandbox on another host, a stopped/deleted sandbox, an
occupied path,
/,/etc, aliases, and traversal. All must fail without a leaked device or changed volume state. - Attempt a second writer/attachment. It must be rejected.
- Detach while files are closed, then reattach to another sandbox on the same host. The marker and checksum must survive and the old guest must lose the device cleanly.
- Stop/start the sandbox three times. The attachment returns at the same path and the marker survives each cycle.
- Restart the host-agent while the VM runs. The VM and Local Volume remain usable and are re-adopted.
- Delete the sandbox. The volume becomes
available, data remains, and a new sandbox can attach it. - Kill Cloud Hypervisor and exercise failed-start/stale-reap cleanup. The volume must not remain falsely attached and must be recoverable.
- Create a Local snapshot only while detached. Verify offline inode-holder
proof, read-only
e2fsck, reflink capture, second validation, andready. - Modify the volume, detach, restore the snapshot, reattach, and prove the old marker/version is restored. A failed fsck or unsupported reflink must fail without changing the live backing inode.
- Verify snapshot create/restore/delete races are fenced; attach and volume delete are denied while snapshot work is active; retained snapshots block volume deletion.
- Delete every Local snapshot, delete the volume, and prove backing files and snapshot directories are gone while unrelated host data is untouched.
5. Shared Volume direct file API
Use nested Unicode paths and binary payloads in addition to simple text.
| Operation | Required checks |
|---|---|
| Upload | Content-Length required; exact bytes/content type round-trip; overwrite adjusts usage; body-length mismatch leaves old object intact; configured direct quota returns 413/409 without a partial live object. |
| List | File/directory entries, path, type, size, mtime, pagination at 1 and 1000, stable cursor behavior, empty directory marker hidden. |
| Stat/download | Existing file metadata and streaming checksum match; missing file is 404; large response is streamed rather than buffered. |
| Mkdir | Empty directory lists as a directory; reserved .platinum-dir is never user-visible or addressable. |
| Copy | Source must exist; destination appears atomically; overwrite and quota accounting are correct. |
| Move | Destination content is correct and source disappears; response says non-atomic; injected delete failure leaves an honest duplicate and retry converges. |
| Delete | Single missing file is 404; recursive deletion is paged/batched, bounded, retry-safe, and never crosses the volume prefix. |
| Presign | Download URL expires within 60-3600 seconds and cannot access another key; upload presign returns shared_presign_upload_disabled. |
| Usage | usage_bytes and object_count converge after every mutation and stale staging uploads are swept. Remember this is direct-API admission only, not a hard FUSE quota. |
| Lease | Two concurrent direct mutations produce one lease owner and a retryable 409; no database transaction remains open during S3 I/O. |
Inject S3 403, 404, 429, 5xx, timeout, malformed listing XML, and mid-stream disconnects. Responses must be bounded and redact endpoint, bucket, key, credential, and provider internals. Retrying must converge without escaping the org/volume namespace.
6. Shared Volume mount matrix
Mounted RW is currently unavailable. First prove that the Admin flag, API, stale host commands, both host constructors, and persisted recovery all fail closed. Run the RW cases below only after a versioned host gateway has durable mutation identity and ambiguous-outcome recovery; they are release gates, not claims about current behavior.
- Attach
rwto a running sandbox at/mnt/shared; verify API attachment state, host metadata, rclone mount, dedicated cache filesystem, virtiofsd, Cloud Hypervisor fs device, and guest mount. - Write in the guest, close the file, then poll the direct API from a separate client until its checksum appears. Write through the API and prove the guest sees it after the documented cache window.
- Keep the same volume
roin a second sandbox while the first owns the sole futurerwattachment. Prove the reader sees a completed write. Detach and swap roles, then prove the former reader can write and the former writer can read. Race tworwrequests and prove exactly one wins; never claim multi-writer support. - Attach
ro; reads work and create/write/rename/delete fail. Confirm host-side--read-only, not merely guest mount flags. - Attach a confined subpath. It exposes only that prefix. Test sibling access,
absolute paths,
..,%2e%2e, backslashes, duplicate separators, invalid Unicode, symlinks, and host mount aliases. - Verify unsupported semantics: no hard links, device nodes, advisory locks, or transactional multi-file rename; document expected symlink-dependent workload failures instead of silently corrupting them.
- Reject quota before S3 accepts bytes and return a deterministic filesystem error from flush/close. Detach must preserve every ambiguous reservation and provider operation for recovery rather than acknowledge data loss.
- Detach/reattach repeatedly and prove Cloud Hypervisor removes the exact fs device id; no PCI/fs-device, mount, process, loop device, or directory leak.
- Stop/start and pause/resume three times. The attachment is reconstructed only after guest readiness, at the same path/mode/subpath, with data intact.
- Restart the host-agent under a mounted volume. Heartbeats start promptly; recovery is bounded/concurrent; rclone and virtiofsd are adopted or safely replaced; the control plane converges to reality.
- Relocate/restore a sandbox to another eligible host. Attachment ownership moves with generation fencing; stale commands from the old host cannot detach or overwrite the new attachment.
- Kill rclone, virtiofsd, and Cloud Hypervisor separately. Each failure must be visible, bounded, reconciled, and recoverable without cross-attachment kills or credential/config leakage.
- Fill the 10 GiB attachment cache and approach host low-space admission. The loopback cache boundary and 20 GiB free-space gate must protect the host; unrelated sandboxes and the host root filesystem remain healthy.
- Apply CPU, memory, task, file-descriptor, and process churn to rclone and virtiofsd. Verify their cgroup/resource budgets contain one tenant without starving host-agent heartbeats or other VMs.
- Simulate S3 outage during reads, writes, detach, stop, and host restart. Reads/writes fail honestly; acknowledged durable data remains; dirty cache is retained; recovery uploads after S3 returns.
7. Explicit volume snapshots and sandbox policies
For Shared snapshots, create a generation, mutate live data, restore, and prove
the exact manifest generation returns. Verify manifest-last publication, staged
object cleanup, writer quiescing/admission, immutable generation keys, usage
accounting, snapshot deletion, reaper behavior, and stale-run fencing. Snapshot
or restore while an rw attachment/direct-write lease is active must fail or
quiesce according to the API contract; ro readers must never turn into writers.
For sandbox snapshot, backup, clone, fork, and restore, prove volume bytes are
never silently embedded or copied. Attachment metadata must follow the explicit
omit, reattach, or clone policy, be revalidated at commit time, and fail
closed if the volume was deleted, renamed, reattached, or its beta grant was
revoked during the operation. Shared ids in legacy volume_ids must be rejected.
8. Dashboard and client acceptance
- Dashboard: create both types, validation/errors, list/get, usage, rename, attach/detach, attachment list, file browser operations, snapshot create/restore/delete, tombstone/reaper progress, and responsive/loading/empty states. Refresh midway through each operation and verify state recovery.
- CLI: run every
pt volumeandpt volume filescommand, JSON and table output, stdin/stdout streaming, paths with spaces/Unicode, and non-zero exits. - SDK TS/Python: run identical golden flows, including async Python, streaming bodies, pagination, 200 versus 202 attach, typed error fields, and no unsafe automatic retry of non-idempotent writes.
- MCP: confirm tools require the intended permission, never expose S3 configuration, and return bounded content rather than embedding large files.
9. Final soak, security, and cleanup gate
Run mixed Local + Shared attachments on several sandboxes for at least one hour: continuous checksum writes/reads, periodic stop/start, attach/detach, snapshots, agent restart, and one controlled S3 fault. Report operation counts, error rate, p50/p95/p99 latency, maximum unavailable interval, host root/data free space, process/RSS/task counts, and reconciliation lag.
Release behind the rollout gates only if there is no data mismatch, cross-org observation, false attached state, unbounded recovery, credential exposure, host-root growth, or leaked process/mount/device. Any one of those is a ship blocker. Finish by deleting test sandboxes, detaching and deleting volumes and snapshots, waiting for the Shared reaper, and proving the isolated object prefix is empty.