Skip to content

fix: resolve test suite failures in backend and frontend#472

Open
rizwandev99 wants to merge 3 commits into
ritik4ever:mainfrom
rizwandev99:fix/test-suites
Open

fix: resolve test suite failures in backend and frontend#472
rizwandev99 wants to merge 3 commits into
ritik4ever:mainfrom
rizwandev99:fix/test-suites

Conversation

@rizwandev99
Copy link
Copy Markdown

@rizwandev99 rizwandev99 commented May 26, 2026

This PR resolves the test suite failures across both the backend and frontend components:

Backend

  • Fixed unhandled promise rejections in indexer.ts and webhookWorker.ts during database connection and startup errors.
  • Handled SQLite database constraint issues during test tear-downs by deleting from dependent tables (stream_events) before deleting parent entries (streams).
  • Fixed JWT challenge verification errors to return a proper statusCode (401) and error payload.
  • Updated authentication assertions in auth.test.ts to align with actual error messages and status codes.

Frontend

  • Mocked WebSocket connections in useWebSocket tests to prevent network connection errors.
  • Added API request mock definitions in api.ts and mock contract configurations for Soroban.
  • Provided missing mock props to components to prevent state update warnings during renders.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added pause and resume stream functionality with blockchain confirmation.
    • Enabled bulk selection and cancellation for multiple streams in the streams table.
    • Added "Paused" and "Resumed" event type filters to the timeline.
    • Implemented client-side sorting for streams table by status, amount, vested amount, and start date.
  • Bug Fixes

    • Improved short address display to avoid unnecessary truncation.
    • Corrected stream progress and status calculations during paused periods.
    • Fixed stream form duration input to use minutes instead of hours.
    • Enhanced error handling for challenge verification and stream state transitions.
  • Tests

    • Updated test infrastructure for improved database schema alignment and mock management.

Review Change Stack

Resolves database foreign key constraint errors during test tear-down, configures correct status codes/errors for challenge auth failures, and handles indexer/webhookWorker database/network promise rejections.
…/contract mocking

Mock API calls, WebSocket connections, component props, and Soroban contract addresses to resolve crashes and unhandled exceptions across all frontend unit and integration tests.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

@rizwandev99 is attempting to deploy a commit to the ritik4ever's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

This PR introduces stream pause/resume functionality with on-chain integration, refactors auth error handling, persists indexer ledger progress, implements webhook dead-letter routing, and significantly enhances frontend UI with bulk stream operations, component improvements, and hook test reliability. Approximately 45 files span backend services, tests, and frontend components.

Changes

Stream Management and Webhook System

