Skip to content

feat(test): add SDK-driven E2E and load test suites#318

Merged
FSM1 merged 26 commits into
mainfrom
feat/sdk-e2e-and-load-tests
Mar 22, 2026
Merged

feat(test): add SDK-driven E2E and load test suites#318
FSM1 merged 26 commits into
mainfrom
feat/sdk-e2e-and-load-tests

Conversation

@FSM1

@FSM1 FSM1 commented Mar 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add @cipherbox/sdk-e2e package with 10 test suites that drive CipherBoxClient directly against the API (no browser, no Playwright, no UI selectors)
  • Add @cipherbox/load-tests package with 5 configurable load test scenarios using SDK client pools
  • Add sdk-e2e.yml CI workflow (PR-triggered, path-filtered) and load-test.yml (manual dispatch)

SDK E2E Tests (tests/sdk-e2e/)

Extracts the proven pattern from packages/sdk/src/__tests__/integration.test.ts into a reusable test harness, then builds 10 comprehensive test suites:

Suite Coverage
vault-lifecycle Init, duplicate 409, GET vault/export/config/quota, auth errors
folder-crud Create, nested, rename, move, delete, 20-folder stress
file-operations Upload (text, 50KB, 500KB), download round-trip, rename, delete
data-integrity 8 sizes (100B–500KB) byte-for-byte round-trip, unicode, nested
bin-operations loadBin, deleteToBin, restore, permanentDelete, emptyBin, IPNS persistence
error-cases Unloaded folder ops, BinNotLoadedError, event emission, post-destroy
share-operations Alice→Bob share, received/sent lists, key unwrap, self-share 409, revoke
invite-link Create, public status, claim, double-claim prevention, revoke
ipns-consistency Sequence monotonicity, persist through IPNS publish/resolve/reload
concurrent-operations Rapid creation, interleaved uploads, create+upload-into, rapid move

Load Tests (tests/load/)

Node.js-native load testing (not k6 — SDK needs full Node.js crypto):

Scenario Default Focus
upload-throughput 10 clients × 20 files Upload pipeline + IPNS publish
ipns-publish-storm 20 clients × 50 cycles IPNS publish contention
mixed-workload 5 clients × 45 ops Realistic weighted operation mix
sustained-load 5 clients × 5 min Latency stability over time
spike-test 2→20 burst Recovery time measurement

