Skip to content

feat: trust system overhaul, GameNative emulator support, vote system fixes, and E2E test cleanup#339

Merged
ObfuscatedVoid merged 6 commits intostagingfrom
feat/gamenative
Apr 18, 2026
Merged

feat: trust system overhaul, GameNative emulator support, vote system fixes, and E2E test cleanup#339
ObfuscatedVoid merged 6 commits intostagingfrom
feat/gamenative

Conversation

@ObfuscatedVoid
Copy link
Copy Markdown
Collaborator

@ObfuscatedVoid ObfuscatedVoid commented Apr 18, 2026

Trust system overhaul, GameNative support, vote fixes, and E2E cleanup

Description

Started as GameNative emulator support, expanded significantly after review uncovered pre-existing bugs, including 4 critical auth bypasses and several vote system race conditions.

What's in here:

  • GameNative emulator — custom fields, config parsing, detection, migration for VOTE_CHANGE_REVERSAL trust action
  • Trust systemreverseLogAction, vote-change reversals, bulk adjustments, trust effects now run inside transactions
  • 4 auth bypasses fixedtrust.ts endpoints called AppError.insufficientRole() without return, so the check fell through and any authenticated user could hit SUPER_ADMIN-only endpoints. All 4 patched + regression tested
  • Banned author approval bug — same missing-return pattern in admin.ts let banned authors' listings get approved despite the rejection branch running
  • Vote race conditionsexistingVote fetched outside $transaction in 4 procedures (pcListings.vote + 3 voteComment). Concurrent votes from the same user would P2002. Moved all inside tx
  • Notification fixesLISTING_VOTED/COMMENT_VOTED no longer fires on toggle-off, mobile now has parity with web, reCAPTCHA added to PC vote
  • 9 email PII leaks removed from admin/moderator select queries
  • Performance — parallel score recalc in nullification service, batchUpdateNullifiedAt helper replaces 8 duplicate loops, bulk approve/reject trust actions parallelized
  • UI cleanupBannedUserBadge component (replaces 6 duplicated inline checks), checkSpamContent utility, ModeratorInfoPanel + vote detail components, configurable sort on admin processed listings
  • E2E tests — gutted the over-abstracted page objects, rewrote specs to use Playwright directly, added pc-voting.spec.ts and trust-integration.spec.ts
  • 40+ new unit tests, all sibling-located (no __tests__/ dirs)

Type of change

  • Bug fix
  • New feature
  • Refactor

How Has This Been Tested?

  • Lint
  • Typecheck
  • Unit tests
  • E2E tests

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have checked that all checks (lint, typecheck, test) pass

Notes for reviewers

The 4 auth bypasses in trust.ts were the most critical find, AppError.insufficientRole() throws but without return the function keeps executing. Regression tests now cover all 4 endpoints across 5 role levels.

Vote nullification refactor keeps the serial-within-batch strategy (connection pool safety) but parallelizes across vote types.

