Skip to content

test(statsbar): cover 30s auto-refresh and guard against overlapping …#243

Merged
ritik4ever merged 2 commits into
Stellar-Ecosystem:mainfrom
PratikshaGayen:feat/statsbar-refresh-test-guard
Jun 29, 2026
Merged

test(statsbar): cover 30s auto-refresh and guard against overlapping …#243
ritik4ever merged 2 commits into
Stellar-Ecosystem:mainfrom
PratikshaGayen:feat/statsbar-refresh-test-guard

Conversation

@PratikshaGayen

@PratikshaGayen PratikshaGayen commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

StatsBar already does the 30s setInterval refresh this issue asks for (load on mount + setInterval(load, 30_000) cleared on unmount), which I flagged on the issue. What was missing was test coverage for it and a guard so a slow fetchStats() can't overlap the next tick.

Two changes here:

The component now keeps an in-flight ref so a refresh that's still pending when the next 30s tick fires is skipped instead of stacking a second overlapping request, and it no longer calls setState after the component unmounts.

Added fake-timer tests covering the fetch on mount, the 30s re-fetch and re-render with updated totals, the in-flight guard, and interval cleanup on unmount.

No change to the existing fetch/error handling or the rendered output.

Closes #72
One note on CI: the frontend type-check/test job is already failing on main before this change (pre-existing tsc errors in lib/sort.ts and a couple of test files, plus a few unrelated failing suites). This PR doesn't touch any of that. The new StatsBar test passes locally (4/4) and the diff is limited to StatsBar.tsx plus the new test file.

Summary by CodeRabbit

  • Bug Fixes
    • Improved automatic stats refreshing so the displayed totals update more reliably.
    • Prevented duplicate refreshes during slow network requests, reducing flicker and inconsistent counts.
    • Stopped background updates cleanly when the stats panel is closed or removed.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 09e2c67c-af51-40c2-bd1b-4f59370853da

📥 Commits

Reviewing files that changed from the base of the PR and between cf2153e and 2964722.

📒 Files selected for processing (2)
  • frontend/__tests__/StatsBar.test.tsx
  • frontend/components/StatsBar.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/tests/StatsBar.test.tsx
  • frontend/components/StatsBar.tsx

📝 Walkthrough

Walkthrough

StatsBar's polling logic is moved into a useEffect with a named REFRESH_INTERVAL_MS constant, an inFlight boolean to prevent overlapping requests, and an active flag to gate state updates after unmount. A new test suite covers initial fetch, interval re-fetch, overlap blocking, and post-unmount cessation using fake timers.

StatsBar Auto-Refresh

Layer / File(s) Summary
Guarded refresh loop in useEffect
frontend/components/StatsBar.tsx
Introduces REFRESH_INTERVAL_MS, moves load() inside useEffect, adds inFlight to skip concurrent fetches, gates setStats behind an active flag, and clears the interval on cleanup.
Auto-refresh test suite
frontend/__tests__/StatsBar.test.tsx
Adds four fake-timer tests: initial fetch and render, interval-triggered re-fetch, in-flight overlap prevention, and no further fetches after unmount.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • ritik4ever

Poem

🐇 Tick-tock, the clock hops along,
No double fetch — that would be wrong!
inFlight guards the burrow gate,
active flags keep updates straight.
Every 30 seconds, fresh stats appear,
And when unmounted — silence, my dear! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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 clearly points to StatsBar auto-refresh coverage and overlap protection, matching the main change.
Linked Issues check ✅ Passed StatsBar now refreshes every 30 seconds, avoids overlapping fetches, and has tests for mount, refresh, and unmount behavior.
Out of Scope Changes check ✅ Passed The changes stay focused on StatsBar refresh logic and its tests, with no unrelated functionality added.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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: 1