CI Workflows

  • sdk-e2e.yml: Triggers on PR/push to packages/**, apps/api/**, tests/sdk-e2e/**. ~50% faster than Playwright E2E (no browser download, no web build, no UI rendering)
  • load-test.yml: Manual dispatch with inputs for environment (local/staging), client count, and scenario

Test plan

  • Run pnpm --filter @cipherbox/sdk-e2e test with local API + IPFS running
  • Run pnpm --filter @cipherbox/load-tests test -- --testPathPattern=mixed-workload against local
  • Verify sdk-e2e.yml triggers on this PR and passes
  • Manually trigger load-test.yml after merge

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests

    • Added SDK end-to-end test suite covering vault operations, file/folder CRUD, concurrent operations, bin operations, data integrity, error cases, and sharing workflows.
    • Added comprehensive load testing framework with five performance test scenarios for measuring throughput and concurrent client operations.
  • Refactor

    • Migrated API client from module-level singleton to instance-scoped configuration, enabling isolated parallel client operations and improved multi-account testing support.
  • Chores

    • Reorganized test directories for clarity: consolidated web E2E tests under unified naming scheme; updated CI workflows and documentation accordingly.

Replace browser-dependent testing with SDK-level tests that drive
CipherBoxClient directly against the API, eliminating Playwright
overhead and UI flakiness.

SDK E2E (@cipherbox/sdk-e2e):
- Test harness extracting the proven integration.test.ts pattern
- Multi-account fixture with api-client singleton switching
- 10 test suites: vault-lifecycle, folder-crud, file-operations,
  data-integrity, bin-operations, error-cases, share-operations,
  invite-link, ipns-consistency, concurrent-operations

Load tests (@cipherbox/load-tests):
- Client pool managing N CipherBoxClient instances with own accounts
- Metrics collector (p50/p95/p99 latency, throughput, error rates)
- Console table + JSON reporter
- 5 scenarios: upload-throughput, ipns-publish-storm, mixed-workload,
  sustained-load, spike-test
- 3 parameterized workloads: file, folder, mixed

CI workflows:
- sdk-e2e.yml: PR-triggered, path-filtered, no browser/web build
- load-test.yml: manual dispatch with environment/clients/scenario inputs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e1698bd3af95
@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown

Walkthrough

This pull request introduces comprehensive load testing infrastructure for the CipherBox application, adds a dedicated SDK end-to-end test suite, refactors the API client from a module-level singleton to per-instance axios architecture, implements a throttle-bypass guard for rate limiting control during testing, and reorganizes E2E test directories from tests/e2e to tests/web-e2e with corresponding CI/workflow updates.

Changes

Cohort / File(s) Summary
Load Testing Infrastructure
.github/workflows/load-test.yml, tests/load/package.json, tests/load/tsconfig.json, tests/load/vitest.config.ts, tests/load/.env.example
New GitHub Actions workflow for parameterized load testing with environment/scenario/client-count inputs; workspace package manifest with Vitest configuration; environment template with load test credentials and tuning parameters.
Load Test Harness & Metrics
tests/load/src/harness/client-pool.ts, tests/load/src/harness/metrics.ts, tests/load/src/harness/reporter.ts
Core load testing harness: client pool provisioning/teardown with per-client metrics collection, aggregation, and JSON reporting; metrics collector with latency percentiles, throughput calculation, and error tracking; reporting utilities for console summaries and JSON output.
Load Test Scenarios
tests/load/src/scenarios/...
Five load test scenarios: IPNS Publish Storm, Mixed Workload, Spike Test (baseline/burst phases), Sustained Load, and Upload Throughput, each with configurable clients/operations and metrics aggregation.
Load Test Workloads
tests/load/src/workloads/folder-workload.ts, tests/load/src/workloads/file-workload.ts, tests/load/src/workloads/mixed-workload.ts
Reusable workload modules for folder CRUD operations, file upload/download/verification, and mixed operation sequences with weighted operation selection.
SDK E2E Test Infrastructure
tests/sdk-e2e/package.json, tests/sdk-e2e/tsconfig.json, tests/sdk-e2e/vitest.config.ts, tests/sdk-e2e/.env.example
New ESM workspace package for SDK E2E tests with Vitest configuration, sequential execution enforcement, and environment template for API credentials.
SDK E2E Fixtures & Helpers
tests/sdk-e2e/src/fixtures/test-harness.ts, tests/sdk-e2e/src/fixtures/multi-account.ts, tests/sdk-e2e/src/helpers/assertions.ts, tests/sdk-e2e/src/helpers/data-generators.ts
Test account provisioning via /auth/test-login with vault initialization and IPNS registration; multi-account fixture factory; domain-specific assertions for folder tree operations; deterministic data generation utilities.
SDK E2E Test Suites
tests/sdk-e2e/src/suites/...
Eight comprehensive test suites: bin operations (recycle bin lifecycle), concurrent operations (rapid mutations), data integrity (round-trip verification), error cases (negative paths and lifecycle), file operations (upload/download/CRUD), folder CRUD (create/rename/move/delete/stress), invite link flow (sharing invites via HTTP API), IPNS consistency (state persistence), share operations (folder sharing with key wrapping), and vault lifecycle (initialization and endpoints).
Throttle Bypass Guard
apps/api/src/common/guards/throttler-bypass.guard.ts, apps/api/src/common/guards/throttler-bypass.guard.spec.ts
New NestJS guard implementing conditional rate-limit bypass via X-Throttle-Bypass header with timing-safe comparison; disabled in production; comprehensive test coverage for bypass conditions, header formats, and environment scenarios.
Controller Guard Integration
apps/api/src/auth/auth.controller.ts, apps/api/src/auth/controllers/identity.controller.ts, apps/api/src/device-approval/device-approval.controller.ts, apps/api/src/ipns/ipns.controller.ts, apps/api/src/shares/invites.controller.ts, apps/api/src/shares/share-invites.controller.ts, apps/api/src/shares/shares.controller.ts
Replace NestJS ThrottlerGuard with local BypassableThrottlerGuard across seven controllers; maintain existing @Throttle configurations while enabling bypass via header.
Controller Test Updates
apps/api/src/auth/auth.controller.spec.ts, apps/api/src/auth/controllers/identity.controller.spec.ts, apps/api/src/device-approval/device-approval.controller.spec.ts, apps/api/src/ipns/__tests__/ipns.integration.spec.ts, apps/api/src/ipns/__tests__/ipns.security.spec.ts, apps/api/src/shares/invites.controller.spec.ts, apps/api/src/shares/share-invites.controller.spec.ts, apps/api/src/shares/shares.controller.spec.ts
Update Nest testing module guard overrides from ThrottlerGuard to BypassableThrottlerGuard for consistency with controller implementations.
Axios Instance Refactoring
packages/sdk-core/src/types.ts, packages/sdk-core/src/ipns/index.ts, packages/sdk-core/src/file/index.ts, packages/sdk-core/src/folder/index.ts, packages/sdk-core/src/ipfs/index.ts
Add optional axiosInstance field to SdkContext; thread context through IPNS resolution/publish, folder/file metadata operations, and IPFS upload/unpin calls; conditional branching to use provided axios instance when available.
SDK Client Axios Integration
packages/sdk/src/types.ts, packages/sdk/src/client.ts, packages/sdk/src/bin/index.ts
Extend CipherBoxClientConfig with optional defaultHeaders and axiosInstance; initialize axios instance in CipherBoxClient constructor; thread context with axios instance through batch publish calls; update bin metadata operations to use context.
SDK Integration Tests
packages/sdk/src/__tests__/integration.test.ts
Remove global setApiClientConfig calls; rely exclusively on per-instance configuration passed to CipherBoxClient constructor for isolated authentication and request handling.
Web App API Client Wiring
apps/web/src/lib/api-config.ts, apps/web/src/lib/sdk-provider.ts
Create shared axios instance via createAxiosInstance(apiConfig) with 401 refresh handling; update setApiClientConfig to supply axios instance explicitly; inject same instance into CipherBoxClient for unified request handling.
App Module Documentation
apps/api/src/app.module.ts
Add inline security comments documenting throttle bypass mechanism and production restrictions (no bypass when NODE_ENV === 'production').
E2E Directory Reorganization
tests/web-e2e/* (package.json, playwright.config.ts, playwright.load.config.ts, tests/load-test.spec.ts, utils/test-files.ts, README.md)
Rename tests/e2e/ package name from @cipherbox/e2e to @cipherbox/web-e2e; update configuration comments and documentation to reflect new directory structure.
CI/Workflow Refactoring
.github/workflows/ci.yml, .github/workflows/e2e.yml, .github/workflows/desktop-e2e.yml, .github/workflows/release-gate.yml, .github/workflows/tag-staging.yml
Add new sdk-e2e job to CI with service containers and environment setup; rename e2e workflow/job to web-e2e and retarget to @cipherbox/web-e2e; rename e2e-desktop job/workflow to desktop-e2e and update all workflow references; add sdk-e2e job as dependency for build job.
Documentation & Planning
tests/TESTING_STRATEGY.md, .planning/STATE.md, .planning/todos/done/2026-03-22-refactor-api-client-from-singleton-to-instance-scoped-axios.md, .planning/todos/done/2026-03-22-investigate-throttle-bypass-gap-for-per-endpoint-throttle-decorators.md, .claude/gsd-local-patches/get-shit-done/workflows/verify-work.md, .learnings/*, .planning/codebase/*, README.md, package.json, scripts/baseline-benchmark.sh, docs/DEVELOPMENT.md
New testing strategy document outlining three-tier test layers (SDK E2E, load testing, web E2E), rate-limiting strategy with bypass, and per-client axios scoping; planning documents for completed refactorings; updated learning notes and development guides with directory renames; root package.json npm scripts updated from test:e2e to test:web-e2e.
SDK Core Test Updates
packages/sdk-core/src/__tests__/ipns.test.ts
Update test expectations for ipnsControllerPublishRecord calls to expect two-argument invocation (including undefined second argument) instead of single argument.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test Runner
    participant API as CipherBox API
    participant IPFS as IPFS Kubo
    participant IPNS as IPNS DHT
    participant CipherBox as CipherBoxClient

    Test->>API: POST /auth/test-login (secret)
    API-->>Test: accessToken, encrypted keys
    Test->>Test: Decrypt key material
    Test->>API: POST /vault/init (encryptedKeys, rootIpnsName)
    API->>IPNS: Publish root folder metadata
    API-->>Test: Vault initialized
    Test->>CipherBox: new CipherBoxClient(config + axiosInstance)
    CipherBox->>CipherBox: registerFolder(rootIpnsName)
    CipherBox-->>Test: Client ready
    
    par Load Test Scenarios
        Test->>Test: createClientPool(clientCount)
        Test->>Test: Start metrics for each PoolClient
        loop Per Client Workload
            Test->>CipherBox: createFolder/uploadFile/renameItem
            CipherBox->>IPFS: Add data
            CipherBox->>IPNS: Publish metadata
            IPNS-->>CipherBox: sequenceNumber
            CipherBox-->>Test: operation complete
            Test->>Test: Record latency sample
        end
    end
    
    Test->>Test: aggregateAndReport()
    Test->>Test: Compute percentiles, throughput
    Test-->>Test: JSON metrics report
Loading
sequenceDiagram
    participant Controller as API Controller
    participant Guard as BypassableThrottlerGuard
    participant Throttler as ThrottlerGuard
    participant Request as Request Handler

    Controller->>Guard: canActivate(context)
    Guard->>Guard: Read THROTTLE_BYPASS_SECRET
    alt BYPASS_SECRET set & NODE_ENV !== 'production'
        Guard->>Guard: Extract x-throttle-bypass header
        alt Header matches secret (timing-safe)
            Guard-->>Controller: return true (bypass)
        else Header missing or mismatches
            Guard->>Throttler: Delegate to parent
            Throttler-->>Guard: return parent result
            Guard-->>Controller: return result
        end
    else Secret empty or production
        Guard->>Throttler: Delegate to parent
        Throttler-->>Guard: return canActivate result
        Guard-->>Controller: return result
    end
    
    alt Guard allows
        Controller->>Request: Execute handler
        Request-->>Controller: Response
    else Guard blocks
        Controller-->>Request: 429 Too Many Requests
    end
Loading
sequenceDiagram
    participant SDK as CipherBoxClient
    participant SDKCore as sdk-core module
    participant AxiosInstance as Axios Instance
    participant API as API Server

    SDK->>SDK: constructor(config + axiosInstance?)
    SDK->>SDK: Create/assign axios instance
    SDK->>SDK: Store in SdkContext.axiosInstance
    
    loop SDK Operations
        SDK->>SDKCore: createFolder(params: {..., ctx})
        SDKCore->>SDKCore: Create metadata
        SDKCore->>SDKCore: createAndPublishIpnsRecord(params, ctx)
        alt ctx.axiosInstance provided
            SDKCore->>AxiosInstance: POST /ipns/publish with instance
            AxiosInstance-->>SDKCore: Success (uses client's token)
        else No instance (fallback)
            SDKCore->>SDK: Get access token
            SDKCore->>API: POST /ipns/publish with manual auth
            API-->>SDKCore: Success
        end
        SDKCore-->>SDK: Updated metadata
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sdk-e2e-and-load-tests

- Extract shared createTestAccount() in test-harness.ts, reused by
  load test client-pool.ts (eliminates 60-line duplication)
- Extract aggregateAndReport() helper for load test scenarios
  (eliminates ~60 lines of repeated aggregation boilerplate)
- Add getReadonlySamples() to MetricsCollector (avoids unnecessary
  array copies during aggregation)
- Replace (client as any).folderTree with client.getFolderTree()
  (uses public @internal API instead of unsafe cast)
- Share single test context across 4 BinNotLoadedError tests
  (was creating 4 separate accounts, now creates 1)
- Remove redundant double-call in BinNotLoadedError deleteToBin test
- Remove unused refreshToken from TestContext interface

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: ff776297d125

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (5)
tests/load/src/workloads/folder-workload.ts (1)

1-6: Minor doc inconsistency: comment mentions "moves" but implementation doesn't include move operations.

The doc comment states "Creates, renames, moves, and deletes folders" but the implementation only performs create, rename, and delete. Consider updating the comment for accuracy.

📝 Proposed doc fix
 /**
  * Folder CRUD Workload
  *
- * Creates, renames, moves, and deletes folders.
+ * Creates, renames, and deletes folders.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/workloads/folder-workload.ts` around lines 1 - 6, Update the
top-of-file JSDoc comment that currently reads "Folder CRUD Workload: Creates,
renames, moves, and deletes folders" to accurately reflect the implemented
operations (create, rename, delete) by removing "moves" or, alternatively,
implement move functionality; locate and edit the file-level comment beginning
with "Folder CRUD Workload" (the top-of-file JSDoc) to either remove the word
"moves" or expand the implementation to include move operations in the workload
functions.
tests/load/src/scenarios/spike-test.test.ts (1)

93-104: Guard against potential division by zero in degradation calculation.

If baseOp.latency.p95 is 0 (possible with extremely fast operations or timing edge cases), the division will produce Infinity. Consider adding a guard.

🛡️ Proposed guard
     for (const baseOp of baselineOps) {
       const burstOp = burstOps.find((b) => b.operation === baseOp.operation);
       if (burstOp) {
+        const basep95 = baseOp.latency.p95 || 1; // Avoid division by zero
         const degradation = (
-          ((burstOp.latency.p95 - baseOp.latency.p95) / baseOp.latency.p95) *
+          ((burstOp.latency.p95 - baseOp.latency.p95) / basep95) *
           100
         ).toFixed(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/scenarios/spike-test.test.ts` around lines 93 - 104, The
degradation computation can divide by zero when baseOp.latency.p95 is 0; update
the loop that iterates baselineOps and computes degradation (referencing
baselineOps, burstOps, baseOp.latency.p95 and burstOp.latency.p95) to guard
against zero by checking if baseOp.latency.p95 is falsy or === 0 and handling
that case (e.g., set degradation to "N/A" or compute an absolute delta) before
calling toFixed/console.log so you never perform the division when base p95 is
zero.
tests/load/src/scenarios/mixed-workload.test.ts (1)

46-54: Throughput calculation may be inaccurate for aggregated metrics.

The aggregated MetricsCollector is started and stopped after all work completes, so getMetrics() will compute throughputOpsPerSec using the brief aggregation window rather than actual test duration. Consider using the pre-computed totalDuration to override or recalculate throughput in the report, or set the collector's timing window to match the actual workload span.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/scenarios/mixed-workload.test.ts` around lines 46 - 54, The
aggregated MetricsCollector (allMetrics) is started/stopped only around the
aggregation loop so getMetrics() computes throughputOpsPerSec over that short
window; change this by either (A) matching the collector timing to the actual
workload span (call allMetrics.start() before the pool work begins and
allMetrics.stop() after pool completes) or (B) recomputing/overriding
throughputOpsPerSec after aggregation using the pre-computed totalDuration (use
totalDuration to calculate throughput = totalOps / totalDuration and set that
value on the aggregated metrics returned by getMetrics()); locate
MetricsCollector, allMetrics, pc.metrics.getRawSamples(), getMetrics(),
totalDuration and update accordingly.
tests/sdk-e2e/src/suites/folder-crud.test.ts (1)

42-71: Avoid registering folders with synthetic key material in this suite.

getChild() only gives you child metadata, so parent.folderKey ?? new Uint8Array(32) and the empty public keys here are placeholders. Feeding those into registerFolder() seeds SDK state that cannot exist in real usage, which makes the nested/move coverage less representative. Keeping the original createFolder() result around, or creating/registering the parent within the same test, would exercise the real path end to end.

As per coding guidelines, "Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics".

Also applies to: 95-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/folder-crud.test.ts` around lines 42 - 71, The test
must not register folders with synthetic key material: replace the use of
getChild() + placeholder keys and the registerFolder(...) call that passes
parent.folderKey ?? new Uint8Array(32) and empty public keys with creating and
registering a real parent via ctx.client.createFolder(...) and then using that
createFolder result's ipnsName, folderKey, and ipnsPrivateKey when calling
ctx.client.registerFolder(...); apply the same change to the later similar
registerFolder usage (the block around the subfolder registration at lines
95-100) so both parent and subfolder are registered with real key material
returned by createFolder instead of placeholders.
tests/sdk-e2e/src/fixtures/multi-account.ts (1)

51-57: Switch the singleton before per-account teardown.

This fixture documents switchTo() as the guard around singleton-backed operations, but cleanupAll() skips it and implicitly relies on whichever account was last configured. Calling it inside the loop keeps teardown aligned with the fixture contract.

💡 Suggested fix
   const cleanupAll = async (): Promise<void> => {
-    for (const [, ctx] of accounts) {
+    for (const [label, ctx] of accounts) {
+      switchTo(label);
       ctx.cleanup();
       await deleteTestAccount(ctx);
     }
     accounts.clear();
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/fixtures/multi-account.ts` around lines 51 - 57, cleanupAll
currently calls ctx.cleanup() and deleteTestAccount(ctx) without calling
switchTo, so singleton-backed resources may operate under the wrong account;
update cleanupAll to call switchTo(ctx.accountId) (or switchTo(ctx)) before
invoking ctx.cleanup() and deleteTestAccount(ctx) for each entry in accounts,
ensuring per-account teardown runs with the correct singleton context, then
clear accounts as before. Ensure you reference the existing functions/variables:
cleanupAll, switchTo, accounts, ctx.cleanup, and deleteTestAccount.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/load-test.yml:
- Around line 133-143: The Start API server step's health-check loop (pnpm
--filter `@cipherbox/api` start:prod &, for i in $(seq 1 30); do ... curl
http://localhost:3000/health ...) never fails if the service never becomes
ready; add an explicit failure path after the loop so the step exits non-zero
when the health check times out (e.g., set a success flag or check the final
curl/loop exit status and run exit 1 with a clear message if the API never
became ready) so the workflow won't proceed to the load test when `@cipherbox/api`
didn't start.
- Around line 172-175: The workflow step "Run load test" uses the Jest-only flag
--testPathPattern; replace that option by passing the scenario pattern/path as a
positional argument to the pnpm vitest invocation. Update the pnpm command in
the "Run load test" step (the line invoking pnpm --filter `@cipherbox/load-tests`
test) to remove --testPathPattern and append "${{ inputs.scenario }}" as a
positional argument so Vitest receives the file/pattern directly.

In `@tests/load/src/harness/client-pool.ts`:
- Around line 150-166: The destroyClientPool cleanup loop currently fires DELETE
/auth/account calls without inspecting responses and will silently drop 429s;
update destroyClientPool to await the fetch response, check res.ok, and
implement an exponential backoff + retry loop (e.g., up to N attempts with
delays doubling each try) when receiving a 429/temporarily-failing status before
giving up; keep calling pc.client.destroy() as now, perform retries sequentially
per PoolClient, and when ultimately failing log a warning that includes the
final status or error alongside the pc.accessToken or client id for diagnostics.
- Around line 46-64: The current batching loop that calls createPoolClient in
Promise.allSettled silently accepts failures and returns fewer clients; update
the batch handling (the for loop that uses batchSize, Promise.allSettled,
clients, createPoolClient, and clientCount) to detect rejections and either
retry failed createPoolClient calls with exponential backoff (e.g., up to 3
attempts with increasing delays) or fail the pool creation when failures exceed
a safe threshold; alternatively throttle batches by adding a pause between
batches to respect the /auth/test-login limit so you reliably produce
clientCount clients, and ensure any final shortfall throws or returns an
explicit error rather than just console.warn.
- Around line 71-74: The test sets a global api-client getAccessToken to always
return clients[0].accessToken, which causes all SDK-core calls (e.g.,
ipnsControllerPublishRecord used by updateFolderMetadataAndPublish) to
authenticate as client 0; change this so each CipherBoxClient uses its own
token: either call setApiClientConfig per client (inside CipherBoxClient
construction) with a closure that returns that client's accessToken, or modify
the shared getAccessToken to look up the token by client id/index from the
clients array (e.g., tokenMap[clientId]) so ipnsControllerPublishRecord and
other api-client singletons obtain the correct per-client token instead of
always using clients[0].accessToken.

In `@tests/load/src/scenarios/sustained-load.test.ts`:
- Around line 38-45: The throughput is being measured over the short copy window
because allMetrics.start()/stop() are called only while copying raw samples;
update MetricsCollector usage so its elapsed time covers the actual replay/test
window (not the copy). Specifically, move allMetrics.start() to the beginning of
the replay/test loop (or the test window start) and call allMetrics.stop() at
the end of that window (or derive ops/sec from sample timestamps instead of
MetricsCollector elapsed time). Ensure references: MetricsCollector,
getMetrics(), start(), stop(), allMetrics, pc.metrics.getRawSamples(), and
record() are adjusted so getMetrics() uses the correct elapsed period.
- Around line 33-49: The test currently uses Promise.allSettled to run
runSustainedClient across pool and only logs a completion count, which masks
failed client loops; change the post-run handling to detect any rejected results
from results (the Promise.allSettled return) and fail the test by throwing or
asserting: after obtaining results, filter for r.status === 'rejected', collect
their reasons (from r.reason) and throw a new Error (or use the test framework's
fail) with a concise message including the number of failed clients and the
aggregated reasons; keep the aggregation of metrics as-is but ensure the thrown
error happens before the test finishes so a single failed runSustainedClient
causes the scenario to fail.

In `@tests/load/src/scenarios/upload-throughput.test.ts`:
- Around line 8-12: The test currently uses Promise.allSettled and only logs a
summary, so failures don't fail the Vitest case; update the workload
orchestration in upload-throughput.test.ts to fail the test when any client
workload rejects: either switch from Promise.allSettled to Promise.all for the
array of runFileWorkload promises (so Vitest will fail on first rejection), or
keep Promise.allSettled but scan its results and throw an aggregated Error if
any result.status === 'rejected' (include rejection reasons). Ensure cleanup
(destroyClientPool) still runs in a finally block and reference
functions/objects like createClientPool, destroyClientPool, runFileWorkload,
MetricsCollector, printSummary, and toJsonReport when making the change.

In `@tests/load/src/workloads/file-workload.ts`:
- Around line 41-55: When verifyDownloads is true and the uploaded file isn't
found in the folder state, fail the test instead of silently skipping
verification: in the block that reads (client as
any).folderTree.get(rootIpnsName) and finds child via children.find(c => c.name
=== fileName), detect the case where child is undefined or
child.fileMetaIpnsName is missing and throw an explicit Error indicating the
uploaded file never appeared in folder state (include rootIpnsName and fileName
for context). Otherwise proceed to call metrics.measure('downloadFile', () =>
client.downloadFromIpns(child.fileMetaIpnsName, rootFolderKey), size) and keep
the existing size check.
- Around line 27-30: The use of crypto.getRandomValues(data) in the
file-workload generator will throw QuotaExceededError for arrays larger than
65,536 bytes; update the loop that creates the Uint8Array (in
tests/load/src/workloads/file-workload.ts, inside the for (let i = 0; i <
fileCount; i++) block) to fill the buffer in chunks no larger than 65536 bytes
by repeatedly calling crypto.getRandomValues on subarrays (e.g.,
data.subarray(offset, offset+chunkSize)) until the entire Uint8Array is
populated; ensure chunkSize is 65536 and iterate offsets accordingly so larger
files are generated safely.

In `@tests/sdk-e2e/package.json`:
- Line 9: The package.json script "test:single" uses the unsupported Vitest flag
"--testPathPattern"; remove that flag from the "test:single" script in
package.json and update invocation/docs to use positional file arguments or
Vitest's test name filter (e.g., pass a file path or use
"-t"/"--testNamePattern" after a "--" when running the "test:single" script),
ensuring references to the "test:single" script are adjusted accordingly.

In `@tests/sdk-e2e/src/fixtures/test-harness.ts`:
- Around line 82-86: The test-harness currently calls setApiClientConfig which
mutates a module-level singleton and causes different createTestContext
instances to share the same getAccessToken; fix by removing reliance on the
global singleton and making API client config per-context: modify
createTestContext to stop calling setApiClientConfig and instead construct or
inject a per-context API client config (getAccessToken/ baseUrl) into
CipherBoxClient (add an optional constructor/initializer parameter in
CipherBoxClient to accept this config), update places that instantiate
CipherBoxClient to pass the context-specific config, and keep switchTo() as a
compatibility helper; alternatively, if you prefer the quick docs-only route,
add a prominent note at the top of test-harness.ts and require tests to call
switchTo() when creating multiple contexts.
- Around line 43-55: createTestContext is calling /auth/test-login for every
invocation and will exceed the endpoint's throttle; modify createTestContext to
reuse or cache test accounts instead of always creating new ones: introduce a
module-level map (e.g., testAccountCache) keyed by label or a small fixed pool
key, check the cache at the top of createTestContext and return the cached
TestContext (accessToken, refreshToken, publicKeyHex, privateKeyHex) when
present, otherwise perform the POST to /auth/test-login, store the returned
tokens/keys in the cache, and return them; ensure the cache key aligns with
existing callers (including createMultiAccountFixture) so suites can share
accounts and avoid hitting /auth/test-login repeatedly.

In `@tests/sdk-e2e/src/suites/concurrent-operations.test.ts`:
- Around line 1-8: The suite's tests claim to exercise concurrent behavior but
run all operations sequentially on a single ctx.client; change tests in
concurrent-operations.test.ts to actually run concurrent operations (e.g., use
two distinct client instances instead of a single ctx.client and launch
operations without awaiting sequentially, using Promise.all or similar) for
scenarios involving createFolder, renameItem, moveItem, deleteItem, and
deleteToBin; ensure at least one test spawns overlapping mutations from
different clients against the same folder and asserts final consistency/expected
conflict behavior rather than relying on serial awaits.

In `@tests/sdk-e2e/src/suites/error-cases.test.ts`:
- Around line 59-63: The test is too broad and accepts any rejection from
ctx.client.moveItem; update the assertion to assert the specific "Destination
folder not loaded" error so it fails for other causes. Replace the generic
rejects.toThrow() with an assertion that checks the error message (e.g., use
await expect(ctx.client.moveItem(ctx.rootIpnsName, fakeIpnsName,
'child-id')).rejects.toThrow('Destination folder not loaded')) so the test
explicitly validates the unloaded-destination branch in moveItem.

In `@tests/sdk-e2e/src/suites/invite-link.test.ts`:
- Around line 24-25: The test uses dummyReWrappedKey which only proves the
endpoint accepts a blob; replace it with a real wrapped folder key produced by
the same re-wrapping/wrapping routine used in the app (use the project helper
that creates a rewrapped/wrapped folder key for Bob instead of dummyReWrappedKey
so the invite contains a valid encrypted share), then after claiming the invite
(the code that sets inviteToken/folderIpnsName) add an assertion where Bob
unwraps/decrypts the received share with his private key (call the project’s
unwrap/decrypt function) and verify the resulting key matches the original
folder key or that Bob can open/access the folder via the SDK; update tests that
reference dummyReWrappedKey across the blocks around variables inviteToken and
folderIpnsName to ensure the full handoff is validated.

In `@tests/sdk-e2e/src/suites/share-operations.test.ts`:
- Line 10: The test currently passes share.encryptedKey (text from the
/shares/received JSON) directly into unwrapKey; instead decode that string into
a Uint8Array before calling unwrapKey so the binary API receives real bytes.
Locate the call that uses unwrapKey (and the related wrapKey/bytesToHex usage)
and replace the direct string with a decoded Uint8Array (e.g.,
Buffer.from(share.encryptedKey, 'base64') or the appropriate hex-to-bytes path)
so unwrapKey receives a Uint8Array rather than a string.

---

Nitpick comments:
In `@tests/load/src/scenarios/mixed-workload.test.ts`:
- Around line 46-54: The aggregated MetricsCollector (allMetrics) is
started/stopped only around the aggregation loop so getMetrics() computes
throughputOpsPerSec over that short window; change this by either (A) matching
the collector timing to the actual workload span (call allMetrics.start() before
the pool work begins and allMetrics.stop() after pool completes) or (B)
recomputing/overriding throughputOpsPerSec after aggregation using the
pre-computed totalDuration (use totalDuration to calculate throughput = totalOps
/ totalDuration and set that value on the aggregated metrics returned by
getMetrics()); locate MetricsCollector, allMetrics, pc.metrics.getRawSamples(),
getMetrics(), totalDuration and update accordingly.

In `@tests/load/src/scenarios/spike-test.test.ts`:
- Around line 93-104: The degradation computation can divide by zero when
baseOp.latency.p95 is 0; update the loop that iterates baselineOps and computes
degradation (referencing baselineOps, burstOps, baseOp.latency.p95 and
burstOp.latency.p95) to guard against zero by checking if baseOp.latency.p95 is
falsy or === 0 and handling that case (e.g., set degradation to "N/A" or compute
an absolute delta) before calling toFixed/console.log so you never perform the
division when base p95 is zero.

In `@tests/load/src/workloads/folder-workload.ts`:
- Around line 1-6: Update the top-of-file JSDoc comment that currently reads
"Folder CRUD Workload: Creates, renames, moves, and deletes folders" to
accurately reflect the implemented operations (create, rename, delete) by
removing "moves" or, alternatively, implement move functionality; locate and
edit the file-level comment beginning with "Folder CRUD Workload" (the
top-of-file JSDoc) to either remove the word "moves" or expand the
implementation to include move operations in the workload functions.

In `@tests/sdk-e2e/src/fixtures/multi-account.ts`:
- Around line 51-57: cleanupAll currently calls ctx.cleanup() and
deleteTestAccount(ctx) without calling switchTo, so singleton-backed resources
may operate under the wrong account; update cleanupAll to call
switchTo(ctx.accountId) (or switchTo(ctx)) before invoking ctx.cleanup() and
deleteTestAccount(ctx) for each entry in accounts, ensuring per-account teardown
runs with the correct singleton context, then clear accounts as before. Ensure
you reference the existing functions/variables: cleanupAll, switchTo, accounts,
ctx.cleanup, and deleteTestAccount.

In `@tests/sdk-e2e/src/suites/folder-crud.test.ts`:
- Around line 42-71: The test must not register folders with synthetic key
material: replace the use of getChild() + placeholder keys and the
registerFolder(...) call that passes parent.folderKey ?? new Uint8Array(32) and
empty public keys with creating and registering a real parent via
ctx.client.createFolder(...) and then using that createFolder result's ipnsName,
folderKey, and ipnsPrivateKey when calling ctx.client.registerFolder(...); apply
the same change to the later similar registerFolder usage (the block around the
subfolder registration at lines 95-100) so both parent and subfolder are
registered with real key material returned by createFolder instead of
placeholders.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0549305a-e3ea-4e0e-91e8-c135bf2ed387

📥 Commits

Reviewing files that changed from the base of the PR and between 9cdea31 and a45feba.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
📒 Files selected for processing (36)
  • .github/workflows/load-test.yml
  • .github/workflows/sdk-e2e.yml
  • release-please-config.json
  • tests/load/.env.example
  • tests/load/package.json
  • tests/load/src/harness/client-pool.ts
  • tests/load/src/harness/metrics.ts
  • tests/load/src/harness/reporter.ts
  • tests/load/src/scenarios/ipns-publish-storm.test.ts
  • tests/load/src/scenarios/mixed-workload.test.ts
  • tests/load/src/scenarios/spike-test.test.ts
  • tests/load/src/scenarios/sustained-load.test.ts
  • tests/load/src/scenarios/upload-throughput.test.ts
  • tests/load/src/workloads/file-workload.ts
  • tests/load/src/workloads/folder-workload.ts
  • tests/load/src/workloads/mixed-workload.ts
  • tests/load/tsconfig.json
  • tests/load/vitest.config.ts
  • tests/sdk-e2e/.env.example
  • tests/sdk-e2e/package.json
  • tests/sdk-e2e/src/fixtures/multi-account.ts
  • tests/sdk-e2e/src/fixtures/test-harness.ts
  • tests/sdk-e2e/src/helpers/assertions.ts
  • tests/sdk-e2e/src/helpers/data-generators.ts
  • tests/sdk-e2e/src/suites/bin-operations.test.ts
  • tests/sdk-e2e/src/suites/concurrent-operations.test.ts
  • tests/sdk-e2e/src/suites/data-integrity.test.ts
  • tests/sdk-e2e/src/suites/error-cases.test.ts
  • tests/sdk-e2e/src/suites/file-operations.test.ts
  • tests/sdk-e2e/src/suites/folder-crud.test.ts
  • tests/sdk-e2e/src/suites/invite-link.test.ts
  • tests/sdk-e2e/src/suites/ipns-consistency.test.ts
  • tests/sdk-e2e/src/suites/share-operations.test.ts
  • tests/sdk-e2e/src/suites/vault-lifecycle.test.ts
  • tests/sdk-e2e/tsconfig.json
  • tests/sdk-e2e/vitest.config.ts

Comment thread .github/workflows/load-test.yml
Comment thread .github/workflows/load-test.yml Outdated
Comment thread tests/load/src/harness/client-pool.ts
Comment thread tests/load/src/harness/client-pool.ts Outdated
Comment thread tests/load/src/harness/client-pool.ts
Comment thread tests/sdk-e2e/src/fixtures/test-harness.ts Outdated
Comment thread tests/sdk-e2e/src/suites/concurrent-operations.test.ts
Comment thread tests/sdk-e2e/src/suites/error-cases.test.ts
Comment thread tests/sdk-e2e/src/suites/invite-link.test.ts
Comment thread tests/sdk-e2e/src/suites/share-operations.test.ts Outdated
- Relax API rate limits in NODE_ENV=test (200 req/s, 10k req/min)
  to prevent 429s during rapid SDK E2E and load test operations
- Use real ECIES-wrapped keys in invite-link tests (API validates
  MinLength(258) on encryptedKey, dummy 128-char hex was too short)
- Convert hex to Uint8Array before unwrapKey in share-operations test
- Re-set api-client singleton token before event emission test
  (other suites' multi-account fixtures overwrite the singleton)

All 83 tests pass in 11.3s against local API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9319e2d02cab

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (6)
tests/sdk-e2e/src/suites/error-cases.test.ts (1)

59-63: ⚠️ Potential issue | 🟡 Minor

Assert the specific unloaded-destination error message.

This test accepts any rejection, so it won't detect if moveItem() starts failing for a different reason. The source folder is loaded, so the expected error is "Destination folder not loaded".

💡 Suggested fix
     await expect(
       ctx.client.moveItem(ctx.rootIpnsName, fakeIpnsName, 'child-id')
-    ).rejects.toThrow();
+    ).rejects.toThrow('Destination folder not loaded');

As per coding guidelines **/*.test.ts: Ensure tests are meaningful and not just for coverage metrics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/error-cases.test.ts` around lines 59 - 63, The test
currently asserts any rejection from ctx.client.moveItem; change it to assert
the specific error message "Destination folder not loaded" so it fails only for
the expected unloaded-destination case. Locate the test in error-cases.test.ts
that calls ctx.client.moveItem(ctx.rootIpnsName, fakeIpnsName, 'child-id') and
update the expectation to check the rejection message (e.g., use rejects.toThrow
with "Destination folder not loaded" or an equivalent matcher) so the test
verifies the precise error from moveItem.
tests/load/src/harness/client-pool.ts (2)

74-81: ⚠️ Potential issue | 🔴 Critical

All pool clients authenticate as client 0, defeating per-account load isolation.

The getAccessToken closure at line 79 always returns clients[0].accessToken. Since the api-client singleton invokes getAccessToken() on every request (see packages/api-client/src/instance.ts:31-37), all API calls from all pool clients authenticate as the first client, regardless of which CipherBoxClient initiated the request.

This collapses load test isolation — N clients actually share one account's auth context.

💡 Potential fix approaches
  1. Thread-local token via AsyncLocalStorage (recommended for Node.js):
import { AsyncLocalStorage } from 'async_hooks';
const tokenStorage = new AsyncLocalStorage<string>();

setApiClientConfig({
  baseUrl: API_URL,
  getAccessToken: async () => tokenStorage.getStore() ?? clients[0].accessToken,
});

// Wrap each client's workload:
tokenStorage.run(pc.accessToken, () => runWorkload(pc));
  1. Per-client api-client instances (requires SDK refactor to accept injected client)

  2. Document limitation if load tests are only meant to stress the server, not validate per-user isolation

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/harness/client-pool.ts` around lines 74 - 81, The current
getAccessToken closure passed to setApiClientConfig always returns
clients[0].accessToken causing every request to authenticate as client 0; fix by
using AsyncLocalStorage to provide a per-request/token context: create a
tokenStorage via AsyncLocalStorage<string>, change getAccessToken to return
tokenStorage.getStore() ?? clients[0].accessToken, and wrap each client workload
invocation (e.g., when starting runWorkload or similar per-CipherBoxClient
tasks) in tokenStorage.run(pc.accessToken, () => runWorkload(pc)) so each
client’s requests use its own token while preserving the shared api-client
singleton.