Out-of-scope items left as TODOs: PC admin processed-listings endpoint (doesn't exist yet), web route spam detection.

Summary by CodeRabbit

  • New Features

    • Moderator info panels on listing pages showing approval details and full vote history.
    • GameNative emulator config import and mapping support.
    • New shared banned-user badge component used across listings and profiles.
  • Improvements

    • Accessibility enhancements: ARIA labels and dialog semantics.
    • Clearer vote UI: direction icons and assistive-state on vote buttons.
    • Optional reCAPTCHA support for PC votes.
  • Bug Fixes

    • More robust trust/ vote reversal and nullification handling; improved vote-count refresh behavior.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
emuready Error Error Apr 18, 2026 10:01pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f29fda8e-e0e1-4568-8110-c2928b45ff4e

📥 Commits

Reviewing files that changed from the base of the PR and between f69e9e2 and 2907802.

📒 Files selected for processing (1)
  • src/shared/emulator-config/gamenative/mapping.ts

Walkthrough

Adds transaction-aware trust handling and moderator tooling (panel, votes UI), GameNative import/seed support, vote-related DB indexes and enum, extensive router/service/refactor for vote/trust/nullification flows, many new tests, and large Playwright E2E test and fixture simplifications.

Changes

Cohort / File(s) Summary
Playwright config & data-setup
playwright.config.ts, tests/data-setup.spec.ts, tests/helpers/data-factory.ts, tests/*
Unify workers/timeouts, add data-setup project/spec, add E2E data-factory helpers and broad test suite/page-object simplifications; review test orchestration and updated ignore lists.
DB schema & migration
prisma/schema.prisma, prisma/migrations/..._add_vote_change_reversal_trust_action/migration.sql
Added VOTE_CHANGE_REVERSAL to TrustAction enum and composite indexes @@index([userId, nullifiedAt]) / SQL index creations for vote tables.
Seeders
prisma/seed.ts, prisma/seeders/gamenativeCustomFieldsSeeder.ts
New GameNative custom-fields seeder added and invoked during seeding; performs upserts and prunes stale definitions with safety checks.
Trust core refactor
src/lib/trust/service.ts, src/lib/trust/service.test.ts, src/lib/trust/config.ts, src/lib/errors.ts
Replaces procedural APIs with a Transaction-aware TrustService class (log/reverse/apply/bulk methods), adds VOTE_CHANGE_REVERSAL mapping, and introduces ResourceError.trust helper; significant logic and API surface changes.
Vote trust effects & utilities
src/server/utils/vote-trust-effects.ts, src/server/utils/vote-trust-effects.test.ts, src/app/listings/.../utils/logVoteError.ts, src/app/listings/.../utils/refreshListingDetail.ts
New handlers handleListingVoteTrustEffects / handleCommentVoteTrustEffects using TrustService, with tests; added standardized vote-error logging and refresh helpers.
Nullification & score recalculation
src/server/services/vote-nullification.service.ts, src/server/services/vote-nullification.service.test.ts
Refactored to accept transaction clients, use groupBy aggregations, batch update nullifiedAt across vote tables, and call TrustService.applyBulkManualAdjustments.
Moderator repo/API & sorting helper
src/server/repositories/.../listings.repository.ts, src/server/repositories/pc-listings.repository.ts, src/server/api/routers/listings/admin.ts, src/server/api/routers/listings.ts, src/server/api/utils/listingHelpers.ts
Added getModeratorInfo repo methods, new TRPC moderatorInfo procedure, flexible processed-list order builder (buildProcessedOrderBy), and updated admin listing flows (sorting and post-transaction trust calls).
Moderator UI components & tests
src/app/listings/components/shared/details/..., src/app/listings/.../ModeratorInfoPanel.tsx, tests
New ModeratorInfoPanel, ApprovalSection, VotesSection, VoteRow, VoteDirectionIcon and tests; componentizes moderator vote/approval display and wiring.
Listing clients & integration
src/app/listings/[id]/components/ListingDetailsClient.tsx, src/app/pc-listings/[id]/components/PcListingDetailsClient.tsx
Replaced local refresh/logging with shared refresh* and log* utilities, added ModeratorInfoPanel for moderators, and swapped banned-user UI to shared component.
Banned-user UI & helper
src/components/ui/BannedUserBadge.tsx, src/components/ui/BannedUserBadge.test.tsx, src/utils/user-bans.ts, src/utils/user-bans.test.ts, src/components/ui/index.ts
Added reusable BannedUserBadge (tooltip support) and hasActiveBans type-guard, exported via UI barrel; updated usages across UI.
Accessibility & small UI tweaks
src/components/ui/VoteButtons.tsx, src/components/ui/Badge.tsx, src/components/ui/modals/Modal.tsx, src/app/admin/users/components/*.tsx
Added ARIA attributes (aria-pressed, dialog roles/aria-modal/aria-labels, input aria-labels) and exported BadgeSize type.
PC/mobile listing routers & notifications
src/server/api/routers/pcListings.ts, src/server/api/routers/mobile/listings.ts, src/server/api/routers/listings/comments.ts, tests
Moved vote/comment trust handling into transaction-scoped flows, emit notifications for created/updated votes, add ReCAPTCHA verification for PC votes, and call trust actions on PC listing lifecycle events; extensive router refactors and tests.
Schemas & request validation
src/schemas/listing.ts, src/schemas/pcListing.ts
Added GetListingModeratorInfoSchema, extended GetProcessedSchema sort fields/direction, and added optional recaptchaToken to PC vote schema.
Moderator utilities
src/server/utils/moderator-info.ts, src/server/utils/moderator-info.test.ts
New computeVoteCounts with types and tests used by moderator info endpoints.
Spam-check utility
src/server/utils/spam-check.ts, src/server/utils/spam-check.test.ts
Added checkSpamContent integrating SpamDetectionService and analytics, throwing AppError on spam detection.
Emulator / GameNative import pipeline
src/shared/emulator-config/gamenative/*, src/server/utils/emulator-config/gamenative/*, tests
Large new GameNative mapper/parser/mapping/converter/defaults/types and parser/converter tests; register mapper; review complex mapping/merge logic and many new defaults/types.
Audit & service typings
src/server/services/audit.service.ts
logAudit signature widened to accept Prisma client or transaction client.
Tests & page-objects overhaul
tests/pages/*, tests/helpers/*, tests/fixtures/*, many tests/*.spec.ts
Massive Playwright/Vitest test simplifications, page-object deletions/changes, switching many suites to super_admin fixtures, and added data-setup spec; review E2E assumptions and fixture usage.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ListingDetailsClient as ListingDetailsClient<br/>(Component)
    participant PrismaDB as Prisma<br/>Database
    participant TrustService
    participant TrustLog as TrustActionLog<br/>Table

    Client->>ListingDetailsClient: User submits vote
    activate ListingDetailsClient
    ListingDetailsClient->>PrismaDB: Start transaction (tx)
    activate PrismaDB
    PrismaDB->>PrismaDB: create/update/delete vote record
    PrismaDB->>TrustService: handleListingVoteTrustEffects(tx, action, ...)
    activate TrustService
    alt created
        TrustService->>TrustLog: logAction(voter and author)
    else updated
        TrustService->>TrustLog: reverseLogAction(previous)
        TrustService->>TrustLog: logAction(new)
    else deleted
        TrustService->>TrustLog: reverseLogAction(previous)
    end
    TrustService-->>PrismaDB: trust logs written within tx
    deactivate TrustService
    PrismaDB-->>ListingDetailsClient: commit transaction
    deactivate PrismaDB
    ListingDetailsClient-->>Client: update UI
    deactivate ListingDetailsClient
Loading
sequenceDiagram
    participant User
    participant ModeratorInfoPanel as ModeratorInfoPanel<br/>(Component)
    participant TRPC as TRPC Router<br/>(admin.moderatorInfo)
    participant ListingsRepo as ListingsRepository
    participant PrismaDB as Prisma<br/>Database

    User->>ModeratorInfoPanel: Click "Show Moderator Info"
    activate ModeratorInfoPanel
    ModeratorInfoPanel->>TRPC: query moderatorInfo(id,type)
    activate TRPC
    TRPC->>ListingsRepo: getModeratorInfo(id)
    activate ListingsRepo
    ListingsRepo->>PrismaDB: fetch listing approval + votes
    PrismaDB-->>ListingsRepo: return rows
    ListingsRepo->>ListingsRepo: computeVoteCounts(votes)
    ListingsRepo-->>TRPC: { approval, votes, voteCounts }
    deactivate ListingsRepo
    TRPC-->>ModeratorInfoPanel: return data
    deactivate TRPC
    ModeratorInfoPanel->>ModeratorInfoPanel: render ApprovalSection & VotesSection
    ModeratorInfoPanel-->>User: display moderator info
    deactivate ModeratorInfoPanel
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • 0.10.0 #156: Overlaps on trust/vote handling and enum/index migrations; likely relevant to TrustService and vote-trust-effects changes.

Poem

🐇 I burrowed through enums and logs tonight,
I planted seeds of GameNative delight.
Moderator panels bloom, votes flip and mend,
Transactions hum as trust scores bend.
A carrot cookie for tests kept light and bright!

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

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: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (11)
src/components/ui/modals/Modal.tsx (1)

68-76: ⚠️ Potential issue | 🟡 Minor

Dialog is unnamed when props.title is omitted.

aria-label={props.title} resolves to undefined when no title is passed (e.g., callers that render custom headers via children and/or set hideCloseButton). React then drops the attribute and the dialog has no accessible name, which defeats the purpose of role="dialog" + aria-modal="true" for screen readers.

Consider falling back to aria-labelledby pointing at the internal <h2> when present, or only applying aria-label when a string title exists and otherwise accepting an ariaLabel/ariaLabelledBy prop from the caller.

Proposed adjustment
-        role="dialog"
-        aria-modal="true"
-        aria-label={props.title}
+        role="dialog"
+        aria-modal="true"
+        {...(props.title ? { 'aria-label': props.title } : {})}

Also note: role="dialog" without focus trapping / initial focus management is an a11y gap, but that is pre-existing and out of scope here.

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

In `@src/components/ui/modals/Modal.tsx` around lines 68 - 76, The dialog's
aria-label is set directly from props.title which can be undefined, leaving the
dialog unnamed to assistive tech; update the Modal component so it only sets
aria-label when props.title is a non-empty string, otherwise accept and apply an
ariaLabel or ariaLabelledBy prop from the caller; additionally, when the
component renders its internal <h2> (the title heading), generate and assign a
stable id (e.g., headerId) to that <h2> and set aria-labelledby to that id when
no aria-label is provided so the dialog always has an accessible name; ensure
the logic in the JSX that currently uses aria-label={props.title} (and the title
rendering path) is adjusted to prefer aria-label when provided, fall back to
ariaLabel/ariaLabelledBy props, and finally fall back to
aria-labelledby=headerId if an internal title <h2> exists.
src/server/api/routers/listings/comments.ts (2)

395-416: ⚠️ Potential issue | 🟠 Major

Move notification and analytics side effects after commit.

These side effects fire from inside the transaction callback. If analytics throws, or if the transaction later fails to commit, the vote changes roll back after a notification may already have been emitted. Return the notification/analytics payload from the transaction and emit after $transaction resolves.

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

In `@src/server/api/routers/listings/comments.ts` around lines 395 - 416, The
notification and analytics calls (notificationEventEmitter.emitNotificationEvent
and analytics.engagement.commentVote) are executed inside the DB transaction
callback; instead return a payload from the transaction (e.g. include the
notification payload and analytics args using identifiers like comment,
commentId, finalVoteValue, existingVote?.value, trustActionNeeded,
ctx.session.user.id) instead of emitting/calling them there, then after the
prisma.$transaction / transaction promise resolves perform
notificationEventEmitter.emitNotificationEvent(...) and
analytics.engagement.commentVote(...) using the returned payload so side effects
only run after a successful commit.

329-366: ⚠️ Potential issue | 🟠 Major

This transaction still does not close the concurrent insert race.

Line 334 reads existingVote inside the transaction, but findUnique does not lock a missing (userId, commentId) row under default Read Committed isolation. Two concurrent first votes can both read null, then both attempt create at line 364, causing the second to fail with Prisma P2002 on the unique constraint.

Add explicit conflict handling via retry logic, a safe upsert pattern, or serializable isolation with transaction restart semantics to prevent duplicate inserts.

Additionally, side effects leak inside the transaction. Lines 379–413 emit notifications and analytics events within the transaction scope. If the transaction fails (e.g., due to the P2002 above), these side effects may still propagate, causing inconsistency between the database and notification/analytics state. Move side effects outside the transaction after successful commit.

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

In `@src/server/api/routers/listings/comments.ts` around lines 329 - 366, The
transaction can still race because tx.commentVote.findUnique followed by
tx.commentVote.create allows two concurrent requests to both see null and cause
a Prisma P2002; change the logic in the voting transaction (functions/blocks
using ctx.prisma.$transaction and tx.commentVote.findUnique/create/update) to
either perform a safe upsert (atomic create-or-update), implement
retry-on-conflict around the transaction for P2002 errors, or run under
serializable isolation with transaction restart semantics so duplicate inserts
cannot occur; also remove all side effects (notification/analytics emissions
that currently run inside the transaction after the db writes) from the
$transaction block and instead collect the results (e.g., voteResult,
scoreChange, trustActionNeeded) and perform notifications/analytics only after
the transaction successfully commits.
src/server/api/routers/listings/core.ts (1)

269-286: ⚠️ Potential issue | 🟠 Major

findUnique does not lock the unique key, leaving a createrace exposed.

Line 271's comment claiming a SELECT ... FOR UPDATE equivalent is incorrect. The read at line 272 does not lock anything; two concurrent first votes can both see no existing row and both reach tx.vote.create on line 284, causing the second to fail with P2002 (unique constraint violation). There is no error handling for this constraint failure. Use an explicit upsert pattern, retry logic on P2002, or raise the transaction isolation level to Serializable with client-side retry handling.

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

In `@src/server/api/routers/listings/core.ts` around lines 269 - 286, The
transaction uses tx.vote.findUnique then tx.vote.create which does not lock and
allows a race causing P2002 unique constraint errors; change the logic to use an
atomic upsert (tx.vote.upsert) for userId_listingId or implement client-side
retry handling around the $transaction to catch Prisma P2002 errors and retry
the transaction (or set Serializable isolation with retry) instead of blindly
calling tx.vote.create; update references in this block (the $transaction
callback, tx.vote.findUnique, tx.vote.create) to use tx.vote.upsert or wrap the
$transaction in retry logic that specifically handles
PrismaClientKnownRequestError with code "P2002".
src/server/api/routers/pcListings.ts (2)

668-680: ⚠️ Potential issue | 🔴 Critical

Preserve per-emulator developer authorization in bulk PC moderation.

bulkApprove/bulkReject only check that the actor is a developer, but unlike the single-item paths they never verify the developer is assigned to each listing’s emulator before updateMany. That lets any developer bulk process PC listings for unrelated emulators.

🛡️ Proposed fix
 const pendingListings = await ctx.prisma.pcListing.findMany({
   where: { id: { in: input.pcListingIds }, status: ApprovalStatus.PENDING },
-  select: { id: true, gameId: true, authorId: true },
+  select: { id: true, gameId: true, authorId: true, emulatorId: true },
 })

+if (!isModerator && isDeveloper) {
+  const verifiedEmulatorIds = await repository.getVerifiedEmulatorIds(ctx.session.user.id)
+  const unauthorizedListings = pendingListings.filter(
+    (listing) => !verifiedEmulatorIds.includes(listing.emulatorId),
+  )
+
+  if (unauthorizedListings.length > 0) {
+    return ResourceError.pcListing.mustBeVerifiedToApprove()
+  }
+}

Apply the same check in bulkReject, returning mustBeVerifiedToReject() there.

Also applies to: 730-745

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

In `@src/server/api/routers/pcListings.ts` around lines 668 - 680, bulkApprove and
bulkReject perform updateMany without ensuring the acting developer is assigned
to each listing's emulator, allowing developers to moderate listings for
emulators they don't own; fix by filtering pendingListings to only those whose
gameId (or emulator assignment) is associated with the current developer before
calling ctx.prisma.pcListing.updateMany. In practice, in the bulkApprove and
bulkReject handlers use the same per-listing authorization check used by
single-item paths: fetch pendingListings (as in the pendingListings variable),
validate each listing's emulator ownership against ctx.session.user.id (or
lookup developer-emulator assignments), and for any listings not owned return
mustBeVerifiedToReject() (or exclude them) so updateMany only touches authorized
IDs; ensure processedByUserId and processedAt behavior remains the same and
reference functions/vars pcListing.updateMany, pendingListings, bulkApprove,
bulkReject, and mustBeVerifiedToReject().

1629-1629: ⚠️ Potential issue | 🟠 Major

Remove the remaining report-author email projection.

This admin report query still returns reportedBy.email, which leaves a PII leak in a PR explicitly reducing moderator/admin email exposure.

🛡️ Proposed fix
-            reportedBy: { select: { id: true, name: true, email: true } },
+            reportedBy: { select: { id: true, name: true } },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/api/routers/pcListings.ts` at line 1629, The admin report query in
the pcListings router is still selecting reportedBy.email and must stop
returning PII; locate the query that includes reportedBy: { select: { id: true,
name: true, email: true } } and remove the email key so it becomes reportedBy: {
select: { id: true, name: true } }; update any type/usage expectations that
assume reportedBy.email is present (e.g., response shaping or serializers) to
avoid referencing the removed field.
src/server/api/routers/listings/admin.ts (1)

621-708: ⚠️ Potential issue | 🔴 Critical

Do not let transaction error results fall through to post-commit work.

The transaction callback can return ResourceError... / AppError..., but the new post-transaction trust code immediately dereferences transactionResult.validListings / transactionResult.listingsToReject. Those validation paths will now mask the intended error with a runtime TypeError.

🛡️ Proposed fix direction
-        if (unauthorizedListings.length > 0) {
-          return ResourceError.listing.mustBeVerifiedToApprove()
-        }
+        if (unauthorizedListings.length > 0) {
+          throw ResourceError.listing.mustBeVerifiedToApprove()
+        }
...
-        if (listingsToApprove.length === 0) {
-          return AppError.badRequest(
+        if (listingsToApprove.length === 0) {
+          throw AppError.badRequest(
             'No valid pending listings found to approve. The listings may have already been processed.',
           )
         }

Apply the same pattern in bulkReject, or move these validation checks before opening the transaction.

Also applies to: 809-873

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

In `@src/server/api/routers/listings/admin.ts` around lines 621 - 708, The
transaction callback is returning ResourceError/AppError objects which can be
returned as transactionResult and then cause post-commit TypeErrors when code
dereferences transactionResult.validListings; fix by ensuring the tx callback
never returns error objects — either perform those validation checks before
opening the transaction (e.g., move the moderator/verified and "no valid pending
listings" checks out of the tx) or, if kept inside the tx, throw the
ResourceError/AppError instead of returning it so the outer try/catch sees the
rejection; then before running post-commit work (the code that reads
transactionResult.validListings and calls applyTrustAction /
TrustAction.LISTING_APPROVED) assert transactionResult is the expected shape (or
catch/propagate thrown errors) to guarantee you only run post-commit actions on
a successful transaction.
src/server/api/routers/mobile/listings.ts (1)

528-614: ⚠️ Potential issue | 🟠 Major

Notification & analytics emit inside $transaction — diverges from vote and can fire on rollback.

In the vote mutation above (lines 240–314) the LISTING_VOTED emit is correctly placed outside ctx.prisma.$transaction, so it only runs after the transaction commits. Here in voteComment, both the COMMENT_VOTED emit (lines 590–600) and analytics.engagement.commentVote(...) (lines 606–611) execute inside the $transaction callback. If any awaited call later in the callback throws (e.g., the comment.update at line 565 is already before the emit, but future edits could add writes after; also any error thrown after the emit would still commit the side effect), the transaction rolls back while the notification/analytics event has already been dispatched — leading to phantom notifications for votes that were never persisted.

Move the notification emission and analytics call out of the transaction (mirror the vote mutation): return the necessary values from the $transaction callback, then emit after await ctx.prisma.$transaction(...) resolves. This keeps DB writes atomic and side effects deferred until after commit.

🛠️ Suggested shape
const result = await ctx.prisma.$transaction(async (tx) => {
  // ... existing findUnique / create / update / delete / score update / handleCommentVoteTrustEffects ...
  return { voteResult, trustActionNeeded, existingVote, finalVoteValue }
})

if (
  result.trustActionNeeded !== null &&
  result.trustActionNeeded !== 'remove' &&
  input.value !== null
) {
  notificationEventEmitter.emitNotificationEvent({ /* ...as before... */ })
}

analytics.engagement.commentVote({ /* ...as before... */ })

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