Layer / File(s) Summary
Auth error codes and challenge verification
backend/src/services/auth.ts, backend/src/auth.test.ts
Auth service now creates explicit error objects with lowercase codes (unauthorized, invalid_token, token_expired) and HTTP metadata; tests updated to assert the new error codes and use getJwtSecret() for token signing.
Pause and resume stream implementation
backend/src/services/streamStore.ts, backend/src/index.ts
pauseStream and resumeStream now validate state and throw typed errors (404/400), perform on-chain pause/resume transactions, persist state changes within transactions, record events, trigger webhooks, and return the updated stream. Endpoint handlers made async to await and respond with resolved streams.
Progress and status calculations for paused streams
backend/src/services/streamStore.ts, backend/src/services/streamStore.progress.test.ts
Progress/status calculations now handle accumulated pausedDuration, extend completion time by paused time, and freeze vesting at pause while extending the effective timeline. Zero-duration edge cases handled explicitly. Legacy duplicate implementations removed.
Webhook dead-letter handling and persistence
backend/src/services/webhookWorker.ts, backend/src/services/webhook.test.ts, backend/src/webhooks.integration.test.ts
Failed deliveries after max attempts now move to webhook_dead_letters (which includes stream_id) and delete from webhook_deliveries. Test setup expanded to delete additional tables and insert required foreign-key rows.
Indexer ledger progress checkpoint
backend/src/services/indexer.ts, backend/src/services/indexer.test.ts
Indexer reads/writes indexer_cursor table (id=1, last_ledger_sequence) to persist progress across restarts. On first run, initializes from INDEXER_START_LEDGER or falls back to currentLedger - 1.
Backend test infrastructure and mocking
backend/src/services/eventHistory.test.ts, backend/src/index.test.ts, backend/src/integration.test.ts, backend/src/services/streamStore.cancel.integration.test.ts, backend/src/services/streamStore.updateStartAt.test.ts, backend/src/services/webhookWorker.test.ts
Test setup across services refactored for cleaner DB isolation: fake timers with controlled system time, foreign-key handling, improved mock state resets, and updated call signatures. Event/webhook/streams test constants and mock expectations harmonized.
Frontend API layer expansion
frontend/src/services/api.ts, frontend/src/services/soroban.ts
API service adds getStream(streamId, signal?) with optional cancellation support and caching, AppConfig type, and getConfig() helper. Soroban service exports claimStream alias for claimOnChain.
Stream table bulk selection and sorting
frontend/src/components/StreamsTable.tsx, frontend/src/components/StreamsTable.test.tsx
StreamsTable adds client-side sorting by status/amount/vested/startDate, bulk selection UI with checkboxes (active/paused/scheduled only), auto-reconciliation on filter changes, and bulk cancellation with progress tracking. onRefresh callback prop added. Comprehensive test coverage for selection/deselection and sequential cancellation.
Hook test async/act refactoring
frontend/src/hooks/useClaimStream.test.ts, frontend/src/hooks/useMetricsHistory.test.ts, frontend/src/hooks/useWebSocket.test.ts, frontend/src/hooks/useClaimStream.ts
useClaimStream, useMetricsHistory, and useWebSocket tests wrapped hook state updates in act(), replaced timer-based async with manual promise resolution, and scoped fake timers more precisely. useClaimStream imports updated to pull in React hooks and soroban service dependencies.
Component UI improvements and test updates
frontend/src/components/CopyableAddress.tsx, frontend/src/components/CopyableAddress.test.tsx, frontend/src/components/CreateStreamForm.tsx, frontend/src/components/FilterBar.test.tsx, frontend/src/components/StreamMetricsChart.test.tsx, frontend/src/components/StreamTimeline.filterbar.test.ts, frontend/src/components/StreamTimeline.test.tsx, frontend/src/components/WalletButton.test.tsx, frontend/src/components/RecipientDashboard.test.tsx, frontend/src/components/StreamDetailDrawer.test.tsx, frontend/src/hooks/useWebSocket.ts
CopyableAddress guards short addresses from truncation; CreateStreamForm switches to minute-based duration with estimated end-time hint; FilterBar uses role-based queries; StreamMetricsChart rewritten for Vitest with mocked recharts; StreamTimeline and FilterBar extend event type coverage to include paused and resumed events; WalletButton and other component tests improved for better assertions; WebSocket hook uses internal state constants instead of WebSocket.*.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly related PRs

  • ritik4ever/stellar-stream#307: Adds stream pause/resume event types and contracts that this PR depends on for pause/resume state persistence and event emission.
  • ritik4ever/stellar-stream#305: Refactors auth error codes (unauthorized, invalid_token, token_expired) that this PR aligns with in auth.ts and auth.test.ts.
  • ritik4ever/stellar-stream#312: Related to indexer cursor checkpoint persistence changes in indexer.ts for ledger progress tracking.

Poem

🐰 A rabbit hops through paused streams so fine,
Indexing ledgers, one cursor at a time,
Dead letters buried, webhooks flow with care,
Bulk selections bloom in tables everywhere,
With sorted streams and hooks that truly care,
The frontend dances through the fragrant air! 🌸

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: resolve test suite failures in backend and frontend' accurately describes the main change: resolving widespread test failures across both backend and frontend components through fixes to error handling, database constraints, mocking, and assertions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
backend/src/webhooks.integration.test.ts (3)

116-117: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing parent stream rows may cause foreign key violations.

This test inserts 2 dead letters with stream_id values "s1" and "s2" without first inserting corresponding rows into the streams table.