🤖 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 `@frontend/components/StatsBar.tsx`:
- Around line 3-27: The in-flight guard in StatsBar is persisting across React
Strict Mode’s mount/replay cycle, which causes the initial fetch to be skipped.
Move the guard off the component-level ref and into the useEffect/load scope in
StatsBar so each effect run starts cleanly, while still preventing overlapping
refreshes within that effect; keep the fetchStats call and active-state check in
load, and ensure the guard is reset during cleanup or completion without relying
on a ref that survives the replay.
🪄 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 Plus

Run ID: b46fec71-2fa2-4d3f-966a-1e40bd7e4881

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe3687 and 72cea78.

📒 Files selected for processing (2)
  • frontend/__tests__/StatsBar.test.tsx
  • frontend/components/StatsBar.tsx

Comment thread frontend/components/StatsBar.tsx Outdated
@PratikshaGayen

Copy link
Copy Markdown
Contributor Author

Good catch from the CodeRabbit review on the StrictMode replay. The useRef guard stayed set across the mount/cleanup/mount cycle, so the second pass skipped the initial fetch and the bar stayed empty until the first 30s tick. Fixed in 3a790c7 by moving the guard to a local flag inside the effect, so each setup starts clean while overlapping interval fetches are still skipped. Tests still green (4/4).

@ritik4ever

Copy link
Copy Markdown
Collaborator

@PratikshaGayen please fix ci

@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: 5

Caution

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

⚠️ Outside diff range comments (2)
backend/src/routes/agents.js (2)

554-556: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Log policy update failures.

Neighboring routes log err and address before handleContractError; this catch drops the actionable context.

Proposed fix
   } catch (err) {
-
+    logger.error({ err, address }, 'POST /agents/:address/update-policy failed');
     return handleContractError(err, res, 'Policy update failed', 'POLICY_ERROR');
   }
🤖 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/routes/agents.js` around lines 554 - 556, The policy update catch
block in the route handling logic drops the useful debugging context by going
straight to handleContractError; update the catch in the agents route to log the
caught err along with the relevant address before returning. Follow the same
pattern used by neighboring route handlers in this file, and keep the existing
handleContractError call with the Policy update failed / POLICY_ERROR arguments
after the log.

535-551: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate policy values before passing them on-chain.

This accepts values like abc, 1.5, negative stroops, NaN, or non-string categories. Reject non-integer/non-negative stroops, normalize category strings, and bound minScoreToEarn before calling updatePolicyOnChain.

🤖 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/routes/agents.js` around lines 535 - 551, The validation in the
policy update route is too permissive and currently lets invalid stroop values
and malformed categories reach updatePolicyOnChain. Tighten the checks in the
request handler in agents.js so maxPerTxStroops and maxPerDayStroops only accept
non-negative integers (reject abc, 1.5, negatives, NaN), ensure
allowedCategories is an array of strings and normalize/trim each entry before
use, and constrain minScoreToEarn to an acceptable numeric range before
forwarding the values to updatePolicyOnChain.
🤖 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 `@agent/agent.js`:
- Around line 253-273: The spend policy checks in agent should not be gated by
scoringEnabled, since that allows paid requests to proceed when
ensureRegistered() fails. Move the MAX_PER_TX, MAX_PER_DAY, and category limit
enforcement into the request path before any payment attempt, and keep
scoringEnabled only for score/outcome recording and related logging. Use the
existing checkSpend, logger.warn/logger.info, and selected/category/serviceId
flow in agent.js to locate and separate the enforcement logic.
- Around line 23-29: Persist the env-driven policy values from AGENT_NAME,
AGENT_DESC, MAX_PER_TX, MAX_PER_DAY, and ALLOWED_CATS during agent setup instead
of only relying on the defaults in agent/agent.js. Update the registration flow
so the backend register route or the post-registration step includes the policy
fields alongside agentAddress, name, and description, or performs an
authenticated policy update immediately after registration. Make sure the
relevant registration handler and any client payload builder use the existing
symbols MAX_PER_TX, MAX_PER_DAY, and ALLOWED_CATS so fresh agents receive the
intended policy values.
- Around line 296-332: The outcome recording calls in agent.js are missing the
required idempotency header, so recordOutcome() posts to
/api/agents/:address/payment are rejected. Update recordOutcome() to generate
and send a stable X-Idempotency-Key for each success/failure submission, and
make sure the calls from the payment flow after selected.id is processed reuse
that key for the same outcome attempt so the server accepts the request.