In `@src/server/api/routers/mobile/listings.ts` around lines 528 - 614, The
notification and analytics side-effects are executed inside the
ctx.prisma.$transaction in voteComment, which can cause phantom notifications if
the transaction later rolls back; move
notificationEventEmitter.emitNotificationEvent and
analytics.engagement.commentVote out of the transaction by returning the minimal
needed values from the $transaction callback (e.g., voteResult,
trustActionNeeded, existingVote, finalVoteValue, comment/listing ids) and then,
after await ctx.prisma.$transaction(...) resolves, perform the conditional emit
and analytics call (mirroring the pattern used in vote) and finally return
voteResult.
src/lib/trust/service.ts (1)

246-383: ⚠️ Potential issue | 🟠 Major

Remove unused targetUserId parameter from both method signatures, or explicitly capture it into metadata if intended for audit purposes.

targetUserId is declared in both logAction (line 249) and reverseLogAction (line 318) but is never destructured or used. Callers in src/server/utils/vote-trust-effects.ts pass targetUserId: voterId explicitly, expecting it to be persisted, yet it's silently dropped.

The TrustActionLog schema has no targetUserId column—only a metadata JSON field—and the schema comment indicates targetUserId should be stored there. However, callers already include voterId in the metadata object (baseMeta), so the separate parameter is redundant and misleading.