🛠️ Proposed fix
     it("should return correct total count", async () => {
       const db = getDb();
+      
+      // Insert parent streams to satisfy FK constraint
+      const streamStmt = db.prepare(`
+        INSERT INTO streams (id, sender, recipient, asset_code, total_amount, duration_seconds, start_at, created_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+      `);
+      streamStmt.run("s1", "sender", "recipient", "USDC", 100, 3600, 0, 0);
+      streamStmt.run("s2", "sender", "recipient", "USDC", 100, 3600, 0, 0);
+      
       db.prepare(`INSERT INTO webhook_dead_letters (stream_id, event, url, payload, last_error, failed_at) VALUES (?, ?, ?, ?, ?, ?)`).run("s1", "e", "u", "p", "err", 100);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/webhooks.integration.test.ts` around lines 116 - 117, The test
inserts rows into webhook_dead_letters for stream_id "s1" and "s2" without
creating the parent rows in the streams table, causing foreign key violations;
before the two db.prepare(...).run("s1", ...) and db.prepare(...).run("s2", ...)
calls, insert minimal parent rows into the streams table (e.g. insert the
required columns for ids "s1" and "s2") so the foreign key constraint is
satisfied, ensuring the webhook_dead_letters inserts succeed.

65-68: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing parent stream row may cause foreign key violation.

This test inserts into webhook_dead_letters with stream_id = "s1" without first inserting a corresponding row into the streams table. If webhook_dead_letters.stream_id has a foreign key constraint referencing streams.id, this INSERT will fail.

The requeue test at lines 132-135 demonstrates the correct pattern by inserting a mock stream before the dead letter.

🛠️ Proposed fix
     it("should return dead letters with correct fields", async () => {
       const db = getDb();
       const now = Math.floor(Date.now() / 1000);
+      
+      // Insert parent stream to satisfy FK constraint
+      db.prepare(`
+        INSERT INTO streams (id, sender, recipient, asset_code, total_amount, duration_seconds, start_at, created_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+      `).run("s1", "sender", "recipient", "USDC", 100, 3600, 0, 0);
+      
       db.prepare(`
         INSERT INTO webhook_dead_letters (stream_id, event, url, payload, last_error, failed_at)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/webhooks.integration.test.ts` around lines 65 - 68, The test
inserts into webhook_dead_letters with stream_id "s1" but never creates the
corresponding streams row, which can trigger a FK violation; before the INSERT
into webhook_dead_letters add a matching INSERT into the streams table (e.g.,
create a mock stream with id "s1", stream type and any required columns) similar
to the pattern used in the requeue test, so the webhook_dead_letters INSERT (in
this test function) succeeds without violating the streams ->
webhook_dead_letters foreign key constraint.

86-93: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing parent stream rows may cause foreign key violations.

This test inserts 5 dead letters with stream_id values "s1" through "s5" without first inserting corresponding rows into the streams table. Apply the same fix pattern as the requeue test.

🛠️ Proposed fix
     it("should respect pagination (limit/offset)", async () => {
       const db = getDb();
+      
+      // Insert parent streams to satisfy FK constraint
+      const streamStmt = db.prepare(`
+        INSERT INTO streams (id, sender, recipient, asset_code, total_amount, duration_seconds, start_at, created_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+      `);
+      for (let i = 1; i <= 5; i++) {
+        streamStmt.run(`s${i}`, "sender", "recipient", "USDC", 100, 3600, 0, 0);
+      }
+      
       for (let i = 1; i <= 5; i++) {
         db.prepare(`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/webhooks.integration.test.ts` around lines 86 - 93, The test
"should respect pagination (limit/offset)" inserts rows into
webhook_dead_letters with stream_ids s1..s5 but doesn't create corresponding
parent rows in the streams table, causing foreign key violations; update the
test (using getDb()) to insert matching entries into the streams table for each
stream id (e.g., s1..s5) before inserting into webhook_dead_letters—follow the
same pattern used in the requeue test to create the parent stream rows so the
foreign key constraint is satisfied.
backend/src/services/streamStore.ts (1)

693-703: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not persist pause/resume locally unless on-chain tx succeeds.

Both pause/resume flows continue to DB writes and webhook dispatch without validating final transaction success. If Soroban rejects/fails, local state diverges from chain state.

Proposed fix pattern
const sendRes = await retryWithBackoff(() => rpcServer!.sendTransaction(built));
-if (sendRes.status === "PENDING") {
+if (sendRes.status !== "PENDING") {
+  throw new Error(`pause/resume tx not accepted: ${JSON.stringify(sendRes)}`);
+}
+{
  let txResult;
  let attempts = 0;
  while (attempts < 10) {
    txResult = await retryWithBackoff(() => rpcServer!.getTransaction(sendRes.hash));
    if (txResult.status !== "NOT_FOUND") break;
    await new Promise((r) => setTimeout(r, 1000));
    attempts++;
  }
+ if (txResult?.status !== "SUCCESS") {
+   throw new Error(`pause/resume tx failed: ${JSON.stringify(txResult)}`);
+ }
}

Also applies to: 751-761

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/streamStore.ts` around lines 693 - 703, The pause/resume
flow currently proceeds to DB writes and webhooks after sending the Soroban
transaction without confirming final on-chain success; update the logic around
retryWithBackoff(()=>rpcServer!.sendTransaction(built)) and the follow-up
polling of rpcServer!.getTransaction(sendRes.hash) so that you wait until the
transaction's final status indicates success (e.g., "SUCCESS"/confirmed) before
persisting local state or dispatching webhooks, and if the final status is
failure or still NOT_FOUND after retries, abort the local DB update and
return/throw an error (do the same for the analogous block using
retryWithBackoff/rpcServer!.getTransaction at the other pause/resume location).
package.json (1)

13-13: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Declare conventional-changelog-cli in root package.json and sync package-lock.json.

  • package.json defines changelog:update using conventional-changelog, but root dependencies/devDependencies are empty.
  • package-lock.json (lockfile v3) nonetheless includes root packages[""].devDependencies.conventional-changelog-cli, so a clean install/CI will either fail due to lockfile mismatch or won’t install the CLI—breaking npm run changelog:update.

Fix: add conventional-changelog-cli to devDependencies in package.json and regenerate package-lock.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 13, The root package.json defines the npm script
"changelog:update" that runs conventional-changelog but doesn't declare the
dependency; add "conventional-changelog-cli" to devDependencies in package.json
(matching the version used in the existing package-lock.json if possible) and
then regenerate the lockfile (run npm install or npm install
--package-lock-only) so package-lock.json and package.json stay in sync; ensure
the added package name exactly matches "conventional-changelog-cli" and update
the lockfile committed changes.
frontend/src/hooks/useClaimStream.ts (1)

54-58: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent same-tick double claims from bypassing the in-flight guard.

Line 57 reads claimState.status from render state, so two back-to-back claim() calls before rerender can both pass and call claimStream(...), violating the single-flight contract.

💡 Proposed fix
 export function useClaimStream(
@@
   const claimIdRef = useRef(0);
+  const inFlightRef = useRef(false);
@@
   const claim = useCallback(
     async ({ streamId, recipientAddress, amount }: ClaimInput) => {
-      // Block concurrent claims
-      if (claimState.status === "pending") return;
+      // Block concurrent claims (same-tick safe)
+      if (inFlightRef.current) return;
 
       // Early return for zero amount to prevent unnecessary API calls
       if (amount === 0) return;
 
+      inFlightRef.current = true;
       const claimId = ++claimIdRef.current;
@@
       } catch (err) {
@@
         setClaimState({ streamId, status: "failed", error: message });
         onFailure(streamId, message);
+      } finally {
+        if (claimIdRef.current === claimId) {
+          inFlightRef.current = false;
+        }
       }
     },
-    [claimState.status, onSuccess, onFailure],
+    [onSuccess, onFailure],
   );

Also applies to: 95-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/useClaimStream.ts` around lines 54 - 58, The guard reads
claimState.status (render state) so two same-tick calls to claim() can both pass
and call claimStream; fix by adding an in-flight mutable ref (e.g., inFlightRef
via useRef(false)) and in claim (and the other function that currently checks
claimState.status) do an atomic check-and-set: if (inFlightRef.current) return;
inFlightRef.current = true; then try { await claimStream(...) } finally {
inFlightRef.current = false; } also keep setClaimState updates for UI, but rely
on the ref for the single-flight protection.
🧹 Nitpick comments (1)
frontend/src/components/StreamsTable.test.tsx (1)

18-21: ⚡ Quick win

Bulk-selection tests should include paused streams.

The component now treats paused as selectable, but the factory/type and eligibility test don’t cover that path.

Proposed fix
 const createMockStream = (
   id: string,
-  status: 'active' | 'scheduled' | 'completed' | 'canceled'
+  status: 'active' | 'paused' | 'scheduled' | 'completed' | 'canceled'
 ): Stream => ({
@@
-  it('renders checkboxes only for active and scheduled streams', () => {
+  it('renders checkboxes only for selectable streams (active, paused, scheduled)', () => {
     const streams = [
       createMockStream('1', 'active'),
+      createMockStream('2', 'paused'),
-      createMockStream('2', 'scheduled'),
-      createMockStream('3', 'completed'),
-      createMockStream('4', 'canceled'),
+      createMockStream('3', 'scheduled'),
+      createMockStream('4', 'completed'),
+      createMockStream('5', 'canceled'),
     ];
@@
-    // Should have 2 row checkboxes (active + scheduled) + 1 header checkbox
+    // Should have 3 row checkboxes (active + paused + scheduled) + 1 header checkbox
     const checkboxes = screen.getAllByRole('checkbox');
-    expect(checkboxes).toHaveLength(3);
+    expect(checkboxes).toHaveLength(4);
   });

Also applies to: 82-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/StreamsTable.test.tsx` around lines 18 - 21, The
bulk-selection tests and the Stream factory omit the new 'paused' status so
selectable-paused streams aren't covered; update the Stream type/factory used in
StreamsTable.test.tsx (the createMockStream function and any Stream type
definition it relies on) to accept 'paused' as a valid status, and add test
cases in the bulk-selection suite (the tests around lines where createMockStream
is used, previously 82-103) that create at least one stream with status 'paused'
and assert it is treated as selectable by the bulk-selection logic in
StreamsTable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/services/auth.ts`:
- Around line 168-176: In verifyChallengeAndIssueToken, replace the uppercase
error code "UNAUTHORIZED" with the lowercase "unauthorized" for both thrown
errors (the expired-challenge error and the challenge verification-failed error)
so the auth API uses consistent lowercase error codes; update the (err as
any).code assignments in those two throw paths to "unauthorized".

In `@backend/src/services/indexer.ts`:
- Around line 162-168: The current startup cursor logic uses a plain SELECT and
an INSERT that can race (duplicate id=1) and later UPDATEs that silently no-op
if the row is missing; replace the INSERT and any later UPDATEs with an
idempotent UPSERT so checkpoint writes are durable. Locate the code using
db.prepare on the indexer_cursor table (queries referencing "SELECT
last_ledger_sequence FROM indexer_cursor WHERE id = 1", the INSERT into
indexer_cursor, and any UPDATE ... WHERE id=1 around indexEvents / indexer
logic) and change those writes to a single UPSERT statement (INSERT ... ON
CONFLICT(id) DO UPDATE SET last_ledger_sequence = excluded.last_ledger_sequence)
so both initial insert and subsequent checkpoint updates are atomic and
race-safe; apply the same change to the other occurrence noted (around line
~198).

In `@backend/src/services/streamStore.ts`:
- Line 369: Summary: Pause time is being double-counted because resumeStream
both adds pausedDuration into durationSeconds and keeps pausedDuration, while
computeStatus/calculateProgress also add pausedDuration when checking
completion/progress. Fix: remove the double-count by choosing one source of
truth—prefer keeping pausedDuration separate and stop mutating durationSeconds
in resumeStream (remove the line that adds stream.pausedDuration to
stream.durationSeconds in resumeStream); ensure computeStatus and
calculateProgress continue using stream.pausedDuration together with
stream.durationSeconds and stream.startAt (references: resumeStream,
computeStatus, calculateProgress, stream.startAt, stream.durationSeconds,
stream.pausedDuration, variable at) and add/adjust unit tests to cover
resume/pause behavior so completion/vesting times are correct.

In `@frontend/src/components/StreamsTable.tsx`:
- Line 3: The bulk-cancel flow currently calls cancelStream directly and
bypasses the component's cancel contract; update the bulk cancel logic in
StreamsTable (the code around the individual cancel path that uses onCancel and
the bulk code at the block handling lines ~187-205) to invoke the same onCancel
callback for each stream (or to call a single onCancel that accepts multiple
ids) instead of calling cancelStream directly so parent-level side effects and
error handling are preserved; ensure you reuse the existing onCancel signature
(or add a bulk variant onCancelBulk) and remove direct cancelStream calls from
the bulk path so both single-row and bulk cancellations follow the same
contract.
- Around line 227-237: The current useEffect that calls setSelectedStreamIds
only retains ids that still exist in streams but doesn't remove ones whose
status became non-selectable (e.g., "completed" or "canceled"); update the
filter inside setSelectedStreamIds to also check each stream's status and only
keep ids for streams that both exist in streams and have a selectable status
(e.g., not "completed" or "canceled"). Locate the effect referencing useEffect
and setSelectedStreamIds and the streams array, and add a status check against
the stream object (stream.status) when building validIds so stale selections are
removed.

In `@frontend/src/services/api.ts`:
- Around line 297-300: getConfig currently treats the entire parsed response
body as AppConfig but parseResponse returns an envelope like { data: AppConfig
}, so change getConfig to call parseResponse<AppConfig>(response) and then
return the inner data (e.g. const body = await
parseResponse<AppConfig>(response); return body.data;) so callers receive the
actual AppConfig (including allowedAssets) rather than the envelope; update the
function signature/comments if needed to reflect that it returns AppConfig.

---

Outside diff comments:
In `@backend/src/services/streamStore.ts`:
- Around line 693-703: The pause/resume flow currently proceeds to DB writes and
webhooks after sending the Soroban transaction without confirming final on-chain
success; update the logic around
retryWithBackoff(()=>rpcServer!.sendTransaction(built)) and the follow-up
polling of rpcServer!.getTransaction(sendRes.hash) so that you wait until the
transaction's final status indicates success (e.g., "SUCCESS"/confirmed) before
persisting local state or dispatching webhooks, and if the final status is
failure or still NOT_FOUND after retries, abort the local DB update and
return/throw an error (do the same for the analogous block using
retryWithBackoff/rpcServer!.getTransaction at the other pause/resume location).

In `@backend/src/webhooks.integration.test.ts`:
- Around line 116-117: The test inserts rows into webhook_dead_letters for
stream_id "s1" and "s2" without creating the parent rows in the streams table,
causing foreign key violations; before the two db.prepare(...).run("s1", ...)
and db.prepare(...).run("s2", ...) calls, insert minimal parent rows into the
streams table (e.g. insert the required columns for ids "s1" and "s2") so the
foreign key constraint is satisfied, ensuring the webhook_dead_letters inserts
succeed.
- Around line 65-68: The test inserts into webhook_dead_letters with stream_id
"s1" but never creates the corresponding streams row, which can trigger a FK
violation; before the INSERT into webhook_dead_letters add a matching INSERT
into the streams table (e.g., create a mock stream with id "s1", stream type and
any required columns) similar to the pattern used in the requeue test, so the
webhook_dead_letters INSERT (in this test function) succeeds without violating
the streams -> webhook_dead_letters foreign key constraint.
- Around line 86-93: The test "should respect pagination (limit/offset)" inserts
rows into webhook_dead_letters with stream_ids s1..s5 but doesn't create
corresponding parent rows in the streams table, causing foreign key violations;
update the test (using getDb()) to insert matching entries into the streams
table for each stream id (e.g., s1..s5) before inserting into
webhook_dead_letters—follow the same pattern used in the requeue test to create
the parent stream rows so the foreign key constraint is satisfied.

In `@frontend/src/hooks/useClaimStream.ts`:
- Around line 54-58: The guard reads claimState.status (render state) so two
same-tick calls to claim() can both pass and call claimStream; fix by adding an
in-flight mutable ref (e.g., inFlightRef via useRef(false)) and in claim (and
the other function that currently checks claimState.status) do an atomic
check-and-set: if (inFlightRef.current) return; inFlightRef.current = true; then
try { await claimStream(...) } finally { inFlightRef.current = false; } also
keep setClaimState updates for UI, but rely on the ref for the single-flight
protection.

In `@package.json`:
- Line 13: The root package.json defines the npm script "changelog:update" that
runs conventional-changelog but doesn't declare the dependency; add
"conventional-changelog-cli" to devDependencies in package.json (matching the
version used in the existing package-lock.json if possible) and then regenerate
the lockfile (run npm install or npm install --package-lock-only) so
package-lock.json and package.json stay in sync; ensure the added package name
exactly matches "conventional-changelog-cli" and update the lockfile committed
changes.

---

Nitpick comments:
In `@frontend/src/components/StreamsTable.test.tsx`:
- Around line 18-21: The bulk-selection tests and the Stream factory omit the
new 'paused' status so selectable-paused streams aren't covered; update the
Stream type/factory used in StreamsTable.test.tsx (the createMockStream function
and any Stream type definition it relies on) to accept 'paused' as a valid
status, and add test cases in the bulk-selection suite (the tests around lines
where createMockStream is used, previously 82-103) that create at least one
stream with status 'paused' and assert it is treated as selectable by the
bulk-selection logic in StreamsTable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2883b61-d01b-4931-b3ae-f308ea0b9646

📥 Commits

Reviewing files that changed from the base of the PR and between 3c1d1b1 and 95c7b59.

⛔ Files ignored due to path filters (2)
  • backend/package-lock.json is excluded by !**/package-lock.json
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (37)
  • backend/package.json
  • backend/src/auth.test.ts
  • backend/src/index.test.ts
  • backend/src/index.ts
  • backend/src/integration.test.ts
  • backend/src/services/auth.ts
  • backend/src/services/eventHistory.test.ts
  • backend/src/services/indexer.test.ts
  • backend/src/services/indexer.ts
  • backend/src/services/streamStore.cancel.integration.test.ts
  • backend/src/services/streamStore.progress.test.ts
  • backend/src/services/streamStore.ts
  • backend/src/services/streamStore.updateStartAt.test.ts
  • backend/src/services/webhook.test.ts
  • backend/src/services/webhookWorker.test.ts
  • backend/src/services/webhookWorker.ts
  • backend/src/webhooks.integration.test.ts
  • frontend/src/components/CopyableAddress.test.tsx
  • frontend/src/components/CopyableAddress.tsx
  • frontend/src/components/CreateStreamForm.tsx
  • frontend/src/components/FilterBar.test.tsx
  • frontend/src/components/RecipientDashboard.test.tsx
  • frontend/src/components/StreamDetailDrawer.test.tsx
  • frontend/src/components/StreamMetricsChart.test.tsx
  • frontend/src/components/StreamTimeline.filterbar.test.ts
  • frontend/src/components/StreamTimeline.test.tsx
  • frontend/src/components/StreamsTable.test.tsx
  • frontend/src/components/StreamsTable.tsx
  • frontend/src/components/WalletButton.test.tsx
  • frontend/src/hooks/useClaimStream.test.ts
  • frontend/src/hooks/useClaimStream.ts
  • frontend/src/hooks/useMetricsHistory.test.ts
  • frontend/src/hooks/useWebSocket.test.ts
  • frontend/src/hooks/useWebSocket.ts
  • frontend/src/services/api.ts
  • frontend/src/services/soroban.ts
  • package.json

Comment on lines +168 to +176
const err = new Error("Challenge has expired. Please request a new one.");
(err as any).statusCode = 401;
(err as any).code = "UNAUTHORIZED";
throw err;
}
throw new Error(`Challenge verification failed: ${error.message}`);
const err = new Error(`Challenge verification failed: ${error.message}`);
(err as any).statusCode = 401;
(err as any).code = "UNAUTHORIZED";
throw err;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use lowercase auth error codes for challenge failures.

verifyChallengeAndIssueToken now emits "UNAUTHORIZED" while the rest of auth paths use lowercase codes. This creates an inconsistent API contract (Line 170, Line 175).

Proposed fix
-      (err as any).code = "UNAUTHORIZED";
+      (err as any).code = "unauthorized";
...
-    (err as any).code = "UNAUTHORIZED";
+    (err as any).code = "unauthorized";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const err = new Error("Challenge has expired. Please request a new one.");
(err as any).statusCode = 401;
(err as any).code = "UNAUTHORIZED";
throw err;
}
throw new Error(`Challenge verification failed: ${error.message}`);
const err = new Error(`Challenge verification failed: ${error.message}`);
(err as any).statusCode = 401;
(err as any).code = "UNAUTHORIZED";
throw err;
const err = new Error("Challenge has expired. Please request a new one.");
(err as any).statusCode = 401;
(err as any).code = "unauthorized";
throw err;
}
const err = new Error(`Challenge verification failed: ${error.message}`);
(err as any).statusCode = 401;
(err as any).code = "unauthorized";
throw err;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/auth.ts` around lines 168 - 176, In
verifyChallengeAndIssueToken, replace the uppercase error code "UNAUTHORIZED"
with the lowercase "unauthorized" for both thrown errors (the expired-challenge
error and the challenge verification-failed error) so the auth API uses
consistent lowercase error codes; update the (err as any).code assignments in
those two throw paths to "unauthorized".

Comment on lines +162 to 168
const cursor = db.prepare("SELECT last_ledger_sequence FROM indexer_cursor WHERE id = 1").get() as any;
if (cursor) {
lastProcessedLedger = cursor.last_ledger_sequence;
} else {
lastProcessedLedger = indexerStartLedger !== null ? indexerStartLedger : currentLedger - 1;
db.prepare("INSERT INTO indexer_cursor (id, last_ledger_sequence) VALUES (1, ?)").run(lastProcessedLedger);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make cursor writes idempotent to avoid startup race failures and silent checkpoint loss.

indexEvents() can overlap on startup, so the first-run INSERT can throw on duplicate id=1. Also, UPDATE ... WHERE id=1 silently does nothing if the row is missing. Use UPSERT for both paths so checkpointing stays durable.

Suggested fix
-      const cursor = db.prepare("SELECT last_ledger_sequence FROM indexer_cursor WHERE id = 1").get() as any;
+      const cursor = db.prepare("SELECT last_ledger_sequence FROM indexer_cursor WHERE id = 1").get() as any;
       if (cursor) {
         lastProcessedLedger = cursor.last_ledger_sequence;
       } else {
         lastProcessedLedger = indexerStartLedger !== null ? indexerStartLedger : currentLedger - 1;
-        db.prepare("INSERT INTO indexer_cursor (id, last_ledger_sequence) VALUES (1, ?)").run(lastProcessedLedger);
+        db.prepare(`
+          INSERT INTO indexer_cursor (id, last_ledger_sequence)
+          VALUES (1, ?)
+          ON CONFLICT(id) DO UPDATE SET last_ledger_sequence = excluded.last_ledger_sequence
+        `).run(lastProcessedLedger);
       }
@@
-      db.prepare("UPDATE indexer_cursor SET last_ledger_sequence = ? WHERE id = 1").run(currentLedger);
+      db.prepare(`
+        INSERT INTO indexer_cursor (id, last_ledger_sequence)
+        VALUES (1, ?)
+        ON CONFLICT(id) DO UPDATE SET last_ledger_sequence = excluded.last_ledger_sequence
+      `).run(currentLedger);

Also applies to: 198-198

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/indexer.ts` around lines 162 - 168, The current startup
cursor logic uses a plain SELECT and an INSERT that can race (duplicate id=1)
and later UPDATEs that silently no-op if the row is missing; replace the INSERT
and any later UPDATEs with an idempotent UPSERT so checkpoint writes are
durable. Locate the code using db.prepare on the indexer_cursor table (queries
referencing "SELECT last_ledger_sequence FROM indexer_cursor WHERE id = 1", the
INSERT into indexer_cursor, and any UPDATE ... WHERE id=1 around indexEvents /
indexer logic) and change those writes to a single UPSERT statement (INSERT ...
ON CONFLICT(id) DO UPDATE SET last_ledger_sequence =
excluded.last_ledger_sequence) so both initial insert and subsequent checkpoint
updates are atomic and race-safe; apply the same change to the other occurrence
noted (around line ~198).

return "scheduled";
}
if (at >= stream.startAt + stream.durationSeconds) {
if (at >= stream.startAt + stream.durationSeconds + stream.pausedDuration) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Pause time is double-counted after resume.

resumeStream adds elapsed pause to durationSeconds (Line 768) and keeps accumulating pausedDuration (Line 766), while computeStatus/calculateProgress still apply pausedDuration again (Line 369, Line 398, Line 401). This stretches vesting/completion longer than intended.

Also applies to: 390-401, 766-769

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/streamStore.ts` at line 369, Summary: Pause time is
being double-counted because resumeStream both adds pausedDuration into
durationSeconds and keeps pausedDuration, while computeStatus/calculateProgress
also add pausedDuration when checking completion/progress. Fix: remove the
double-count by choosing one source of truth—prefer keeping pausedDuration
separate and stop mutating durationSeconds in resumeStream (remove the line that
adds stream.pausedDuration to stream.durationSeconds in resumeStream); ensure
computeStatus and calculateProgress continue using stream.pausedDuration
together with stream.durationSeconds and stream.startAt (references:
resumeStream, computeStatus, calculateProgress, stream.startAt,
stream.durationSeconds, stream.pausedDuration, variable at) and add/adjust unit
tests to cover resume/pause behavior so completion/vesting times are correct.


import { useState, useMemo, useRef, useEffect, type RefObject } from "react";
import { Stream } from "../types/stream";
import { getExportCsvUrl, ListStreamsFilters, cancelStream } from "../services/api";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Bulk cancel bypasses the component’s cancel contract.

Bulk flow calls cancelStream directly instead of onCancel, so it can skip parent-level side effects/error handling and diverge from single-row cancellation behavior.

Proposed fix
-import { getExportCsvUrl, ListStreamsFilters, cancelStream } from "../services/api";
+import { getExportCsvUrl, ListStreamsFilters } from "../services/api";
@@
-        await cancelStream(streamId);
+        await onCancel(streamId);

Also applies to: 187-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/StreamsTable.tsx` at line 3, The bulk-cancel flow
currently calls cancelStream directly and bypasses the component's cancel
contract; update the bulk cancel logic in StreamsTable (the code around the
individual cancel path that uses onCancel and the bulk code at the block
handling lines ~187-205) to invoke the same onCancel callback for each stream
(or to call a single onCancel that accepts multiple ids) instead of calling
cancelStream directly so parent-level side effects and error handling are
preserved; ensure you reuse the existing onCancel signature (or add a bulk
variant onCancelBulk) and remove direct cancelStream calls from the bulk path so
both single-row and bulk cancellations follow the same contract.

Comment on lines +227 to +237
// Clear selections when streams change (e.g., after filter change)
useEffect(() => {
setSelectedStreamIds((prev) => {
const validIds = new Set(streams.map((s) => s.id));
const next = new Set<string>();
prev.forEach((id) => {
if (validIds.has(id)) next.add(id);
});
return next;
});
}, [streams]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Selection cleanup should also drop no-longer-selectable streams.

Current cleanup only checks ID existence. If a selected stream transitions to completed/canceled, it can remain selected with no row checkbox, leaving stale bulk-selection state.

Proposed fix
   useEffect(() => {
     setSelectedStreamIds((prev) => {
-      const validIds = new Set(streams.map((s) => s.id));
+      const validIds = new Set(
+        streams.filter(isStreamSelectable).map((s) => s.id)
+      );
       const next = new Set<string>();
       prev.forEach((id) => {
         if (validIds.has(id)) next.add(id);
       });
       return next;
     });
   }, [streams]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/StreamsTable.tsx` around lines 227 - 237, The current
useEffect that calls setSelectedStreamIds only retains ids that still exist in
streams but doesn't remove ones whose status became non-selectable (e.g.,
"completed" or "canceled"); update the filter inside setSelectedStreamIds to
also check each stream's status and only keep ids for streams that both exist in
streams and have a selectable status (e.g., not "completed" or "canceled").
Locate the effect referencing useEffect and setSelectedStreamIds and the streams
array, and add a status check against the stream object (stream.status) when
building validIds so stale selections are removed.

Comment on lines +297 to +300
export async function getConfig(): Promise<AppConfig> {
const response = await fetch(`${API_BASE}/config`);
return parseResponse<AppConfig>(response);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

getConfig() unwraps the wrong payload shape.

parseResponse returns the full response body; this currently returns { data: ... } as AppConfig, so allowedAssets can be undefined at runtime for callers expecting AppConfig directly.

Proposed fix
 export async function getConfig(): Promise<AppConfig> {
   const response = await fetch(`${API_BASE}/config`);
-  return parseResponse<AppConfig>(response);
+  const body = await parseResponse<{ data: AppConfig }>(response);
+  return body.data;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function getConfig(): Promise<AppConfig> {
const response = await fetch(`${API_BASE}/config`);
return parseResponse<AppConfig>(response);
}
export async function getConfig(): Promise<AppConfig> {
const response = await fetch(`${API_BASE}/config`);
const body = await parseResponse<{ data: AppConfig }>(response);
return body.data;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/services/api.ts` around lines 297 - 300, getConfig currently
treats the entire parsed response body as AppConfig but parseResponse returns an
envelope like { data: AppConfig }, so change getConfig to call
parseResponse<AppConfig>(response) and then return the inner data (e.g. const
body = await parseResponse<AppConfig>(response); return body.data;) so callers
receive the actual AppConfig (including allowedAssets) rather than the envelope;
update the function signature/comments if needed to reflect that it returns
AppConfig.

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.

1 participant