In `@backend/src/routes/agents.js`:
- Around line 532-533: The direct /agents/:address/update-policy route is
relying on ownerAuth’s spoofable x-caller-address header before calling
updatePolicyOnChain, which allows forged ownership claims. Update the route in
agents.js to require cryptographic verification of the caller or route it
through the existing wallet-signed build/submit flow instead of server-side
signing. Use the existing ownerAuth, updatePolicyOnChain, and any build/submit
policy mutation path symbols to keep the fix aligned with the authenticated
flow.

In `@frontend/app/agents/page.tsx`:
- Line 19: Restore the shared PAGE_SIZE export used by AgentsPage and its test
import. The constant in the agents page module is currently file-local, which
breaks frontend/__tests__/AgentsPage.test.tsx at import time, so either
re-export PAGE_SIZE from frontend/app/agents/page.tsx or move it into a shared
module and update both the page logic and the test to import the same symbol.

---

Outside diff comments:
In `@backend/src/routes/agents.js`:
- Around line 554-556: The policy update catch block in the route handling logic
drops the useful debugging context by going straight to handleContractError;
update the catch in the agents route to log the caught err along with the
relevant address before returning. Follow the same pattern used by neighboring
route handlers in this file, and keep the existing handleContractError call with
the Policy update failed / POLICY_ERROR arguments after the log.
- Around line 535-551: The validation in the policy update route is too
permissive and currently lets invalid stroop values and malformed categories
reach updatePolicyOnChain. Tighten the checks in the request handler in
agents.js so maxPerTxStroops and maxPerDayStroops only accept non-negative
integers (reject abc, 1.5, negatives, NaN), ensure allowedCategories is an array
of strings and normalize/trim each entry before use, and constrain
minScoreToEarn to an acceptable numeric range before forwarding the values to
updatePolicyOnChain.
🪄 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 Plus

Run ID: ddaeabec-f207-4892-946f-74b62e03d496

📥 Commits

Reviewing files that changed from the base of the PR and between 3a790c7 and cf2153e.

📒 Files selected for processing (16)
  • agent/agent.js
  • backend/src/lib/contract.js
  • backend/src/lib/stellar.js
  • backend/src/routes/agents.js
  • backend/test/agents.test.js
  • frontend/__tests__/RegistryPage.test.tsx
  • frontend/__tests__/ScoreBadge.test.tsx
  • frontend/__tests__/WalletPickerModal.test.tsx
  • frontend/__tests__/wallet.test.ts
  • frontend/app/agents/page.tsx
  • frontend/app/registry/page.tsx
  • frontend/custom.d.ts
  • frontend/lib/registry.test.ts
  • frontend/lib/sort.test.ts
  • frontend/lib/sort.ts
  • frontend/tsconfig.json
💤 Files with no reviewable changes (2)
  • backend/test/agents.test.js
  • frontend/lib/registry.test.ts
✅ Files skipped from review due to trivial changes (7)
  • frontend/custom.d.ts
  • frontend/tests/ScoreBadge.test.tsx
  • frontend/tests/WalletPickerModal.test.tsx
  • frontend/tsconfig.json
  • backend/src/lib/stellar.js
  • frontend/lib/sort.test.ts
  • backend/src/lib/contract.js