45-72: ⚠️ Potential issue | 🟠 Major

Batch creation silently downgrades pool size on failures.

With rate-limited endpoints, Promise.allSettled + warning logs allow the pool to be created with fewer clients than requested. Load tests may run with an under-provisioned pool without failing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/harness/client-pool.ts` around lines 45 - 72, The current
batch creation uses Promise.allSettled and only logs failures so the resulting
clients array can be smaller than the requested clientCount; update the logic
around createTestAccount / the batching loop so the pool never silently shrinks:
either retry failed creations (e.g., re-enqueue failed indices and retry until
they succeed or a retry limit) or throw an error when clients.length <
clientCount after the batching completes; refer to the symbols clientCount,
clients, batchSize, createTestAccount, PoolClient and the Promise.allSettled
block and implement the chosen strategy (retry with a limit or fail-fast) to
ensure the final clients.length equals clientCount or the creation fails loudly.
tests/sdk-e2e/src/fixtures/test-harness.ts (2)

90-104: ⚠️ Potential issue | 🟠 Major

The global api-client singleton creates token isolation issues in multi-context scenarios.

setApiClientConfig() at line 91-94 mutates a module-level singleton. When multiple TestContext instances exist (e.g., ctx and freshCtx in error-cases.test.ts), the later call overwrites the singleton's getAccessToken, causing both CipherBoxClient instances to potentially share the wrong user's token for API calls routed through the singleton.

This is documented as a known caveat but contributes to the 401 errors seen in the pipeline (Request failed with status code 401 at line 106 in error-cases.test.ts).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/fixtures/test-harness.ts` around lines 90 - 104, The test
harness currently calls the module-level singleton setter
setApiClientConfig(...) which mutates global getAccessToken and causes token
leakage between TestContext instances; remove or avoid the global call and
instead make each test context pass its own token provider directly to clients
(e.g., provide getAccessToken to CipherBoxClient constructor and ensure any code
that would use the singleton uses the per-instance client), or if the singleton
must be used, reset it per-context immediately after use; update the code around
setApiClientConfig, CipherBoxClient instantiation, and registerFolder usage so
each TestContext has its own token provider and no cross-test global mutation
occurs.

48-65: ⚠️ Potential issue | 🟠 Major

createTestAccount() will hit the /auth/test-login throttle during the E2E suite run.

The pipeline failure (429: Request failed with status code 429) confirms this. The /auth/test-login endpoint is rate-limited, and creating 22+ test accounts across suites within the throttle window exceeds the limit.

Consider:

  • Caching/reusing test accounts across suites
  • Exempting /auth/test-login from throttling in test environments
  • Adding retry with exponential backoff for 429 responses
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/fixtures/test-harness.ts` around lines 48 - 65,
createTestAccount currently calls the rate-limited /auth/test-login directly and
triggers 429s; modify createTestAccount to (1) first check a shared in-memory
cache keyed by opts.emailPrefix/opts.label (or another stable key) and return
the cached TestAccount if present to reuse accounts across suites, and (2) wrap
the fetch to "/auth/test-login" in a retry loop that detects 429 responses and
retries with exponential backoff (e.g., initial delay, multiply on each attempt,
cap attempts) before throwing; update the logic that consumes
accessToken/publicKeyHex/privateKeyHex to work the same when returning a cached
account.
tests/load/src/scenarios/sustained-load.test.ts (1)

36-44: ⚠️ Potential issue | 🟠 Major

Consider failing the test when client loops reject.

Promise.allSettled() with log-only success count lets the test pass even when multiple clients abort early. The reported load may be significantly lower than requested.

💡 Suggested fix
     const results = await Promise.allSettled(
       pool.map((pc) => runSustainedClient(pc, endTime, OPS_PER_SEC_PER_CLIENT))
     );
 
-    const succeeded = results.filter((r) => r.status === 'fulfilled').length;
-    console.log(`\nClients completed: ${succeeded}/${pool.length}`);
+    const failed = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');
+    if (failed.length > 0) {
+      console.error(`${failed.length}/${pool.length} clients failed`);
+      // Optionally throw if too many failures
+    }
+    console.log(`\nClients completed: ${pool.length - failed.length}/${pool.length}`);

As per coding guidelines **/*.test.ts: Ensure tests are meaningful and not just for coverage metrics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/scenarios/sustained-load.test.ts` around lines 36 - 44, The
test currently uses Promise.allSettled so failures are only logged and the test
can pass silently; update it to fail when any client loop rejects by either
replacing Promise.allSettled(...) with Promise.all(...) or, if you want to
preserve per-client result reporting, keep Promise.allSettled but inspect
results and throw when any r.status === 'rejected' (include count and at least
one error message) before calling aggregateAndReport; reference
runSustainedClient, pool and aggregateAndReport when making the change so the
test fails on client aborts rather than only logging them.
🧹 Nitpick comments (4)
tests/load/src/scenarios/sustained-load.test.ts (1)

85-87: Silent error swallowing is intentional here but reduces observability.

The empty catch keeps the sustained loop running, which is correct for load testing. Consider at least incrementing an error counter or logging at debug level to aid troubleshooting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/scenarios/sustained-load.test.ts` around lines 85 - 87, The
empty catch in the sustained load test silently swallows errors and reduces
observability; update the catch block inside the sustained load loop in
tests/load/src/scenarios/sustained-load.test.ts to record failures (e.g.,
increment a local error counter) and/or emit a debug-level log via the test
logger so the loop continues but incidents are visible for troubleshooting;
ensure the counter and/or logger are declared in the test scope and used inside
the catch so metrics or debug output are available after the run.
tests/sdk-e2e/src/helpers/assertions.ts (1)

53-68: Consider adding explicit return types for better type safety.

getChild and getChildren return any/any[]. If the folder tree's child type is available from @cipherbox/sdk, using it would improve IDE support and catch type mismatches in tests.

💡 Suggested improvement
-export function getChild(client: CipherBoxClient, folderIpnsName: string, name: string): any {
+export function getChild(client: CipherBoxClient, folderIpnsName: string, name: string): FolderChild {

Import FolderChild from @cipherbox/sdk if exported.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/helpers/assertions.ts` around lines 53 - 68, The functions
getChild and getChildren use any/any[] which weakens type safety; import the
proper child type (e.g., FolderChild or whatever the SDK exports for folder
nodes) from `@cipherbox/sdk` and update the signatures to return that type and an
array of that type (change getChild(...): any to getChild(...): FolderChild and
getChildren(...): any[] to getChildren(...): FolderChild[]), and update any
local usages if needed to satisfy the stronger types.
tests/sdk-e2e/src/suites/error-cases.test.ts (1)

139-157: Lifecycle test creates additional test account without cleanup in afterAll.

tempCtx is created inline at line 141 and deleted at line 155, but if the test fails before line 155, the account is orphaned. Consider using a try/finally or moving cleanup to afterAll.

