Waldrop · Beta testing
Testing Guide
A walkthrough for beta testers, and a checklist for QA. Start at section
0, upload a file, then poke at whatever interests you. You do not need
to know anything about Sui or Walrus to be useful here — if something
confuses you, that is itself worth reporting.
Surface: app.waldrop.xyz
Network: Sui Mainnet + Walrus Mainnet
Package: 0x2f548bb4…c20b45
Plan for testing: Starter (0 USDC in beta)
Last reviewed: 2026-08-13
Size limits are layered — there is no single "upload cap"
The old "20 MB" figure in this document was wrong. What actually
constrains an upload, in the order you hit it:
| Limit |
Value |
Enforced by |
| Quilt patch (one small file in a batch) |
10 MB |
App — QUILT_PATCH_MAX_BYTES |
| Quilt batch total |
250 MB |
App — QUILT_TOTAL_MAX_BYTES |
| Tar bundle mode |
250 MB |
App — browsers can't reliably allocate one buffer past ~300 MB
|
| Cloud connector file (S3/Azure/GCS/Drive/Dropbox) |
100 MB |
App — MAX_BYTES in each cloud route |
| Local file, separate mode |
no app cap |
Walrus publisher --max-body-size + nginx vhost
|
| Total stored |
40 GB on Starter |
Contract — register_blob aborts over quota |
Files over ~100 MB switch to the direct-to-node path (encoding →
registering → slivers → certifying) rather than a single publisher
PUT, so the progress stages you see will differ. That is expected.
New here? Do this, in this order
-
Section 0 — sign in, get a little SUI, pick the
Starter plan, upload one small file. About 15 minutes.
-
Upload from wherever you keep files — your laptop,
Google Drive, Dropbox, S3. Section 1 has one short table per source;
only do the ones you actually use.
-
Try a transform or two — section 3. You do not need
to test all of them.
-
Make a folder, drag files into it, share one —
section 4.
-
Anything odd? File it using the template in section
11.
You do not have to work through this document top to bottom. Use the
parts that match what you are testing.
What the tags mean:
P0
must work — if one of these fails, report it.
P1
should work — worth reporting, less urgent.
P2
edge cases — only if you are being thorough.
0 · First run — from zero to first upload
This is mainnet. There is no faucet, blobs cost real
WAL, and every register_blob burns real gas. Walk this
section end-to-end once before touching anything below — most "bugs"
reported during beta are really a step skipped here.
The Free plan cannot upload
Free is capped at
0 GB of storage on chain — it exists
to let you look around, not to store anything. Every upload test in this
document requires
Starter, which is priced at
0 USDC during beta. If uploads fail with a storage
limit error, check your plan before filing anything.
0.1 Sign in P0
Two ways in, both fully supported. Pick whichever you have — you do not
need a crypto wallet to test Waldrop.
A
Sign in with Google
No wallet, no extension, no seed phrase. A Sui address is created
for you behind the scenes (zkLogin). Easiest way to start.
B
Connect a Sui wallet
Slush, Suiet, Nightly, Ethos, Surf — any Sui wallet. Use this if you
already hold SUI, since you can fund yourself.
| Case |
Priority |
Expected |
Open app.waldrop.xyz in a clean/incognito window
|
P0 |
Landing loads. Both sign-in options are offered |
| A — Sign in with Google |
P0 |
Comes back signed in, header shows a 0x… address
|
| B — Connect a Sui wallet |
P0 |
Wallet popup appears, approving connects, header shows your wallet
address
|
| Reload the page |
P0 |
Still signed in, same address. A changed address
means your files and funds are stranded — stop and report
|
| Sign out, sign back in the same way |
P0 |
Same address again |
| Switch between the two methods |
P1 |
They are different accounts with different addresses and
separate files. That is expected, not a bug
|
| Sign in with a second Google account or wallet |
P1 |
Empty file list. No data visible from the first account |
Why "same address" is a P0 check
Everything you own — SUI, your plan, your uploaded files — belongs to
your address. If it changes between sessions, all of it becomes
unreachable and looks like data loss. With Google sign-in the address is
derived rather than stored, so this is worth confirming once before you
go further. With a wallet it should never change.
0.2 Get mainnet SUI P0
You need gas. There is no mainnet faucet — SUI has to come from
somewhere real.
01
From the team
Paste your 0x… address in the beta channel. Fastest
route, and how most testers should start.
02
Exchange withdrawal
Buy SUI on any exchange listing it, withdraw to your address on the
Sui network — not a wrapped/bridged variant.
03
How much
0.5 SUI covers a full test session comfortably.
Each register_blob burns roughly
0.008 SUI.
| Case |
Priority |
Expected |
| Balance shows in the header after funding |
P0 |
Header reads e.g. 0.8509 SUI, updates within ~10s of
the transfer landing
|
| Attempt an upload with 0 SUI |
P1 |
Clear "insufficient gas" message. Not a raw
wallet error or a silent hang
|
0.3 Subscribe to Starter P0
Starter is 0 USDC during beta, so you do not need USDC
— only enough SUI for the transaction gas.
| Case |
Priority |
Expected |
Open /plans |
P0 |
Four tiers listed. Starter shows its beta price |
| Subscribe to Starter, approve the transaction |
P0 |
Succeeds. Sidebar storage meter switches to
0 KB / 40.0 GB and the plan label reads
Starter
|
| Reject the transaction in the wallet instead |
P1 |
App returns to idle with no error toast. A user cancelling is not
a failure
|
| Try to subscribe again while already subscribed |
P1 |
Blocked or shown as "current plan" — never a duplicate
Subscription object
|
| Reload after subscribing |
P0 |
Plan persists. It lives on chain, so a reload must not lose it
|
Live plan limits (read from mainnet)
| Tier |
Storage |
Files |
Max epochs |
SEAL |
| Free |
0 GB |
20 |
1 |
— |
| Starter |
40 GB |
5,000 |
26 |
✓ |
| Pro |
200 GB |
50,000 |
53 |
✓ |
| Enterprise |
unlimited |
unlimited |
53 |
✓ |
One Walrus epoch is
14 days, so Starter's 26-epoch cap
is about a year of storage.
0.4 First upload — the smoke test
P0
Do this once with a small local CSV before testing any connector. If it
fails, nothing below is worth running.
| Step |
Priority |
Expected |
| Upload → Local Files → drop a small CSV (< 1 MB) |
P0 |
File listed with name and size |
| Continue through config, leave defaults |
P0 |
Storage Settings sidebar shows file, size, format, publisher,
folder
|
| Run the upload, approve the transaction |
P0 |
Progress advances through encoding → publisher → registering →
done
|
Open /files |
P0 |
File appears with a blob id, size, and expiry |
| Click through to the blob detail page |
P0 |
Contents render or download correctly |
| Sidebar storage meter |
P1 |
Increases by roughly the file size |
Stop-and-report conditions
Do not continue testing — and do not file a normal bug — if any of these
happen. They indicate something structurally wrong, and further testing
will produce noise:
-
Your address changes between sessions with the same Google account
- A blob uploads but never appears in
/files
-
An encrypted file downloads as unreadable bytes after unlocking
- Another account's files are visible in your list
0.5 Environment hygiene P1
Once the smoke test passes, keep the session clean. Stale state causes
more false positives than real defects.
01
Browser P0
Incognito, or clear cache between runs. Stale OAuth cookies are the
single biggest false-positive source for Dropbox and Drive tests.
02
DevTools P1
Network + Console open. Catches silent failures and unhandled
promise rejections that never surface in the UI.
03
Service health P0
Check
app.waldrop.xyz/api/health first. It reports engine,
publisher, aggregator and Sui RPC separately — if one is
down, failures below are that, not a bug.
04
Connector creds P1
Have S3 / Azure / GCS keys and a Drive + Dropbox account ready
before starting section 1, so a missing credential doesn't stall a
run.
Check health before filing anything
curl https://app.waldrop.xyz/api/health returns
200 ok,
200 degraded (a non-critical component
is down) or
503 down (a critical one is). Paste that output
into any bug report — it separates "the product is broken" from "Walrus
was having a moment".
1 · Step 1 — Source selection
Eight source tiles. Each connector has its own credential model and
failure surface. Test the picker grid first, then each connector.
Connector coverage:
Five connectors are required for every release:
Local Files, Public URL, SFTP, Dropbox, Google Drive — all free to set
up. Three are optional: AWS S3, GCS, Azure — paid
cloud accounts required. Test these when you have credentials; don't
block releases on them. Mark untested ones as "n/a" in the release log
rather than "blocked".
1.1 General source picker
| Case |
Priority |
Expected |
|
All 8 tiles render (Local, URL, S3, GCS, Azure, SFTP, Drive,
Dropbox)
|
P0 |
No missing icons, no broken grid |
| Stepper shows "1 Source" active (amber circle) |
P0 |
Steps 2, 3 dimmed |
| Each tile clickable, routes to its config step |
P1 |
URL updates to ?source=<name> |
| Tile pills color-correct (Cloud / Server / Upload / Fetch) |
P1 |
Amber per design palette |
| Keyboard nav: Tab cycles tiles, Enter opens |
P2 |
Focus ring visible |
1.2 Local Files SOURCE
| Case |
Priority |
Expected |
| Drag-drop one CSV |
P0 |
File listed with name + size + ✓ |
| Click-to-browse picks one file |
P0 |
Same result as drag-drop |
| Upload a 100 MB file |
P0 |
Accepted, Continue enables |
| Upload 21 MB file (over limit) |
P0 |
Accepted — the app sets no per-file cap on local files |
| Drop 5 small CSVs at once |
P1 |
All listed, total size shown, "Each file separately" default
|
Drop .xlsx — auto-converts to .csv
|
P1 |
UI shows converted indicator, content matches |
Drop .json + .parquet |
P1 |
Both detected with correct file-type pill |
Drop a binary (.png) |
P1 |
Marked "stored as-is", pipeline disabled |
| Drop a zero-byte file |
P2 |
Rejected clearly OR uploaded as empty blob — no silent freeze
|
Drop Unicode filename (परीक्षण.csv) |
P2 |
Name preserved through upload + BlobRef |
| Drop 19.9 MB file (just under limit) |
P2 |
Accepts cleanly, no off-by-one |
1.3 Public URL SOURCE
| Case |
Priority |
Expected |
| Fetch a public CSV URL |
P0 |
File downloaded, preview shows rows |
| Fetch a 404 URL |
P0 |
"URL fetch failed: 404" — no silent failure |
Fetch non-HTTPS (http://...) |
P1 |
Rejected with security message OR upgraded to HTTPS |
| Fetch URL returning > 100 MB |
P1 |
Server aborts mid-stream, clear error |
| Fetch JSON URL |
P1 |
Detected as JSON, pipeline applies |
| Follow redirect (302 → 200) |
P2 |
Lands the final bytes |
SSRF probe: http://169.254.169.254/... |
P2 |
Blocked. Same for localhost, 127.0.0.1,
10.*
|
1.4 AWS S3 SOURCE
OPTIONAL
| Case |
Priority |
Expected |
| Valid bucket + region + access key + secret |
P0 |
Bucket listing loads |
| Wrong secret access key |
P0 |
"S3 authentication failed — check Access Key ID and Secret" |
| Typo'd bucket name |
P0 |
"Bucket not found" |
| Wrong region (bucket us-east-1, entered eu-west-2) |
P0 |
Region mismatch OR bucket-not-found |
Key has ListBucket but not GetObject
|
P1 |
Listing works, fetch fails with permission error |
| Pick S3 object > 100 MB |
P1 |
Rejected with size limit message |
| Object key with special chars (spaces, +, %) |
P1 |
Fetched correctly, no URL-encoding bugs |
| KMS-encrypted objects |
P2 |
Works with KMS perms, clean error without |
| Verify credentials not logged |
P2 |
Grep server logs for access key value — zero hits |
1.6 Azure Blob SOURCE
OPTIONAL
| Case |
Priority |
Expected |
Valid bucket + fresh ya29. token |
P0 |
Listing loads |
| Expired token (> 1h old) |
P0 |
"GCS auth failed — provide a valid access token" |
Token without devstorage.read_only scope |
P0 |
Permission error, not generic auth |
| Service account JSON instead of OAuth |
P1 |
Documented path works |
Bucket name with dots (my.bucket.name) |
P1 |
No HTTPS cert issues |
| Token with leading whitespace (paste bug) |
P2 |
Trimmed OR clear format error |
1.6 Azure Blob SOURCE
| Case |
Priority |
Expected |
Valid account + container + SAS starting with ?
|
P0 |
Container listing loads |
SAS pasted without leading ? |
P0 |
Explicit "must start with '?'" — not silent fail |
| SAS scoped without Read permission |
P0 |
"SAS token needs Read permission on this blob" |
Expired SAS (past se= param) |
P1 |
Clean expiry error |
| SAS oversold with Write+Delete |
P1 |
Works (read-only used) — document as known harmless |
| Container name with Unicode |
P2 |
Properly URL-encoded |
1.7 SFTP SOURCE
| Case |
Priority |
Expected |
| Host + port 22 + valid username + password |
P0 |
Connection success, listing loads |
| SSH private key auth (paste OpenSSH key) |
P0 |
Same result as password |
SSRF block: localhost, 127.0.0.1,
10.0.0.1, 192.168.1.1
|
P0 |
All rejected with "private/loopback not allowed" |
| Wrong password |
P0 |
"Auth failed" — not generic "connection failed" |
| Non-default port (e.g. 2222) |
P1 |
Works |
Invalid host (nonexistent.example.invalid) |
P1 |
DNS error, fast fail (< 5s) |
| Malformed private key (truncated paste) |
P1 |
"Invalid private key format" — not stack trace |
| SFTP file > 100 MB |
P1 |
Verify limit (20 vs 25 MB), fix docs or code |
| Path with deep nesting |
P2 |
Works |
| Path points to directory not file |
P2 |
Error clarifies the difference |
1.8 Google Drive SOURCE
| Case |
Priority |
Expected |
| OAuth sign-in → approve → return |
P0 |
"Connected ✓", file list loads |
| Service account JSON paste flow |
P0 |
Currently shows "not yet wired" — verify intentional |
| SA email not added to file share list |
P0 |
File invisible (expected). UX hints about sharing |
OAuth env vars missing in .env.local |
P1 |
"Drive OAuth not configured — set GDRIVE_CLIENT_ID …" |
| Consent screen Testing mode + non-test-user |
P1 |
Google blocks at OAuth, error visible |
| State cookie expired (wait > 10 min on OAuth screen) |
P1 |
"OAuth state mismatch (possible CSRF) — please retry" |
| OAuth token expired (1h) — try upload after wait |
P1 |
Clean re-auth prompt or auto-refresh |
| Google Workspace doc (not regular file) |
P2 |
Skip or export-to-format; no silent fail |
1.9 Dropbox SOURCE
| Case |
Priority |
Expected |
| OAuth: Connect Dropbox → approve → return |
P0 |
"Connected ✓", List Files works |
Paste-token (sl.B…) instead of OAuth |
P0 |
List Files works without OAuth |
| Revoke app in Dropbox, retry upload |
P0 |
"Dropbox auth failed — token expired or invalid" |
| Dev-mode app + non-dev-user account |
P1 |
Dropbox blocks, clear error |
App with files.content.read not ticked |
P1 |
Listing works, fetch fails — scope issue surfaced |
| Dropbox Paper / shared link target |
P2 |
Skipped or "unsupported" error |
2 · Step 2 — Config
Storage duration slider + upload strategy radio + sidebar settings.
Verify state persists across step navigation.
2.1 Storage duration slider
| Case |
Priority |
Expected |
| Slider min = 1 epoch (14 days on mainnet) |
P0 |
Cannot go below 1 |
| Slider max = 26 epochs |
P0 |
Cannot exceed 26. "Max" button → 26 |
| Drag slider → epoch count + day estimate update live |
P0 |
"~1 day" → "~12 days" → "~26 days" |
| Value persists Step 2 → Step 3 → back |
P1 |
Retained, not reset to 1 |
| Plan tier cap (e.g. Free = max 7 epochs) |
P1 |
Auto-clamps OR shows upgrade hint |
2.2 Upload strategy radio
| Case |
Priority |
Expected |
| "Each file separately" default (recommended) |
P0 |
Amber-highlighted card |
| Switch to "Bundle into archive" |
P0 |
Tooltip explains "one tx for whole batch" |
| Switch to "Quilt batching" |
P0 |
Notes about ≤ 10 MB + "encryption not supported yet" |
| Quilt + Encrypt simultaneously |
P0 |
UI prevents OR errors clearly at upload |
| Quilt with one file > 10 MB |
P1 |
Rejected: "Quilt is for ≤ 10 MB files" |
| Strategy persists across step nav |
P1 |
Doesn't reset on every back nav |
2.3 Storage Settings sidebar
| Case |
Priority |
Expected |
| Sidebar shows File, Size, Format, Publisher, Encryption |
P1 |
All 5 populated. Encryption = SEAL · threshold when
on
|
| Upload Method pill matches Step 2 strategy |
P1 |
"Quilt batch" green pill when Quilt chosen |
| Estimated Cost — disclaimer or live estimate |
P1 |
"Walrus pricing not loaded yet" OR FROST/SUI estimate |
| Share Access — paste Sui address, click Add |
P2 |
Bundled into upload PTB, visible in BlobStore viewers |
Share Access — malformed address (no 0x) |
P2 |
Validation error before submit |
3 · Step 3 — Transform pipeline
Five categories, twenty-three transforms. Each tested with happy path +
1-2 edge cases. Use one baseline CSV across every transform so output
diffs stay interpretable.
Baseline test CSV
Use this for every transform test:
match_no,stage,team_a,team_b,score_a,score_b,date
1,Group A,Hitters,Kings,45,42,2026-04-01
2,Group B,Bosses,Royals,38,41,2026-04-02
3,Group A,Stump Smashers,Lions,52,49,2026-04-03
4,Group B,Highvoltage,Warriors,30,35,2026-04-04
5,Group A,Hitters,Stump Smashers,48,46,2026-04-05
Add a row with null score_b to test null handling. Gives
you nulls, numeric + string columns, duplicate stage values for
filtering, and dates for sorting.
3.1 General pipeline behavior
| Case |
Priority |
Expected |
| All 5 category buttons render and toggle |
P0 |
Filter, Shape, Enrich, Security, Aggregate |
| Each category reveals transforms with themed color |
P0 |
Amber, green, blue, coral, purple |
| Empty-state placeholder visible when no steps |
P0 |
"No transform steps — data stored as-is" |
| Refresh Preview → Data Preview reflects pipeline |
P0 |
15-row preview matches applied transforms |
| Add 3 transforms in order, all apply cumulatively |
P1 |
Preview shows compound result |
| Remove middle transform from 3-step pipeline |
P1 |
Remaining 2 still apply, preview re-renders |
| Reorder transforms (drag/drop if supported) |
P1 |
Output reflects new order |
| Pipeline of 10+ transforms |
P2 |
No UI lag, preview computes < 3s |
Beginners: you can stop here
Every transform behaves the same way — add it, fill in its fields, watch
the preview update, then reorder or remove it. Try that with two or
three of your choice and section 3 is covered.
The tables below are an exhaustive per-transform reference. Open them
only if you are chasing a problem in one specific transform.
Per-transform reference — every field and edge case
(Filter, Shape, Enrich, Security, Aggregate)
3.2 Filter & Select
6 TRANSFORMS
| Transform · Case |
Priority |
Expected |
Filter: stage == "Group A" |
P0 |
3 rows remain |
Filter: score_a > 40 |
P0 |
Correct rows kept |
Select: keep
match_no, team_a, team_b
|
P0 |
Other columns gone |
| Limit: first 2 rows |
P0 |
Exactly 2 rows |
| Sample: random 2 rows |
P0 |
2 rows, different on each Refresh |
|
Deduplicate: after injecting duplicate row
|
P0 |
Duplicate removed |
Validate: schema
score_a is integer
|
P0 |
Passes clean; fails clearly on injected string |
| Filter with empty result set |
P1 |
0 rows, no crash |
| Select non-existent column |
P1 |
"Column not found" |
| Limit 0 or negative |
P1 |
Validation prevents OR returns 0 rows cleanly |
3.3 Shape & Transform
9 TRANSFORMS
| Transform · Case |
Priority |
Expected |
Rename: team_a → home_team |
P0 |
Header changes |
Cast: match_no int → string |
P0 |
String-styled values |
Sort: by score_a desc |
P0 |
Row order matches |
Fill Null: missing score_b →
0
|
P0 |
Nulls replaced |
Expression:
diff = score_a - score_b
|
P0 |
New column with correct values |
Replace: "Group" → "Pool" in stage
|
P0 |
Values updated |
Split: date on - →
y/m/d
|
P0 |
3 new columns |
| Flatten: on nested JSON column |
P0 |
Nested keys become top-level |
Map: uppercase on
team_a
|
P0 |
Values uppercased |
| Rename to existing column name |
P1 |
Validation error or auto-suffix |
| Cast string → int with non-numeric value |
P1 |
Null'd with warning OR error with row number |
| Expression: divide by zero / null operand |
P1 |
Null result, no crash |
| Split: delimiter not in string |
P1 |
Single column with original, rest null |
3.4 Enrich & Combine
4 TRANSFORMS
| Transform · Case |
Priority |
Expected |
Concat:
team_a + " vs " + team_b → matchup
|
P0 |
"Hitters vs Kings" etc. |
| Join: with second CSV on key column |
P0 |
Joined rows visible; verify left/inner/outer |
|
Lookup: stage code → stage name from static
table
|
P0 |
Resolved column appears |
Window: rolling 2-row sum of
score_a
|
P0 |
Window column correct |
| Join with no matching keys |
P1 |
Left rows preserved with nulls OR dropped — matches mode |
| Lookup key not in lookup table |
P1 |
Null returned, no crash |
| Window over column with nulls |
P1 |
Handled per documented semantics |
3.5 Security & Privacy
2 TRANSFORMS
| Transform · Case |
Priority |
Expected |
Mask: redact team_a to
***
|
P0 |
Preview shows masked values |
Encrypt: SEAL-encrypt
score_a column
|
P0 |
Ciphertext placeholder; sidebar pill =
SEAL · threshold
|
| Encrypt + Quilt strategy combo |
P0 |
Blocked per Quilt's "encryption not supported yet" |
| Free plan tier + Encrypt |
P1 |
Blocked at register_blob OR earlier with upgrade prompt |
Mask format-preserving (emails → x***@***.com)
|
P1 |
Works if documented |
| Encrypt multiple columns |
P2 |
All columns encrypted under single SEAL identity |
3.6 Aggregate & Analyze
3 TRANSFORMS
| Transform · Case |
Priority |
Expected |
Aggregate: group by stage, sum
score_a
|
P0 |
2 rows (Group A, B) with sums |
| Aggregate: count, avg, min, max |
P0 |
Each function returns correct value |
Pivot: rows → columns on stage
|
P0 |
New "Group A" / "Group B" columns |
Unpivot: collapse score_a +
score_b into one column
|
P0 |
Row count doubles, metric/value pair |
| Aggregate on column with nulls |
P1 |
Nulls excluded per documented behavior |
| Pivot creating duplicate column names |
P1 |
Auto-suffixed or clear error |
4 · Folders
Folders are flat — no nesting, by design. "General" is
not a real folder: it is the absence of one, so it can never be renamed,
deleted or shared. The cap is 10 folders per store,
admin-adjustable on chain.
A move is a permission change
Folder access is resolved
at decrypt time, not snapshotted when
the folder was shared. Dropping a file into a shared folder makes it
readable by that folder's viewers
immediately, and
moving it out revokes just as fast. Most of the P0 cases below exist to
check the UI says so before you sign.
4.1 Create, rename, delete FOLDERS
| Case |
Priority |
Expected |
/files → "+ New folder" → name it → approve |
P0 |
Card appears in the FOLDERS grid showing "0 files" |
| Read the create dialog before confirming |
P0 |
Warns that folder names are public on chain. If
this text is missing, that is a P0 bug — users cannot discover it
anywhere else
|
| Submit an empty name |
P1 |
Blocked client-side, button disabled |
| Paste a 200-character name |
P1 |
Blocked at 128 bytes with a byte counter — not a
character count
|
| Name it with emoji, e.g. 40 × 🎉 |
P2 |
Counter reflects bytes (emoji cost 4 each), so ~32 emoji
hits the cap. A character counter here would be a bug
|
| Create an 11th folder |
P1 |
"+ New folder" is hidden, or the call aborts with a limit message
— never a raw Move abort code
|
| Rename a folder, reload |
P0 |
New name persists (it is on chain) |
| Delete an empty folder |
P0 |
Removed from the grid |
| Try to delete a folder containing files |
P0 |
Delete is disabled with "Move N files out first".
It must not let you sign a transaction that is guaranteed to abort
|
| Check the "General" card's ⋯ menu |
P1 |
There is no ⋯ menu. General is not an on-chain object |
4.2 Moving files FOLDERS
| Case |
Priority |
Expected |
| Drag a file row (grip handle) onto a folder card |
P0 |
Card highlights amber, ghost chip follows the cursor, drop moves
the file
|
| Select 3 files, drag one of them |
P0 |
All 3 move together — dragging a selected row drags the whole
selection
|
| Drag an unselected row while others are selected |
P1 |
Only that one moves |
| Drop on empty space |
P1 |
Nothing happens, no transaction |
| Press Escape mid-drag |
P2 |
Drag cancels cleanly |
| Open a folder, drag a file onto the "All files" breadcrumb |
P1 |
Moves back to General — this is the only way out once inside a
folder
|
| Use checkboxes + "Move to…" bar instead |
P0 |
Same result. This path must work — it is the keyboard-accessible
one
|
| On a phone: drag using the grip handle |
P0 |
Works with a finger, and the list still scrolls when you swipe
anywhere other than the grip
|
| Move a file into a folder, then reload |
P0 |
File stays in the folder; counts on both cards are correct |
| Move the same file to the same folder twice |
P1 |
Counts do not drift. A folder showing "1 file" that is actually
empty can never be deleted
|
4.3 Folder sharing SECURITY
| Case |
Priority |
Expected |
| Folder ⋯ → Share → add a second address |
P0 |
Address listed; card shows a copper dot and "shared N" |
| Read the share dialog before confirming |
P0 |
States it shares the folder
including files added later — not a snapshot of N
files
|
| Add your own address |
P1 |
Blocked with "you already have access" |
| Add the same address twice |
P1 |
Blocked before signing |
| Enter a malformed address |
P1 |
Validation message, Grant disabled |
| Drag a file INTO the shared folder |
P0 |
Confirmation appears naming how many people gain access. Button
reads "Move and share", not "Move"
|
Check that file's row in /files |
P0 |
Shows a VIA FOLDER · N badge, distinct from the
per-file SHARED badge — they are revoked in different places
|
| Sign in as the shared-with account → Shared with Me |
P0 |
Can see and decrypt files in that folder |
| Owner moves the file OUT, viewer refreshes |
P0 |
Access is gone for new decryptions. Note: SEAL keys already issued
stay valid until they expire — the dialog says so, and that is
correct behaviour, not a bug
|
| Revoke the viewer, refresh as them |
P0 |
Folder no longer listed |
4.4 Upload straight into a folder
| Case |
Priority |
Expected |
| Upload wizard → Storage Settings → Folder dropdown |
P0 |
Lists General + every folder. Defaults to General |
| Pick a shared folder |
P0 |
Warns "N people will be able to read this upload"
before the upload runs
|
| Complete the upload |
P0 |
File lands in that folder in one transaction — never uploaded to
General then moved
|
| Start a second upload afterwards |
P1 |
Picker is back to General. A sticky folder would
silently drop the next file into a shared one
|
5 · Morse (AI)
Morse turns plain English into a validated transform pipeline, and
scans for PII. It is metered per model call against your plan tier.
Privacy boundary — the thing to actually verify
The UI claims Morse sees
column names, statistics and masked value shapes — never your
actual data. That claim is the product's differentiator, so test it like a
security control, not a feature. Upload a file containing a
distinctive string (
ZZQQ-CANARY-12345), run a scan, and
confirm the string never appears in any request body in the Network
tab.
| Case |
Priority |
Expected |
Upload a CSV with an email column, run a scan
|
P0 |
Findings card flags the PII column and proposes a policy |
|
Canary check — search the Network tab for your
canary string
|
P0 |
Zero matches in any outbound request body. A
hit here is a P0 privacy bug, not a feature gap
|
| Ask "remove duplicate rows" in the Ask bar |
P0 |
Returns a "Pipeline ready" card with steps — not "nothing
sensitive detected"
|
| Click "Review N steps" |
P0 |
Before/after preview on a sample, with changed cells highlighted
|
| Cancel at the preview |
P0 |
No steps added to the pipeline. Nothing is applied without
explicit approval
|
| Approve, then check the pipeline |
P0 |
Steps appear and are editable — the AI proposes, you decide
|
| Try a filter/dedup step in the preview |
P1 |
Row counts differ, so cell highlighting is suppressed. Rows
can't be matched one-to-one and pretending otherwise would make
the diff untrustworthy
|
| Reorder columns in a proposal |
P2 |
Diff does not read as "everything changed" — columns match by
name, not position
|
| Ask something nonsensical, e.g. "make it purple" |
P1 |
Clear failure message. Never invalid steps handed to the engine
|
| Run ~12 scans in a minute |
P1 |
Rate limited with a Retry-After, stating how long
to wait
|
| Exhaust the hourly AI allowance |
P1 |
Message names your tier and limit — "AI limit reached for the
Starter plan (N calls/hour)", not a bare 429
|
6 · End-to-end happy paths
Once per-step suites pass, run full E2E flows. Each should complete in
under 30 seconds on a 1 MB file.
E2E-1 · Local CSV → no transforms → plaintext upload
Drop 1 MB CSV → Continue → no transforms → wallet sign → success.
Result page shows blob_id + suivision link. On-chain confirm < 5s.
Re-fetch via Files tab — bytes match original SHA-256.
E2E-2 · Local CSV → Filter + Sort → encrypted upload
Filter (score_a > 40) + Sort (date asc) + Encrypt.
Wallet signs once (SEAL encrypt needs no signature). BlobRef has
encrypted: true + sealMarker set. Decrypt
prompts SEAL session sig → returns filtered + sorted CSV.
E2E-3 · S3 → Aggregate → Quilt batching
Pick 5 small CSVs from S3 (≤ 1 MB each). Aggregate group-by-stage.
Strategy: Quilt. Single Walrus blob, each output addressable. Single
register_blob tx — cheaper than 5 separate uploads.
E2E-4 · SFTP → Mask PII → Bundle into archive
Fetch 3 CSVs from SFTP. Mask the email column. Strategy: Bundle (tar).
Post-upload: download bundle, untar locally, verify PII is masked in
all 3 files.
E2E-5 · Drive → Encrypt + Share with second wallet
OAuth Drive, pick a CSV. Encrypt + add second Sui address as viewer.
Wallet 1 signs upload. Switch to Wallet 2, fresh session, open from
Shared with Me — decrypt succeeds. Try a third (unauthorized) wallet —
seal_approve denies with explicit "not authorized".
E2E-6 · Resume after register failure
Drain wallet to ~0.001 SUI. Upload — publisher PUT succeeds,
register_blob fails with InsufficientGasError. UI surfaces "wallet
needs N MIST + Retry". Top up the address (no mainnet faucet —
exchange withdrawal or the beta channel), click Retry → calls
registerOnly with the checkpoint, no re-PUT, success.
7 · Limits & quotas
Resolved 2026-08-13 — the guide was right
This document previously claimed a
20 MB per-file cap pending AWS verification. Verified
against the source:
no 20 MB constant exists anywhere in the codebase.
All five cloud connectors enforce
MAX_BYTES = 100 * 1024 * 1024. The Connectors guide's 100
MB was correct; this checklist was stale. Every limit in the table
below is now read from code, with the constant named so it can be
re-checked rather than trusted.
| Surface |
Current limit |
Test case |
Expected |
| Local Files |
no explicit cap |
Drop a 150 MB file |
Accepted by the app. Bounded downstream by the publisher's
--max-body-size and browser memory
|
| S3 / GCS / Azure / Dropbox / Drive |
100 MB / file |
Pick a 101 MB object |
Rejected pre-fetch on Content-Length: "File too
large… limit is 100 MB per file"
|
| S3 / GCS / Azure / Dropbox / Drive |
100 MB / file |
Object whose Content-Length under-reports its real
size
|
Stream aborts mid-transfer at the cap — capStream
enforces it byte-by-byte, not just on the declared header
|
| Quilt patch |
10 MB / file |
Add an 11 MB file to a Quilt batch |
Rejected: "Quilt requires ≤ 10 MB" |
| Quilt batch total |
250 MB |
Batch summing to 260 MB |
Rejected before upload starts |
| Bundle (tar) |
250 MB total |
Bundle summing to 260 MB |
Rejected: browsers can't reliably allocate one buffer past ~300
MB, and tar mode needs the whole archive in memory
|
| Total storage |
Per plan (40 GB Starter) |
Upload exceeding quota |
Block at register_blob with plan-tier error |
| Blobs per BlobStore |
Per plan tier |
File #N+1 on Free |
Block with upgrade prompt |
| Storage duration |
1–26 epochs (Starter cap; 53 on Pro) |
Try epoch 0 / 27 |
Slider clamps, API rejects |
8 · Error handling
Every SDK error class should surface as a useful UI message — never a
stack trace, never a silent failure.
| Error class |
Trigger |
UI must show |
InsufficientGasError |
Drain wallet, upload |
"Top up wallet" + Retry button. No faucet link — mainnet has none
|
RegistrationError |
Reject tx in wallet |
Retry register without re-upload (checkpoint flow) |
BlobStoreNotFoundError |
Read blobs for non-subscribed address |
Empty state, not "error" |
BlobNotFoundError |
Fetch with invalid blob_id |
"Blob not found — id malformed or expired" |
AggregatorError |
Walrus aggregator 5xx |
"Walrus is having trouble — retry shortly" |
DecryptionError |
Decrypt as unauthorized wallet |
"You don't have access to this blob" |
SealNotInstalledError |
Should never happen in dapp |
If seen → file P0 bug |
| Plan-tier denied |
Free tier tries Encrypt |
"Encryption requires Starter+ — Upgrade" |
| Subscription expired |
Past-expiry Subscription |
"Subscription expired — renew to upload" |
Recovery actions P0
Every error displays a recovery CTA — Top up / Retry / Upgrade /
Reconnect / Re-auth. Never a dead message.
No raw traces P0
Stack traces only in DevTools console, never visible to users.
Offline detection P1
Network drops mid-upload → UI pauses, offers retry when back online.
Wallet popup closed P1
User closes wallet mid-sig → "Upload cancelled", checkpoint
preserved.
9 · Cross-browser & responsive
| Target |
Priority |
Notes |
| Brave (latest, macOS) |
P0 |
Primary dev browser |
| Chrome (latest) |
P0 |
All flows pass |
| Safari (latest macOS) |
P1 |
Webkit-specific: file picker, drag-drop, SubtleCrypto |
| Firefox (latest) |
P1 |
All flows pass |
| Mobile Safari (iOS) |
P1 |
iOS file picker sheet |
| Mobile Chrome (Android) |
P2 |
Android share sheet |
| Viewports: 1920, 1440, 1024, 768, 390 |
P1 |
No overflow, sidebar collapses < 1024 |
| Light mode (if supported) |
P2 |
If not, enforce dark — don't half-break |
| Wallets: Sui Wallet, Slush, Phantom, Suiet |
P1 |
Each signs register_blob PTB |
10 · Regression list
Fill as you ship fixes:
Specific bugs found in past releases. Re-test these every time. Every
fix that ships leaves a row here — otherwise the same bug ships again
three milestones later.
| Bug |
Found in |
Test |
| (empty — fill as bugs are fixed) |
— |
— |
| Example: SAS without `?` silently failed |
M2 |
Paste SAS without `?` → must show explicit error |
| Example: Quilt + Encrypt didn't block at UI |
M2 |
Quilt → Encrypt step → must block before Continue |
11 · Bug report template
When something fails, log it in this exact format:
### Bug — <one-line summary>
**Surface:** First run / Step 1 / Step 2 / Step 3 / Folders / Morse / E2E
**Connector / Transform:** <name>
**Priority:** P0 / P1 / P2
**Browser + device:** Brave 1.x macOS / Safari iOS 18 / etc.
**Sign-in:** Google / Slush / Suiet / other wallet
**Plan:** Free / Starter / Pro
**Network:** Mainnet
**Build:** <commit sha>
**/api/health at the time:** <paste — separates a product bug from a
dependency outage>
**Steps to reproduce:**
1. ...
2. ...
3. ...
**Expected:** ...
**Actual:** ...
**Screenshot / video:** [attach]
**Console errors:** [paste]
**Network tab:** [request/response if relevant]
**Wallet tx (if got that far):** <suivision link>
Coverage summary
| Section |
P0 |
P1 |
P2 |
Total |
| 0 · First run — from zero to first upload |
20 |
9 |
0 |
29 |
| 1 · Step 1 — Source selection |
28 |
26 |
14 |
68 |
| 2 · Step 2 — Config |
7 |
7 |
2 |
16 |
| 3 · Step 3 — Transform pipeline |
31 |
17 |
2 |
50 |
| 4 · Folders |
20 |
12 |
2 |
34 |
| 8 · Error handling |
2 |
2 |
0 |
4 |
| 9 · Cross-browser & responsive |
2 |
5 |
2 |
9 |
| Total |
110 |
78 |
22 |
210 |
Run all P0 (~85 cases) before any release. Time estimate: 3-4 hours
solo, 1.5-2 hours with two testers in parallel.