Comment thread agent/agent.js Outdated
Comment on lines +23 to +29
const AGENT_NAME = process.env.AGENT_NAME ?? 'LodestarAgent';
const AGENT_DESC = process.env.AGENT_DESC ?? 'An autonomous Lodestar agent';
const MAX_PER_TX = parseFloat(process.env.MAX_PER_TX_USDC ?? '1.00');
const MAX_PER_DAY = parseFloat(process.env.MAX_PER_DAY_USDC ?? '10.00');
const ALLOWED_CATS = (process.env.ALLOWED_CATEGORIES ?? 'weather,search,data').split(',');

const agentKeypair = Keypair.fromSecret(AGENT_SECRET);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Persist the env-driven policy values.

MAX_PER_TX, MAX_PER_DAY, and ALLOWED_CATS are posted during registration, but the backend register route only reads agentAddress, name, and description; fresh agents will keep the default on-chain policy. Wire these fields into registration or apply an authenticated policy update after registration.

🤖 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 `@agent/agent.js` around lines 23 - 29, Persist the env-driven policy values
from AGENT_NAME, AGENT_DESC, MAX_PER_TX, MAX_PER_DAY, and ALLOWED_CATS during
agent setup instead of only relying on the defaults in agent/agent.js. Update
the registration flow so the backend register route or the post-registration
step includes the policy fields alongside agentAddress, name, and description,
or performs an authenticated policy update immediately after registration. Make
sure the relevant registration handler and any client payload builder use the
existing symbols MAX_PER_TX, MAX_PER_DAY, and ALLOWED_CATS so fresh agents
receive the intended policy values.

