Skip to content

test: rewards & progression coverage (#970)#1003

Merged
senamakel merged 5 commits into
tinyhumansai:mainfrom
oxoxDev:feat/970-rewards-coverage
Apr 29, 2026
Merged

test: rewards & progression coverage (#970)#1003
senamakel merged 5 commits into
tinyhumansai:mainfrom
oxoxDev:feat/970-rewards-coverage

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented Apr 28, 2026

Summary

  • Closes the §12 (Rewards & Progression) gap in the test coverage matrix — six leaves go from missing to covered with VU+WD layers.
  • Adds 14 Vitest cases that exercise the snapshot-derivation contract (the de-facto reducer for the rewards domain — there is no Redux slice).
  • Adds two WDIO E2E specs (rewards-unlock-flow.spec.ts + rewards-progression-persistence.spec.ts, 6 cases total) that drive the Rewards page through the full unlock taxonomy and a simulated app restart.
  • Adds a /rewards/me mock endpoint in scripts/mock-api-core.mjs with mockBehavior knobs (rewardsScenario, rewardsServiceError, rewardsLastSyncedAt) so each E2E case can flip server state without restarting the mock.
  • Documents the rewards-domain reality (frontend-only; no Rust counterpart; no Redux slice) at the top of §12 in the matrix so future agents do not waste cycles searching for a non-existent core module.

Problem

The test coverage matrix introduced in #773 marked all six §12 (Rewards & Progression) leaves missing, with the work tracked under #970:

ID Feature Was
12.1.1 Activity-Based Unlock missing
12.1.2 Integration-Based Unlock missing
12.1.3 Plan-Based Unlock missing
12.2.1 Message Count Tracking missing
12.2.2 Usage Metrics missing
12.2.3 State Persistence missing

The issue body also flagged an open question — does the rewards domain exist in Rust? Investigation answer: no. There is no src/openhuman/rewards/ module and no Redux rewardsSlice in app/src/store/; the snapshot is fetched per-mount via app/src/services/api/rewardsApi.ts and held in Rewards.tsx component state. Backend ownership lives in tinyhumansai/backend (/rewards/me).

Solution

Layer split (mirrors the matrix):

  • Vitest (app/src/store/__tests__/rewardsSlice.test.ts, 14 cases): exercises normalizeRewardsSnapshot — the reducer-equivalent that turns the raw /rewards/me payload into the canonical client snapshot every UI selector reads. Cases mirror the unlock taxonomy (activity / integration / plan) and the progress-tracking surface (message-count proxy via featuresUsedCount, usage metrics, persistence semantics including idempotent re-load and duplicate-id defense).
  • WDIO E2E:
    • app/test/e2e/specs/rewards-unlock-flow.spec.ts (3 cases — 12.1.1 / 12.1.2 / 12.1.3): primes mockBehavior.rewardsScenario for each unlock branch, navigates to /rewards, and asserts the rendered summary line + per-card state matches.
    • app/test/e2e/specs/rewards-progression-persistence.spec.ts (3 cases — 12.2.1 / 12.2.2 / 12.2.3): the persistence case primes high_usage with a pre-restart lastSyncedAt, simulates a restart by unmounting + remounting the page (cheapest equivalent under tauri-driver), flips to post_restart with a later lastSyncedAt, and asserts both the durable counters survive AND the admin request log shows >=2 GET /rewards/me hits (proves the simulated restart actually re-fetched).

Mock backend addition: the /rewards/me handler responds to six scenarios (default, activity_unlocked, integration_unlocked, plan_unlocked, high_usage, post_restart) plus a rewardsServiceError failure switch. Snapshot shape mirrors the existing RewardsSnapshot type so the same fixture set is usable by future component-level tests.

Architectural note (also in the matrix and the Vitest header): the issue plan asked for app/src/store/__tests__/rewardsSlice.test.ts. Rather than introduce a dead Redux slice purely to satisfy the path, the file lives at the requested path and exercises the de-facto rewards state layer. The test prose calls this out so reviewers can audit the choice without spelunking.

Submission Checklist

  • Unit tests — 14 new Vitest cases in app/src/store/__tests__/rewardsSlice.test.ts. Full suite: 952 passed (was 938).
  • E2E / integration — Two new WDIO specs (6 cases total). Note: WDIO E2E is deferred to CI (Linux tauri-driver) per project convention; local arm64 macOS Appium runs require a full Tauri build that exceeds CI resource budgets here. Specs follow the same structural patterns as whatsapp-flow.spec.ts / slack-flow.spec.ts / insights-dashboard.spec.ts (introduced in test(foundation): coverage matrix + strategy + gap-fill batch (#773) #980).
  • Doc comments — File-level headers on every new spec explain scope, the per-case strategy, the Mac2 skip rationale, and which matrix rows are covered. The Vitest file documents the no-slice architectural reality.
  • Inline comments — Grep-friendly notes on the persistence case (why unmount-remount is the restart equivalent), the mock scenario taxonomy, and the duplicate-id defense.

Impact

  • Runtime impact: zero — only test fixtures, the mock-server scenario presets (only active during E2E runs), and a documentation update. No production code paths changed.
  • CI: adds 14 Vitest cases (~12ms total) and two WDIO specs that run alongside the existing batch in pnpm test:e2e:all:flows. Each new spec uses the existing triggerAuthDeepLinkBypass + onboarding helpers, so no new harness costs.
  • Migration / compatibility: none. The new mock endpoint is additive; existing tests that did not call /rewards/me are unaffected. The matrix layout is unchanged for all rows except §12.

Related

  • Closes [Test] Rewards & Progression coverage #970
  • Follow-up TODOs:
    • When the Appium Mac2 helpers gain a label mapping for the Rewards bottom-tab icon, the supportsExecuteScript() skip blocks in both new specs can be lifted (same pattern as the four §10 / §11 specs that already skip on Mac2).
    • If/when a real messageCount field is exposed in the /rewards/me payload, the proxy assertion in 12.2.1 should switch from featuresUsedCount to that field directly.

Summary by CodeRabbit

  • Tests

    • Added comprehensive test coverage for rewards progression and achievement unlocking behavior, including persistence validation and multiple unlock scenarios.
  • Documentation

    • Updated test coverage matrix to reflect new unit and end-to-end test specifications for rewards functionality.

oxoxDev and others added 5 commits April 28, 2026 17:55
…ai#970)

Drives the rewards coverage WDIO specs introduced in this PR. The mock
exposes mockBehavior knobs (rewardsScenario, rewardsServiceError,
rewardsLastSyncedAt) so each E2E case can flip the snapshot returned by
GET /rewards/me without restarting the server. Scenarios cover the
matrix taxonomy: default, activity_unlocked (12.1.1),
integration_unlocked (12.1.2), plan_unlocked (12.1.3), high_usage
(12.2.1/12.2.2), post_restart (12.2.3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

Adds 14 cases across the unlock taxonomy (activity / integration / plan)
and the progress-tracking surface (message-count proxy via
featuresUsedCount, usage metrics, persistence semantics) by exercising
normalizeRewardsSnapshot — the de-facto reducer for the rewards domain
in this codebase. There is no Redux rewardsSlice; snapshot lives in
Rewards.tsx component state. The file is placed at the path the issue
plan called out so the matrix audit trail is unambiguous, and the test
prose documents the no-slice architectural reality.

Covers matrix rows 12.1.1, 12.1.2, 12.1.3, 12.2.1, 12.2.2, 12.2.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three cases that drive the Rewards page through the activity-,
integration-, and plan-based unlock flavours via mockBehavior scenario
priming. Each case resets the mock state, primes the relevant scenario,
re-mounts the page (hash router unmount-remount fires a fresh
/rewards/me fetch), and asserts the rendered summary line + achievement
copy match the server-authoritative snapshot.

Mac2 skipped — same rationale as whatsapp-flow / slack-flow / insights:
no Appium label mapping for the bottom-tab Rewards icon yet. Linux
tauri-driver is the source of truth for this spec.

Covers matrix rows 12.1.1, 12.1.2, 12.1.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…umansai#970)

Adds three cases for matrix rows 12.2.1, 12.2.2, 12.2.3:

- 12.2.1 message-count tracking — primes high_usage and asserts the
  server-authoritative unlock count + per-card achievement titles.
- 12.2.2 usage metrics — asserts the metrics footer renders the streak
  + cumulative-tokens values.
- 12.2.3 state persistence across restart — primes high_usage with a
  pre-restart lastSyncedAt, captures the durable counters from the
  rendered DOM, navigates away, flips to post_restart with a later
  lastSyncedAt, navigates back, and confirms (a) durable counters
  survive unchanged, (b) the admin request log shows >=2 GET
  /rewards/me hits, proving the simulated restart re-fetched.

Restart simulation uses page unmount-remount because tauri-driver does
not support cheap full-app restarts; this is the realistic equivalent
for the Rewards page which fetches once on mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

Promotes the six §12 leaves from missing to covered with VU+WD layer
designations and per-row notes that document the unlock taxonomy
branches the new tests exercise. Adds a section header callout for the
rewards-domain reality (frontend-only; no Rust counterpart; no Redux
slice) so future agents do not waste cycles searching src/openhuman/
for a non-existent rewards/ module — answer to the issue's open
question.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@oxoxDev oxoxDev requested a review from a team April 28, 2026 12:32
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

Adds comprehensive test coverage for Rewards & Progression feature (12.x matrix) with unit tests validating reward snapshot normalization logic, two e2e specs testing activity/integration/plan-based unlock flows and persistence across app restart, and a mock API endpoint providing configurable reward scenarios.

Changes

Cohort / File(s) Summary
Unit Tests
app/src/store/__tests__/rewardsSlice.test.ts
Vitest suite validating normalizeRewardsSnapshot against various backend payloads, including activity unlock behavior, Discord integration state, plan tier/subscription requirements, metric coercion, and defensive handling of invalid/missing numeric values.
E2E Specs
app/test/e2e/specs/rewards-unlock-flow.spec.ts, app/test/e2e/specs/rewards-progression-persistence.spec.ts
Two WDIO specs covering role-unlock scenarios (activity, integration, plan-based), message count tracking, usage metrics, and state durability across app restart via mock scenario switching.
Mock Infrastructure
scripts/mock-api-core.mjs
New GET /rewards/me endpoint supporting conditional failure responses and scenario-driven reward snapshots (activity\_unlocked, integration\_unlocked, plan\_unlocked, post\_restart) with configurable metrics and unlock states.
Documentation
docs/TEST-COVERAGE-MATRIX.md
Updates Rewards role-unlock and progress-tracking matrix entries (12.1.1–12.2.3) from missing specs () to verified with unit + e2e coverage (), adding coverage notes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • senamakel
  • graycyrus

Poem

🐰 A test-weaving rabbit hops with glee,
Verifying rewards through mock and decree,
From unlocks that sparkle to streaks that persist,
Each scenario tested—not one shall be missed! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% 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 'test: rewards & progression coverage (#970)' clearly identifies the main change as adding test coverage for the Rewards & Progression feature section, directly matching the PR's primary objective.
Linked Issues check ✅ Passed The PR fulfills all coding requirements from #970: adds unit tests for normalizeRewardsSnapshot, implements 2 WDIO E2E specs with 6 test cases covering unlock flows and persistence, creates /rewards/me mock endpoint with scenario controls, and updates test coverage matrix. Persistence verified via simulated restart.
Out of Scope Changes check ✅ Passed All changes remain directly scoped to #970 objectives: test files, mock endpoint enhancements, and documentation updates. No unrelated code modifications, refactorings, or feature changes outside the test coverage scope.

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

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

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

Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (1)
app/test/e2e/specs/rewards-unlock-flow.spec.ts (1)

111-208: Add backend-effect assertions per scenario (not UI-only).

These cases verify rendered text but never confirm the scenario-triggering /rewards/me fetch actually happened after each mock flip. Adding a per-test request-log delta check will prevent stale-state false positives and make failures easier to triage.

Based on learnings: Applies to app/test/e2e/specs/*.spec.ts : Assert both UI outcomes and backend/mock effects in E2E tests when relevant; add failure diagnostics (request logs, dumpAccessibilityTree()) for faster debugging.

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

In `@app/test/e2e/specs/rewards-unlock-flow.spec.ts` around lines 111 - 208, Tests
setMockBehavior/resetMockBehavior then assert UI only, but never verify the
scenario-triggering /rewards/me fetch occurred; update each spec (e.g., the
tests using resetMockBehavior(), setMockBehavior(), waitForRewardsSnapshot()) to
capture the request-log snapshot before flipping the mock, then after
navigateToRewards()/waitForRewardsSnapshot() assert a new request for
"/rewards/me" (or the mock endpoint) appears; on assertion failure also call
diagnostic helpers like dumpAccessibilityTree() and include recent
network/request logs to aid triage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/test/e2e/specs/rewards-progression-persistence.spec.ts`:
- Around line 210-220: The current assertion uses the global request log and can
be satisfied by earlier tests; change to a baseline+delta check by calling the
same browser.execute snippet twice: once before the restart/remount to capture
initialRewardsRequestCount and again after the restart to capture
rewardsRequestCount, then assert that (rewardsRequestCount -
initialRewardsRequestCount) is at least the expected number of new requests
(e.g., >= 1 or >= 2). Update the stepLog calls and the expect to use these two
variables so the test asserts the delta caused by this test's remount rather
than the cumulative log.

---

Nitpick comments:
In `@app/test/e2e/specs/rewards-unlock-flow.spec.ts`:
- Around line 111-208: Tests setMockBehavior/resetMockBehavior then assert UI
only, but never verify the scenario-triggering /rewards/me fetch occurred;
update each spec (e.g., the tests using resetMockBehavior(), setMockBehavior(),
waitForRewardsSnapshot()) to capture the request-log snapshot before flipping
the mock, then after navigateToRewards()/waitForRewardsSnapshot() assert a new
request for "/rewards/me" (or the mock endpoint) appears; on assertion failure
also call diagnostic helpers like dumpAccessibilityTree() and include recent
network/request logs to aid triage.
🪄 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: 68d31c59-c502-487f-bec1-2f1879427378

📥 Commits

Reviewing files that changed from the base of the PR and between 1a25f5b and 0fbc662.

📒 Files selected for processing (5)
  • app/src/store/__tests__/rewardsSlice.test.ts
  • app/test/e2e/specs/rewards-progression-persistence.spec.ts
  • app/test/e2e/specs/rewards-unlock-flow.spec.ts
  • docs/TEST-COVERAGE-MATRIX.md
  • scripts/mock-api-core.mjs

Comment on lines +210 to +220
const rewardsRequestCount = await browser.execute(async () => {
const apiBase =
(window as unknown as { __OPENHUMAN_API_BASE__?: string }).__OPENHUMAN_API_BASE__ ??
'http://127.0.0.1:18473';
const res = await fetch(`${apiBase}/__admin/requests`);
const json = (await res.json()) as { data?: Array<{ method: string; url: string }> };
const log = json.data ?? [];
return log.filter(r => r.method === 'GET' && /^\/rewards\/me/.test(r.url)).length;
});
stepLog('rewards/me request count after restart simulation', { rewardsRequestCount });
expect(rewardsRequestCount).toBeGreaterThanOrEqual(2);
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.

⚠️ Potential issue | 🟠 Major

Request-count assertion is not isolated to this test run.

rewardsRequestCount >= 2 uses the global mock log since suite start, so earlier tests can satisfy it. This can falsely pass even if the remount re-fetch in this case regresses.

🔧 Proposed fix (baseline + delta assertion)
+async function getRewardsMeRequestCount(): Promise<number> {
+  return browser.execute(async () => {
+    const apiBase =
+      (window as unknown as { __OPENHUMAN_API_BASE__?: string }).__OPENHUMAN_API_BASE__ ??
+      'http://127.0.0.1:18473';
+    const res = await fetch(`${apiBase}/__admin/requests`);
+    const json = (await res.json()) as { data?: Array<{ method: string; url: string }> };
+    const log = json.data ?? [];
+    return log.filter(r => r.method === 'GET' && /^\/rewards\/me/.test(r.url)).length;
+  });
+}
+
 it('12.2.3 — state persists across a simulated restart (re-fetch on remount)', async () => {
+  const baselineRewardsRequestCount = await getRewardsMeRequestCount();
   // Phase 1...
   ...
-  const rewardsRequestCount = await browser.execute(async () => {
-    ...
-  });
+  const rewardsRequestCount = await getRewardsMeRequestCount();
   stepLog('rewards/me request count after restart simulation', { rewardsRequestCount });
-  expect(rewardsRequestCount).toBeGreaterThanOrEqual(2);
+  expect(rewardsRequestCount - baselineRewardsRequestCount).toBeGreaterThanOrEqual(2);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/test/e2e/specs/rewards-progression-persistence.spec.ts` around lines 210
- 220, The current assertion uses the global request log and can be satisfied by
earlier tests; change to a baseline+delta check by calling the same
browser.execute snippet twice: once before the restart/remount to capture
initialRewardsRequestCount and again after the restart to capture
rewardsRequestCount, then assert that (rewardsRequestCount -
initialRewardsRequestCount) is at least the expected number of new requests
(e.g., >= 1 or >= 2). Update the stepLog calls and the expect to use these two
variables so the test asserts the delta caused by this test's remount rather
than the cumulative log.

@senamakel senamakel merged commit f7f9fa2 into tinyhumansai:main Apr 29, 2026
9 checks passed
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
…#1003)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

[Test] Rewards & Progression coverage

2 participants