Testing
The Upload Engine test suite validates infrastructure correctness — lifecycle, concurrency, persistence, and retry behavior. All tests run with Jest. No test depends on UI presence.
Run Tests
# All Upload Engine tests
npm test -- src/__tests__/lib/internal/UploadEngine/UploadEngine.test.ts
# Run in band (for debugging)
npm test -- src/__tests__/lib/internal/UploadEngine/UploadEngine.test.ts --runInBand
# Individual test file
npm test -- src/__tests__/lib/internal/UploadEngine/UploadWorker.test.ts
Test Suite Inventory
| File | Tests | What It Validates |
|---|---|---|
UploadEngine.test.ts |
10 | Engine orchestration, pause/resume persistence, rehydration, parallel flag propagation |
UploadSession.test.ts |
9 | State transitions, event emission, illegal transition rejection, internalFlags |
UploadWorker.test.ts |
12 | Chunk execution, pause/abort control flow, retry+recovery, parallel vs sequential, PARALLEL_CHUNK_LIMIT |
UploadQueue.test.ts |
2 | Concurrency enforcement, remove() behavior |
UploadPersistence.test.ts |
6 | Save/load, tenant filtering, SSR safety, malformed JSON handling |
RetryPolicy.test.ts |
3 | Retry bounds, linear backoff calculation, retryable error detection |
| Total | 42 |
Key Test Scenarios
UploadEngine.test.ts (10 tests)
Start with defaults:
- Creates session with correct
uploadIdformat - Applies default chunk size (5 MB) when no override provided
- Enqueues upload into queue immediately
Pause + persistence:
pause()callsworker.requestPause()+session.pause()- Persists state to
sessionStorageviaUploadPersistence.save() - Frees queue slot via
queue.finish()
Resume:
- Re-enqueues session into
UploadQueue - Emits
UPLOAD_RESUMEDimmediately - Worker's
initialize()detectsuploadServiceData→ takes resume path
Abort + cleanup:
- Calls
worker.abort()+session.abort() handleEvent(UPLOAD_ABORTED)removes persistence + deletes session- Session no longer appears in
list()
Rehydration filtering:
- Only
pausedandneeds-attentionstates are rehydrated uploadingstate in persistence is filtered out (safety)- Missing tenant cookies → no rehydration attempt
enableParallelChunkProcessing flag:
- Propagated from
StartUploadOptions→UploadSession.internalFlags - Falls back to
Config.UploadEngine.enableParallelChunkUpload - Preserved through session lifecycle and passed to worker
UploadSession.test.ts (9 tests)
Lifecycle events:
startInitialization()emitsUPLOAD_STARTEDpause()emitsUPLOAD_PAUSEDcomplete(fileUrl)emitsUPLOAD_COMPLETEDwithfileUrlabort()emitsUPLOAD_ABORTED
State machine enforcement:
fail()transitions toneeds-attention, emitsUPLOAD_NEEDS_ATTENTIONabort()from terminal state → idempotent (no-op, no event)- Illegal transition (e.g.,
idle → completed) → throws[UploadStateMachine] Illegal state transition
internalFlags:
- Accepted in constructor → stored in snapshot
- Available via
getSnapshot().internalFlags - Preserved through
rehydrate()
UploadWorker.test.ts (12 tests)
Happy path:
initialize()→uploadAllWindows()→completeMultipart()→runCompletionHandler()- Progress emitted per chunk completion
completedPartssorted byPartNumberbefore completion
Pause without persisting:
requestPause()sets flag + aborts controller- Worker catches
PauseError→ returns silently - No persistence write (engine handles that)
Abort:
abort()triggers controller abort signal- Worker catches abort → calls
session.abort()
Chunk retry + recovery:
- Non-retryable presigned URL error → throws (no retry)
- Chunk upload failure → retries up to 3x with 500ms linear backoff
markRetrying()→startUploading()cycle during retries- If all retries exhausted →
session.fail('upload', ...)+ persist
Completion failure:
completeMultipartUpload()failure → 2 retries at 1000ms- If exhausted →
session.fail('completion', ...)+ persist (doesn't throw)
Part tracking (Set + Map):
completedPartNumbers: Set<number>— skip already-completed chunks on resumecompletedPartMap: Map<number, string>— PartNumber → ETag mapping- Resume path: both populated from
snapshot.completedParts
Parallel vs sequential:
- Sequential (default): for-of loop over chunks
- Parallel (
enableParallelChunkProcessing):Promise.race()sliding window PARALLEL_CHUNK_LIMIT = 5enforced — validated that no more than 5 concurrent fetches
Sorted completedParts:
Array.from(completedPartMap.entries()).sort(([a], [b]) => a - b)beforesetCompletedParts()- Same sort applied before
completeMultipartUpload()call
UploadQueue.test.ts (2 tests)
Concurrency enforcement:
- With
limit=5, 6th upload stays in pending until one finishes finish()triggersmaybeStartNext()→ drains pending
remove() behavior:
- Removes from pending if not yet started
- If active: deletes from active + calls
onFinish+ starts next
UploadPersistence.test.ts (6 tests)
Save/load round-trip:
save()writes tosessionStoragekeyed byKeys.UploadPersistenceKeyloadAll()returns all persisted statesloadForTenant(site, siteId)filters by tenant
Remove + clearAll:
remove(key)deletes single entryclearAll()removes entire storage key
SSR safety:
- All methods guard
typeof window === 'undefined'— return[]or no-op - No throws in SSR context
Malformed JSON:
read()catchesJSON.parsefailures → returns{}- Corrupted storage doesn't crash the engine
RetryPolicy.test.ts (3 tests)
Retry bounds:
| Category | Max Retries | Base Delay |
|---|---|---|
chunk-upload |
3 | 500ms |
presigned-url |
2 | 500ms |
complete-upload |
2 | 1000ms |
completion-handler |
1 | 0ms |
Linear backoff:
getRetryDelayMs(category, attempt) = baseDelayMs × attempt- Attempt 1 → 500ms, Attempt 2 → 1000ms, Attempt 3 → 1500ms
Retryable error detection:
TypeError→ retryable (network/fetch errors)- HTTP 500+ → retryable
- HTTP 408, 429 → retryable
- HTTP 400, 403, 404 → not retryable
Test Architecture Diagram

- Unit tests validate individual modules in isolation (mocked dependencies)
- Integration tests validate cross-module behavior (engine→worker→session→retry)
What Is Intentionally Not Tested
| Concern | Why |
|---|---|
| UI rendering | Infrastructure layer — no React dependency |
| Route changes | Upload correctness is UI-independent |
| Backend cleanup | Storage hygiene is a backend concern |
| Cross-browser | Validated at integration/E2E level |
UploadService network calls |
Mocked — real HTTP validated in E2E |
Adding New Tests
- New invariants must be accompanied by tests
- Tests must target infrastructure behavior, not UI
- Failure cases must be tested explicitly — no "assume success" patterns
- Use the existing mock patterns (mock
UploadService,CookieManager,UploadPersistence)