Comment thread agent/agent.js Outdated
Comment on lines 253 to 273
if (scoringEnabled) {
const spendCheck = await checkSpend(selected.price_usdc, category);
if (!spendCheck.allowed) {
logger.warn(
{
event: EVENT.SPEND_CHECK_BLOCKED,
category,
serviceId: selected.id,
priceUsdc: selected.price_usdc,
reason: spendCheck.reason,
},
'Spend blocked by policy'
);
failed.add(selected.id);
continue;
}
logger.info(
{ event: EVENT.SPEND_CHECK_PASSED, category, serviceId: selected.id },
'Spend check passed'
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not tie spend enforcement to scoring availability.

If ensureRegistered() returns false, this skips policy checks but still signs paid requests. Enforce local MAX_PER_TX, MAX_PER_DAY, and category limits before payment attempts; use scoringEnabled only for score/outcome recording.

🤖 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 `@agent/agent.js` around lines 253 - 273, The spend policy checks in agent
should not be gated by scoringEnabled, since that allows paid requests to
proceed when ensureRegistered() fails. Move the MAX_PER_TX, MAX_PER_DAY, and
category limit enforcement into the request path before any payment attempt, and
keep scoringEnabled only for score/outcome recording and related logging. Use
the existing checkSpend, logger.warn/logger.info, and
selected/category/serviceId flow in agent.js to locate and separate the
enforcement logic.

Comment thread agent/agent.js Outdated
Comment on lines +296 to +332
if (scoringEnabled) await recordOutcome(selected.price_usdc, true, selected.id);
await submitReputation(selected.id, true);
logger.info(
{
event: EVENT.PAYMENT_SUCCESS,
category,
serviceId: selected.id,
serviceName: selected.name,
priceUsdc: selected.price_usdc,
txHash,
taskDurationMs,
},
'Payment successful'
);
return { success: true, priceUsdc: selected.price_usdc };
}

return { success: true, priceUsdc: selected.price_usdc };
// Non-2xx response — service endpoint rejected the request
logger.error(
{
event: EVENT.PAYMENT_FAILED,
category,
serviceId: selected.id,
httpStatus: response.status,
},
'Service returned non-2xx — trying next'
);
if (scoringEnabled) await recordOutcome(selected.price_usdc, false, selected.id);
await submitReputation(selected.id, false);
failed.add(selected.id);
} catch (err) {
// Network-level failure before payment completed — no reputation penalty
logger.error(
{ event: EVENT.PAYMENT_FAILED, category, serviceId: selected.id, err },
'Fetch threw — trying next service'
);
if (scoringEnabled) await recordOutcome(selected.price_usdc, false, selected.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Send an idempotency key for outcome recording.

recordOutcome() posts to /api/agents/:address/payment, whose route rejects requests without X-Idempotency-Key. These success/failure calls therefore won’t update score or spend state.

Proposed fix
-async function recordOutcome(amountUsdc, success, serviceId) {
+async function recordOutcome(amountUsdc, success, serviceId, idempotencyKey = crypto.randomUUID()) {
   try {
     const body = JSON.stringify({ amountUsdc, success, serviceId });
-    const headers = { 'Content-Type': 'application/json' };
+    const headers = {
+      'Content-Type': 'application/json',
+      'X-Idempotency-Key': idempotencyKey,
+    };
🤖 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 `@agent/agent.js` around lines 296 - 332, The outcome recording calls in
agent.js are missing the required idempotency header, so recordOutcome() posts
to /api/agents/:address/payment are rejected. Update recordOutcome() to generate
and send a stable X-Idempotency-Key for each success/failure submission, and
make sure the calls from the payment flow after selected.id is processed reuse
that key for the same outcome attempt so the server accepts the request.

Comment on lines +532 to +533
router.post('/agents/:address/update-policy', requireAgentsContract, ownerAuth, async (req, res) => {
const { address } = req.params;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Do not expose server-signed policy mutation behind a spoofable header.

ownerAuth only checks x-caller-address; this direct route then calls server-side updatePolicyOnChain(...). A client can forge the owner header unless wallet signature/challenge verification is added. Prefer the existing build/submit wallet-signed flow or require cryptographic proof before server-side signing.

🤖 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/routes/agents.js` around lines 532 - 533, The direct
/agents/:address/update-policy route is relying on ownerAuth’s spoofable
x-caller-address header before calling updatePolicyOnChain, which allows forged
ownership claims. Update the route in agents.js to require cryptographic
verification of the caller or route it through the existing wallet-signed
build/submit flow instead of server-side signing. Use the existing ownerAuth,
updatePolicyOnChain, and any build/submit policy mutation path symbols to keep
the fix aligned with the authenticated flow.

Comment thread frontend/app/agents/page.tsx Outdated
@ritik4ever

Copy link
Copy Markdown
Collaborator

@PratikshaGayen please resolve conflicts

…fetches

StatsBar already refreshes stats every 30s, but the behavior had no test
coverage and a slow fetchStats() could overlap the next interval tick.

- Add an in-flight guard so a refresh slower than the interval is skipped
  rather than stacking another request, and avoid setState after unmount.
- Add fake-timer tests covering the mount fetch, the 30s re-fetch and
  re-render, the in-flight guard, and interval cleanup on unmount.
Keeping the guard in a useRef meant it stayed set across StrictMode's
mount/cleanup/mount replay, so the second pass saw it still pending and
skipped the initial fetch, leaving the bar empty until the first 30s
tick. A local flag inside the effect resets on every setup and still
prevents overlapping interval fetches.
@PratikshaGayen PratikshaGayen force-pushed the feat/statsbar-refresh-test-guard branch from cf2153e to 2964722 Compare June 29, 2026 14:44
@PratikshaGayen

Copy link
Copy Markdown
Contributor Author

sorry for the churn on this one. rebased onto latest main so it merges clean now, and i trimmed the PR back to just the statsbar test + the in-flight guard since that's what #72 actually needs. the extra cross-package changes i'd pushed earlier were out of scope so i dropped them. checks are green now too.

@ritik4ever ritik4ever merged commit a3fd824 into Stellar-Ecosystem:main Jun 29, 2026
5 checks passed
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.

Add StatsBar auto-refresh when registry data changes

2 participants