Options:

  1. Remove targetUserId from both method signatures (it's unused and callers already pass voterId in metadata).
  2. If targetUserId should be a standard audit field, extract it in the methods and merge it into metadata as a fallback.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/trust/service.ts` around lines 246 - 383, Remove the unused
targetUserId parameter from the logAction and reverseLogAction method signatures
(in Service.logAction and Service.reverseLogAction), delete any destructuring or
references to it, and update their param types to only include userId,
action/originalAction, and metadata; ensure metadata continues to carry any
voter/target identifiers (merge targetUserId into metadata only if you decide to
keep it as audit info) so trustActionLog.create still persists the needed info
in metadata.
src/server/services/vote-nullification.service.ts (1)

201-275: ⚠️ Potential issue | 🟡 Minor

Silent serialization risk when caller passes a Prisma.TransactionClient.

The signature accepts PrismaClientOrTransaction and comments advertise parallelism across different vote types ("across different vote types in parallel", lines 255–256, 264). This is true when prisma is a PrismaClient, but once a transactional flow passes tx, the Promise.all blocks (lines 208, 257, 270 in nullifyUserVotes; lines 372, 414, 427 in restoreUserVotes) execute serially on the single connection owned by the interactive transaction. The code remains correct, just no longer parallel—and a long-running interactive transaction performing sequential batches of 4 findMany → 4 updateMany → 4 recalc passes risks hitting the interactive-transaction timeout on large users.

TrustService already implements a solution pattern (if ('$transaction' in this.prisma)) to detect the context and conditionally wrap execution. Apply a similar approach here to preserve parallelism when using PrismaClient while gracefully degrading to serial execution when passed a TransactionClient, or document at each Promise.all site that parallelism only applies with a top-level client.

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

In `@src/server/services/vote-nullification.service.ts` around lines 201 - 275,
nullifyUserVotes currently uses Promise.all for parallel findMany/update/recalc
calls which is safe with a top-level PrismaClient but will silently serialize
when given a Prisma TransactionClient; detect transaction context (e.g. if
('$transaction' in prisma) or similar check used in TrustService) and in that
case avoid concurrent Promise.all batches—run the batches sequentially (await
each call in order) to prevent long-running interactive-transaction timeouts;
apply the same pattern to the Promise.all sites in nullifyUserVotes (the initial
findMany block, the batchUpdateNullifiedAt block, and the
recalculateListingScores/recalculateCommentScores block) and likewise update
restoreUserVotes equivalents so behavior is parallel for PrismaClient and serial
for TransactionClient.
src/app/listings/new/NewListingPage.tsx (1)

125-137: ⚠️ Potential issue | 🟡 Minor

Eden driver reconciliation runs for non-Eden emulators.

The reconciliation block is keyed only by f.name === 'dynamic_driver_version'. The new gamenativeCustomFieldsSeeder defines a field with the same name, so on a GameNative import this block will pass the GameNative driver string through reconcileDriverValue against Eden's release list. In the common case it’s a no-op, but if a GameNative value coincidentally matches an Eden filename it will be silently rewritten to the Eden canonical option value, which isn’t a valid GameNative selection.

Consider gating this on selectedEmulatorSlug === 'eden' (or moving it into the Eden-specific path).

🛡️ Suggested guard
-      // Reconcile Eden driver filename (if present) to canonical driver option value
-      {
+      // Reconcile Eden driver filename (if present) to canonical driver option value
+      if (selectedEmulatorSlug === 'eden') {
         const driverField = parsedCustomFields.find((f) => f.name === 'dynamic_driver_version')
         const releases = driverVersionsQuery.data?.releases
         if (driverField && releases) {
           const current = valueMap.get(driverField.id)
           const canonical = reconcileDriverValue(
             typeof current === 'string' ? current : String(current ?? ''),
             releases,
           )
           if (canonical) valueMap.set(driverField.id, canonical)
         }
       }

Remember to add selectedEmulatorSlug to the handleImportResult dependency list.

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

In `@src/app/listings/new/NewListingPage.tsx` around lines 125 - 137, The
Eden-specific reconciliation currently runs for any import because it only
checks parsedCustomFields for a field named 'dynamic_driver_version'; restrict
this by wrapping the block (the lookup using parsedCustomFields.find,
driverField, reconcileDriverValue, and valueMap.set) in a guard that checks
selectedEmulatorSlug === 'eden' so only Eden imports are reconciled, and add
selectedEmulatorSlug to the handleImportResult dependency list to avoid stale
closures.
🟡 Minor comments (17)
tests/error-handling.spec.ts-114-127 (1)

114-127: ⚠️ Potential issue | 🟡 Minor

jsErrors is collected but never asserted — test doesn't actually verify resilience to console errors.

The pageerror listener pushes into jsErrors, but the array is never read. As written, this is equivalent to "navigate to games, then home" and the test name is misleading. Either assert on jsErrors (e.g., expect(jsErrors).toEqual([]) or filter out known-benign ones) or remove the listener and rename.

🐛 Proposed fix
   test('should remain functional even if console errors occur', async ({ page }) => {
     const jsErrors: string[] = []
     page.on('pageerror', (error) => {
       jsErrors.push(error.message)
     })

     const gamesPage = new GamesPage(page)
     await gamesPage.goto()

     await gamesPage.verifyPageLoaded()

     await gamesPage.navigateToHome()
     await expect(page).toHaveURL('/')
+
+    // Surface unexpected runtime errors that would otherwise be silently swallowed
+    expect(jsErrors, `Unexpected page errors: ${jsErrors.join('\n')}`).toEqual([])
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/error-handling.spec.ts` around lines 114 - 127, The test sets up
jsErrors and a page.on('pageerror') listener but never asserts on jsErrors;
update the test to either assert that jsErrors is empty (e.g.,
expect(jsErrors).toEqual([]) or expect(jsErrors.length).toBe(0)) after the
navigation steps, or filter out known benign messages before asserting, so the
test backing the name "should remain functional even if console errors occur"
actually verifies resilience; locate the jsErrors array and the
page.on('pageerror') listener near GamesPage.goto(),
GamesPage.verifyPageLoaded(), and gamesPage.navigateToHome() to add the
assertion or remove the listener and rename the test accordingly.
tests/custom-fields.spec.ts-46-53 (1)

46-53: ⚠️ Potential issue | 🟡 Minor

page.locator('h3') is too broad as a "template cards" locator.

Any h3 on the page (modal headings, sidebar sections, etc.) will satisfy templateCards.first().or(emptyState), so the empty-state branch is effectively unreachable and the test no longer meaningfully distinguishes the two states. Consider a scoped selector such as a test id on template cards or a container-scoped query (e.g., page.locator('[data-testid="template-card"] h3')).

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

In `@tests/custom-fields.spec.ts` around lines 46 - 53, The locator
page.locator('h3') in the test 'should show template cards or empty state in the
list' is too broad and can match unrelated headings (making emptyState
unreachable); update the test to use a scoped selector that targets template
cards specifically (e.g., use a data-testid or container-scoped query such as
page.locator('[data-testid="template-card"] h3') or page.locator('.template-card
h3') instead of page.locator('h3')), keep the emptyState = page.getByText(/no
custom field templates/i) and the final visibility assertion
(templateCards.first().or(emptyState)) so the test correctly distinguishes
between cards and the empty state.
tests/pages/AuthPage.ts-15-17 (1)

15-17: ⚠️ Potential issue | 🟡 Minor

verifyUserNotAuthenticated can false-positive on footer "Sign in" text.

this.page.getByText(/sign in/i).first() matches any "sign in" text anywhere on the page, including footer links, marketing copy, or inline "Sign in to comment" prompts — all of which can be visible even when the user is authenticated on some layouts. Relying on signInButton (the navbar control) alone is a stronger signal; the .or(...) fallback weakens the guarantee this helper is intended to provide.

Suggested tightening
-    await expect(this.signInButton.or(this.page.getByText(/sign in/i).first())).toBeVisible()
+    await expect(this.signInButton).toBeVisible()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/pages/AuthPage.ts` around lines 15 - 17, The helper
verifyUserNotAuthenticated currently uses a broad fallback
this.page.getByText(/sign in/i).first() which can match unrelated "Sign in" text
(e.g., footer), causing false positives; tighten the check by removing the
.or(...) fallback and rely on the navbar-specific locator this.signInButton
only, or if a fallback is still desired scope it to the navbar (e.g., use the
nav locator like nav.getByText(...) or the existing navbar locator) so
verifyUserNotAuthenticated only asserts visibility of the actual navbar sign-in
control (referencing verifyUserNotAuthenticated and this.signInButton).
tests/accessibility.spec.ts-28-52 (1)

28-52: ⚠️ Potential issue | 🟡 Minor

Don’t sample only the first few interactive elements.

Lines 28-52 and 265-277 can miss unlabeled buttons/links later in the DOM, so the accessibility tests may pass while regressions remain on the page. Since these are already bulk evaluateAll calls, check the full collection or filter to visible elements instead of using slice(...).

♻️ Proposed fix
-      elements.slice(0, 5).map((el) => ({
+      elements.map((el) => ({
         ariaLabel: el.getAttribute('aria-label'),
         text: el.textContent,
         title: el.getAttribute('title'),
       })),
@@
-      elements.slice(0, 5).map((el) => ({
+      elements.map((el) => ({
         text: el.textContent,
         ariaLabel: el.getAttribute('aria-label'),
       })),
@@
-      elements.slice(0, 10).map((el) => ({
+      elements.map((el) => ({
         text: el.textContent?.trim() ?? '',
         ariaLabel: el.getAttribute('aria-label'),
         title: el.getAttribute('title'),
       })),

Also applies to: 265-277

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

In `@tests/accessibility.spec.ts` around lines 28 - 52, The test currently samples
only the first five elements by using slice(...) inside the
page.locator(...).evaluateAll calls (producing buttonData and linkData), which
can miss unlabeled interactive elements later in the DOM; update both
evaluateAll invocations to operate over the full collection (remove slice)
and/or filter to visible elements (e.g., check element.offsetParent or
getClientRects().length inside the evaluator) before mapping so the assertions
on buttonData and linkData cover all relevant/visible buttons and links rather
than just the first five.
tests/accessibility.spec.ts-153-171 (1)

153-171: ⚠️ Potential issue | 🟡 Minor

Don’t treat placeholders as form labels.

Line 170 allows inputs with only a placeholder to pass the “proper form labels” test. That weakens the accessibility coverage because placeholders disappear during input and are not a reliable label replacement.

♻️ Proposed fix
     for (const input of inputAccessibility) {
       const hasAccessibleName = input.hasLabel || input.ariaLabel || input.ariaLabelledBy
-      expect(hasAccessibleName || !!input.placeholder).toBe(true)
+      expect(hasAccessibleName).toBe(true)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/accessibility.spec.ts` around lines 153 - 171, The test currently
treats placeholder as an acceptable accessible name for inputs (computed in
inputAccessibility and asserted in the loop), which is incorrect; update the
assertion so that only a visible/associated label or ARIA labeling passes:
compute hasAccessibleName using input.hasLabel || input.ariaLabel ||
input.ariaLabelledBy (keep the existing properties) and remove the placeholder
fallback from the expectation so the test requires hasAccessibleName to be
truthy for each input; update the for loop assertion around inputAccessibility
to expect(hasAccessibleName).toBe(true) (no placeholder check).
tests/admin-reports.spec.ts-23-25 (1)

23-25: ⚠️ Potential issue | 🟡 Minor

Make the table-row assertion meaningful.

Line 25 always passes because count() cannot be negative. If the seeded reports fail to render, this test still passes while later tests fail elsewhere.

💚 Proposed fix
     const reportRows = reportsTable.locator('tbody tr')
     const rowCount = await reportRows.count()
-    expect(rowCount).toBeGreaterThanOrEqual(0)
+    expect(rowCount).toBeGreaterThan(0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/admin-reports.spec.ts` around lines 23 - 25, The current assertion
expects rowCount to be >= 0 which always passes; change it to assert a
meaningful minimum (e.g., expect(rowCount).toBeGreaterThan(0)) or compare
against the known seeded report count variable (if available) so failures
surface when reports don't render; update the assertion that uses reportRows,
rowCount and reportsTable in the test to use the stricter check or the
seededReports.length reference.
tests/auth.spec.ts-129-132 (1)

129-132: ⚠️ Potential issue | 🟡 Minor

Assert the actual back-navigation target.

Line 131 passes for any local route, including staying on /games/new, so this test can miss a broken goBack() flow. Since the previous page is /games, assert that route explicitly.

💚 Proposed fix
     await page.goto('/games/new')
     await page.goBack()
-    await expect(page).toHaveURL(/localhost|127\.0\.0\.1/)
+    await expect(page).toHaveURL(/\/games(?:[?#].*)?$/)
     await authPage.verifyUserNotAuthenticated()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/auth.spec.ts` around lines 129 - 132, The test currently uses a loose
URL assertion after page.goBack()
(expect(page).toHaveURL(/localhost|127\.0\.0\.1/)) which will pass for any local
route; change it to assert the exact back-navigation target (/games) so the back
flow is validated. Locate the sequence using page.goto('/games/new'),
page.goBack(), expect(page).toHaveURL(...), and
authPage.verifyUserNotAuthenticated() and replace the broad regex with an
assertion that the page URL is the /games route (e.g., check for '/games'
explicitly or a regex that anchors to the /games path).
tests/helpers/data-factory.ts-191-194 (1)

191-194: ⚠️ Potential issue | 🟡 Minor

PC listing submit assertion can mask a failed submission.

await expect(page).toHaveURL(/\/pc-listings/) matches /pc-listings/new as well. If the submit silently fails (validation error, network, etc.) the page stays on /pc-listings/new and this assertion still passes, leaving the test suite to fail later with a misleading "listing not found" style error. The handheld helper at line 159 correctly uses a negative lookahead — please mirror that here.

🛠️ Proposed fix
-  await expect(page).toHaveURL(/\/pc-listings/)
+  await expect(page).toHaveURL(/\/pc-listings(?!\/new)/)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/helpers/data-factory.ts` around lines 191 - 194, The current submit
assertion uses expect(page).toHaveURL(/\/pc-listings/) which also matches
/pc-listings/new and can hide a failed submission; update the assertion that
follows page.getByRole('button', { name: /create compatibility report/i
}).click() to use a regex that excludes the "new" path (e.g., a negative
lookahead or an exact-match pattern) so the test only passes when navigation
reaches the actual listings page and not the form at /pc-listings/new.
tests/helpers/data-factory.ts-332-372 (1)

332-372: ⚠️ Potential issue | 🟡 Minor

Trust-score parsing assumes the label contains only digits.

parseInt((await scoreElement.textContent()) ?? '0', 10) on line 347 will return NaN if the element ever contains anything other than a leading digit (e.g. "Score: 500", "1,000", "+500"). NaN > 0 is false so the while loop is skipped and the helper silently returns without resetting the score, and the final assertion on line 371 (toHaveText('0')) would then fail with a confusing message. Consider validating the parsed value and/or extracting the numeric run explicitly.

Additionally, the batching loop at lines 355-364 awaits only the toast trust score adjusted; if the same toast lingers from the previous batch, the assertion could pass before the new adjustment is actually applied. Waiting for the toast to be hidden between iterations (or for a fresh toast instance) would make the loop deterministic.

🛡️ Sketch
-  const currentScore = parseInt((await scoreElement.textContent()) ?? '0', 10)
+  const rawScore = (await scoreElement.textContent())?.replace(/[^\d-]/g, '') ?? '0'
+  const currentScore = Number.parseInt(rawScore, 10)
+  if (!Number.isFinite(currentScore)) {
+    throw new Error(`resetUserTrustScore: unexpected score text "${rawScore}"`)
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/helpers/data-factory.ts` around lines 332 - 372, The helper
resetUserTrustScore wrongly assumes scoreElement.textContent() is a plain
integer and may parse NaN; extract the numeric value from
dialog.getByLabel('Trust score value') using a regex that strips non-digit
characters (handle commas and optional +/−), parseInt the clean string and
validate (throw or fail the test if NaN) before using currentScore/remaining;
also make the batch loop deterministic by, after clicking Apply and seeing the
"trust score adjusted" toast, wait for that toast to be hidden (or wait for a
new toast instance) before continuing to the next iteration so each batch is
applied sequentially and awaited.
src/server/repositories/pc-listings.repository.ts-980-988 (1)

980-988: ⚠️ Potential issue | 🟡 Minor

Unbounded findMany on votes.

For listings with very high vote counts this loads every vote row (with a user join) into memory on every moderator panel open. Consider capping with take (e.g. most recent N) and/or exposing pagination; the counts already come from computeVoteCounts so trimming the row list doesn't affect displayed totals.

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

In `@src/server/repositories/pc-listings.repository.ts` around lines 980 - 988,
The unbounded prisma.pcListingVote.findMany call inside PcListingsRepository
(wrapped by this.handleDatabaseOperation and using
PcListingsRepository.moderatorInfoVoteSelect) can blow memory for high-vote
listings; limit the result set by adding a take (e.g. take: 100) and optionally
skip/page parameters (or accept a pageSize argument) and wire those through the
calling moderator endpoint, while leaving computeVoteCounts to provide totals;
ensure the change is applied where pcListingId votes are fetched so pagination
parameters can be passed through and default to a safe cap if none provided.
src/server/utils/spam-check.ts-25-30 (1)

25-30: ⚠️ Potential issue | 🟡 Minor

entityId: params.userId misrepresents the analytics event.

The spam check fires before any listing/comment row exists, so there's no real entityId to report — but stuffing userId into the entityId field will corrupt downstream analytics (joins, funnels, per-entity aggregations will treat users as entities). Consider either omitting entityId (pass undefined/null if the schema allows), using a sentinel like 'pre-create', or adding an explicit userId field to the event schema.

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

In `@src/server/utils/spam-check.ts` around lines 25 - 30, The analytics event
currently misuses params.userId as entityId in the
analytics.contentQuality.spamDetected call; update the invocation so it does not
populate entityId with the user id (set entityId to undefined/null or a clear
sentinel like "pre-create") and instead include the user id in a dedicated field
(e.g., userId: params.userId) so downstream joins/aggregations aren’t
corrupted—change the call site in spam-check.ts where
analytics.contentQuality.spamDetected(...) is invoked to pass entityId as
null/undefined/"pre-create" and add userId: params.userId.
src/app/listings/components/shared/details/ModeratorInfoPanel.tsx-28-35 (1)

28-35: ⚠️ Potential issue | 🟡 Minor

Expose the panel state to assistive tech.

The toggle visually expands/collapses content, but the button does not announce that state. Add aria-expanded; add aria-controls too if you assign the panel body an id.

♿ Proposed fix
       <button
         type="button"
+        aria-expanded={isExpanded}
         onClick={() => setIsExpanded((prevValue) => !prevValue)}
         className="w-full flex items-center gap-2 px-4 py-3 text-sm font-semibold text-amber-800 dark:text-amber-200 hover:bg-amber-100/50 dark:hover:bg-amber-900/30 transition-colors"
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/listings/components/shared/details/ModeratorInfoPanel.tsx` around
lines 28 - 35, The toggle button in ModeratorInfoPanel.tsx should expose its
state to assistive tech: wire aria-expanded to the isExpanded state used by
setIsExpanded (e.g., aria-expanded={isExpanded}) and add aria-controls pointing
to the panel body’s id; assign an id (like "moderator-info-panel" or a generated
unique id) to the panel content element and ensure that element uses that same
id so the button’s aria-controls references it (optionally also reflect hidden
state on the panel with aria-hidden when collapsed).
src/server/api/routers/pcListings.test.ts-17-21 (1)

17-21: ⚠️ Potential issue | 🟡 Minor

Stale mock export — handleVoteTrustEffects was removed.

Per vote-trust-effects.ts, the old handleVoteTrustEffects helper was replaced by the listing/comment-specific variants and no longer exists. The extra handleVoteTrustEffects: vi.fn() on line 20 is dead weight and can mislead future readers. Safe to drop.

🧹 Proposed cleanup
 vi.mock('@/server/utils/vote-trust-effects', () => ({
   handleListingVoteTrustEffects: (...args: unknown[]) => mockHandleListingVoteTrustEffects(...args),
   handleCommentVoteTrustEffects: (...args: unknown[]) => mockHandleCommentVoteTrustEffects(...args),
-  handleVoteTrustEffects: vi.fn(),
 }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/api/routers/pcListings.test.ts` around lines 17 - 21, The test
mock exports an obsolete symbol handleVoteTrustEffects; remove the dead export
from the vi.mock call so only the current handlers are exported—keep
handleListingVoteTrustEffects and handleCommentVoteTrustEffects mapped to the
mocks (mockHandleListingVoteTrustEffects/mockHandleCommentVoteTrustEffects) and
delete the handleVoteTrustEffects: vi.fn() entry to avoid misleading readers and
stale API surface in src/server/api/routers/pcListings.test.ts.
src/server/utils/emulator-config/gamenative/gamenative.converter.ts-244-252 (1)

244-252: ⚠️ Potential issue | 🟡 Minor

dxvk_version='other' will serialize as a literal version=other.

The seeder defines { value: 'other', label: 'Other (specify in notes)' } for dxvk_version. When a user picks that, this merge step will append version=other into dxwrapperConfig, which GameNative won't recognize as an actual version and which downstream parsers (including the re-importer in parser.ts) will round-trip as the string 'other'. Consider skipping the merge (or falling back to DEFAULT_DXVK_VERSION) when the value is the 'other' sentinel.

🛠️ Suggested guard
-  const dxvkVersion = fieldValuesByName.get('dxvk_version')
-  if (dxvkVersion && String(dxvkVersion).trim()) {
+  const dxvkVersion = fieldValuesByName.get('dxvk_version')
+  const dxvkVersionStr = dxvkVersion ? String(dxvkVersion).trim() : ''
+  if (dxvkVersionStr && dxvkVersionStr.toLowerCase() !== 'other') {
     config.dxwrapperConfig = appendToConfigString(
       config.dxwrapperConfig,
       'version',
-      String(dxvkVersion).trim(),
+      dxvkVersionStr,
     )
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/utils/emulator-config/gamenative/gamenative.converter.ts` around
lines 244 - 252, The merge step currently appends dxvkVersion verbatim
(dxvkVersion, via appendToConfigString into config.dxwrapperConfig), causing the
sentinel 'other' to become an invalid literal; update the logic in
gamenative.converter.ts to detect the sentinel (e.g., dxvkVersion === 'other' or
equals the seeder's sentinel) and instead skip appending or substitute
DEFAULT_DXVK_VERSION before calling appendToConfigString so downstream parsers
(parser.ts) don't receive the literal 'other'.
prisma/seeders/gamenativeCustomFieldsSeeder.ts-37-47 (1)

37-47: ⚠️ Potential issue | 🟡 Minor

Wrapper option values are inconsistent and should be standardized to lowercase.

The seeder uses 'Wrapper' (capitalized, no kebab-case) while the variants use 'Wrapper-v2', 'Wrapper-leegao', 'Wrapper-legacy' (capitalized with kebab-case). The GRAPHICS_DRIVER_MAPPING handles both 'Wrapper' and lowercase 'wrapper' (and all variants), and detectGraphicsDriver performs case-insensitive matching, so this inconsistency is functionally benign.

However, the GraphicsDriver type definition (gamenative.types.ts, lines 22–31) uses only lowercase kebab-case as the source of truth. For consistency and cleaner reverse-mapping, standardize all wrapper options in the seeder to lowercase: 'wrapper', 'wrapper-v2', 'wrapper-leegao', 'wrapper-legacy'. This aligns with the type system and avoids unnecessary mapping indirection.

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

In `@prisma/seeders/gamenativeCustomFieldsSeeder.ts` around lines 37 - 47, The
wrapper option values in the seeder are inconsistent with the canonical
GraphicsDriver type; update the options array entries currently using 'Wrapper',
'Wrapper-v2', 'Wrapper-leegao', 'Wrapper-legacy' to use lowercase kebab-case
'wrapper', 'wrapper-v2', 'wrapper-leegao', 'wrapper-legacy' so they match the
GraphicsDriver definition and avoid extra mapping via
GRAPHICS_DRIVER_MAPPING/detectGraphicsDriver.
src/server/utils/emulator-config/gamenative/gamenative.converter.test.ts-263-284 (1)

263-284: ⚠️ Potential issue | 🟡 Minor

Set the bionic container in this bionic-specific test.

As written, this only proves these Box64 versions are accepted under the default container variant. Add container_variant: 'bionic' to the fixture, or rename the test to avoid false coverage.

💚 Proposed test adjustment
       customFieldValues: [
+        {
+          customFieldDefinition: {
+            name: 'container_variant',
+            label: 'Container Variant',
+            type: 'SELECT',
+          },
+          value: 'bionic',
+        },
         {
           customFieldDefinition: {
             name: 'box64_version',
             label: 'Box64 Version',
             type: 'SELECT',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/utils/emulator-config/gamenative/gamenative.converter.test.ts`
around lines 263 - 284, The test named "should accept bionic-specific Box64
versions" is missing the bionic container variant and thus doesn't prove
bionic-specific behavior; update the fixture passed to convertToGameNativeConfig
in that test to include container_variant: 'bionic' (or alternatively rename the
test if you intend to test default container behavior) so the assertion on
config.box64Version actually exercises the bionic branch—locate the call to
convertToGameNativeConfig in the test and add container_variant: 'bionic' inside
the object passed to it.
src/server/utils/emulator-config/gamenative/gamenative.types.ts-33-40 (1)

33-40: ⚠️ Potential issue | 🟡 Minor

Align the SD 8 Elite version key with the driver literal.

GraphicsDriver uses 'sd-8-elite', but GraphicsDriverVersions uses sd8elite, forcing the GraphicsDriverVersionFor conditional type to special-case the mapping. Rename the key to 'sd-8-elite' to align the types.

Proposed fix
 export type GraphicsDriverVersions = {
   turnip: '25.1.0' | '25.2.0' | '25.3.0' | '25.0.0' | '24.1.0'
   virgl: '23.1.9'
   vortek: '2.1'
   zink: '22.2.5'
   adreno: '819.2' | '805'
-  sd8elite: '800.51' | '2-842.6'
+  'sd-8-elite': '800.51' | '2-842.6'
 }
         : T extends 'sd-8-elite'
-          ? GraphicsDriverVersions['sd8elite']
+          ? GraphicsDriverVersions['sd-8-elite']
           : string
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/utils/emulator-config/gamenative/gamenative.types.ts` around lines
33 - 40, The type key in GraphicsDriverVersions must match the driver literal
used by GraphicsDriver so change the sd8elite property name to the exact literal
'sd-8-elite' (i.e., replace the identifier sd8elite with the string key
'sd-8-elite') and keep its version union unchanged; then remove any special-case
handling in the conditional type GraphicsDriverVersionFor that was added to map
sd8elite to 'sd-8-elite' so all driver-to-version lookups use the unified
GraphicsDriverVersions and GraphicsDriver names consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4caaaf98-fa95-4c47-ab02-b4d2f696e019

📥 Commits

Reviewing files that changed from the base of the PR and between 42105ea and 13c2c68.

📒 Files selected for processing (126)
  • playwright.config.ts
  • prisma/migrations/20260414175740_add_vote_change_reversal_trust_action/migration.sql
  • prisma/schema.prisma
  • prisma/seed.ts
  • prisma/seeders/gamenativeCustomFieldsSeeder.ts
  • src/app/admin/users/components/UserDetailsModal.tsx
  • src/app/admin/users/components/UserRoleModal.tsx
  • src/app/listings/ListingsPage.tsx
  • src/app/listings/[id]/components/ListingDetailsClient.tsx
  • src/app/listings/components/shared/details/AdminUserLink.tsx
  • src/app/listings/components/shared/details/ApprovalSection.tsx
  • src/app/listings/components/shared/details/ModeratorInfoPanel.tsx
  • src/app/listings/components/shared/details/VoteDirectionIcon.test.tsx
  • src/app/listings/components/shared/details/VoteDirectionIcon.tsx
  • src/app/listings/components/shared/details/VoteRow.test.tsx
  • src/app/listings/components/shared/details/VoteRow.tsx
  • src/app/listings/components/shared/details/VotesSection.tsx
  • src/app/listings/components/shared/details/utils/logVoteError.test.ts
  • src/app/listings/components/shared/details/utils/logVoteError.ts
  • src/app/listings/components/shared/details/utils/refreshListingDetail.test.ts
  • src/app/listings/components/shared/details/utils/refreshListingDetail.ts
  • src/app/listings/new/NewListingPage.tsx
  • src/app/pc-listings/PcListingsPage.tsx
  • src/app/pc-listings/[id]/components/PcListingDetailsClient.tsx
  • src/app/users/[id]/UserProfilePage.tsx
  • src/components/listings/AuthorDisplay.tsx
  • src/components/ui/Badge.tsx
  • src/components/ui/BannedUserBadge.test.tsx
  • src/components/ui/BannedUserBadge.tsx
  • src/components/ui/TrustLevelBadge.test.tsx
  • src/components/ui/TrustLevelBadge.tsx
  • src/components/ui/VoteButtons.tsx
  • src/components/ui/index.ts
  • src/components/ui/modals/Modal.tsx
  • src/lib/errors.ts
  • src/lib/trust/config.ts
  • src/lib/trust/service.test.ts
  • src/lib/trust/service.ts
  • src/schemas/listing.ts
  • src/schemas/pcListing.ts
  • src/server/api/routers/listings.ts
  • src/server/api/routers/listings/admin.ts
  • src/server/api/routers/listings/comments.test.ts
  • src/server/api/routers/listings/comments.ts
  • src/server/api/routers/listings/core.test.ts
  • src/server/api/routers/listings/core.ts
  • src/server/api/routers/mobile/listings.test.ts
  • src/server/api/routers/mobile/listings.ts
  • src/server/api/routers/mobile/pcListings.ts
  • src/server/api/routers/pcListings.test.ts
  • src/server/api/routers/pcListings.ts
  • src/server/api/routers/trust.test.ts
  • src/server/api/routers/trust.ts
  • src/server/api/routers/userBans.ts
  • src/server/api/utils/listingHelpers.test.ts
  • src/server/api/utils/listingHelpers.ts
  • src/server/repositories/listings.repository.ts
  • src/server/repositories/pc-listings.repository.ts
  • src/server/services/audit.service.ts
  • src/server/services/vote-nullification.service.test.ts
  • src/server/services/vote-nullification.service.ts
  • src/server/utils/emulator-config/gamenative/gamenative.converter.test.ts
  • src/server/utils/emulator-config/gamenative/gamenative.converter.ts
  • src/server/utils/emulator-config/gamenative/gamenative.defaults.ts
  • src/server/utils/emulator-config/gamenative/gamenative.types.ts
  • src/server/utils/moderator-info.test.ts
  • src/server/utils/moderator-info.ts
  • src/server/utils/spam-check.test.ts
  • src/server/utils/spam-check.ts
  • src/server/utils/spamDetection.ts
  • src/server/utils/vote-trust-effects.test.ts
  • src/server/utils/vote-trust-effects.ts
  • src/shared/emulator-config/gamenative/index.ts
  • src/shared/emulator-config/gamenative/mapping.ts
  • src/shared/emulator-config/gamenative/parser.test.ts
  • src/shared/emulator-config/gamenative/parser.ts
  • src/test/setup.ts
  • src/utils/user-bans.test.ts
  • src/utils/user-bans.ts
  • tests/accessibility.spec.ts
  • tests/admin-dashboard.spec.ts
  • tests/admin-permissions.spec.ts
  • tests/admin-reports.spec.ts
  • tests/admin-users.spec.ts
  • tests/android-downloads.spec.ts
  • tests/auth.spec.ts
  • tests/badge-system.spec.ts
  • tests/browsing.spec.ts
  • tests/commenting.spec.ts
  • tests/custom-fields.spec.ts
  • tests/data-setup.spec.ts
  • tests/error-handling.spec.ts
  • tests/filtering.spec.ts
  • tests/fixtures/test-fixtures.ts
  • tests/forms.spec.ts
  • tests/full-listing-flow.spec.ts
  • tests/game-management.spec.ts
  • tests/helpers/cookie-helper.ts
  • tests/helpers/data-factory.ts
  • tests/helpers/navigation.ts
  • tests/helpers/test-config.ts
  • tests/igdb-search.spec.ts
  • tests/listing-approval.spec.ts
  • tests/listings-success-rate-sorting.spec.ts
  • tests/navigation.spec.ts
  • tests/notification-system.spec.ts
  • tests/pages/AuthPage.ts
  • tests/pages/BasePage.ts
  • tests/pages/CookieBanner.ts
  • tests/pages/GameFormPage.ts
  • tests/pages/GamesPage.ts
  • tests/pages/HomePage.ts
  • tests/pages/ListingFormPage.ts
  • tests/pages/ListingsPage.ts
  • tests/pagination.spec.ts
  • tests/pc-listings.spec.ts
  • tests/pc-voting.spec.ts
  • tests/performance.spec.ts
  • tests/router-coverage.spec.ts
  • tests/search.spec.ts
  • tests/trust-integration.spec.ts
  • tests/trust-system.spec.ts
  • tests/user-flows.spec.ts
  • tests/user-moderation.spec.ts
  • tests/validate-tests.spec.ts
  • tests/voting.spec.ts
💤 Files with no reviewable changes (7)
  • tests/android-downloads.spec.ts
  • tests/helpers/cookie-helper.ts
  • tests/pages/ListingFormPage.ts
  • tests/pages/HomePage.ts
  • tests/fixtures/test-fixtures.ts
  • tests/helpers/navigation.ts
  • tests/pages/CookieBanner.ts

Comment thread prisma/seeders/gamenativeCustomFieldsSeeder.ts Outdated
Comment on lines +170 to +174
await applyTrustAction({
userId: ctx.session.user.id,
action: TrustAction.LISTING_CREATED,
context: { pcListingId: created.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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 'applyTrustAction\s*\(' --type=ts
rg -nP -C5 'LISTING_CREATED' --type=ts

Repository: Producdevity/EmuReady

Length of output: 17629


🏁 Script executed:

#!/bin/bash

# First, let's see the full context of the mobile create method including repository.create
sed -n '140,190p' src/server/api/routers/mobile/pcListings.ts

echo "---"

# Check the web version for comparison
sed -n '250,300p' src/server/api/routers/pcListings.ts

Repository: Producdevity/EmuReady

Length of output: 3610


🏁 Script executed:

#!/bin/bash

# Let's look at the repository.create implementation to understand transaction handling
fd -t f 'repository|Repository' --type f | head -20
rg -l 'class.*Repository|export.*create.*repository' --type ts | head -10

Repository: Producdevity/EmuReady

Length of output: 1554


🏁 Script executed:

#!/bin/bash

# Check if there's try/catch wrapping around applyTrustAction calls
rg -B5 -A5 'applyTrustAction' src/server/api/routers/mobile/pcListings.ts | head -40

echo "---"

# Also check the web version
rg -B5 -A5 'applyTrustAction' src/server/api/routers/pcListings.ts | head -40

Repository: Producdevity/EmuReady

Length of output: 1893


🏁 Script executed:

#!/bin/bash

# Get the create method implementation in the repository
rg -A 50 'create\s*\(' src/server/repositories/pc-listings.repository.ts | head -80

Repository: Producdevity/EmuReady

Length of output: 2474


🏁 Script executed:

#!/bin/bash

# Check the applyTrustAction function for error handling
sed -n '32,120p' src/lib/trust/service.ts

Repository: Producdevity/EmuReady

Length of output: 2639


🏁 Script executed:

#!/bin/bash

# Let's check if there's a try/catch or error handling around the applyTrustAction call in the mobile create
sed -n '150,185p' src/server/api/routers/mobile/pcListings.ts

Repository: Producdevity/EmuReady

Length of output: 1320


🏁 Script executed:

#!/bin/bash

# Check the repository.create to see if it wraps the entire operation in a transaction
rg -B 10 'async create' src/server/repositories/pc-listings.repository.ts | head -30

Repository: Producdevity/EmuReady

Length of output: 268


🏁 Script executed:

#!/bin/bash

# Look for the full create method including the transaction wrapper if any
wc -l src/server/repositories/pc-listings.repository.ts

Repository: Producdevity/EmuReady

Length of output: 119


🏁 Script executed:

#!/bin/bash

# Get the full create method from the repository (starts around line 125)
sed -n '125,250p' src/server/repositories/pc-listings.repository.ts

Repository: Producdevity/EmuReady

Length of output: 4277


🏁 Script executed:

#!/bin/bash

# Check if there's any transaction wrapper or error handling in the create mutation
rg -B 5 -A 30 'create: mobileProtectedProcedure' src/server/api/routers/mobile/pcListings.ts

Repository: Producdevity/EmuReady

Length of output: 1280


🏁 Script executed:

#!/bin/bash

# Check the full repository.create implementation to see if there's transaction handling
sed -n '130,280p' src/server/repositories/pc-listings.repository.ts

Repository: Producdevity/EmuReady

Length of output: 5152


🏁 Script executed:

#!/bin/bash

# Look for PR description or comments mentioning transactions
rg -i 'transaction|runs trust effects' --type=ts --type=md | head -20

Repository: Producdevity/EmuReady

Length of output: 1834


🏁 Script executed:

#!/bin/bash

# Get the actual create method end to see if there's transaction handling
sed -n '130,220p' src/server/repositories/pc-listings.repository.ts

Repository: Producdevity/EmuReady

Length of output: 2785


🏁 Script executed:

#!/bin/bash

# Check if there's a try-catch wrapper anywhere in the mutations
rg -B 2 -A 15 'applyTrustAction' src/server/api/routers/mobile/pcListings.ts | grep -A 15 'create:'

Repository: Producdevity/EmuReady

Length of output: 47


🏁 Script executed:

#!/bin/bash

# Search more specifically for the create method that takes the input parameter
rg -A 100 'async create\(input:' src/server/repositories/pc-listings.repository.ts | head -150

Repository: Producdevity/EmuReady

Length of output: 3438


🏁 Script executed:

#!/bin/bash

# Check if repository.create is wrapped in a transaction
rg 'prisma.\$transaction|this.prisma.\$transaction' src/server/repositories/pc-listings.repository.ts

Repository: Producdevity/EmuReady

Length of output: 47


🏁 Script executed:

#!/bin/bash

# Let's see the complete create method end to understand the full scope
sed -n '130,250p' src/server/repositories/pc-listings.repository.ts | tail -100

Repository: Producdevity/EmuReady

Length of output: 3469


🏁 Script executed:

#!/bin/bash

# Check if there's any error handling pattern in other mutations in the same file
rg -B 3 -A 10 'try\s*{' src/server/api/routers/mobile/pcListings.ts | head -30

Repository: Producdevity/EmuReady

Length of output: 142


🏁 Script executed:

#!/bin/bash

# Check for try-catch error handling in the mobile create mutation
rg -B 3 -A 10 'try {' src/server/api/routers/mobile/pcListings.ts | head -50

Repository: Producdevity/EmuReady

Length of output: 138


🏁 Script executed:

#!/bin/bash

# Check the web version for comparison - does it have error handling?
rg -B 3 -A 10 'try {' src/server/api/routers/pcListings.ts | head -50

Repository: Producdevity/EmuReady

Length of output: 138


🏁 Script executed:

#!/antml/bash

# Get the remaining part of the repository create method
sed -n '195,230p' src/server/repositories/pc-listings.repository.ts

Repository: Producdevity/EmuReady

Length of output: 1402


🏁 Script executed:

#!/bin/bash

# Use grep instead for try-catch pattern
grep -n 'catch' src/server/api/routers/mobile/pcListings.ts | head -10

Repository: Producdevity/EmuReady

Length of output: 47


🏁 Script executed:

#!/bin/bash

# Get the end of the create method in the repository
sed -n '200,235p' src/server/repositories/pc-listings.repository.ts

Repository: Producdevity/EmuReady

Length of output: 1404


🏁 Script executed:

#!/bin/bash

# Check if any create mutations have try-catch
grep -n 'catch' src/server/api/routers/pcListings.ts | head -10

Repository: Producdevity/EmuReady

Length of output: 47


🏁 Script executed:

#!/bin/bash

# Look at the full create method without line limit issues
awk '/async create\(input:/{flag=1} flag{print} /^  \}$/{if(flag) {count++; if(count==1) exit}}' src/server/repositories/pc-listings.repository.ts | head -200

Repository: Producdevity/EmuReady

Length of output: 3803


🏁 Script executed:

#!/bin/bash

# Search for catch keyword differently
grep -c 'catch' src/server/api/routers/mobile/pcListings.ts
grep -c 'catch' src/server/api/routers/pcListings.ts

Repository: Producdevity/EmuReady

Length of output: 68


🏁 Script executed:

#!/bin/bash

# Check the listings/core.ts to see if web listing creation has any error handling
sed -n '150,220p' src/server/api/routers/listings/core.ts

Repository: Producdevity/EmuReady

Length of output: 2553


🏁 Script executed:

#!/bin/bash

# Let's verify the listings/core.ts repository implementation for comparison
rg -A 80 'async create\(input:' src/server/repositories/listings.repository.ts | head -150

Repository: Producdevity/EmuReady

Length of output: 2723


Trust action runs outside any transaction and lacks error handling.

applyTrustAction is invoked after repository.create returns. If the trust call throws, the listing is already persisted but the mutation rejects, leaving the listing without its associated trust event and returning an error to the client.

Note: While the web listing repository wraps creation in a transaction, applyTrustAction is still invoked outside that transaction in both implementations, creating the same risk. Recommend either:

  • Moving the trust call into the same transaction as the listing insert (via the repository), or
  • Wrapping it in a try/catch with structured logging so trust failures don't fail the listing creation flow.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/api/routers/mobile/pcListings.ts` around lines 170 - 174, The
trust event call applyTrustAction is executed after repository.create and can
throw, causing the whole mutation to fail even though the listing was persisted;
either move the trust invocation into the same DB transaction inside the
repository.create flow (add code in the listing repository to call
applyTrustAction/record the trust event as part of its transaction) or wrap
applyTrustAction(...) here in a try/catch that logs structured errors (include
userId, TrustAction.LISTING_CREATED and created.id) and does not re-throw so the
listing creation succeeds even if the trust step fails.

Comment on lines +965 to +974
if (input.recaptchaToken) {
const clientIP = ctx.headers ? getClientIP(ctx.headers) : undefined
const captchaResult = await verifyRecaptcha({
token: input.recaptchaToken,
expectedAction: RECAPTCHA_CONFIG.actions.VOTE,
userIP: clientIP,
})

if (!captchaResult.success) return AppError.captcha(captchaResult.error)
}
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

Require the captcha token instead of verifying only when present.

Line 965 makes captcha optional: a client can omit recaptchaToken and skip verification entirely. If PC voting is meant to be captcha-protected, fail closed when the token is missing.

🛡️ Proposed fix
-    if (input.recaptchaToken) {
-      const clientIP = ctx.headers ? getClientIP(ctx.headers) : undefined
-      const captchaResult = await verifyRecaptcha({
-        token: input.recaptchaToken,
-        expectedAction: RECAPTCHA_CONFIG.actions.VOTE,
-        userIP: clientIP,
-      })
-
-      if (!captchaResult.success) return AppError.captcha(captchaResult.error)
-    }
+    if (!input.recaptchaToken) {
+      return AppError.captcha('Missing reCAPTCHA token')
+    }
+
+    const clientIP = ctx.headers ? getClientIP(ctx.headers) : undefined
+    const captchaResult = await verifyRecaptcha({
+      token: input.recaptchaToken,
+      expectedAction: RECAPTCHA_CONFIG.actions.VOTE,
+      userIP: clientIP,
+    })
+
+    if (!captchaResult.success) return AppError.captcha(captchaResult.error)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/api/routers/pcListings.ts` around lines 965 - 974, The current
logic in the vote path (pcListings.ts) only verifies captcha when
input.recaptchaToken exists, allowing omission; change it to fail when the token
is missing by checking input.recaptchaToken at the top of the block and
returning AppError.captcha (or a similar error) if absent, then proceed to call
verifyRecaptcha({ token: input.recaptchaToken, expectedAction:
RECAPTCHA_CONFIG.actions.VOTE, userIP: ctx.headers ? getClientIP(ctx.headers) :
undefined }) and keep the existing handling of captchaResult.success (return
AppError.captcha(captchaResult.error) on failure).

Comment on lines +982 to 996
// Fetch existingVote INSIDE the transaction to avoid race conditions between
// concurrent votes on the same (user, pcListing) pair.
const voteResult = await ctx.prisma.$transaction(async (tx) => {
const existingVote = await tx.pcListingVote.findUnique({
where: { userId_pcListingId: { userId, pcListingId } },
})

return vote
})
let result: {
vote: { userId: string; pcListingId: string; value: boolean } | null
action: 'created' | 'updated' | 'deleted'
previousValue: boolean | null
}
} else {
// Create new vote and update counts in transaction
voteResult = await ctx.prisma.$transaction(async (tx) => {

if (!existingVote) {
const vote = await tx.pcListingVote.create({
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether the repository already has a retryable transaction helper
# that supports Prisma isolation options or P2002/P2034 retry handling.
fd -i '^transactions\.ts$' src --exec sed -n '1,240p' {}
rg -n -C3 'P2002|P2034|TransactionIsolationLevel|isolationLevel|withRetryTransaction' src/server src/lib

Repository: Producdevity/EmuReady

Length of output: 14885


🏁 Script executed:

sed -n '975,1000p' src/server/api/routers/pcListings.ts

Repository: Producdevity/EmuReady

Length of output: 1053


🏁 Script executed:

sed -n '1335,1380p' src/server/api/routers/pcListings.ts

Repository: Producdevity/EmuReady

Length of output: 1787


Use withRetryTransaction with P2002 handling instead of plain $transaction.

The current code uses ctx.prisma.$transaction without automatic retry logic. Even with the transaction, concurrent requests can both read no existing vote and both attempt to create, hitting a P2002 unique constraint violation. The comment claiming isolation prevents this is incorrect.

Use withRetryTransaction from src/server/utils/transactions.ts, but first update isRetryableError to handle P2002:

function isRetryableError(error: unknown): boolean {
  if (error instanceof Error) {
    const message = error.message.toLowerCase()
    return (
      message.includes('deadlock') ||
      message.includes('timeout') ||
      message.includes('connection') ||
      message.includes('transaction') ||
      message.includes('p2002')  // Add unique constraint violation
    )
  }
  return false
}

Then wrap the transaction call with withRetryTransaction and use SERIALIZABLE isolation to fully prevent the race.

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

In `@src/server/api/routers/pcListings.ts` around lines 982 - 996, Replace the
direct use of ctx.prisma.$transaction in the vote handling block with the
project's withRetryTransaction helper so transient P2002 unique-constraint races
are retried; specifically, call withRetryTransaction(async (tx) => { ... }, {
isolation: 'SERIALIZABLE' }) around the code that uses
tx.pcListingVote.findUnique and tx.pcListingVote.create/update/delete, and
update the isRetryableError helper to treat P2002 (unique constraint) as
retryable by checking for "p2002" in the error message so the retry wrapper will
re-run transactions that fail due to concurrent creates.

Comment on lines 151 to 155
beforeEach(() => {
prisma = createMockPrisma()
mockApplyManualTrustAdjustment.mockResolvedValue(undefined)
mockApplyManualAdjustment.mockResolvedValue(undefined)
mockApplyBulkManualAdjustments.mockResolvedValue(0)
})
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

Clear mock call history between tests.

mockApplyBulkManualAdjustments is asserted with exact call counts in later tests, but beforeEach only resets implementations. Prior test calls can leak and make this suite order-dependent.

💚 Proposed fix
   beforeEach(() => {
+    vi.clearAllMocks()
     prisma = createMockPrisma()
     mockApplyManualAdjustment.mockResolvedValue(undefined)
     mockApplyBulkManualAdjustments.mockResolvedValue(0)
   })
📝 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
beforeEach(() => {
prisma = createMockPrisma()
mockApplyManualTrustAdjustment.mockResolvedValue(undefined)
mockApplyManualAdjustment.mockResolvedValue(undefined)
mockApplyBulkManualAdjustments.mockResolvedValue(0)
})
beforeEach(() => {
vi.clearAllMocks()
prisma = createMockPrisma()
mockApplyManualAdjustment.mockResolvedValue(undefined)
mockApplyBulkManualAdjustments.mockResolvedValue(0)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/services/vote-nullification.service.test.ts` around lines 151 -
155, The tests leak previous mock call history because beforeEach only sets
return values; update the beforeEach in vote-nullification.service.test.ts to
also clear/reset mock histories for mockApplyBulkManualAdjustments and
mockApplyManualAdjustment (e.g., call mockClear() or mockReset() on both) before
re-applying mockResolvedValue so later exact-call-count assertions on
mockApplyBulkManualAdjustments are reliable; locate the beforeEach that
initializes prisma/createMockPrisma and modify it to clear these mocks first.

Comment on lines +95 to 119
const styles = await page
.locator('p, span, div, h1, h2, h3, h4, h5, h6')
.filter({ hasText: /\S+/ })
const sampleSize = Math.min(await textElements.count(), 10)

for (let i = 0; i < sampleSize; i++) {
const element = textElements.nth(i)

const styles = await element.evaluate((el) => {
const computed = window.getComputedStyle(el)
return {
color: computed.color,
backgroundColor: computed.backgroundColor,
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
}
})

// Skip transparent elements as they inherit parent background
if (
styles.backgroundColor === 'rgba(0, 0, 0, 0)' ||
styles.backgroundColor === 'transparent'
) {
.evaluateAll((elements) =>
elements.slice(0, 10).map((el) => {
const computed = window.getComputedStyle(el)
return {
color: computed.color,
backgroundColor: computed.backgroundColor,
fontSize: computed.fontSize,
}
}),
)

for (const s of styles) {
if (s.backgroundColor === 'rgba(0, 0, 0, 0)' || s.backgroundColor === 'transparent') {
continue
}

// Basic check: text should not be same color as background
if (styles.color !== 'rgba(0, 0, 0, 0)' && styles.color !== 'transparent') {
expect(styles.color).not.toBe(styles.backgroundColor)
if (s.color !== 'rgba(0, 0, 0, 0)' && s.color !== 'transparent') {
expect(s.color).not.toBe(s.backgroundColor)
}

// Text should be readable size
const fontSize = parseInt(styles.fontSize)
expect(fontSize).toBeGreaterThanOrEqual(12)
expect(parseInt(s.fontSize)).toBeGreaterThanOrEqual(12)
}
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

Restore a real contrast-ratio assertion.

This now only checks that text and background colors differ, which lets low-contrast combinations pass. The test name says “sufficient color contrast”, so it should compute the contrast ratio or delegate to an accessibility checker.

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

In `@tests/accessibility.spec.ts` around lines 95 - 119, The test currently only
checks that text and background colors differ; replace that with a real
contrast-ratio check: after collecting styles (variable styles) compute the WCAG
contrast ratio between s.color and s.backgroundColor using relative luminance
and contrastRatio helper logic, then assert the ratio meets the appropriate
threshold (use 4.5:1 for normal text and 3:1 for "large" text determined from
parseInt(s.fontSize) — e.g., treat fontSize >= 18px as large). Keep existing
skips for transparent backgrounds and ensure you call the contrast check where
the loop iterates over styles (referring to styles, s.color, s.backgroundColor,
s.fontSize) and replace the simple color inequality assertion with
expect(contrastRatio).toBeGreaterThanOrEqual(threshold).

Comment thread tests/filtering.spec.ts
Comment on lines +5 to +13
async function selectFirstFilterOption(page: Page, filterButton: Locator) {
await filterButton.click()
const firstOption = page.locator('label:has(input[type="checkbox"])').first()
await expect(firstOption).toBeVisible()
await firstOption.click()
// MultiSelect doesn't close on Escape and a lingering open dropdown blocks
// subsequent clicks, so toggle the filter button to dismiss it.
await filterButton.click()
await expect(firstOption).toBeHidden()
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

Scope the option lookup to the opened dropdown.

Line 7 uses the first checkbox label on the whole page. In emulator/performance tests, that can select a device option or wait on a hidden label instead of the menu just opened.

💚 Proposed fix
 async function selectFirstFilterOption(page: Page, filterButton: Locator) {
   await filterButton.click()
-  const firstOption = page.locator('label:has(input[type="checkbox"])').first()
+  const firstOption = page.locator('label:has(input[type="checkbox"]):visible').first()
   await expect(firstOption).toBeVisible()
   await firstOption.click()
   // MultiSelect doesn't close on Escape and a lingering open dropdown blocks
   // subsequent clicks, so toggle the filter button to dismiss it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/filtering.spec.ts` around lines 5 - 13, In selectFirstFilterOption, the
locator for firstOption is global and can match labels outside the opened menu;
after clicking filterButton, scope the lookup to the dropdown container that was
just opened (e.g., locate the popup or the nearest sibling/ancestor menu
associated with filterButton) and then find the first
label:has(input[type="checkbox"]) inside that container; also wait for that
container to be visible before interacting and continue toggling filterButton to
dismiss and assert the scoped option is hidden. Use the existing symbols
selectFirstFilterOption, filterButton and firstOption to replace the global
page.locator with a locator scoped to the opened dropdown.

Comment thread tests/listings-success-rate-sorting.spec.ts
Comment on lines 70 to +79
const nextButton = page.getByRole('button', { name: /go to next page/i })
const hasNextPage = await nextButton.isVisible().catch(() => false)
await expect(nextButton).toBeVisible()
await expect(nextButton).toBeEnabled()

if (hasNextPage && (await nextButton.isEnabled())) {
await nextButton.click()
await page.waitForLoadState('domcontentloaded')

// Sort params should persist on page 2
await expect(page).toHaveURL(/sortField=successRate/)
await expect(page).toHaveURL(/sortDirection=desc/)
await nextButton.click()
await page.waitForLoadState('domcontentloaded')

// Table should still have rows
const rows = page.locator('tbody tr')
expect(await rows.count()).toBeGreaterThan(0)
}
await expect(page).toHaveURL(/sortField=successRate/)
await expect(page).toHaveURL(/sortDirection=desc/)
expect(await page.locator('tbody tr').count()).toBeGreaterThan(0)
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect data-setup to confirm it seeds enough listings for pagination
fd -t f 'data-setup.spec.ts' tests | xargs rg -nP -C3 'listing|seed|create'

Repository: Producdevity/EmuReady

Length of output: 1491


🏁 Script executed:

# Read the full test file to understand context and requirements
fd -t f 'listings-success-rate-sorting.spec.ts' tests | xargs cat -n

Repository: Producdevity/EmuReady

Length of output: 4175


🏁 Script executed:

# Search for pagination/page size configuration
rg -nP 'pageSize|limit|page.*size|pagination' tests/ --type ts

Repository: Producdevity/EmuReady

Length of output: 2344


🏁 Script executed:

# Check if there are other data creation mechanisms or fixtures
rg -nP 'createListing|create.*listing' tests/ --type ts -A2

Repository: Producdevity/EmuReady

Length of output: 643


🏁 Script executed:

# Search for page size, limit, or pagination configuration in the app
rg -nP 'pageSize|itemsPerPage|limit|perPage|PAGE_SIZE|LIMIT' . --type ts --type js --type tsx --type jsx -i

Repository: Producdevity/EmuReady

Length of output: 92


🏁 Script executed:

# Check the data factory to see if createHandheldListing/createPcListing create one or multiple listings
fd -t f 'data-factory' tests | xargs cat -n | head -100

Repository: Producdevity/EmuReady

Length of output: 4305


🏁 Script executed:

# Search for pagination configuration or mock data setup
rg -nP 'pagination|mock.*listing|fixture.*listing' tests/ --type ts -A2 -B2

Repository: Producdevity/EmuReady

Length of output: 4410


🏁 Script executed:

# Search for items per page or page size in config/app files
rg -nP 'itemsPerPage|pageSize|itemLimit|ITEMS_PER_PAGE' . --type ts -B2 -A2

Repository: Producdevity/EmuReady

Length of output: 29294


🏁 Script executed:

# Look at the pagination tests to see how many listings are expected
fd -t f 'pagination.spec.ts' tests | xargs cat -n

Repository: Producdevity/EmuReady

Length of output: 5071


🏁 Script executed:

# Check if there's a seeder or setup that creates more listings
rg -nP 'seeder|seed.*listing' . --type ts --type js -i

Repository: Producdevity/EmuReady

Length of output: 7672


🏁 Script executed:

# Check the listingsSeeder to see how many listings it creates
fd -t f 'listingsSeeder.ts' prisma | xargs wc -l

Repository: Producdevity/EmuReady

Length of output: 101


🏁 Script executed:

# Read the listingsSeeder to understand the scope
fd -t f 'listingsSeeder.ts' prisma/seeders | xargs head -150

Repository: Producdevity/EmuReady

Length of output: 5151


🏁 Script executed:

# Search for how many listings are created in listingsSeeder
rg -nP 'for.*|while.*|\.create\(' prisma/seeders/listingsSeeder.ts | head -30

Repository: Producdevity/EmuReady

Length of output: 1846


🏁 Script executed:

# Check how many devices are created in the devicesSeeder
rg -nP 'prisma.device.create|createMany' prisma/seeders/devicesSeeder.ts | wc -l

Repository: Producdevity/EmuReady

Length of output: 66


🏁 Script executed:

# Look at the structure to understand device creation better
fd -t f 'devicesSeeder.ts' prisma/seeders | xargs tail -100

Repository: Producdevity/EmuReady

Length of output: 3210


🏁 Script executed:

# Check if pagination tests are isolated to the test user or use all listings
rg -nP 'data-setup|afterEach|beforeEach|test.use' tests/pagination.spec.ts -A2

Repository: Producdevity/EmuReady

Length of output: 47


🏁 Script executed:

# Read the full pagination spec file to see setup
fd -t f 'pagination.spec.ts' tests | xargs cat -n | head -30

Repository: Producdevity/EmuReady

Length of output: 1713


🏁 Script executed:

# Check the listings page to see if it uses specific user or all listings
rg -nP 'goto|api|query' tests/pages/ListingsPage.ts -A2

Repository: Producdevity/EmuReady

Length of output: 359


🏁 Script executed:

# Find how many listings the listingsSeeder actually creates
fd -t f 'listingsSeeder.ts' prisma/seeders | xargs cat | grep -A 20 'for (let i = 0; i < selectedGames'

Repository: Producdevity/EmuReady

Length of output: 905


🏁 Script executed:

# Find the listings API route/endpoint to check default page size
fd -t f 'route.ts|listings' src/server/api | grep -i listing | head -10

Repository: Producdevity/EmuReady

Length of output: 311


🏁 Script executed:

# Search for the default limit/pageSize in listings endpoint
rg -nP 'limit.*=|pageSize.*=|DEFAULT.*LIMIT' src/server/api --type ts -B2 -A2

Repository: Producdevity/EmuReady

Length of output: 14053


🏁 Script executed:

# Check what the Pagination component defaults to
rg -nP 'limit.*default|pageSize.*default' src/app/listings --type ts -B2 -A2

Repository: Producdevity/EmuReady

Length of output: 670


🏁 Script executed:

# Find the listings schema to check the default limit
rg -nP 'GetListingsSchema|limit.*default' src/server/api/routers/listings --type ts -A5

Repository: Producdevity/EmuReady

Length of output: 1015


🏁 Script executed:

# Check the ListingsPage implementation for default page size
fd -t f 'ListingsPage.tsx' src/app | xargs cat | grep -A 30 'const.*limit\|page.*size' | head -50

Repository: Producdevity/EmuReady

Length of output: 2337


🏁 Script executed:

# Look at the schema definition for the handheld listings query
rg -nP 'export.*GetListingsSchema|limit.*=' src/server --type ts | grep -i listing | head -10

Repository: Producdevity/EmuReady

Length of output: 1076


🏁 Script executed:

# Find the PAGINATION constant definition
rg -nP 'PAGINATION.*=.*{|DEFAULT_LIMIT' src/server --type ts -A2 -B1

Repository: Producdevity/EmuReady

Length of output: 9000


🏁 Script executed:

# Check how listingsState is initialized in ListingsPage
fd -t f 'ListingsPage.tsx' src/app | xargs grep -A 30 'listingsState\|useListingsState\|useState.*limit'

Repository: Producdevity/EmuReady

Length of output: 50377


🏁 Script executed:

# Search for the listings state initialization
rg -nP 'limit.*=' src/app/listings/ListingsPage.tsx -B2 -A2

Repository: Producdevity/EmuReady

Length of output: 47


🏁 Script executed:

# Find useListingsState hook definition
fd -t f 'useListingsState' src/app/listings | xargs cat | head -100

Repository: Producdevity/EmuReady

Length of output: 3120


🏁 Script executed:

# Search for PAGINATION constant and DEFAULT_LIMIT value
rg -nP 'export.*PAGINATION|DEFAULT_LIMIT.*=' src/server/constants --type ts -A1

Repository: Producdevity/EmuReady

Length of output: 125


🏁 Script executed:

# Find PAGINATION constant definition
rg -nP 'PAGINATION.*=|DEFAULT_LIMIT' src/data --type ts -A2 -B1

Repository: Producdevity/EmuReady

Length of output: 527


🏁 Script executed:

# Find where PAGINATION constant is defined
rg -nP 'export.*PAGINATION' src --type ts -l | head -5

Repository: Producdevity/EmuReady

Length of output: 86


Test will fail without sufficient seeded listings. The pagination assertion at lines 71–72 expects the "next page" button to be visible and enabled. However, data-setup.spec.ts creates only 2 listings (1 handheld + 1 PC), and the default page size is 10 items. This means the test user will only see page 1 with all listings, making the "next page" button invisible or disabled—causing the test to fail.

Expand data-setup.spec.ts to create at least 11 listings (or match your target page size) to ensure pagination is actually triggered. The unconditional assertions are good for surfacing real issues; ensure the test data supports them.

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

In `@tests/listings-success-rate-sorting.spec.ts` around lines 70 - 79, The test
assumes pagination (checks nextButton from page.getByRole and expects more than
one page) but the seed data only creates 2 listings; update the test data seeder
(data-setup.spec.ts) to create at least pageSize + 1 listings (e.g., 11 if
default page size is 10) so the "next page" button becomes visible/enabled;
modify the seeding helper (seedListings / createListing / createListings
function used in data-setup.spec.ts) to loop and insert the required number of
listings (mixing handheld and PC as needed) before running the tests.

Comment thread tests/pages/BasePage.ts
Comment on lines +24 to 27
protected constructor(page: Page) {
this.page = page
this.cookieBanner = new CookieBanner(page)
}

// Navigation elements that appear on all pages
get navigation() {
return this.page.locator('nav').first()
page.context().addInitScript(COOKIE_CONSENT_SCRIPT)
}
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

🧩 Analysis chain

🌐 Web query:

Verify the current Playwright TypeScript API for BrowserContext.addInitScript return type and whether callers must await it before navigation.

💡 Result:

The current Playwright TypeScript API for BrowserContext.addInitScript returns Promise<Disposable>. Callers must await the call because it is asynchronous (returns a Promise), but they do not need to await it specifically before calling navigation methods like page.goto. The init script will be evaluated on subsequent page creations and navigations after the await resolves, as per the official documentation and TypeScript definitions. Examples from docs: await browserContext.addInitScript({ path: 'preload.js' }); await page.goto('https://example.com'); // Can follow immediately after await The Disposable (added in v1.59) allows removing the init script via disposable.dispose for future navigations.

Citations:


🏁 Script executed:

# First, let's look at the complete BasePage class
cat -n tests/pages/BasePage.ts

Repository: Producdevity/EmuReady

Length of output: 3517


🏁 Script executed:

# Check if there are subclasses that override navigation methods
fd -e ts -e js tests/pages/ | grep -v node_modules | head -20

Repository: Producdevity/EmuReady

Length of output: 418


🏁 Script executed:

# Look for usage patterns of BasePage in tests to understand the race condition risk
rg "new.*BasePage\|extends BasePage" tests/ -A 3 -B 1 --max-count=20

Repository: Producdevity/EmuReady

Length of output: 47


🏁 Script executed:

# Find BasePage subclasses
rg "extends BasePage" tests/ -A 5

Repository: Producdevity/EmuReady

Length of output: 1418


🏁 Script executed:

# Look at the test structure to understand instantiation patterns
fd . tests/ --full-path --extension ts | head -20

Repository: Producdevity/EmuReady

Length of output: 586


🏁 Script executed:

# Check test files that import BasePage or its subclasses
rg "from.*BasePage|import.*BasePage" tests/ -B 2 -A 5 --max-count=10

Repository: Producdevity/EmuReady

Length of output: 1907


Await init script registration before navigating to prevent race condition.

Line 26 drops the addInitScript(...) promise. Since constructors cannot be async, tests that instantiate a page object and immediately navigate can load before the cookie-consent script completes registration with the browser context. Playwright's browserContext.addInitScript returns Promise<Disposable> which must resolve before init scripts take effect on subsequent navigations.

Store the promise and await it before any navigation. Add the check to navigateToHome(), navigateToHandheld(), navigateToPC(), navigateToGames(), and clickLogo().

Suggested fix
export abstract class BasePage {
  readonly page: Page
+ private readonly cookieConsentReady: Promise<void>

  protected constructor(page: Page) {
    this.page = page
-   page.context().addInitScript(COOKIE_CONSENT_SCRIPT)
+   this.cookieConsentReady = page.context().addInitScript(COOKIE_CONSENT_SCRIPT)
  }
+
+ protected async ensureCookieConsentScriptReady() {
+   await this.cookieConsentReady
+ }

  async navigateToHome() {
+   await this.ensureCookieConsentScriptReady()
    await this.page.goto('/')
    await expect(this.page).toHaveURL('/')
  }

Apply ensureCookieConsentScriptReady() to the other navigation methods and any subclass overrides.

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

In `@tests/pages/BasePage.ts` around lines 24 - 27, The constructor currently
calls page.context().addInitScript(COOKIE_CONSENT_SCRIPT) without awaiting it,
causing a race when tests navigate immediately; modify the class to store the
addInitScript promise (e.g., a private field like cookieScriptReady =
page.context().addInitScript(...)) and add an async helper
ensureCookieConsentScriptReady() that awaits that promise, then call await
this.ensureCookieConsentScriptReady() at the start of navigateToHome(),
navigateToHandheld(), navigateToPC(), navigateToGames(), and clickLogo() so
navigation only proceeds after the init script registration completes.

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

🧹 Nitpick comments (1)
src/shared/emulator-config/gamenative/mapping.ts (1)

106-114: Hoist the reverse maps and rename the misleading upper variable.

createReverseLookup(BOX64_PRESET_MAPPING) / createReverseLookup(FEXCORE_PRESET_MAPPING) are rebuilt on every invocation — unlike the other three reverse maps which are module-level constants. Also, const upper = String(value) never uppercases anything, which makes the subsequent .toLowerCase() fallback confusing.

♻️ Proposed refactor
 const GRAPHICS_DRIVER_REVERSE = createReverseLookup(GRAPHICS_DRIVER_MAPPING)
 const DX_WRAPPER_REVERSE = createReverseLookup(DX_WRAPPER_MAPPING)
 const STARTUP_SELECTION_REVERSE = createReverseLookup(STARTUP_SELECTION_MAPPING)
+const BOX64_PRESET_REVERSE = createReverseLookup(BOX64_PRESET_MAPPING)
+const FEXCORE_PRESET_REVERSE = createReverseLookup(FEXCORE_PRESET_MAPPING)
@@
   box64_preset: {
     jsonPath: 'box64Preset',
     fromConfig: (value) => {
-      const upper = String(value)
-      // Reverse: uppercase → lowercase for custom field
-      const reversed = createReverseLookup(BOX64_PRESET_MAPPING)
-      return reversed[upper] ?? upper.toLowerCase()
+      const str = String(value)
+      return BOX64_PRESET_REVERSE[str] ?? str.toLowerCase()
     },
   },
@@
   fex_core_preset: {
     jsonPath: 'fexcorePreset',
     fromConfig: (value) => {
-      const upper = String(value)
-      const reversed = createReverseLookup(FEXCORE_PRESET_MAPPING)
-      return reversed[upper] ?? upper.toLowerCase()
+      const str = String(value)
+      return FEXCORE_PRESET_REVERSE[str] ?? str.toLowerCase()
     },
   },

Also applies to: 177-184

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

Inline comments:
In `@src/shared/emulator-config/gamenative/mapping.ts`:
- Around line 97-100: The startup_selection mapping currently returns undefined
for unknown values because fromConfig uses
STARTUP_SELECTION_REVERSE[String(value)] with no fallback; change fromConfig in
the startup_selection entry to return STARTUP_SELECTION_REVERSE[String(value)]
|| String(value) (matching the fallback used by graphics_driver and dx_wrapper)
so unrecognized startupSelection values are preserved as strings instead of
being dropped.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b80f8eea-ec8d-49a5-83f5-2a65c6b7590b

📥 Commits

Reviewing files that changed from the base of the PR and between 13c2c68 and f69e9e2.

📒 Files selected for processing (3)
  • prisma/seeders/gamenativeCustomFieldsSeeder.ts
  • src/shared/emulator-config/gamenative/mapping.ts
  • tests/listings-success-rate-sorting.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • prisma/seeders/gamenativeCustomFieldsSeeder.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/listings-success-rate-sorting.spec.ts

Comment thread src/shared/emulator-config/gamenative/mapping.ts
@ObfuscatedVoid ObfuscatedVoid merged commit e8e3c03 into staging Apr 18, 2026
3 of 6 checks passed
@ObfuscatedVoid ObfuscatedVoid deleted the feat/gamenative branch April 18, 2026 21:59
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