💡 Suggested fix
   describe('Client lifecycle', () => {
+    let tempCtx: TestContext | null = null;
+
+    afterAll(async () => {
+      if (tempCtx) {
+        tempCtx.cleanup();
+        await deleteTestAccount(tempCtx);
+      }
+    });
+
     it('should not work after destroy()', async () => {
-      const tempCtx = await createTestContext('destroy-test');
+      tempCtx = await createTestContext('destroy-test');
       // ... rest of test
-      await deleteTestAccount(tempCtx);
     });
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/error-cases.test.ts` around lines 139 - 157, The
test creates a temporary account via createTestContext('destroy-test') assigned
to tempCtx but only calls deleteTestAccount(tempCtx) at the end, which leaves
the account orphaned if the test fails; wrap the test body so tempCtx is cleaned
up in all outcomes (use try/finally) or move creation to a beforeEach and
cleanup to afterEach/afterAll; specifically, ensure tempCtx is declared in the
test scope, then call deleteTestAccount(tempCtx) inside a finally block after
the destroy/assertions (or register cleanup in afterEach) so deleteTestAccount
always runs even if an assertion throws.
tests/load/src/scenarios/spike-test.test.ts (1)

64-77: Guard against division by zero in degradation calculation.

If baseOp.latency.p95 is 0 (e.g., no successful operations in baseline), the percentage calculation will produce Infinity or NaN.

💡 Suggested fix
       if (burstOp) {
-        const degradation = (
-          ((burstOp.latency.p95 - baseOp.latency.p95) / baseOp.latency.p95) *
-          100
-        ).toFixed(0);
+        const degradation = baseOp.latency.p95 > 0
+          ? (((burstOp.latency.p95 - baseOp.latency.p95) / baseOp.latency.p95) * 100).toFixed(0)
+          : 'N/A';
         console.log(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/scenarios/spike-test.test.ts` around lines 64 - 77, The
degradation percentage calculation can divide by zero when baseOp.latency.p95
=== 0; update the loop that compares baselineMetrics to burstMetrics (the for
... of baselineMetrics block and the degradation computation using
baseOp.latency.p95 and burstOp.latency.p95) to guard against zero or falsy
baseline p95—e.g., if baseOp.latency.p95 is 0 or NaN, skip the percentage calc
and log a meaningful placeholder like "N/A" or "0%" (or compute absolute
difference instead) so you never produce Infinity/NaN in the console output.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/load/src/harness/client-pool.ts`:
- Around line 74-81: The current getAccessToken closure passed to
setApiClientConfig always returns clients[0].accessToken causing every request
to authenticate as client 0; fix by using AsyncLocalStorage to provide a
per-request/token context: create a tokenStorage via AsyncLocalStorage<string>,
change getAccessToken to return tokenStorage.getStore() ??
clients[0].accessToken, and wrap each client workload invocation (e.g., when
starting runWorkload or similar per-CipherBoxClient tasks) in
tokenStorage.run(pc.accessToken, () => runWorkload(pc)) so each client’s
requests use its own token while preserving the shared api-client singleton.
- Around line 45-72: The current batch creation uses Promise.allSettled and only
logs failures so the resulting clients array can be smaller than the requested
clientCount; update the logic around createTestAccount / the batching loop so
the pool never silently shrinks: either retry failed creations (e.g., re-enqueue
failed indices and retry until they succeed or a retry limit) or throw an error
when clients.length < clientCount after the batching completes; refer to the
symbols clientCount, clients, batchSize, createTestAccount, PoolClient and the
Promise.allSettled block and implement the chosen strategy (retry with a limit
or fail-fast) to ensure the final clients.length equals clientCount or the
creation fails loudly.

In `@tests/load/src/scenarios/sustained-load.test.ts`:
- Around line 36-44: The test currently uses Promise.allSettled so failures are
only logged and the test can pass silently; update it to fail when any client
loop rejects by either replacing Promise.allSettled(...) with Promise.all(...)
or, if you want to preserve per-client result reporting, keep Promise.allSettled
but inspect results and throw when any r.status === 'rejected' (include count
and at least one error message) before calling aggregateAndReport; reference
runSustainedClient, pool and aggregateAndReport when making the change so the
test fails on client aborts rather than only logging them.

In `@tests/sdk-e2e/src/fixtures/test-harness.ts`:
- Around line 90-104: The test harness currently calls the module-level
singleton setter setApiClientConfig(...) which mutates global getAccessToken and
causes token leakage between TestContext instances; remove or avoid the global
call and instead make each test context pass its own token provider directly to
clients (e.g., provide getAccessToken to CipherBoxClient constructor and ensure
any code that would use the singleton uses the per-instance client), or if the
singleton must be used, reset it per-context immediately after use; update the
code around setApiClientConfig, CipherBoxClient instantiation, and
registerFolder usage so each TestContext has its own token provider and no
cross-test global mutation occurs.
- Around line 48-65: createTestAccount currently calls the rate-limited
/auth/test-login directly and triggers 429s; modify createTestAccount to (1)
first check a shared in-memory cache keyed by opts.emailPrefix/opts.label (or
another stable key) and return the cached TestAccount if present to reuse
accounts across suites, and (2) wrap the fetch to "/auth/test-login" in a retry
loop that detects 429 responses and retries with exponential backoff (e.g.,
initial delay, multiply on each attempt, cap attempts) before throwing; update
the logic that consumes accessToken/publicKeyHex/privateKeyHex to work the same
when returning a cached account.

In `@tests/sdk-e2e/src/suites/error-cases.test.ts`:
- Around line 59-63: The test currently asserts any rejection from
ctx.client.moveItem; change it to assert the specific error message "Destination
folder not loaded" so it fails only for the expected unloaded-destination case.
Locate the test in error-cases.test.ts that calls
ctx.client.moveItem(ctx.rootIpnsName, fakeIpnsName, 'child-id') and update the
expectation to check the rejection message (e.g., use rejects.toThrow with
"Destination folder not loaded" or an equivalent matcher) so the test verifies
the precise error from moveItem.

---

Nitpick comments:
In `@tests/load/src/scenarios/spike-test.test.ts`:
- Around line 64-77: The degradation percentage calculation can divide by zero
when baseOp.latency.p95 === 0; update the loop that compares baselineMetrics to
burstMetrics (the for ... of baselineMetrics block and the degradation
computation using baseOp.latency.p95 and burstOp.latency.p95) to guard against
zero or falsy baseline p95—e.g., if baseOp.latency.p95 is 0 or NaN, skip the
percentage calc and log a meaningful placeholder like "N/A" or "0%" (or compute
absolute difference instead) so you never produce Infinity/NaN in the console
output.

In `@tests/load/src/scenarios/sustained-load.test.ts`:
- Around line 85-87: The empty catch in the sustained load test silently
swallows errors and reduces observability; update the catch block inside the
sustained load loop in tests/load/src/scenarios/sustained-load.test.ts to record
failures (e.g., increment a local error counter) and/or emit a debug-level log
via the test logger so the loop continues but incidents are visible for
troubleshooting; ensure the counter and/or logger are declared in the test scope
and used inside the catch so metrics or debug output are available after the
run.

In `@tests/sdk-e2e/src/helpers/assertions.ts`:
- Around line 53-68: The functions getChild and getChildren use any/any[] which
weakens type safety; import the proper child type (e.g., FolderChild or whatever
the SDK exports for folder nodes) from `@cipherbox/sdk` and update the signatures
to return that type and an array of that type (change getChild(...): any to
getChild(...): FolderChild and getChildren(...): any[] to getChildren(...):
FolderChild[]), and update any local usages if needed to satisfy the stronger
types.

In `@tests/sdk-e2e/src/suites/error-cases.test.ts`:
- Around line 139-157: The test creates a temporary account via
createTestContext('destroy-test') assigned to tempCtx but only calls
deleteTestAccount(tempCtx) at the end, which leaves the account orphaned if the
test fails; wrap the test body so tempCtx is cleaned up in all outcomes (use
try/finally) or move creation to a beforeEach and cleanup to afterEach/afterAll;
specifically, ensure tempCtx is declared in the test scope, then call
deleteTestAccount(tempCtx) inside a finally block after the destroy/assertions
(or register cleanup in afterEach) so deleteTestAccount always runs even if an
assertion throws.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e738c095-6b6e-45ca-99e9-79505d884d8d

📥 Commits

Reviewing files that changed from the base of the PR and between a45feba and c59e851.

📒 Files selected for processing (13)
  • tests/load/src/harness/client-pool.ts
  • tests/load/src/harness/metrics.ts
  • tests/load/src/scenarios/ipns-publish-storm.test.ts
  • tests/load/src/scenarios/mixed-workload.test.ts
  • tests/load/src/scenarios/spike-test.test.ts
  • tests/load/src/scenarios/sustained-load.test.ts
  • tests/load/src/scenarios/upload-throughput.test.ts
  • tests/load/src/workloads/file-workload.ts
  • tests/load/src/workloads/mixed-workload.ts
  • tests/load/tsconfig.json
  • tests/sdk-e2e/src/fixtures/test-harness.ts
  • tests/sdk-e2e/src/helpers/assertions.ts
  • tests/sdk-e2e/src/suites/error-cases.test.ts
✅ Files skipped from review due to trivial changes (2)
  • tests/load/tsconfig.json
  • tests/load/src/scenarios/ipns-publish-storm.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/load/src/scenarios/mixed-workload.test.ts
  • tests/load/src/workloads/mixed-workload.ts
  • tests/load/src/workloads/file-workload.ts
  • tests/load/src/scenarios/upload-throughput.test.ts

FSM1 and others added 3 commits March 22, 2026 18:39
…coped axios

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3fa3e9df5c9b
Add BypassableThrottlerGuard that extends NestJS ThrottlerGuard.
When THROTTLE_BYPASS_SECRET is configured and a request includes a
matching X-Throttle-Bypass header, rate limiting is skipped.

This replaces the NODE_ENV=test conditional throttle relaxation with
a proper secret-based bypass that works across all environments:
- Production: secret not set, bypass impossible
- Staging: secret in environment, load tests can bypass
- CI: secret hardcoded in workflow .env creation
- Local dev: secret in apps/api/.env

Implementation:
- BypassableThrottlerGuard with constant-time secret comparison
- All 7 controllers updated to use the new guard
- defaultHeaders added to ApiClientConfig for axios instance
- testFetch() helper in test harness for raw fetch calls
- CI workflows inject THROTTLE_BYPASS_SECRET in .env and env vars

All 83 SDK E2E tests pass in 11.5s with production throttle limits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 801431440395
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: d81a6d44fd4a

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/sdk-e2e/src/suites/share-operations.test.ts (2)

118-120: Assert the unwrapped bytes, not just the key length.

length === 32 still passes if the API stores the wrong 32-byte ciphertext. Persist the original folder key from the create step and compare the actual bytes returned by unwrapKey().

🧪 Suggested change
-    const folderKey = await unwrapKey(hexToBytes(share.encryptedKey), bob.privateKey);
-    expect(folderKey.length).toBe(32); // AES-256 key
+    const folderKey = await unwrapKey(hexToBytes(share.encryptedKey), bob.privateKey);
+    expect(folderKey).toEqual(sharedFolderKey);

Add this near the suite state:

let sharedFolderKey: Uint8Array;

Then assign it when Alice creates the shared folder:

sharedFolderKey = new Uint8Array(folder.folderKey);

As per coding guidelines, "**/*.test.ts: Ensure tests are meaningful and not just for coverage metrics."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/share-operations.test.ts` around lines 118 - 120,
The test currently only asserts the length of the unwrapped key; instead persist
the original folder key from the create step (assign new
Uint8Array(folder.folderKey) to a suite-scoped variable like sharedFolderKey
when Alice creates the folder) and then in the assertion replace the length
check with a byte-wise comparison between the returned folderKey (from
unwrapKey(hexToBytes(share.encryptedKey), bob.privateKey)) and sharedFolderKey
to ensure the unwrapped bytes match exactly; keep using unwrapKey, hexToBytes
and share.encryptedKey to locate the code to change.

133-139: Make the self-share request valid apart from the conflict.

Using an all-zero encryptedKey means this can fail for payload-validation reasons instead of the self-share check. Reuse a real ECIES-wrapped key for the same item so the 409 is tied to the behavior this test is supposed to prove.

🧪 Suggested change
-        ipnsName: alice.rootIpnsName,
-        itemName: 'SelfShare',
-        encryptedKey: bytesToHex(new Uint8Array(64)),
+        ipnsName: sharedFolderIpnsName,
+        itemName: 'SharedFolder',
+        encryptedKey: bytesToHex(await wrapKey(sharedFolderKey, alice.publicKey)),

Support code needed near the suite state/create step:

let sharedFolderIpnsName: string;
sharedFolderIpnsName = folder.ipnsName;

As per coding guidelines, "**/*.test.ts: Ensure tests are meaningful and not just for coverage metrics."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/share-operations.test.ts` around lines 133 - 139,
The test currently sends an all-zero encryptedKey which can trigger payload
validation errors instead of the intended self-share conflict; instead reuse a
real ECIES-wrapped key for the same item by replacing the zeroed encryptedKey
with the actual wrapped key used when sharing the folder (e.g., use the
ECIES-wrapped key bytes for folder.ipnsName / sharedFolderIpnsName from the
suite setup), so that the POST body (recipientPublicKey:
'0x'+bytesToHex(alice.publicKey), itemType: 'folder', ipnsName:
alice.rootIpnsName, itemName: 'SelfShare', encryptedKey:
bytesToHex(...realWrappedKey...)) is valid and the test exercises the 409
self-share behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/common/guards/throttler-bypass.guard.ts`:
- Around line 19-27: The guard currently trusts the presence of
THROTTLE_BYPASS_SECRET alone; change the logic in throttler-bypass.guard.ts to
require an explicit opt-in before using the secret by adding a second gate
(e.g., process.env.THROTTLE_BYPASS_ENABLED === 'true' or an explicit allowlist
env like THROTTLE_BYPASS_ALLOWLIST) checked before reading secret/header; only
if the opt-in flag/allowlist permits should you read
request.headers['x-throttle-bypass'] and call timingSafeEqual(secret, header) to
return true, otherwise always fail-closed and do not consult the secret.
- Around line 33-40: Replace the custom timingSafeEqual function with Node's
native implementation: remove the hand-rolled timingSafeEqual function and
instead import timingSafeEqual from 'crypto' at the top of the file, and when
comparing secrets convert the strings to Buffers and call crypto's
timingSafeEqual (e.g., timingSafeEqual(Buffer.from(a), Buffer.from(b))); ensure
you no longer return early on length mismatch and handle unequal lengths by
comparing equal-length buffers or explicitly returning false after a safe
compare.

---

Nitpick comments:
In `@tests/sdk-e2e/src/suites/share-operations.test.ts`:
- Around line 118-120: The test currently only asserts the length of the
unwrapped key; instead persist the original folder key from the create step
(assign new Uint8Array(folder.folderKey) to a suite-scoped variable like
sharedFolderKey when Alice creates the folder) and then in the assertion replace
the length check with a byte-wise comparison between the returned folderKey
(from unwrapKey(hexToBytes(share.encryptedKey), bob.privateKey)) and
sharedFolderKey to ensure the unwrapped bytes match exactly; keep using
unwrapKey, hexToBytes and share.encryptedKey to locate the code to change.
- Around line 133-139: The test currently sends an all-zero encryptedKey which
can trigger payload validation errors instead of the intended self-share
conflict; instead reuse a real ECIES-wrapped key for the same item by replacing
the zeroed encryptedKey with the actual wrapped key used when sharing the folder
(e.g., use the ECIES-wrapped key bytes for folder.ipnsName /
sharedFolderIpnsName from the suite setup), so that the POST body
(recipientPublicKey: '0x'+bytesToHex(alice.publicKey), itemType: 'folder',
ipnsName: alice.rootIpnsName, itemName: 'SelfShare', encryptedKey:
bytesToHex(...realWrappedKey...)) is valid and the test exercises the 409
self-share behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 981354a4-83a1-4bea-98c0-dc0e74a2c41f

📥 Commits

Reviewing files that changed from the base of the PR and between c59e851 and 2dc9174.

⛔ Files ignored due to path filters (1)
  • packages/api-client/src/instance.ts is excluded by !packages/api-client/**
📒 Files selected for processing (21)
  • .github/workflows/load-test.yml
  • .github/workflows/sdk-e2e.yml
  • .planning/STATE.md
  • .planning/todos/pending/2026-03-22-refactor-api-client-from-singleton-to-instance-scoped-axios.md
  • apps/api/src/app.module.ts
  • apps/api/src/auth/auth.controller.ts
  • apps/api/src/auth/controllers/identity.controller.ts
  • apps/api/src/common/guards/throttler-bypass.guard.ts
  • apps/api/src/device-approval/device-approval.controller.ts
  • apps/api/src/ipns/ipns.controller.ts
  • apps/api/src/shares/invites.controller.ts
  • apps/api/src/shares/share-invites.controller.ts
  • apps/api/src/shares/shares.controller.ts
  • tests/TESTING_STRATEGY.md
  • tests/load/.env.example
  • tests/sdk-e2e/.env.example
  • tests/sdk-e2e/src/fixtures/test-harness.ts
  • tests/sdk-e2e/src/suites/error-cases.test.ts
  • tests/sdk-e2e/src/suites/invite-link.test.ts
  • tests/sdk-e2e/src/suites/share-operations.test.ts
  • tests/sdk-e2e/src/suites/vault-lifecycle.test.ts
✅ Files skipped from review due to trivial changes (6)
  • .planning/STATE.md
  • apps/api/src/app.module.ts
  • tests/load/.env.example
  • .planning/todos/pending/2026-03-22-refactor-api-client-from-singleton-to-instance-scoped-axios.md
  • .github/workflows/sdk-e2e.yml
  • tests/sdk-e2e/src/suites/error-cases.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/sdk-e2e/.env.example
  • .github/workflows/load-test.yml
  • tests/sdk-e2e/src/suites/vault-lifecycle.test.ts
  • tests/sdk-e2e/src/fixtures/test-harness.ts

Comment thread apps/api/src/common/guards/throttler-bypass.guard.ts
Comment thread apps/api/src/common/guards/throttler-bypass.guard.ts Outdated
FSM1 and others added 2 commits March 22, 2026 19:09
- Add try/catch to folder and file workloads so individual operation
  failures are recorded as errors but don't abort the entire workload
  (matching the mixed-workload pattern)
- Fix crypto.getRandomValues() 65536-byte limit in file-workload
  by chunking large file generation
- Add error logging to file-workload for debugging upload failures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: cec3086cee82
…Throttle decorators

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 0be7844f5d05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SDK-driven end-to-end correctness tests and Node-native load test scenarios to exercise CipherBox without a browser, plus CI workflows and an API throttling bypass mechanism to support high-volume test runs.

Changes:

  • Introduces @cipherbox/sdk-e2e Vitest suites + fixtures/helpers to provision test accounts and drive CipherBoxClient against the live API.
  • Introduces @cipherbox/load-tests scenarios + harness/metrics reporter for configurable multi-client workload generation.
  • Adds CI workflows for SDK E2E (PR/push, path-filtered) and manual load tests, and adds an API throttling bypass header/guard path for tests.

Reviewed changes

Copilot reviewed 50 out of 51 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/sdk-e2e/vitest.config.ts Vitest config for SDK E2E runtime/timeouts/serialization.
tests/sdk-e2e/tsconfig.json TS config for SDK E2E package compilation/typechecking.
tests/sdk-e2e/src/suites/vault-lifecycle.test.ts Vault init/GET/export/config/quota + auth negative tests via raw fetch.
tests/sdk-e2e/src/suites/share-operations.test.ts Multi-account share creation/received/sent/revoke validation.
tests/sdk-e2e/src/suites/ipns-consistency.test.ts IPNS sequence monotonicity + persistence across reloads.
tests/sdk-e2e/src/suites/invite-link.test.ts Invite link lifecycle (create/status/data/claim/double-claim/revoke).
tests/sdk-e2e/src/suites/folder-crud.test.ts Folder create/rename/move/delete + stress + negative cases.
tests/sdk-e2e/src/suites/file-operations.test.ts File upload/download/rename/delete + round-trip verification.
tests/sdk-e2e/src/suites/error-cases.test.ts Negative-path coverage (unloaded folders, bin not loaded, events, destroy).
tests/sdk-e2e/src/suites/data-integrity.test.ts Byte-for-byte integrity tests across sizes + unicode + nested.
tests/sdk-e2e/src/suites/concurrent-operations.test.ts “Rapid” mixed operation sequences to shake out ordering/state issues.
tests/sdk-e2e/src/suites/bin-operations.test.ts Recycle-bin lifecycle coverage and persistence checks.
tests/sdk-e2e/src/helpers/data-generators.ts Deterministic in-memory payload generators for tests.
tests/sdk-e2e/src/helpers/assertions.ts Domain-specific assertions for folder tree + byte equality.
tests/sdk-e2e/src/fixtures/test-harness.ts Shared account provisioning harness + throttle-bypass header injection.
tests/sdk-e2e/src/fixtures/multi-account.ts Multi-account fixture that swaps api-client singleton between users.
tests/sdk-e2e/package.json New workspace package definition + scripts/deps for SDK E2E.
tests/sdk-e2e/.env.example Example env vars for SDK E2E auth + throttle bypass.
tests/load/vitest.config.ts Vitest config for long-running load scenarios.
tests/load/tsconfig.json TS config for load test package (including shared harness import).
tests/load/src/workloads/mixed-workload.ts Weighted mixed-operation workload runner.
tests/load/src/workloads/folder-workload.ts Folder CRUD cycle workload runner.
tests/load/src/workloads/file-workload.ts Upload + optional download/verify workload runner.
tests/load/src/scenarios/upload-throughput.test.ts Scenario: upload throughput measurement.
tests/load/src/scenarios/sustained-load.test.ts Scenario: sustained load over time.
tests/load/src/scenarios/spike-test.test.ts Scenario: baseline → burst comparison.
tests/load/src/scenarios/mixed-workload.test.ts Scenario: mixed workload across clients.
tests/load/src/scenarios/ipns-publish-storm.test.ts Scenario: IPNS publish contention stress.
tests/load/src/harness/reporter.ts Console table + JSON report formatting.
tests/load/src/harness/metrics.ts Per-operation timing/error/bytes metrics collection and aggregation.
tests/load/src/harness/client-pool.ts Pool creation/cleanup + aggregation wrapper around scenario runs.
tests/load/package.json New workspace package definition + scripts/deps for load tests.
tests/load/.env.example Example env vars for load tests and throttle bypass.
tests/TESTING_STRATEGY.md Documentation of test suite roles, triggers, and throttle strategy.
release-please-config.json Includes new test packages in release-please extraFiles list.
pnpm-lock.yaml Lockfile updates for the two new workspace test packages.
packages/api-client/src/instance.ts Adds defaultHeaders support to singleton axios instance creation.
apps/api/src/shares/shares.controller.ts Switches rate-limit guard import to bypassable throttler guard.
apps/api/src/shares/share-invites.controller.ts Switches rate-limit guard import to bypassable throttler guard.
apps/api/src/shares/invites.controller.ts Switches rate-limit guard import to bypassable throttler guard.
apps/api/src/ipns/ipns.controller.ts Switches rate-limit guard import to bypassable throttler guard.
apps/api/src/device-approval/device-approval.controller.ts Switches rate-limit guard import to bypassable throttler guard.
apps/api/src/common/guards/throttler-bypass.guard.ts New guard enabling X-Throttle-Bypass header-based throttling bypass.
apps/api/src/auth/controllers/identity.controller.ts Switches rate-limit guard import to bypassable throttler guard.
apps/api/src/auth/auth.controller.ts Switches rate-limit guard import to bypassable throttler guard.
apps/api/src/app.module.ts Documents test bypass behavior alongside throttler module config.
.planning/todos/pending/2026-03-22-refactor-api-client-from-singleton-to-instance-scoped-axios.md Planning doc capturing singleton limitations and proposed refactor.
.planning/todos/pending/2026-03-22-investigate-throttle-bypass-gap-for-per-endpoint-throttle-decorators.md Planning doc about observed 429s and investigation steps.
.planning/STATE.md Updates planning state to reference pending todos.
.github/workflows/sdk-e2e.yml New CI workflow to run SDK E2E tests on PR/push with service deps.
.github/workflows/load-test.yml New manual workflow to run load test scenarios and upload artifacts.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread apps/api/src/common/guards/throttler-bypass.guard.ts
Comment thread apps/api/src/common/guards/throttler-bypass.guard.ts Outdated
Comment thread tests/sdk-e2e/src/suites/folder-crud.test.ts Outdated
Comment thread tests/sdk-e2e/src/suites/bin-operations.test.ts
Comment thread tests/load/src/harness/client-pool.ts Outdated
Comment thread tests/load/src/harness/client-pool.ts Outdated
Comment thread .github/workflows/load-test.yml
Comment thread tests/sdk-e2e/src/fixtures/multi-account.ts Outdated
- Add NODE_ENV !== 'production' guard to BypassableThrottlerGuard
- Use crypto.timingSafeEqual instead of custom implementation
- Handle string[] header values (Express duplicate headers)
- Add API health check failure path in CI workflows
- Fix aggregateAndReport throughput calculation (use sample timestamps)
- Write metrics JSON file for CI artifact upload
- Preserve defaultHeaders in multi-account switchTo()
- Assert specific error on unloaded destination moveItem
- Fail file-workload verification when child not found in folder state
- Fix concurrent-operations test description (serial, not concurrent)
- Rename misleading nested folder test in folder-crud

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 415d2ea03a0a
Comment thread tests/load/src/harness/client-pool.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
tests/load/src/harness/client-pool.ts (1)

64-71: ⚠️ Potential issue | 🟠 Major

Fail fast when pool provisioning under-delivers requested clients.

At Line 64-71, failed account creations are only warned. The function then returns a smaller pool (Line 88), which skews scenario semantics and throughput expectations.

Suggested patch
   const elapsed = performance.now() - start;
+  if (clients.length !== clientCount) {
+    throw new Error(
+      `Pool provisioning incomplete: requested ${clientCount}, created ${clients.length}`
+    );
+  }
   console.log(
     `Created ${clients.length}/${clientCount} clients in ${(elapsed / 1000).toFixed(1)}s`
   );

   return clients;

Also applies to: 83-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/harness/client-pool.ts` around lines 64 - 71, The current
pooling logic collects Promise.allSettled results and only warns on rejections,
returning a smaller clients array; change this to fail fast: after awaiting
Promise.allSettled(promises) (the results variable), if any result.status ===
'rejected' throw an Error (include count and reasons) instead of pushing
successes and warning, so the function does not return an under-provisioned
pool; apply the same change to the second similar block (the other
Promise.allSettled handling around the clients array) so both code paths enforce
exact client provisioning.
apps/api/src/common/guards/throttler-bypass.guard.ts (1)

23-30: ⚠️ Potential issue | 🟠 Major

Add an explicit enable flag for throttle bypass.

At Line 23, bypass is enabled whenever THROTTLE_BYPASS_SECRET exists and NODE_ENV !== 'production'. That is still too permissive; require a second explicit opt-in so bypass stays fail-closed unless intentionally enabled.

Suggested patch
   async canActivate(context: ExecutionContext): Promise<boolean> {
     const secret = process.env.THROTTLE_BYPASS_SECRET;
+    const bypassEnabled = process.env.THROTTLE_BYPASS_ENABLED === 'true';

-    if (secret && process.env.NODE_ENV !== 'production') {
+    if (bypassEnabled && secret && process.env.NODE_ENV !== 'production') {
       const request = context.switchToHttp().getRequest();
       const rawHeader = request.headers['x-throttle-bypass'];
       const header = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
       if (header && safeEqual(header, secret)) {
         return true;
       }
     }

As per coding guidelines, "apps/api/**: Focus on security vulnerabilities and input validation".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/common/guards/throttler-bypass.guard.ts` around lines 23 - 30,
The current bypass logic in the ThrottlerBypass guard enables bypass whenever
THROTTLE_BYPASS_SECRET exists and NODE_ENV !== 'production'; change this to
require an explicit enable flag so bypass is fail-closed: read a second
environment variable (e.g. THROTTLE_BYPASS_ENABLED or THROTTLE_BYPASS ===
'true') and only proceed to check the secret when that flag is truthy and
NODE_ENV !== 'production'; update the code path around the secret variable, the
request header extraction (context.switchToHttp().getRequest() / rawHeader /
header) and the safeEqual(secret, header) check so bypass returns true only when
both the enable flag is set and the header matches the secret.
.github/workflows/load-test.yml (1)

179-183: ⚠️ Potential issue | 🟠 Major

Run the scenario via test:single so filtering is actually applied.

At Line 181-182, the workflow invokes test and passes --testPathPattern directly. In this repo, scenario filtering is expected through the test:single script, not the plain test script.

Suggested patch
       - name: Run load test
         run: |
-          pnpm --filter `@cipherbox/load-tests` test -- \
-            --testPathPattern="${{ inputs.scenario }}"
+          pnpm --filter `@cipherbox/load-tests` test:single "${{ inputs.scenario }}"
#!/bin/bash
# Verify workflow command vs package scripts
set -euo pipefail

echo "=== load workflow command ==="
rg -n --type yml -C2 "Run load test|@cipherbox/load-tests|testPathPattern|test:single" .github/workflows/load-test.yml

echo "=== load package scripts ==="
rg -n --type json -C2 "\"test\"|\"test:single\"|testPathPattern" tests/load/package.json

Based on learnings: "In .github/workflows/load-test.yml, ensure the CI step runs test:single (not test) so scenario filtering via --testPathPattern is applied correctly."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/load-test.yml around lines 179 - 183, The workflow step
"Run load test" currently invokes the package script `test` with
`--testPathPattern`, but scenario filtering is implemented in the `test:single`
script; change the command to invoke `pnpm --filter `@cipherbox/load-tests` run
test:single` (preserving the existing `--testPathPattern="${{ inputs.scenario
}}"` argument) so the `testPathPattern` filtering is actually applied by the
`test:single` script.
🧹 Nitpick comments (1)
tests/sdk-e2e/src/suites/concurrent-operations.test.ts (1)

1-22: Update describe block name to match the clarified purpose.

The header comments (lines 1-15) now clearly describe these as "Rapid Sequential Operations Tests," which addresses the earlier concern. However, the describe block on line 22 still says 'Concurrent Operations'. Rename it to match the header for consistency.

✏️ Suggested rename
-describe('Concurrent Operations', () => {
+describe('Rapid Sequential Operations', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/concurrent-operations.test.ts` around lines 1 - 22,
Rename the top-level test suite string in the describe call
(describe('Concurrent Operations', ...)) to match the file header and intent —
e.g., change it to "Rapid Sequential Operations Tests" — so the describe block
string and the header comment are consistent; update the describe invocation
text accordingly in the tests/sdk-e2e/src/suites/concurrent-operations.test.ts
file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/load/src/harness/client-pool.ts`:
- Around line 77-80: setApiClientConfig is currently configured with baseUrl and
getAccessToken but omits propagating the X-Throttle-Bypass header; update the
setApiClientConfig call (used with API_URL and clients[0].accessToken) to
include a headers or getDefaultHeaders option that sets "X-Throttle-Bypass" to
the THROTTLE_BYPASS_SECRET (or reads it from env/config) so api-client-backed
calls include the bypass header and avoid unnecessary throttling during load
runs.

In `@tests/load/src/workloads/file-workload.ts`:
- Around line 58-62: The size mismatch check in file-workload.ts that currently
does console.warn when downloaded.length !== data.length should instead mark the
verification as failed: replace the warn with code that logs an error including
pc.id and both lengths, increment the verification failure metric (e.g.,
verificationFailures or similar metrics collector used by this workload), and
set/return a failure status for the current verification operation (or throw an
Error) so callers know the verification failed; update the block referencing
downloaded, data and pc.id accordingly and ensure any calling code that expects
a boolean/result handles the failure path.

---

Duplicate comments:
In @.github/workflows/load-test.yml:
- Around line 179-183: The workflow step "Run load test" currently invokes the
package script `test` with `--testPathPattern`, but scenario filtering is
implemented in the `test:single` script; change the command to invoke `pnpm
--filter `@cipherbox/load-tests` run test:single` (preserving the existing
`--testPathPattern="${{ inputs.scenario }}"` argument) so the `testPathPattern`
filtering is actually applied by the `test:single` script.

In `@apps/api/src/common/guards/throttler-bypass.guard.ts`:
- Around line 23-30: The current bypass logic in the ThrottlerBypass guard
enables bypass whenever THROTTLE_BYPASS_SECRET exists and NODE_ENV !==
'production'; change this to require an explicit enable flag so bypass is
fail-closed: read a second environment variable (e.g. THROTTLE_BYPASS_ENABLED or
THROTTLE_BYPASS === 'true') and only proceed to check the secret when that flag
is truthy and NODE_ENV !== 'production'; update the code path around the secret
variable, the request header extraction (context.switchToHttp().getRequest() /
rawHeader / header) and the safeEqual(secret, header) check so bypass returns
true only when both the enable flag is set and the header matches the secret.

In `@tests/load/src/harness/client-pool.ts`:
- Around line 64-71: The current pooling logic collects Promise.allSettled
results and only warns on rejections, returning a smaller clients array; change
this to fail fast: after awaiting Promise.allSettled(promises) (the results
variable), if any result.status === 'rejected' throw an Error (include count and
reasons) instead of pushing successes and warning, so the function does not
return an under-provisioned pool; apply the same change to the second similar
block (the other Promise.allSettled handling around the clients array) so both
code paths enforce exact client provisioning.

---

Nitpick comments:
In `@tests/sdk-e2e/src/suites/concurrent-operations.test.ts`:
- Around line 1-22: Rename the top-level test suite string in the describe call
(describe('Concurrent Operations', ...)) to match the file header and intent —
e.g., change it to "Rapid Sequential Operations Tests" — so the describe block
string and the header comment are consistent; update the describe invocation
text accordingly in the tests/sdk-e2e/src/suites/concurrent-operations.test.ts
file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1abb79fb-5cd8-4e9e-b8d9-9fef654dbcea

📥 Commits

Reviewing files that changed from the base of the PR and between 7c28ce8 and b521d51.

📒 Files selected for processing (17)
  • .github/workflows/load-test.yml
  • .github/workflows/sdk-e2e.yml
  • .planning/todos/pending/2026-03-22-investigate-throttle-bypass-gap-for-per-endpoint-throttle-decorators.md
  • apps/api/src/common/guards/throttler-bypass.guard.ts
  • tests/load/src/harness/client-pool.ts
  • tests/load/src/harness/metrics.ts
  • tests/load/src/scenarios/ipns-publish-storm.test.ts
  • tests/load/src/scenarios/mixed-workload.test.ts
  • tests/load/src/scenarios/spike-test.test.ts
  • tests/load/src/scenarios/sustained-load.test.ts
  • tests/load/src/scenarios/upload-throughput.test.ts
  • tests/load/src/workloads/file-workload.ts
  • tests/load/src/workloads/folder-workload.ts
  • tests/sdk-e2e/src/fixtures/multi-account.ts
  • tests/sdk-e2e/src/suites/concurrent-operations.test.ts
  • tests/sdk-e2e/src/suites/error-cases.test.ts
  • tests/sdk-e2e/src/suites/folder-crud.test.ts
✅ Files skipped from review due to trivial changes (6)
  • tests/load/src/scenarios/ipns-publish-storm.test.ts
  • tests/load/src/scenarios/spike-test.test.ts
  • tests/load/src/scenarios/sustained-load.test.ts
  • tests/sdk-e2e/src/suites/error-cases.test.ts
  • tests/sdk-e2e/src/suites/folder-crud.test.ts
  • .github/workflows/sdk-e2e.yml
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/load/src/scenarios/mixed-workload.test.ts
  • tests/load/src/scenarios/upload-throughput.test.ts

Comment thread tests/load/src/harness/client-pool.ts Outdated
Comment thread tests/load/src/workloads/file-workload.ts Outdated
The controller specs were overriding ThrottlerGuard from @nestjs/throttler,
but controllers now use BypassableThrottlerGuard. NestJS couldn't resolve
the guard's THROTTLER:MODULE_OPTIONS dependency in the test module context.

Fix: override BypassableThrottlerGuard instead of ThrottlerGuard in all
8 affected spec files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 644af0f89425
…smatch

- Add defaultHeaders with X-Throttle-Bypass to setApiClientConfig in
  createClientPool (was missing, causing 429s in load tests)
- Throw on download size mismatch in file-workload instead of just
  logging a warning (properly records verification failure as error)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 1441655f35a9

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 58 out of 59 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread packages/api-client/src/instance.ts
Comment thread apps/api/src/common/guards/throttler-bypass.guard.ts Outdated
Comment thread tests/sdk-e2e/src/suites/vault-lifecycle.test.ts Outdated
Comment thread tests/load/src/harness/client-pool.ts Outdated
Comment thread tests/load/src/harness/reporter.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
.github/workflows/load-test.yml (2)

179-182: ⚠️ Potential issue | 🟠 Major

Use the scenario-filtered npm script in CI (test:single).

The workflow currently invokes test and passes --testPathPattern directly, so scenario filtering can be skipped/misapplied in CI.

Suggested patch
       - name: Run load test
         run: |
-          pnpm --filter `@cipherbox/load-tests` test -- \
-            --testPathPattern="${{ inputs.scenario }}"
+          pnpm --filter `@cipherbox/load-tests` test:single -- \
+            "${{ inputs.scenario }}"
#!/bin/bash
set -euo pipefail

echo "Workflow run step:"
sed -n '176,184p' .github/workflows/load-test.yml

echo
echo "Load test scripts:"
cat tests/load/package.json | jq '.scripts'

Expected verification result: workflow should call test:single, and scenario value should be passed as positional argument.

Based on learnings, the load-test workflow should call test:single (not test) so scenario filtering is applied correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/load-test.yml around lines 179 - 182, The workflow's "Run
load test" step calls the npm script "test" and passes --testPathPattern, which
bypasses the intended scenario filtering; change the step to run the
scenario-filtered script "test:single" instead and pass the scenario as a
positional argument (using inputs.scenario) so the script can apply filtering
correctly; update the run command referenced in the step (the block that
currently invokes pnpm --filter `@cipherbox/load-tests` test --
--testPathPattern="${{ inputs.scenario }}") to call pnpm --filter
`@cipherbox/load-tests` run test:single and forward the scenario value as an
argument to that script.

139-146: ⚠️ Potential issue | 🟡 Minor

Require HTTP success in readiness checks (curl -f).

curl -s treats HTTP 500 as a successful command, so the API can be marked “ready” while unhealthy.

Suggested patch
           for i in $(seq 1 30); do
-            if curl -s http://localhost:3000/health > /dev/null 2>&1; then
+            if curl -fsS http://localhost:3000/health > /dev/null; then
               echo "API is ready"
               break
             fi
             sleep 1
           done
-          if ! curl -s http://localhost:3000/health > /dev/null 2>&1; then
+          if ! curl -fsS http://localhost:3000/health > /dev/null; then
             echo "API failed to start within 30s"
             exit 1
           fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/load-test.yml around lines 139 - 146, The readiness check
uses plain `curl -s` which treats HTTP 5xx as success; update both curl
invocations in the readiness loop and the final check to use `curl -sf` (or
`curl -s --fail`) so non-2xx responses make the command fail, ensuring the
health endpoint must return HTTP success before marking the API ready; modify
the `if curl -s http://localhost:3000/health` in the loop and the `if ! curl -s
http://localhost:3000/health` final check accordingly.
tests/load/src/workloads/file-workload.ts (1)

58-62: ⚠️ Potential issue | 🟡 Minor

Treat byte-length mismatch as a verification failure.

This branch only warns, so integrity regressions are underreported in workload results.

Suggested patch
           if (downloaded.length !== data.length) {
-            console.warn(
-              `[Client ${pc.id}] Size mismatch: uploaded ${data.length}, downloaded ${downloaded.length}`
-            );
+            throw new Error(
+              `[Client ${pc.id}] Size mismatch: uploaded ${data.length}, downloaded ${downloaded.length}`
+            );
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/workloads/file-workload.ts` around lines 58 - 62, The
size-mismatch branch currently only logs a warning which hides verification
failures; replace the console.warn call inside the block that compares
downloaded.length and data.length with a proper failure signal (e.g., throw a
descriptive Error or call the test harness failure API) so the workload records
a verification failure. Use the existing variables pc.id, data.length and
downloaded.length in the message so the thrown Error (or failure call) clearly
identifies the client and sizes.
tests/load/src/harness/client-pool.ts (1)

77-80: ⚠️ Potential issue | 🟠 Major

Propagate throttle-bypass headers into api-client config.

The singleton config currently omits defaultHeaders, so api-client-backed calls may miss X-Throttle-Bypass and get throttled during load runs.

Suggested patch
   if (clients.length > 0) {
+    const defaultHeaders = process.env.THROTTLE_BYPASS_SECRET
+      ? { 'X-Throttle-Bypass': process.env.THROTTLE_BYPASS_SECRET }
+      : undefined;
+
     setApiClientConfig({
       baseUrl: API_URL,
       getAccessToken: async () => clients[0].accessToken,
+      defaultHeaders,
     });
   }

As per coding guidelines, “verify BypassableThrottlerGuard.canActivate() actually sees X-Throttle-Bypass on IPNS publish requests (header propagation via axios defaultHeaders).”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/harness/client-pool.ts` around lines 77 - 80, The api-client
config needs to propagate the X-Throttle-Bypass header so
BypassableThrottlerGuard.canActivate() sees it: update the setApiClientConfig
call to include defaultHeaders that expose the throttle-bypass value from the
test client (e.g. add defaultHeaders: { 'X-Throttle-Bypass':
clients[0].throttleBypass } or the appropriate field where the test stores that
header) alongside baseUrl and getAccessToken so axios-backed calls include the
header during load tests.
🧹 Nitpick comments (1)
tests/load/src/scenarios/spike-test.test.ts (1)

18-18: Harden LOAD_TEST_CLIENTS parsing to avoid accidental empty burst runs.

If LOAD_TEST_CLIENTS is non-numeric, parseInt yields NaN and the burst phase can silently run with zero clients.

Suggested patch
-const BURST_CLIENTS = parseInt(process.env.LOAD_TEST_CLIENTS ?? '20', 10);
+const parsedBurstClients = Number.parseInt(process.env.LOAD_TEST_CLIENTS ?? '20', 10);
+const BURST_CLIENTS = Number.isFinite(parsedBurstClients) && parsedBurstClients > 0
+  ? parsedBurstClients
+  : 20;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/scenarios/spike-test.test.ts` at line 18, The BURST_CLIENTS
constant in spike-test.test.ts is using parseInt directly which can produce NaN
for non-numeric env values and lead to zero-client bursts; change the assignment
so you parse the env value into a variable (e.g., let parsed =
parseInt(process.env.LOAD_TEST_CLIENTS ?? '', 10)), then validate it (if
Number.isNaN(parsed) or parsed <= 0) and fallback to a safe default (e.g., 20)
before assigning to const BURST_CLIENTS; update only the variable initialization
logic around BURST_CLIENTS to ensure it never becomes NaN or a non-positive
number.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.github/workflows/load-test.yml:
- Around line 179-182: The workflow's "Run load test" step calls the npm script
"test" and passes --testPathPattern, which bypasses the intended scenario
filtering; change the step to run the scenario-filtered script "test:single"
instead and pass the scenario as a positional argument (using inputs.scenario)
so the script can apply filtering correctly; update the run command referenced
in the step (the block that currently invokes pnpm --filter
`@cipherbox/load-tests` test -- --testPathPattern="${{ inputs.scenario }}") to
call pnpm --filter `@cipherbox/load-tests` run test:single and forward the
scenario value as an argument to that script.
- Around line 139-146: The readiness check uses plain `curl -s` which treats
HTTP 5xx as success; update both curl invocations in the readiness loop and the
final check to use `curl -sf` (or `curl -s --fail`) so non-2xx responses make
the command fail, ensuring the health endpoint must return HTTP success before
marking the API ready; modify the `if curl -s http://localhost:3000/health` in
the loop and the `if ! curl -s http://localhost:3000/health` final check
accordingly.

In `@tests/load/src/harness/client-pool.ts`:
- Around line 77-80: The api-client config needs to propagate the
X-Throttle-Bypass header so BypassableThrottlerGuard.canActivate() sees it:
update the setApiClientConfig call to include defaultHeaders that expose the
throttle-bypass value from the test client (e.g. add defaultHeaders: {
'X-Throttle-Bypass': clients[0].throttleBypass } or the appropriate field where
the test stores that header) alongside baseUrl and getAccessToken so
axios-backed calls include the header during load tests.

In `@tests/load/src/workloads/file-workload.ts`:
- Around line 58-62: The size-mismatch branch currently only logs a warning
which hides verification failures; replace the console.warn call inside the
block that compares downloaded.length and data.length with a proper failure
signal (e.g., throw a descriptive Error or call the test harness failure API) so
the workload records a verification failure. Use the existing variables pc.id,
data.length and downloaded.length in the message so the thrown Error (or failure
call) clearly identifies the client and sizes.

---

Nitpick comments:
In `@tests/load/src/scenarios/spike-test.test.ts`:
- Line 18: The BURST_CLIENTS constant in spike-test.test.ts is using parseInt
directly which can produce NaN for non-numeric env values and lead to
zero-client bursts; change the assignment so you parse the env value into a
variable (e.g., let parsed = parseInt(process.env.LOAD_TEST_CLIENTS ?? '', 10)),
then validate it (if Number.isNaN(parsed) or parsed <= 0) and fallback to a safe
default (e.g., 20) before assigning to const BURST_CLIENTS; update only the
variable initialization logic around BURST_CLIENTS to ensure it never becomes
NaN or a non-positive number.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9c529729-e6a5-4ae7-85d6-538e239e304c

📥 Commits

Reviewing files that changed from the base of the PR and between 7c28ce8 and 44523a6.

📒 Files selected for processing (25)
  • .github/workflows/load-test.yml
  • .github/workflows/sdk-e2e.yml
  • .planning/todos/pending/2026-03-22-investigate-throttle-bypass-gap-for-per-endpoint-throttle-decorators.md
  • apps/api/src/auth/auth.controller.spec.ts
  • apps/api/src/auth/controllers/identity.controller.spec.ts
  • apps/api/src/common/guards/throttler-bypass.guard.ts
  • apps/api/src/device-approval/device-approval.controller.spec.ts
  • apps/api/src/ipns/__tests__/ipns.integration.spec.ts
  • apps/api/src/ipns/__tests__/ipns.security.spec.ts
  • apps/api/src/shares/invites.controller.spec.ts
  • apps/api/src/shares/share-invites.controller.spec.ts
  • apps/api/src/shares/shares.controller.spec.ts
  • tests/load/src/harness/client-pool.ts
  • tests/load/src/harness/metrics.ts
  • tests/load/src/scenarios/ipns-publish-storm.test.ts
  • tests/load/src/scenarios/mixed-workload.test.ts
  • tests/load/src/scenarios/spike-test.test.ts
  • tests/load/src/scenarios/sustained-load.test.ts
  • tests/load/src/scenarios/upload-throughput.test.ts
  • tests/load/src/workloads/file-workload.ts
  • tests/load/src/workloads/folder-workload.ts
  • tests/sdk-e2e/src/fixtures/multi-account.ts
  • tests/sdk-e2e/src/suites/concurrent-operations.test.ts
  • tests/sdk-e2e/src/suites/error-cases.test.ts
  • tests/sdk-e2e/src/suites/folder-crud.test.ts
✅ Files skipped from review due to trivial changes (11)
  • apps/api/src/auth/controllers/identity.controller.spec.ts
  • tests/load/src/scenarios/ipns-publish-storm.test.ts
  • apps/api/src/common/guards/throttler-bypass.guard.ts
  • tests/load/src/scenarios/mixed-workload.test.ts
  • tests/sdk-e2e/src/suites/concurrent-operations.test.ts
  • tests/load/src/scenarios/upload-throughput.test.ts
  • tests/load/src/scenarios/sustained-load.test.ts
  • .github/workflows/sdk-e2e.yml
  • tests/sdk-e2e/src/suites/error-cases.test.ts
  • tests/load/src/harness/metrics.ts
  • tests/sdk-e2e/src/suites/folder-crud.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/sdk-e2e/src/fixtures/multi-account.ts
  • tests/load/src/workloads/folder-workload.ts

@codecov

codecov Bot commented Mar 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.88372% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.49%. Comparing base (1be3986) to head (e404ae3).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
packages/api-client/src/instance.ts 74.00% 13 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #318      +/-   ##
==========================================
+ Coverage   50.36%   50.49%   +0.13%     
==========================================
  Files         114      116       +2     
  Lines        9162     9888     +726     
  Branches      716      720       +4     
==========================================
+ Hits         4614     4993     +379     
- Misses       4371     4718     +347     
  Partials      177      177              
Flag Coverage Δ
api 83.01% <84.88%> (+0.24%) ⬆️
api-client 83.01% <84.88%> (+0.24%) ⬆️
core 83.01% <84.88%> (+0.24%) ⬆️
crypto 83.01% <84.88%> (+0.24%) ⬆️
desktop 28.64% <ø> (+2.73%) ⬆️
sdk 83.01% <84.88%> (+0.24%) ⬆️
sdk-core 83.01% <84.88%> (+0.24%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
apps/api/src/auth/auth.controller.ts 61.90% <100.00%> (ø)
...ps/api/src/auth/controllers/identity.controller.ts 69.56% <100.00%> (+0.33%) ⬆️
...ps/api/src/common/guards/throttler-bypass.guard.ts 100.00% <100.00%> (ø)
.../src/device-approval/device-approval.controller.ts 66.66% <100.00%> (+1.14%) ⬆️
apps/api/src/ipns/ipns.controller.ts 72.72% <100.00%> (+0.85%) ⬆️
apps/api/src/shares/invites.controller.ts 80.76% <100.00%> (ø)
apps/api/src/shares/share-invites.controller.ts 70.83% <100.00%> (ø)
apps/api/src/shares/shares.controller.ts 67.18% <100.00%> (ø)
packages/sdk/src/client.ts 77.70% <100.00%> (+0.33%) ⬆️
packages/api-client/src/instance.ts 83.69% <74.00%> (+5.78%) ⬆️

... and 10 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

FSM1 and others added 2 commits March 22, 2026 20:12
- Apply defaultHeaders in createAxiosInstance() (was only in singleton)
- Fix safeEqual to compare Buffer byte lengths, not string lengths
- Use grouped regex /^(k51|bafz)/ for clarity
- Guard against division by zero in reporter throughput calculation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f413a30234b4
Vitest v3 doesn't support --testPathPattern as a CLI flag (that's Jest).
The test script silently ignored it, causing all scenarios to run.
Use test:single which has --testPathPattern embedded in the script def.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4ee44666a27d

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
.github/workflows/load-test.yml (1)

138-146: ⚠️ Potential issue | 🟠 Major

Use HTTP-failing curl flags in readiness checks.

At Line 139 and Line 145, curl -s returns success even for HTTP 500, so the API can be treated as “ready” while unhealthy. Use -fsS so only successful HTTP responses pass.

Suggested patch
       - name: Start API server
         if: inputs.environment == 'local'
         run: |
           pnpm --filter `@cipherbox/api` start:prod &
           for i in $(seq 1 30); do
-            if curl -s http://localhost:3000/health > /dev/null 2>&1; then
+            if curl -fsS http://localhost:3000/health > /dev/null; then
               echo "API is ready"
               break
             fi
             sleep 1
           done
-          if ! curl -s http://localhost:3000/health > /dev/null 2>&1; then
+          if ! curl -fsS http://localhost:3000/health > /dev/null; then
             echo "API failed to start within 30s"
             exit 1
           fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/load-test.yml around lines 138 - 146, The readiness check
uses curl -s which treats HTTP error responses as success; update both curl
invocations in the readiness loop and the final check to use curl -fsS (or
equivalent failing flags) so only 2xx/3xx responses mark the API as ready;
locate the two curl calls in the loop and the subsequent if (the loop that
echoes "API is ready" and the final if that echoes "API failed to start within
30s") and replace the -s flag with -fsS.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/load-test.yml:
- Around line 14-17: The workflow accepts a free-form input "client_count" but
exports it directly as LOAD_TEST_CLIENTS, which can be empty, non-numeric, or
non-positive; update the workflow to validate and normalize "client_count"
before exporting by coalescing to a safe positive integer (e.g., parse to int,
ensure >=1) and either set a fallback default or fail fast with a clear error
message; locate the input name "client_count" and the export of
"LOAD_TEST_CLIENTS" in the steps that set environment variables and add a
validation step that checks numeric/positive-ness and transforms the value into
a valid integer for downstream scenario parsing.

In `@tests/load/src/workloads/file-workload.ts`:
- Around line 47-67: The verification errors (file not found and size mismatch)
are thrown outside metrics.measure('downloadFile', ...) so they aren't captured
as measured errors; wrap the download plus the subsequent verification logic
inside the same metrics.measure callback (i.e., move the file existence check
and the size comparison into the async function passed to metrics.measure used
around client.downloadFromIpns), or alternatively call metrics.measure again for
the verification step so any thrown Error is recorded; update the block that
references verifyDownloads, fileIpnsName, client.downloadFromIpns, downloaded
and metrics.measure to ensure throws occur inside the measured callback so
aggregateAndReport will count them.

---

Duplicate comments:
In @.github/workflows/load-test.yml:
- Around line 138-146: The readiness check uses curl -s which treats HTTP error
responses as success; update both curl invocations in the readiness loop and the
final check to use curl -fsS (or equivalent failing flags) so only 2xx/3xx
responses mark the API as ready; locate the two curl calls in the loop and the
subsequent if (the loop that echoes "API is ready" and the final if that echoes
"API failed to start within 30s") and replace the -s flag with -fsS.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8986f2b5-a239-4d52-886f-a6fd0b01f1f5

📥 Commits

Reviewing files that changed from the base of the PR and between 44523a6 and b6c4cf4.

⛔ Files ignored due to path filters (1)
  • packages/api-client/src/instance.ts is excluded by !packages/api-client/**
📒 Files selected for processing (7)
  • .github/workflows/load-test.yml
  • apps/api/src/common/guards/throttler-bypass.guard.ts
  • tests/load/src/harness/client-pool.ts
  • tests/load/src/harness/reporter.ts
  • tests/load/src/workloads/file-workload.ts
  • tests/sdk-e2e/src/suites/folder-crud.test.ts
  • tests/sdk-e2e/src/suites/vault-lifecycle.test.ts
✅ Files skipped from review due to trivial changes (2)
  • tests/load/src/harness/reporter.ts
  • tests/sdk-e2e/src/suites/folder-crud.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/common/guards/throttler-bypass.guard.ts
  • tests/load/src/harness/client-pool.ts

Comment thread .github/workflows/load-test.yml
Comment thread tests/load/src/workloads/file-workload.ts Outdated
- Validate client_count input is a positive integer before exporting
- Move verification errors inside metrics.measure callback so they
  are captured in error metrics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3e950d8ba73e
@FSM1 FSM1 requested a review from Copilot March 22, 2026 19:33
FSM1 and others added 3 commits March 22, 2026 21:31
Eliminates the dual-path architecture where the orval singleton and
CipherBoxClient had separate axios instances:

- createAxiosInstance now supports 401 refresh (moved from getCachedInstance)
- setApiClientConfig accepts optional pre-built instance
- api-config.ts creates one shared instance, registers it as the singleton
- CipherBoxClientConfig gains optional axiosInstance field
- sdk-provider.ts passes the shared instance to CipherBoxClient
- Both orval-generated function calls and SDK operations now use the
  exact same axios instance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: ecba7ad248b8
addToIpfs and unpinFromIpfs now use ctx.axiosInstance (with auth
interceptors and default headers) instead of bare axios with manual
token injection. This ensures IPFS operations get the same 401 refresh
and default headers as IPNS operations. fetchFromIpfs still uses native
fetch for streaming download progress support.

Closes the last remaining direct-axios path in sdk-core.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 40cf1e51e1ea
@FSM1

FSM1 commented Mar 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai please review the latest changes since the last review

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown

@FSM1 Sure! I'll kick off a fresh review of the latest changes on PR #318 right away.


🧠 Learnings used
Learnt from: FSM1
Repo: FSM1/cipher-box PR: 318
File: tests/load/src/harness/client-pool.ts:77-80
Timestamp: 2026-03-22T18:39:38.011Z
Learning: In `tests/load/src/harness/client-pool.ts`, the shared `cipherbox/api-client` singleton `setApiClientConfig` is configured once using `clients[0].accessToken` as the `getAccessToken` callback, meaning all SDK-core calls (e.g., `ipnsControllerPublishRecord` via `updateFolderMetadataAndPublish`) authenticate as client 0 regardless of which pool client is making the call. This is a known, pre-existing design gap inherited from the initial web-app extraction of the api-client singleton. It is tracked for resolution in `.planning/todos/pending/2026-03-22-refactor-api-client-from-singleton-to-instance-scoped-axios.md` (refactor orval-generated functions to accept a per-instance axios instance). Do NOT re-flag this as a new issue in future PRs.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 296
File: packages/sdk/src/client.ts:286-360
Timestamp: 2026-03-20T23:50:52.646Z
Learning: In `packages/sdk/src/client.ts` (`CipherBoxClient`): Concurrent mutations to the same folder (e.g., `createFolder`, `renameItem`, `moveItem`, `deleteItem`, `deleteToBin`) are intentionally not serialized with a per-folder mutex in the SDK. Concurrent safety is provided at the UI layer — buttons/actions are disabled while an operation is in flight. A keyed async mutex/queue per folder IPNS name (and a separate one for bin state) is a known deferred improvement and should not be re-flagged as a regression in this or future PRs (first flagged in PR `#296`).

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 318
File: tests/load/src/scenarios/upload-throughput.test.ts:8-15
Timestamp: 2026-03-22T18:39:51.469Z
Learning: In `tests/load/src/scenarios/*.test.ts` (load test scenarios in FSM1/cipher-box): Load tests are measurement tools, not pass/fail correctness tests. `Promise.allSettled` is used intentionally — partial client completion is expected, and error counts are captured and reported in the metrics JSON via `aggregateAndReport`. Do NOT flag the absence of `expect(failures).toHaveLength(0)` assertions or suggest switching to `Promise.all`. The test should remain green even when some client workloads reject.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 296
File: .release-please-manifest.json:3-6
Timestamp: 2026-03-20T14:24:58.097Z
Learning: In FSM1/cipher-box, the newly extracted packages `packages/core`, `packages/api-client`, `packages/sdk-core`, and `packages/sdk` are all `private: true` and share the monorepo version (`0.24.2`). Their entries in `.release-please-manifest.json` must match the version declared in their respective `package.json` files (currently `0.24.2`). Do NOT suggest initialising them at `0.1.0` — that would create a manifest/package.json mismatch and break Release Please bump calculations. Only `packages/crypto` and the root `"."` were pre-existing; the new packages simply join the same coupled versioning scheme.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 318
File: tests/sdk-e2e/package.json:9-9
Timestamp: 2026-03-22T18:40:15.990Z
Learning: In FSM1/cipher-box (`tests/sdk-e2e/package.json` and `tests/load/package.json`), the `"test:single"` npm script uses `vitest run --no-coverage --testPathPattern` as an intentional shorthand prefix. Users append a pattern value as a positional argument (e.g. `pnpm test:single mixed-workload`). Although `--testPathPattern` is not a documented Vitest CLI flag, it is accepted by Vitest's underlying `cac` CLI parser without erroring. Do NOT flag this as a broken or unsupported flag in future PRs — the maintainer has confirmed it works as designed.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 253
File: tests/e2e-desktop/scripts/test-conflict-detection.ps1:156-157
Timestamp: 2026-03-03T14:55:05.884Z
Learning: In `tests/e2e-desktop/scripts/*.{sh,ps1}` (FUSE E2E tests): Fixed sleep-based waits after file mutations are an inherent limitation of FUSE-mount testing. The FUSE daemon provides no IPC signal or status endpoint to indicate when a debounced IPNS metadata publish completes. Tests use conservative fixed waits (e.g., 8s for initial publish accounting for 1.5s debounce + 10s safety valve, 15s for conflict resolution) and can be increased if CI proves flaky. Eliminating sleep-based waits would require exposing an IPC readiness signal from the FUSE daemon.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 295
File: .planning/phases/19.1-extract-core-crypto-sdk-as-shared-package/19.1-04-PLAN.md:89-97
Timestamp: 2026-03-20T00:44:56.582Z
Learning: In `packages/sdk/src/types.ts` (and planning docs under `.planning/phases/19.1-extract-core-crypto-sdk-as-shared-package/`), the `CipherBoxClientConfig` consumer contract intentionally contains only `{ apiUrl, getAccessToken, vaultKeypair, onOperationStart?, onOperationEnd?, onError? }`. The fields `rootIpnsName`, `rootFolderKey`, and `teeKeys` are NOT constructor config — they are internal state the `CipherBoxClient` discovers dynamically: `rootIpnsName` via API call, `rootFolderKey` derived from vault blob, `teeKeys` fetched from TEE service. Do not flag these as missing from the config type.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 318
File: tests/load/src/scenarios/sustained-load.test.ts:36-42
Timestamp: 2026-03-22T18:39:53.925Z
Learning: In `tests/load/src/scenarios/*.test.ts` (load test scenarios for `cipherbox/load-tests`): All load test scenarios intentionally use `Promise.allSettled` instead of `Promise.all` and are designed to always stay green (pass) regardless of individual client failures. These tests are performance measurement tools, not correctness tests — partial client completion is expected under high load, and error rates are captured in the metrics output. Throwing on rejected client loops would prevent collection of partial results and is explicitly rejected as a design pattern for this package.
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown

@FSM1 Sure! I'll kick off a fresh review of the latest changes on PR #318 right away.


🧠 Learnings used
Learnt from: FSM1
Repo: FSM1/cipher-box PR: 318
File: tests/load/src/harness/client-pool.ts:77-80
Timestamp: 2026-03-22T18:39:38.011Z
Learning: In `tests/load/src/harness/client-pool.ts`, the shared `cipherbox/api-client` singleton `setApiClientConfig` is configured once using `clients[0].accessToken` as the `getAccessToken` callback, meaning all SDK-core calls (e.g., `ipnsControllerPublishRecord` via `updateFolderMetadataAndPublish`) authenticate as client 0 regardless of which pool client is making the call. This is a known, pre-existing design gap inherited from the initial web-app extraction of the api-client singleton. It is tracked for resolution in `.planning/todos/pending/2026-03-22-refactor-api-client-from-singleton-to-instance-scoped-axios.md` (refactor orval-generated functions to accept a per-instance axios instance). Do NOT re-flag this as a new issue in future PRs.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 296
File: packages/sdk/src/client.ts:286-360
Timestamp: 2026-03-20T23:50:52.646Z
Learning: In `packages/sdk/src/client.ts` (`CipherBoxClient`): Concurrent mutations to the same folder (e.g., `createFolder`, `renameItem`, `moveItem`, `deleteItem`, `deleteToBin`) are intentionally not serialized with a per-folder mutex in the SDK. Concurrent safety is provided at the UI layer — buttons/actions are disabled while an operation is in flight. A keyed async mutex/queue per folder IPNS name (and a separate one for bin state) is a known deferred improvement and should not be re-flagged as a regression in this or future PRs (first flagged in PR `#296`).

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 318
File: tests/load/src/scenarios/upload-throughput.test.ts:8-15
Timestamp: 2026-03-22T18:39:51.469Z
Learning: In `tests/load/src/scenarios/*.test.ts` (load test scenarios in FSM1/cipher-box): Load tests are measurement tools, not pass/fail correctness tests. `Promise.allSettled` is used intentionally — partial client completion is expected, and error counts are captured and reported in the metrics JSON via `aggregateAndReport`. Do NOT flag the absence of `expect(failures).toHaveLength(0)` assertions or suggest switching to `Promise.all`. The test should remain green even when some client workloads reject.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 296
File: .release-please-manifest.json:3-6
Timestamp: 2026-03-20T14:24:58.097Z
Learning: In FSM1/cipher-box, the newly extracted packages `packages/core`, `packages/api-client`, `packages/sdk-core`, and `packages/sdk` are all `private: true` and share the monorepo version (`0.24.2`). Their entries in `.release-please-manifest.json` must match the version declared in their respective `package.json` files (currently `0.24.2`). Do NOT suggest initialising them at `0.1.0` — that would create a manifest/package.json mismatch and break Release Please bump calculations. Only `packages/crypto` and the root `"."` were pre-existing; the new packages simply join the same coupled versioning scheme.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 296
File: apps/web/src/hooks/useDropUpload.ts:63-74
Timestamp: 2026-03-20T14:24:39.034Z
Learning: In `apps/web/src/hooks/useDropUpload.ts`, the divergent parent-folder resolution between the collision-check path (uses `useFolderStore.getState().folders[folderId]` directly) and the upload path (uses `folderId === 'root' ? getRootFolderState(vault, folders) : folders[folderId]`) is pre-existing code moved during the Phase 19.1 SDK extraction refactor (PR `#296`). Consolidating both into a single resolution call is a known deferred improvement — do not flag this as a new regression introduced by the PR.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 249
File: apps/api/src/auth/services/auth-method.service.ts:159-194
Timestamp: 2026-03-02T22:32:04.963Z
Learning: In PR `#249` (Tier 2 structural refactoring), pre-existing patterns from moved code (e.g., auth.service.ts → auth-method.service.ts) are intentionally preserved as-is. Behavior changes—such as adding pessimistic locking or changing error handling—are out of scope for structural split PRs and should be tracked as separate follow-up improvements.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 295
File: .planning/phases/19.1-extract-core-crypto-sdk-as-shared-package/19.1-04-PLAN.md:89-97
Timestamp: 2026-03-20T00:44:56.582Z
Learning: In `packages/sdk/src/types.ts` (and planning docs under `.planning/phases/19.1-extract-core-crypto-sdk-as-shared-package/`), the `CipherBoxClientConfig` consumer contract intentionally contains only `{ apiUrl, getAccessToken, vaultKeypair, onOperationStart?, onOperationEnd?, onError? }`. The fields `rootIpnsName`, `rootFolderKey`, and `teeKeys` are NOT constructor config — they are internal state the `CipherBoxClient` discovers dynamically: `rootIpnsName` via API call, `rootFolderKey` derived from vault blob, `teeKeys` fetched from TEE service. Do not flag these as missing from the config type.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 253
File: tests/e2e-desktop/scripts/test-conflict-detection.ps1:156-157
Timestamp: 2026-03-03T14:55:05.884Z
Learning: In `tests/e2e-desktop/scripts/*.{sh,ps1}` (FUSE E2E tests): Fixed sleep-based waits after file mutations are an inherent limitation of FUSE-mount testing. The FUSE daemon provides no IPC signal or status endpoint to indicate when a debounced IPNS metadata publish completes. Tests use conservative fixed waits (e.g., 8s for initial publish accounting for 1.5s debounce + 10s safety valve, 15s for conflict resolution) and can be increased if CI proves flaky. Eliminating sleep-based waits would require exposing an IPC readiness signal from the FUSE daemon.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 318
File: tests/sdk-e2e/package.json:9-9
Timestamp: 2026-03-22T18:40:15.990Z
Learning: In FSM1/cipher-box (`tests/sdk-e2e/package.json` and `tests/load/package.json`), the `"test:single"` npm script uses `vitest run --no-coverage --testPathPattern` as an intentional shorthand prefix. Users append a pattern value as a positional argument (e.g. `pnpm test:single mixed-workload`). Although `--testPathPattern` is not a documented Vitest CLI flag, it is accepted by Vitest's underlying `cac` CLI parser without erroring. Do NOT flag this as a broken or unsupported flag in future PRs — the maintainer has confirmed it works as designed.

Learnt from: FSM1
Repo: FSM1/cipher-box PR: 318
File: tests/load/src/scenarios/sustained-load.test.ts:36-42
Timestamp: 2026-03-22T18:39:53.925Z
Learning: In `tests/load/src/scenarios/*.test.ts` (load test scenarios for `cipherbox/load-tests`): All load test scenarios intentionally use `Promise.allSettled` instead of `Promise.all` and are designed to always stay green (pass) regardless of individual client failures. These tests are performance measurement tools, not correctness tests — partial client completion is expected under high load, and error rates are captured in the metrics output. Throwing on rejected client loops would prevent collection of partial results and is explicitly rejected as a design pattern for this package.
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (1)
tests/sdk-e2e/src/fixtures/test-harness.ts (1)

141-159: ⚠️ Potential issue | 🟠 Major

Retry transient account-deletion failures in the shared helper.

Every SDK E2E suite and the load harness funnel teardown through this function. Right now any 429/5xx/network blip is logged and dropped, which leaves orphaned test users behind and contaminates later runs.

🛠️ Suggested retry loop
 export async function deleteTestAccount(
   ctx: { accessToken: string },
   apiUrl = API_URL
 ): Promise<void> {
-  try {
-    const res = await fetch(`${apiUrl}/auth/account`, {
-      method: 'DELETE',
-      headers: fetchHeaders({
-        Authorization: `Bearer ${ctx.accessToken}`,
-        'Content-Type': 'application/json',
-      }),
-      body: JSON.stringify({ confirmation: 'DELETE' }),
-    });
-    if (!res.ok) {
-      console.warn(`Account deletion failed (${res.status}): ${await res.text()}`);
-    }
-  } catch (err) {
-    console.warn(`Account deletion error: ${(err as Error).message}`);
-  }
+  let delayMs = 500;
+  for (let attempt = 1; attempt <= 5; attempt++) {
+    try {
+      const res = await fetch(`${apiUrl}/auth/account`, {
+        method: 'DELETE',
+        headers: fetchHeaders({
+          Authorization: `Bearer ${ctx.accessToken}`,
+          'Content-Type': 'application/json',
+        }),
+        body: JSON.stringify({ confirmation: 'DELETE' }),
+      });
+
+      if (res.ok) return;
+      if (![429, 502, 503, 504].includes(res.status) || attempt === 5) {
+        console.warn(`Account deletion failed (${res.status}): ${await res.text()}`);
+        return;
+      }
+    } catch (err) {
+      if (attempt === 5) {
+        console.warn(`Account deletion error: ${(err as Error).message}`);
+        return;
+      }
+    }
+
+    await new Promise((resolve) => setTimeout(resolve, delayMs));
+    delayMs *= 2;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/fixtures/test-harness.ts` around lines 141 - 159, The
deleteTestAccount helper silently drops transient failures; change
deleteTestAccount to retry transient errors (HTTP 429 and 5xx) and network
exceptions with an exponential/backoff loop: wrap the fetch call in a retry loop
(e.g., maxAttempts 3–5) that checks response.status (retry when status === 429
or status >= 500) and catches network errors to retry, awaiting an increasing
delay between attempts; if a non-retryable error (2xx/4xx other than 429) occurs
log and return, and if attempts are exhausted log an error including final
status/text or exception—refer to deleteTestAccount, ctx.accessToken, API_URL,
and fetchHeaders to locate and modify the code.
🧹 Nitpick comments (11)
tests/sdk-e2e/src/helpers/data-generators.ts (1)

10-15: Validate sizeBytes to fail fast with a clearer error.

Uint8Array(sizeBytes) will throw for invalid sizes, but adding an explicit guard makes failures easier to diagnose in tests.

♻️ Suggested patch
 export function generateBytes(sizeBytes: number, seed = 42): Uint8Array {
+  if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {
+    throw new Error(`sizeBytes must be a non-negative integer, got: ${sizeBytes}`);
+  }
   const data = new Uint8Array(sizeBytes);
   for (let i = 0; i < sizeBytes; i++) {
     data[i] = (seed + i * 7 + (i >> 8) * 13) & 0xff;
   }
   return data;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/helpers/data-generators.ts` around lines 10 - 15, The
generateBytes function should validate the sizeBytes argument to fail fast with
a clear error: check that sizeBytes is a finite integer >= 0 (and of type
number) before constructing the Uint8Array and throw a descriptive RangeError or
TypeError if invalid; update the input guard in generateBytes to validate and
throw something like "generateBytes: sizeBytes must be a non-negative integer"
so tests get a clear diagnostic instead of an internal Uint8Array error.
apps/api/src/common/guards/throttler-bypass.guard.ts (1)

25-27: Fail closed on multi-value bypass headers.

For a security-sensitive bypass header, accepting string[] and using the first value is ambiguous. Prefer only a single string value and reject arrays outright.

🔐 Suggested hardening
-      const rawHeader = request.headers['x-throttle-bypass'];
-      const header = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
+      const rawHeader = request.headers['x-throttle-bypass'];
+      const header = typeof rawHeader === 'string' ? rawHeader : undefined;
       if (header && safeEqual(header, secret)) {
         return true;
       }
As per coding guidelines, "`apps/api/**`: Focus on security vulnerabilities and input validation".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/common/guards/throttler-bypass.guard.ts` around lines 25 - 27,
The guard currently accepts multi-value headers by taking the first element
(rawHeader -> header) which is ambiguous and unsafe; update the check in the
throttler bypass logic (the rawHeader/header/secret comparison that uses
safeEqual) to reject arrays outright by returning false or treating the header
as absent when Array.isArray(rawHeader) is true, and only proceed if typeof
rawHeader === 'string' (assign header = rawHeader as string) before calling
safeEqual(header, secret), ensuring a fail-closed behavior for multi-value
headers.
tests/sdk-e2e/src/helpers/assertions.ts (1)

53-68: Consider adding type annotations for getChild and getChildren.

The any return types lose type safety. Since the folder tree contains FolderChild entries from @cipherbox/core, typed returns would improve IDE support and catch type errors in tests.

💡 Optional type improvement
+import type { FolderChild } from '@cipherbox/core';
+
 /**
  * Get a child entry from the folder tree by name.
  * Throws if not found.
  */
-export function getChild(client: CipherBoxClient, folderIpnsName: string, name: string): any {
+export function getChild(client: CipherBoxClient, folderIpnsName: string, name: string): FolderChild {
   const folder = client.getFolderTree().get(folderIpnsName);
   if (!folder) throw new Error(`Folder ${folderIpnsName} not in tree`);
   const match = folder.children.find((c) => c.name === name);
   if (!match) throw new Error(`Child "${name}" not found in folder`);
   return match;
 }

 /**
  * Get all children from the folder tree.
  */
-export function getChildren(client: CipherBoxClient, folderIpnsName: string): any[] {
+export function getChildren(client: CipherBoxClient, folderIpnsName: string): FolderChild[] {
   const folder = client.getFolderTree().get(folderIpnsName);
   if (!folder) throw new Error(`Folder ${folderIpnsName} not in tree`);
   return folder.children;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/helpers/assertions.ts` around lines 53 - 68, The functions
getChild and getChildren currently return any; import the FolderChild type from
`@cipherbox/core` and update signatures so getChild(client: CipherBoxClient,
folderIpnsName: string, name: string): FolderChild and getChildren(client:
CipherBoxClient, folderIpnsName: string): FolderChild[]; also ensure the local
variables (the folder lookup and match result in getChild) are typed/used as
FolderChild to avoid any casting surprises.
tests/sdk-e2e/.env.example (1)

8-8: Prefer non-usable placeholders for secret defaults.

Using obvious placeholders reduces accidental reuse of weak/shared secrets outside local dev.

🔧 Suggested update
-SDK_E2E_SECRET=e2e-test-secret-do-not-use-in-production
+SDK_E2E_SECRET=CHANGE_ME_LOCAL_ONLY

-THROTTLE_BYPASS_SECRET=test-throttle-bypass-secret
+THROTTLE_BYPASS_SECRET=CHANGE_ME_LOCAL_ONLY

Also applies to: 11-11

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/.env.example` at line 8, Replace the real-looking default
secret value for SDK_E2E_SECRET with a non-usable placeholder to avoid
accidental reuse; edit the SDK_E2E_SECRET entry (and the other occurrence
mentioned) so the value is a clearly-non-functional token such as
REPLACE_ME_SDK_E2E_SECRET or UNSET_SDK_E2E_SECRET and add a short comment if
supported indicating it must be set in CI/local env before running tests.
tests/load/.env.example (1)

8-8: Prefer non-functional placeholders for secret defaults.

Using concrete secret-like defaults in example files increases the chance they get copied unchanged into shared environments. Consider using explicit CHANGE_ME_* placeholders instead.

Suggested update
-LOAD_TEST_SECRET=e2e-test-secret-do-not-use-in-production
+LOAD_TEST_SECRET=CHANGE_ME_LOAD_TEST_SECRET
@@
-THROTTLE_BYPASS_SECRET=test-throttle-bypass-secret
+THROTTLE_BYPASS_SECRET=CHANGE_ME_THROTTLE_BYPASS_SECRET

Also applies to: 17-17

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/.env.example` at line 8, Replace the concrete secret default for
LOAD_TEST_SECRET with a non-functional placeholder to avoid accidental reuse
(e.g., use a CHANGE_ME_* token); update the value for LOAD_TEST_SECRET in the
tests/load/.env.example and likewise replace the other occurrence noted (line
17) so both use an explicit placeholder such as CHANGE_ME_LOAD_TEST_SECRET or
<CHANGE_ME_LOAD_TEST_SECRET> instead of an actual-looking secret.
tests/load/src/scenarios/spike-test.test.ts (1)

66-77: Consider guarding against zero baseline latency in degradation calculation.

If baseOp.latency.p95 is zero (edge case if no samples collected), the division will produce Infinity or NaN.

♻️ Suggested defensive check
       if (burstOp) {
-        const degradation = (
-          ((burstOp.latency.p95 - baseOp.latency.p95) / baseOp.latency.p95) *
-          100
-        ).toFixed(0);
+        const degradation = baseOp.latency.p95 > 0
+          ? (((burstOp.latency.p95 - baseOp.latency.p95) / baseOp.latency.p95) * 100).toFixed(0)
+          : 'N/A';
         console.log(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/load/src/scenarios/spike-test.test.ts` around lines 66 - 77, The
degradation calculation can divide by zero when baseOp.latency.p95 is 0; in the
loop that iterates baselineMetrics and finds burstOp from burstMetrics (using
baseOp and burstOp variables), add a defensive check on baseOp.latency.p95
before computing degradation — if it is 0 or falsy, skip the percentage
calculation and log a safe placeholder like "N/A" (or treat degradation as 0)
instead of performing the division, then print the same console message using
that placeholder; ensure you reference baseOp.latency.p95 and
burstOp.latency.p95 when performing the guarded computation.
tests/sdk-e2e/src/suites/concurrent-operations.test.ts (1)

22-22: Consider renaming the describe block to match the updated header.

The file header comments were updated to "Rapid Sequential Operations" (per the previous review fix), but the describe block still uses 'Concurrent Operations'. This mismatch could cause confusion when viewing test results.

Suggested fix
-describe('Concurrent Operations', () => {
+describe('Rapid Sequential Operations', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/concurrent-operations.test.ts` at line 22, Rename
the describe block string to match the updated file header by changing the
existing describe('Concurrent Operations', ...) to describe('Rapid Sequential
Operations', ...); update any matching test suite title usages in the same file
(the describe block and any snapshot/test reporting strings) so the suite name
and header comment are consistent.
tests/sdk-e2e/src/suites/bin-operations.test.ts (1)

50-100: Prefer public loadBin() assertions over internal binState access.

Relying on (ctx.client as any).binState ties tests to internals and makes them brittle. For E2E quality, assert state transitions through public APIs (loadBin) after each mutation.

As per coding guidelines, "**/*.test.ts: Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics".

Also applies to: 116-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/bin-operations.test.ts` around lines 50 - 100,
Replace direct internal access to (ctx.client as any).binState with assertions
using the public API ctx.client.loadBin(): after operations like deleteToBin,
restoreFromBin, and permanentDelete call await ctx.client.loadBin() and assert
on the returned entries array (e.g., check length, find by name, itemType)
instead of reading binState; update tests that currently reference binState
(including the case that checks BinFolder and bin-file.txt) to use loadBin() for
both pre- and post-mutation verification so tests rely only on public methods
(also update any other occurrences that access binState directly, e.g., the
other test block that inspects BinFolder).
tests/sdk-e2e/src/suites/error-cases.test.ts (1)

151-153: Make the post-destroy() failure assertion specific.

Using a generic .toThrow() allows unrelated errors to pass this test. Prefer asserting a stable destroyed-client error substring (or specific error type) to keep this check semantically tight.

As per coding guidelines, "**/*.test.ts: Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/error-cases.test.ts` around lines 151 - 153, The
test currently uses a generic await
expect(tempCtx.client.createFolder(...)).rejects.toThrow(); change this to
assert a specific destroyed-client error substring or type so unrelated errors
don't pass: update the expectation to await
expect(tempCtx.client.createFolder(tempCtx.rootIpnsName,
'PostDestroy')).rejects.toThrowError(/destroy(ed)? client|client has been
destroyed/i) or match the exact error message/type your client throws after
calling destroy(), referencing tempCtx.client.createFolder and the client's
destroy() behavior so the assertion is tightly scoped.
tests/sdk-e2e/src/suites/file-operations.test.ts (1)

32-151: Reduce cross-test state coupling in this suite.

A few cases depend on side effects from earlier tests (e.g., hello.txt → rename → delete). Consider making each case self-contained (or seed state in hooks) to avoid cascade failures and improve single-test reruns.

As per coding guidelines, "**/*.test.ts: Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/file-operations.test.ts` around lines 32 - 151,
Tests in this suite are tightly coupled (e.g., hello.txt created in one test is
renamed and deleted in later tests) causing cascade failures; make each test
self-contained by having each test create (via ctx.client.uploadFile or
ctx.client.createFolder + registerFolder) the specific files/folders it needs
using unique names (e.g., include Date.now() or a UUID), use beforeEach to seed
required items and afterEach to clean up via deleteItem, and update references
that currently rely on shared state (getChild, expectChildNamed, renameItem,
deleteItem) to operate on the locally created item's id/ipnsName so tests can
run independently and be rerun individually.
tests/sdk-e2e/src/suites/folder-crud.test.ts (1)

17-29: Avoid splitting one stateful workflow across multiple it blocks.

Most cases here assume folders created or renamed by earlier tests. That makes targeted reruns brittle and turns one failure into a cascade. If the single shared account is important for speed, I’d collapse the dependent chain into one flow test instead of spreading it across separate cases.

As per coding guidelines, **/*.test.ts: "Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics."

Also applies to: 31-140

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sdk-e2e/src/suites/folder-crud.test.ts` around lines 17 - 29, The tests
under describe('Folder CRUD') split a single stateful workflow across multiple
it blocks using the shared ctx from createTestContext in beforeAll, which makes
reruns brittle; either collapse the dependent steps into one end-to-end test
(e.g., a single it('full folder CRUD flow') that performs create → rename →
delete in sequence) or make each test independent by moving setup/teardown into
beforeEach/afterEach and creating/cleaning its own context (use
createTestContext and ctx.cleanup/deleteTestAccount within per-test
setup/teardown) so no test relies on side effects from others.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app.module.ts`:
- Around line 50-51: Update the module comment explaining
BypassableThrottlerGuard to accurately reflect runtime behavior: state that the
X-Throttle-Bypass header can be used to skip rate limits only when
THROTTLE_BYPASS_SECRET is set and NODE_ENV !== 'production' (bypass is blocked
in production), so modify the comment above BypassableThrottlerGuard in
app.module.ts to include the NODE_ENV === 'production' restriction for
operators.

In `@tests/load/src/harness/client-pool.ts`:
- Around line 32-38: Update the docblock to remove references to a removed
shared api-client singleton and clarify the current behavior: state that the
helper creates N authenticated CipherBoxClient instances by delegating to
createTestAccount(), each client gets its own test account, vault, and metrics
collector, and each client manages its own auth tokens (no shared api-client or
global token handling). Mention createTestAccount and CipherBoxClient by name so
readers can locate the implementation.

In `@tests/load/src/workloads/file-workload.ts`:
- Around line 29-30: Check and guard the minSize/maxSize bounds before computing
size in file-workload.ts: validate that maxSize >= minSize (or that both are
non-negative) at the start of the workload setup (the code that defines minSize,
maxSize and computes size), and if the check fails either throw a clear error or
normalize the values (e.g., swap or set maxSize = minSize) before the size
calculation and new Uint8Array(size) allocation; reference the variables
minSize, maxSize and the allocation of new Uint8Array(size) so the validation is
added immediately prior to computing size.
- Around line 60-65: The current check in the download verification (inside the
code that calls client.downloadFromIpns and compares downloaded.length to
data.length) can miss corruption when sizes match; update the verification in
the verify/download logic to compare the actual payload contents rather than
just lengths—e.g., compute and compare a digest (hash) or perform a
byte-for-byte equality between downloaded and data (referencing variables
downloaded and data, and the client.downloadFromIpns call) and, on mismatch,
throw a descriptive error that includes context such as pc.id and rootFolderKey.

In `@tests/load/src/workloads/mixed-workload.ts`:
- Around line 36-37: The merged weights (weights = { ...DEFAULT_WEIGHTS,
...opts.weights }) must be validated before using totalWeight: ensure every key
in weights is a finite number (not undefined, NaN, or Infinity) and >= 0, and
that totalWeight (computed via Object.values(weights).reduce...) is > 0; if any
check fails, throw a clear error (or reject the config) referencing the
offending key(s) so the caller can fix opts.weights rather than silently
producing wrong workload behavior—apply this validation just after computing
weights/totalWeight in mixed-workload.ts.

In `@tests/sdk-e2e/src/fixtures/test-harness.ts`:
- Around line 80-127: After successfully obtaining accessToken from loginRes,
the setup steps (initializeVault, encryptVaultKeys, deriveIpnsName, the
/vault/init fetch, CipherBoxClient construction, and client.registerFolder) can
fail and currently leak the partly-provisioned test account; wrap steps that
begin with initializeVault and end after client.registerFolder in a try/catch,
and in the catch call await deleteTestAccount({ accessToken }, apiUrl) to
rollback the created account before rethrowing the original error so failures
don’t leave test accounts behind.
- Around line 107-116: CipherBoxClientConfig should not include rootIpnsName,
rootFolderKey, or teeKeys because these are discovered at runtime; remove those
fields from the CipherBoxClientConfig type, delete the constructor copy that
sets this.internalRootFolderKey from config (the assignment where
this.internalRootFolderKey = new Uint8Array(config.rootFolderKey)), and update
all code paths that read this.config.teeKeys (calls inside CipherBoxClient
methods such as where teeKeys are referenced) to obtain teeKeys via an internal
fetch/initialization path or an injectable test helper instead of reading from
config; finally, update the test harness to stop passing
rootIpnsName/rootFolderKey/teeKeys into new CipherBoxClient(...) and instead let
the client discover them or call registerFolder(...) only where appropriate.

---

Duplicate comments:
In `@tests/sdk-e2e/src/fixtures/test-harness.ts`:
- Around line 141-159: The deleteTestAccount helper silently drops transient
failures; change deleteTestAccount to retry transient errors (HTTP 429 and 5xx)
and network exceptions with an exponential/backoff loop: wrap the fetch call in
a retry loop (e.g., maxAttempts 3–5) that checks response.status (retry when
status === 429 or status >= 500) and catches network errors to retry, awaiting
an increasing delay between attempts; if a non-retryable error (2xx/4xx other
than 429) occurs log and return, and if attempts are exhausted log an error
including final status/text or exception—refer to deleteTestAccount,
ctx.accessToken, API_URL, and fetchHeaders to locate and modify the code.

---

Nitpick comments:
In `@apps/api/src/common/guards/throttler-bypass.guard.ts`:
- Around line 25-27: The guard currently accepts multi-value headers by taking
the first element (rawHeader -> header) which is ambiguous and unsafe; update
the check in the throttler bypass logic (the rawHeader/header/secret comparison
that uses safeEqual) to reject arrays outright by returning false or treating
the header as absent when Array.isArray(rawHeader) is true, and only proceed if
typeof rawHeader === 'string' (assign header = rawHeader as string) before
calling safeEqual(header, secret), ensuring a fail-closed behavior for
multi-value headers.

In `@tests/load/.env.example`:
- Line 8: Replace the concrete secret default for LOAD_TEST_SECRET with a
non-functional placeholder to avoid accidental reuse (e.g., use a CHANGE_ME_*
token); update the value for LOAD_TEST_SECRET in the tests/load/.env.example and
likewise replace the other occurrence noted (line 17) so both use an explicit
placeholder such as CHANGE_ME_LOAD_TEST_SECRET or <CHANGE_ME_LOAD_TEST_SECRET>
instead of an actual-looking secret.

In `@tests/load/src/scenarios/spike-test.test.ts`:
- Around line 66-77: The degradation calculation can divide by zero when
baseOp.latency.p95 is 0; in the loop that iterates baselineMetrics and finds
burstOp from burstMetrics (using baseOp and burstOp variables), add a defensive
check on baseOp.latency.p95 before computing degradation — if it is 0 or falsy,
skip the percentage calculation and log a safe placeholder like "N/A" (or treat
degradation as 0) instead of performing the division, then print the same
console message using that placeholder; ensure you reference baseOp.latency.p95
and burstOp.latency.p95 when performing the guarded computation.

In `@tests/sdk-e2e/.env.example`:
- Line 8: Replace the real-looking default secret value for SDK_E2E_SECRET with
a non-usable placeholder to avoid accidental reuse; edit the SDK_E2E_SECRET
entry (and the other occurrence mentioned) so the value is a
clearly-non-functional token such as REPLACE_ME_SDK_E2E_SECRET or
UNSET_SDK_E2E_SECRET and add a short comment if supported indicating it must be
set in CI/local env before running tests.

In `@tests/sdk-e2e/src/helpers/assertions.ts`:
- Around line 53-68: The functions getChild and getChildren currently return
any; import the FolderChild type from `@cipherbox/core` and update signatures so
getChild(client: CipherBoxClient, folderIpnsName: string, name: string):
FolderChild and getChildren(client: CipherBoxClient, folderIpnsName: string):
FolderChild[]; also ensure the local variables (the folder lookup and match
result in getChild) are typed/used as FolderChild to avoid any casting
surprises.

In `@tests/sdk-e2e/src/helpers/data-generators.ts`:
- Around line 10-15: The generateBytes function should validate the sizeBytes
argument to fail fast with a clear error: check that sizeBytes is a finite
integer >= 0 (and of type number) before constructing the Uint8Array and throw a
descriptive RangeError or TypeError if invalid; update the input guard in
generateBytes to validate and throw something like "generateBytes: sizeBytes
must be a non-negative integer" so tests get a clear diagnostic instead of an
internal Uint8Array error.

In `@tests/sdk-e2e/src/suites/bin-operations.test.ts`:
- Around line 50-100: Replace direct internal access to (ctx.client as
any).binState with assertions using the public API ctx.client.loadBin(): after
operations like deleteToBin, restoreFromBin, and permanentDelete call await
ctx.client.loadBin() and assert on the returned entries array (e.g., check
length, find by name, itemType) instead of reading binState; update tests that
currently reference binState (including the case that checks BinFolder and
bin-file.txt) to use loadBin() for both pre- and post-mutation verification so
tests rely only on public methods (also update any other occurrences that access
binState directly, e.g., the other test block that inspects BinFolder).

In `@tests/sdk-e2e/src/suites/concurrent-operations.test.ts`:
- Line 22: Rename the describe block string to match the updated file header by
changing the existing describe('Concurrent Operations', ...) to describe('Rapid
Sequential Operations', ...); update any matching test suite title usages in the
same file (the describe block and any snapshot/test reporting strings) so the
suite name and header comment are consistent.

In `@tests/sdk-e2e/src/suites/error-cases.test.ts`:
- Around line 151-153: The test currently uses a generic await
expect(tempCtx.client.createFolder(...)).rejects.toThrow(); change this to
assert a specific destroyed-client error substring or type so unrelated errors
don't pass: update the expectation to await
expect(tempCtx.client.createFolder(tempCtx.rootIpnsName,
'PostDestroy')).rejects.toThrowError(/destroy(ed)? client|client has been
destroyed/i) or match the exact error message/type your client throws after
calling destroy(), referencing tempCtx.client.createFolder and the client's
destroy() behavior so the assertion is tightly scoped.

In `@tests/sdk-e2e/src/suites/file-operations.test.ts`:
- Around line 32-151: Tests in this suite are tightly coupled (e.g., hello.txt
created in one test is renamed and deleted in later tests) causing cascade
failures; make each test self-contained by having each test create (via
ctx.client.uploadFile or ctx.client.createFolder + registerFolder) the specific
files/folders it needs using unique names (e.g., include Date.now() or a UUID),
use beforeEach to seed required items and afterEach to clean up via deleteItem,
and update references that currently rely on shared state (getChild,
expectChildNamed, renameItem, deleteItem) to operate on the locally created
item's id/ipnsName so tests can run independently and be rerun individually.

In `@tests/sdk-e2e/src/suites/folder-crud.test.ts`:
- Around line 17-29: The tests under describe('Folder CRUD') split a single
stateful workflow across multiple it blocks using the shared ctx from
createTestContext in beforeAll, which makes reruns brittle; either collapse the
dependent steps into one end-to-end test (e.g., a single it('full folder CRUD
flow') that performs create → rename → delete in sequence) or make each test
independent by moving setup/teardown into beforeEach/afterEach and
creating/cleaning its own context (use createTestContext and
ctx.cleanup/deleteTestAccount within per-test setup/teardown) so no test relies
on side effects from others.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8ecc225-c8da-4945-8a43-c862b9b86636

📥 Commits

Reviewing files that changed from the base of the PR and between 9cdea31 and 04cf8e4.

⛔ Files ignored due to path filters (2)
  • packages/api-client/src/instance.ts is excluded by !packages/api-client/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
📒 Files selected for processing (71)
  • .github/workflows/load-test.yml
  • .github/workflows/sdk-e2e.yml
  • .planning/STATE.md
  • .planning/todos/done/2026-03-22-investigate-throttle-bypass-gap-for-per-endpoint-throttle-decorators.md
  • .planning/todos/done/2026-03-22-refactor-api-client-from-singleton-to-instance-scoped-axios.md
  • .planning/todos/pending/2026-03-22-investigate-throttle-bypass-gap-for-per-endpoint-throttle-decorators.md
  • .planning/todos/pending/2026-03-22-refactor-api-client-from-singleton-to-instance-scoped-axios.md
  • apps/api/src/app.module.ts
  • apps/api/src/auth/auth.controller.spec.ts
  • apps/api/src/auth/auth.controller.ts
  • apps/api/src/auth/controllers/identity.controller.spec.ts
  • apps/api/src/auth/controllers/identity.controller.ts
  • apps/api/src/common/guards/throttler-bypass.guard.ts
  • apps/api/src/device-approval/device-approval.controller.spec.ts
  • apps/api/src/device-approval/device-approval.controller.ts
  • apps/api/src/ipns/__tests__/ipns.integration.spec.ts
  • apps/api/src/ipns/__tests__/ipns.security.spec.ts
  • apps/api/src/ipns/ipns.controller.ts
  • apps/api/src/shares/invites.controller.spec.ts
  • apps/api/src/shares/invites.controller.ts
  • apps/api/src/shares/share-invites.controller.spec.ts
  • apps/api/src/shares/share-invites.controller.ts
  • apps/api/src/shares/shares.controller.spec.ts
  • apps/api/src/shares/shares.controller.ts
  • apps/web/src/lib/api-config.ts
  • apps/web/src/lib/sdk-provider.ts
  • packages/sdk-core/src/__tests__/ipns.test.ts
  • packages/sdk-core/src/file/index.ts
  • packages/sdk-core/src/folder/index.ts
  • packages/sdk-core/src/ipfs/index.ts
  • packages/sdk-core/src/ipns/index.ts
  • packages/sdk-core/src/types.ts
  • packages/sdk/src/__tests__/integration.test.ts
  • packages/sdk/src/bin/index.ts
  • packages/sdk/src/client.ts
  • packages/sdk/src/types.ts
  • release-please-config.json
  • tests/TESTING_STRATEGY.md
  • tests/load/.env.example
  • tests/load/package.json
  • tests/load/src/harness/client-pool.ts
  • tests/load/src/harness/metrics.ts
  • tests/load/src/harness/reporter.ts
  • tests/load/src/scenarios/ipns-publish-storm.test.ts
  • tests/load/src/scenarios/mixed-workload.test.ts
  • tests/load/src/scenarios/spike-test.test.ts
  • tests/load/src/scenarios/sustained-load.test.ts
  • tests/load/src/scenarios/upload-throughput.test.ts
  • tests/load/src/workloads/file-workload.ts
  • tests/load/src/workloads/folder-workload.ts
  • tests/load/src/workloads/mixed-workload.ts
  • tests/load/tsconfig.json
  • tests/load/vitest.config.ts
  • tests/sdk-e2e/.env.example
  • tests/sdk-e2e/package.json
  • tests/sdk-e2e/src/fixtures/multi-account.ts
  • tests/sdk-e2e/src/fixtures/test-harness.ts
  • tests/sdk-e2e/src/helpers/assertions.ts
  • tests/sdk-e2e/src/helpers/data-generators.ts
  • tests/sdk-e2e/src/suites/bin-operations.test.ts
  • tests/sdk-e2e/src/suites/concurrent-operations.test.ts
  • tests/sdk-e2e/src/suites/data-integrity.test.ts
  • tests/sdk-e2e/src/suites/error-cases.test.ts
  • tests/sdk-e2e/src/suites/file-operations.test.ts
  • tests/sdk-e2e/src/suites/folder-crud.test.ts
  • tests/sdk-e2e/src/suites/invite-link.test.ts
  • tests/sdk-e2e/src/suites/ipns-consistency.test.ts
  • tests/sdk-e2e/src/suites/share-operations.test.ts
  • tests/sdk-e2e/src/suites/vault-lifecycle.test.ts
  • tests/sdk-e2e/tsconfig.json
  • tests/sdk-e2e/vitest.config.ts
💤 Files with no reviewable changes (1)
  • packages/sdk/src/tests/integration.test.ts

Comment thread apps/api/src/app.module.ts Outdated
Comment thread tests/load/src/harness/client-pool.ts
Comment thread tests/load/src/workloads/file-workload.ts
Comment thread tests/load/src/workloads/file-workload.ts
Comment thread tests/load/src/workloads/mixed-workload.ts Outdated
Comment thread tests/sdk-e2e/src/fixtures/test-harness.ts Outdated
Comment thread tests/sdk-e2e/src/fixtures/test-harness.ts Outdated
- app.module.ts: note bypass is blocked in production
- client-pool.ts: update docblock to reflect instance-scoped architecture
- file-workload.ts: validate minSize/maxSize bounds, add byte-level
  content verification for downloads
- mixed-workload.ts: validate weight values are non-negative finite numbers
- test-harness.ts: wrap setup in try/catch to clean up partially
  provisioned accounts on failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4548f0405d9e

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 71 out of 73 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/load/src/harness/client-pool.ts`:
- Around line 124-130: The catch currently swallows writeFile errors; change it
to catch the thrown error and log a non-fatal diagnostic (including the
scenarioName/slug and the error message/stack) instead of doing nothing so CI
shows why the artifact was not produced—modify the try around the
import('node:fs/promises') and await fs.writeFile(...) to use catch(error) and
emit a clear error via your logging facility (or console.error) while keeping
the failure non-fatal.
- Around line 83-88: The teardown in destroyClientPool currently only calls
pc.client.destroy() and deleteTestAccount, leaving PoolClient fields with secret
material (privateKey, rootFolderKey, rootIpnsKeypair.privateKey) alive; update
destroyClientPool to explicitly wipe/zero-out or nullify these fields on each pc
(e.g., set pc.privateKey = undefined/null, pc.rootFolderKey = undefined/null,
and pc.rootIpnsKeypair.privateKey = undefined/null) after destroying the SDK
client and before/after calling deleteTestAccount so no raw key material remains
in memory after cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: efef595e-10e5-4534-a3cb-392e620466c6

📥 Commits

Reviewing files that changed from the base of the PR and between 04cf8e4 and b3612f1.

📒 Files selected for processing (5)
  • apps/api/src/app.module.ts
  • tests/load/src/harness/client-pool.ts
  • tests/load/src/workloads/file-workload.ts
  • tests/load/src/workloads/mixed-workload.ts
  • tests/sdk-e2e/src/fixtures/test-harness.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/api/src/app.module.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/load/src/workloads/file-workload.ts
  • tests/sdk-e2e/src/fixtures/test-harness.ts

Comment thread tests/load/src/harness/client-pool.ts
Comment thread tests/load/src/harness/client-pool.ts
FSM1 and others added 3 commits March 22, 2026 22:47
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: bd43ec70520f
7 test cases covering all branches:
- Bypass with valid header and non-production env
- Fall-through on wrong secret, missing header, production env,
  unset secret, and length-mismatch (timing-safe rejection)
- Array header handling (first element)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e3fadb8ee0cf
The Playwright-based browser tests are now at tests/ui-e2e/ to
distinguish them from the SDK E2E tests (tests/sdk-e2e/) and load
tests (tests/load/). Updated everywhere:

- Package name: @cipherbox/e2e → @cipherbox/ui-e2e
- Root scripts: test:e2e → test:ui-e2e
- GitHub workflows: e2e.yml, release-gate.yml, tag-staging.yml
- release-please-config.json
- Documentation, .learnings, .claude commands
- Internal comments and README

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@FSM1 FSM1 force-pushed the feat/sdk-e2e-and-load-tests branch from 841ff73 to bbc9725 Compare March 22, 2026 22:04
FSM1 and others added 2 commits March 22, 2026 23:12
The job name is referenced by branch protection — renaming it causes
the required check to show as "expected — waiting for status" on all
PRs. Reverted to "Verify E2E Passed".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4d5de64d2942
The SDK E2E job now runs inside ci.yml after unit tests pass, instead
of being a separate workflow. This gives a single CI check on PRs and
ensures SDK E2E only runs when unit tests succeed (fail-fast).

Job dependency chain: changes → lint → typecheck → test → sdk-e2e → build

The standalone sdk-e2e.yml is removed. Branch protection does not
reference it, so no rule updates needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 8842514ec58b

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
apps/api/src/common/guards/throttler-bypass.guard.spec.ts (2)

24-28: Consider using NestJS testing utilities for more robust instantiation.

Object.create(BypassableThrottlerGuard.prototype) bypasses the constructor, which works here since canActivate doesn't rely on instance state. However, if ThrottlerGuard ever initializes required instance properties in its constructor, these tests will fail unexpectedly.

A more robust (but heavier) approach would use Test.createTestingModule with mocked throttler providers. Given the current implementation's simplicity, this is a pragmatic trade-off.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/common/guards/throttler-bypass.guard.spec.ts` around lines 24 -
28, The test uses Object.create(BypassableThrottlerGuard.prototype) in
beforeEach which bypasses the constructor and could break if ThrottlerGuard
later adds instance state; replace this manual instantiation with a proper
NestJS test module: use Test.createTestingModule to provide and compile
BypassableThrottlerGuard (mocking any ThrottlerGuard/ThrottlerService
dependencies), retrieve the guard via module.get(BypassableThrottlerGuard) in
beforeEach, keep clearing superCanActivate and restoring process.env as before,
and ensure tests still call canActivate on the retrieved instance so constructor
initialization (if added later) runs.

45-53: Consider asserting return value propagation.

These tests verify superCanActivate is called but don't assert that the return value from the parent is properly returned. Since the mock returns true, adding expect(result).toBe(true) would ensure the return super.canActivate(context) path correctly propagates the parent's result.

Example for one test case
 it('should fall through to parent when secret does not match', async () => {
   process.env.THROTTLE_BYPASS_SECRET = 'test-secret';
   process.env.NODE_ENV = 'test';

   const ctx = createMockContext({ 'x-throttle-bypass': 'wrong-secret' });
-  await guard.canActivate(ctx);
+  const result = await guard.canActivate(ctx);

+  expect(result).toBe(true);
   expect(superCanActivate).toHaveBeenCalledWith(ctx);
 });

Also applies to: 55-63, 75-83, 96-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/common/guards/throttler-bypass.guard.spec.ts` around lines 45 -
53, The tests for ThrottlerBypassGuard currently assert that superCanActivate
(the mocked parent canActivate) was called but don't verify its return value is
propagated; update the test cases that call guard.canActivate(ctx) (the ones
using createMockContext and asserting superCanActivate) to capture the result
(e.g., const result = await guard.canActivate(ctx)) and add an assertion that
result is true to confirm return value propagation from the parent canActivate;
apply this change to the test blocks referencing superCanActivate at the
indicated locations (including the other similar tests around lines 55-63,
75-83, 96-104).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/api/src/common/guards/throttler-bypass.guard.spec.ts`:
- Around line 24-28: The test uses
Object.create(BypassableThrottlerGuard.prototype) in beforeEach which bypasses
the constructor and could break if ThrottlerGuard later adds instance state;
replace this manual instantiation with a proper NestJS test module: use
Test.createTestingModule to provide and compile BypassableThrottlerGuard
(mocking any ThrottlerGuard/ThrottlerService dependencies), retrieve the guard
via module.get(BypassableThrottlerGuard) in beforeEach, keep clearing
superCanActivate and restoring process.env as before, and ensure tests still
call canActivate on the retrieved instance so constructor initialization (if
added later) runs.
- Around line 45-53: The tests for ThrottlerBypassGuard currently assert that
superCanActivate (the mocked parent canActivate) was called but don't verify its
return value is propagated; update the test cases that call
guard.canActivate(ctx) (the ones using createMockContext and asserting
superCanActivate) to capture the result (e.g., const result = await
guard.canActivate(ctx)) and add an assertion that result is true to confirm
return value propagation from the parent canActivate; apply this change to the
test blocks referencing superCanActivate at the indicated locations (including
the other similar tests around lines 55-63, 75-83, 96-104).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20140c6d-5411-4854-bf73-e649d60ef2f5

📥 Commits

Reviewing files that changed from the base of the PR and between 04cf8e4 and e404ae3.

⛔ Files ignored due to path filters (3)
  • .claude/commands/resolve-pr-reviews.md is excluded by !.claude/commands/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
  • tests/web-e2e/fixtures/files/test-image.png is excluded by !**/*.png
📒 Files selected for processing (86)
  • .claude/gsd-local-patches/get-shit-done/workflows/verify-work.md
  • .github/workflows/ci.yml
  • .github/workflows/desktop-e2e.yml
  • .github/workflows/e2e.yml
  • .github/workflows/release-gate.yml
  • .github/workflows/tag-staging.yml
  • .learnings/2026-02-07-empty-state-upload-regression.md
  • .learnings/2026-02-08-desktop-testing-strategy.md
  • .learnings/2026-02-24-run-e2e-locally-before-push.md
  • .learnings/2026-02-25-ipns-stale-resolution-staging.md
  • .learnings/2026-03-20-playwright-mcp-login-and-upload.md
  • .planning/codebase/STACK.md
  • .planning/codebase/TESTING.md
  • README.md
  • apps/api/src/app.module.ts
  • apps/api/src/common/guards/throttler-bypass.guard.spec.ts
  • docs/DEVELOPMENT.md
  • package.json
  • release-please-config.json
  • scripts/baseline-benchmark.sh
  • tests/TESTING_STRATEGY.md
  • tests/desktop-e2e/fixtures/crypto/aes-ctr-vectors.json
  • tests/desktop-e2e/fixtures/crypto/aes-gcm-vectors.json
  • tests/desktop-e2e/fixtures/crypto/metadata-vectors.json
  • tests/desktop-e2e/scripts/run-all.ps1
  • tests/desktop-e2e/scripts/run-all.sh
  • tests/desktop-e2e/scripts/test-conflict-detection.ps1
  • tests/desktop-e2e/scripts/test-conflict-detection.sh
  • tests/desktop-e2e/scripts/test-fuse-operations.ps1
  • tests/desktop-e2e/scripts/test-fuse-operations.sh
  • tests/desktop-e2e/scripts/test-recycle-bin.ps1
  • tests/desktop-e2e/scripts/test-recycle-bin.sh
  • tests/desktop-e2e/scripts/test-round-trip.ps1
  • tests/desktop-e2e/scripts/test-round-trip.sh
  • tests/desktop-e2e/scripts/wait-for-mount.ps1
  • tests/desktop-e2e/scripts/wait-for-mount.sh
  • tests/load/src/harness/client-pool.ts
  • tests/load/src/workloads/file-workload.ts
  • tests/load/src/workloads/mixed-workload.ts
  • tests/sdk-e2e/src/fixtures/test-harness.ts
  • tests/web-e2e/.env.example
  • tests/web-e2e/.gitignore
  • tests/web-e2e/README.md
  • tests/web-e2e/fixtures/files/.gitkeep
  • tests/web-e2e/package.json
  • tests/web-e2e/page-objects/base.page.ts
  • tests/web-e2e/page-objects/dialogs/confirm-dialog.page.ts
  • tests/web-e2e/page-objects/dialogs/create-folder-dialog.page.ts
  • tests/web-e2e/page-objects/dialogs/details-dialog.page.ts
  • tests/web-e2e/page-objects/dialogs/index.ts
  • tests/web-e2e/page-objects/dialogs/invite-link-tab.page.ts
  • tests/web-e2e/page-objects/dialogs/move-dialog.page.ts
  • tests/web-e2e/page-objects/dialogs/rename-dialog.page.ts
  • tests/web-e2e/page-objects/dialogs/search-palette.page.ts
  • tests/web-e2e/page-objects/dialogs/share-dialog.page.ts
  • tests/web-e2e/page-objects/dialogs/text-editor-dialog.page.ts
  • tests/web-e2e/page-objects/file-browser/breadcrumbs.page.ts
  • tests/web-e2e/page-objects/file-browser/context-menu.page.ts
  • tests/web-e2e/page-objects/file-browser/file-list.page.ts
  • tests/web-e2e/page-objects/file-browser/index.ts
  • tests/web-e2e/page-objects/file-browser/parent-dir.page.ts
  • tests/web-e2e/page-objects/file-browser/selection-action-bar.page.ts
  • tests/web-e2e/page-objects/file-browser/shared-file-browser.page.ts
  • tests/web-e2e/page-objects/file-browser/upload-zone.page.ts
  • tests/web-e2e/page-objects/index.ts
  • tests/web-e2e/page-objects/login.page.ts
  • tests/web-e2e/page-objects/pages/bin.page.ts
  • tests/web-e2e/page-objects/pages/invite.page.ts
  • tests/web-e2e/playwright.config.ts
  • tests/web-e2e/playwright.load.config.ts
  • tests/web-e2e/tests/conflict-detection.spec.ts
  • tests/web-e2e/tests/full-workflow.spec.ts
  • tests/web-e2e/tests/invite-link-workflow.spec.ts
  • tests/web-e2e/tests/load-test.spec.ts
  • tests/web-e2e/tests/mfa-flows.spec.ts
  • tests/web-e2e/tests/recycle-bin.spec.ts
  • tests/web-e2e/tests/search-workflow.spec.ts
  • tests/web-e2e/tests/sharing-workflow.spec.ts
  • tests/web-e2e/tests/wallet-login.spec.ts
  • tests/web-e2e/tsconfig.json
  • tests/web-e2e/utils/api-helpers.ts
  • tests/web-e2e/utils/conflict-helpers.ts
  • tests/web-e2e/utils/mfa-helpers.ts
  • tests/web-e2e/utils/multi-account-wallet.ts
  • tests/web-e2e/utils/test-files.ts
  • tests/web-e2e/utils/wallet-login-helpers.ts
✅ Files skipped from review due to trivial changes (21)
  • docs/DEVELOPMENT.md
  • README.md
  • .learnings/2026-02-07-empty-state-upload-regression.md
  • .planning/codebase/STACK.md
  • .claude/gsd-local-patches/get-shit-done/workflows/verify-work.md
  • tests/web-e2e/playwright.load.config.ts
  • tests/web-e2e/tests/load-test.spec.ts
  • .learnings/2026-03-20-playwright-mcp-login-and-upload.md
  • .planning/codebase/TESTING.md
  • tests/web-e2e/playwright.config.ts
  • tests/web-e2e/README.md
  • scripts/baseline-benchmark.sh
  • release-please-config.json
  • .learnings/2026-02-08-desktop-testing-strategy.md
  • tests/web-e2e/utils/test-files.ts
  • .learnings/2026-02-24-run-e2e-locally-before-push.md
  • tests/web-e2e/package.json
  • .github/workflows/tag-staging.yml
  • .learnings/2026-02-25-ipns-stale-resolution-staging.md
  • apps/api/src/app.module.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/load/src/workloads/file-workload.ts
  • tests/sdk-e2e/src/fixtures/test-harness.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants