Skip to content

CC-64: Add First Playwright Tests#333

Merged
ianpaschal merged 16 commits into
ian/cc-2-improve-pairing-workflowfrom
ian/cc-64-add-first-playwright-tests
May 22, 2026
Merged

CC-64: Add First Playwright Tests#333
ianpaschal merged 16 commits into
ian/cc-2-improve-pairing-workflowfrom
ian/cc-64-add-first-playwright-tests

Conversation

@ianpaschal
Copy link
Copy Markdown
Owner

@ianpaschal ianpaschal commented May 22, 2026

Resolves CC-64

Summary by CodeRabbit

  • New Features

    • Enhanced tournament pairing generation workflow with improved table assignment and competitor management.
    • User avatars are now deterministic based on username for consistency.
    • Added comprehensive end-to-end testing infrastructure.
  • Bug Fixes

    • Updated tournament pairing restrictions to apply to draft status instead of published status.
  • Accessibility

    • Improved screen reader support with aria-labels across UI components.
  • Documentation

    • Added testing documentation and setup guidance.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

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

Project Deployment Actions Updated (UTC)
combat-command Ready Ready Preview, Comment May 22, 2026 5:35pm

Repository owner deleted a comment from coderabbitai Bot May 22, 2026
Repository owner deleted a comment from coderabbitai Bot May 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3ab692cb-5b54-4d26-9e12-2f4afd46d132

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ian/cc-64-add-first-playwright-tests

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

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

Inline comments:
In @.github/workflows/ci.yml:
- Around line 1-12: Add an explicit top-level permissions block to the workflow
(e.g., set minimal scopes like contents: read) to avoid relying on repo
defaults, and update the checkout step in the test job (the actions/checkout@v4
step) to include persist-credentials: false so the GITHUB_TOKEN is not left in
the checked-out repository; modify the job under "name: CI" / "jobs: test" to
include these changes.
- Around line 12-14: Replace the floating action tags with immutable commit
SHAs: find each uses: entry (e.g., actions/checkout@v4, actions/setup-node@v4,
actions/upload-artifact@v4, and actions/github-script@v7) in the CI workflows
and replace the `@vN` tag with the corresponding full commit SHA for that action's
repo (owner/repo@<commit-sha>); update the three occurrences in
.github/workflows/ci.yml and the occurrences in .github/workflows/pr-opened.yml
and .github/workflows/pr-closed.yml so every actions/* reference uses a specific
commit SHA instead of the floating `@v`* tag.

In `@convex/_model/users/actions/internal/setUserDefaultAvatar.ts`:
- Line 26: The Dicebear avatar URL is built from raw user.username, which can
contain reserved URL characters; update the construction in setUserDefaultAvatar
(the avatarUrl variable) to URL-encode the seed by applying
encodeURIComponent(user.username) (or equivalent) before interpolating into the
query string so the generated URL is always valid.

In `@convex/_model/utils/createTestTournament.ts`:
- Line 42: The expression for maxCompetitors incorrectly uses operator
precedence so (competitorCount ?? useTeams) becomes the ternary condition;
update the definition of maxCompetitors to use competitorCount ?? (useTeams ? 12
: 24) so the nullish coalescing selects competitorCount when not null/undefined
and otherwise uses the ternary fallback; locate the symbol maxCompetitors in
createTestTournament (and the variables competitorCount and useTeams) and
parenthesize the ternary fallback accordingly.

In `@playwright.config.ts`:
- Around line 19-21: The Playwright webServer command in playwright.config.ts
currently hardcodes a user-specific CONVEX_DEPLOYMENT value
(CONVEX_DEPLOYMENT=local:local-ian_paschal-combat_command-1) which breaks other
developers; update the webServer command to run npx convex dev without the
personal slug (or read CONVEX_DEPLOYMENT from an environment variable like
process.env.CONVEX_DEPLOYMENT if callers need to override), and ensure the url
remains http://127.0.0.1:3210 so tests target the same local server.

In `@src/pages/TournamentPairingsPage/TournamentPairingsPage.tsx`:
- Around line 96-101: The current logic uses draftPairings.length === 0 to mean
“uninitialized”, which conflates an intentionally-empty draft with
never-initialized and breaks unsaved-change detection; change draftPairings to
an explicit optional/uninitialized state (e.g., initialize with null or a
separate boolean) and update the useEffect and any checks that now read
draftPairings.length to instead check the initializer (e.g., draftPairings ===
null) before copying existingPairings; update setDraftPairings usages and
downstream checks (including any included/excluded fallback logic) to handle the
new null/initialized empty-array distinction so clearing all slots no longer
gets treated as uninitialized.

In `@test/fixtures.ts`:
- Around line 33-38: Replace the current single-origin lookup with a scan over
all Playwright origins: iterate state.origins (each origin's localStorage array)
to locate an item whose name startsWith('__convexAuthJWT_'), call
client.setAuth(found.value) as soon as you find it, and if no such entry exists
after scanning all origins throw an Error immediately to fail fast; update
references around jwtEntry and client.setAuth to use this new logic so missing
JWTs do not silently proceed.

In `@test/tournament-pairings.spec.ts`:
- Around line 279-281: The equality check is unstable because savedPairings and
confirmedPairings are arrays of DraftTournamentPairing and are being sorted
without a comparator; add a deterministic comparator and sort both arrays with
it before comparing. Implement a comparator (used when calling
Array.prototype.sort) that compares stable pairing fields from
snapshotPairings/DraftTournamentPairing (e.g., player1Id, player2Id, tableNumber
or a joined key like `${player1Id}|${player2Id}|${table}`) and then replace
savedPairings.sort()/confirmedPairings.sort() with
savedPairings.sort(yourComparator) and confirmedPairings.sort(yourComparator)
prior to expect(...).toEqual(...).
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 190b066b-0fe6-4b0c-8d35-ffa9767273ca

📥 Commits

Reviewing files that changed from the base of the PR and between 1a2c2ee and 17d9d08.

⛔ Files ignored due to path filters (2)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (39)
  • .github/workflows/ci.yml
  • .gitignore
  • convex/_model/_test/_helpers/testUsers.ts
  • convex/_model/_test/actions/populateUsers.ts
  • convex/_model/_test/index.ts
  • convex/_model/_test/mutations/cleanUp.ts
  • convex/_model/common/errors.ts
  • convex/_model/tournamentPairings/_helpers/assignTables.test.ts
  • convex/_model/tournamentPairings/mutations/createTournamentPairings.ts
  • convex/_model/tournamentPairings/mutations/deleteTournamentPairings.ts
  • convex/_model/tournamentRegistrations/queries/getTournamentRegistrationByTournamentUser.ts
  • convex/_model/tournaments/queries/getTournamentOpenRound.ts
  • convex/_model/users/actions/internal/setUserDefaultAvatar.ts
  • convex/_model/utils/createTestTournament.ts
  • convex/_model/utils/createTestTournamentPairings.ts
  • convex/_model/utils/createTestUsers.ts
  • convex/_model/utils/index.ts
  • convex/test.ts
  • convex/utils.ts
  • package.json
  • playwright.config.ts
  • src/components/ContextMenu/ContextMenu.tsx
  • src/components/TournamentPairingGenerationForm/TournamentPairingGenerationForm.schema.ts
  • src/components/TournamentPairingGenerationForm/components/TournamentCompetitorChecklist/TournamentCompetitorChecklist.tsx
  • src/components/TournamentPairingRow/TournamentPairingRow.tsx
  • src/components/TournamentProvider/TournamentContextMenu.tsx
  • src/components/generic/Form/FormField.tsx
  • src/hooks/useFormDialog.tsx
  • src/pages/TournamentPairingsPage/TournamentPairingsPage.schema.ts
  • src/pages/TournamentPairingsPage/TournamentPairingsPage.tsx
  • src/pages/TournamentPairingsPage/TournamentPairingsPage.utils.tsx
  • src/pages/TournamentPairingsPage/components/Droppable/Droppable.tsx
  • src/pages/TournamentPairingsPage/components/PairingsGrid/PairingsGrid.hooks.ts
  • src/pages/TournamentPairingsPage/components/PairingsGrid/PairingsGrid.tsx
  • test/README.md
  • test/auth.setup.ts
  • test/fixtures.ts
  • test/global-setup.ts
  • test/tournament-pairings.spec.ts
💤 Files with no reviewable changes (4)
  • convex/_model/tournamentPairings/mutations/createTournamentPairings.ts
  • src/components/generic/Form/FormField.tsx
  • convex/_model/common/errors.ts
  • convex/_model/tournamentPairings/mutations/deleteTournamentPairings.ts

Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread convex/_model/users/actions/internal/setUserDefaultAvatar.ts Outdated
Comment thread convex/_model/utils/createTestTournament.ts Outdated
Comment thread playwright.config.ts
Comment thread src/pages/TournamentPairingsPage/TournamentPairingsPage.tsx
Comment thread test/fixtures.ts Outdated
Comment thread test/tournament-pairings.spec.ts Outdated
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: 3

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

Inline comments:
In `@convex/_model/users/actions/inviteUser.ts`:
- Around line 59-62: The username-generation loop in inviteUser.ts using
generateUsername() and ctx.runQuery(internal.users.getUserByUsername, { username
}) is unbounded; add a max-attempt guard (e.g., const MAX_USERNAME_ATTEMPTS = 10
or 100) and an attempts counter around that while loop, retrying until attempts
>= MAX_USERNAME_ATTEMPTS, and on exceeding the cap throw a clear, descriptive
error (e.g., "Failed to generate unique username after X attempts") so
inviteUser fails fast instead of hanging.
- Around line 59-62: inviteUser's current generateUsername + loop using
internal.users.getUserByUsername is race-prone because the users table only has
a non-unique index by_username and other code paths (createAccount /
createOrUpdateUser) also use query-then-insert; replace this with an atomic
reservation approach: derive a deterministic reservation key from the candidate
username, attempt a single write (e.g., insert a "username_reservations" doc or
a reservation field) that will conflict on duplicate keys, and on conflict
generate a new username and retry; update inviteUser to attempt reservation via
that atomic insert and only insert the actual user after successful reservation,
and update other creators (createAccount / createOrUpdateUser) to use the same
reservation write path so uniqueness is enforced without relying on
getUserByUsername or index('by_username').

In `@test/globalSetup.ts`:
- Around line 18-43: The cleanup invocation is vulnerable to shell injection and
may target the wrong deployment because it interpolates CONVEX_PREVIEW_NAME into
a shell string; update the globalSetup to (1) require an explicit preview target
before running destructive cleanup (throw or skip if
process.env.CONVEX_PREVIEW_NAME is missing or doesn't match the convexUrl host),
(2) stop using execSync with a single interpolated command and use a
spawn/execFile variant that accepts an argv array (e.g.,
child_process.execFileSync or spawnSync) to call "npx" with arguments
["convex","run","--preview-name", previewName, "test:cleanUp",
JSON.stringify(cleanUpArgs")] so arguments are not shell-interpolated, and (3)
pass the JSON payload as a single safe argument (the existing cleanUpArgs
variable) and similarly invoke "test:populateUsers" with argv to avoid shells;
reference ConvexHttpClient, convexUrl, process.env.CONVEX_PREVIEW_NAME,
cleanUpArgs, execSync, and the "test:cleanUp"/"test:populateUsers" commands when
making these changes.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 2dea19b7-9482-4954-a32e-d3faf1163443

📥 Commits

Reviewing files that changed from the base of the PR and between 17d9d08 and 516f5f2.

📒 Files selected for processing (17)
  • .github/workflows/ci.yml
  • .github/workflows/pr-closed.yml
  • .github/workflows/pr-opened.yml
  • convex/_model/_test/index.ts
  • convex/_model/_test/mutations/cleanUp.ts
  • convex/_model/users/actions/internal/setUserDefaultAvatar.ts
  • convex/_model/users/actions/inviteUser.ts
  • convex/_model/users/index.ts
  • convex/_model/users/queries/internal/getUserByUsername.ts
  • convex/_model/users/table.ts
  • convex/_model/utils/createTestTournament.ts
  • convex/test.ts
  • convex/users.ts
  • playwright.config.ts
  • test/fixtures.ts
  • test/globalSetup.ts
  • test/tournament-pairings.spec.ts

Comment thread convex/_model/users/actions/inviteUser.ts
Comment thread test/globalSetup.ts Outdated
Comment on lines +18 to +43
const preview = process.env.CONVEX_PREVIEW_NAME ? `--preview-name ${process.env.CONVEX_PREVIEW_NAME}` : '';

const client = new ConvexHttpClient(convexUrl!);
const { page: users } = await client.query(api.users.getUsers, {
paginationOpts: { numItems: 1, cursor: null },
});
const usersExist = users.length > 0;

const cleanUpArgs = {
preservedTables: usersExist ? [
'authAccounts',
'authSessions',
'authRefreshTokens',
'authVerificationCodes',
'authVerifiers',
'authRateLimits',
'users',
'userPreferences',
] : [],
};

execSync( `npx convex run ${preview} test:cleanUp '${JSON.stringify(cleanUpArgs)}'`, { stdio: 'inherit' });

if (!usersExist) {
execSync(`npx convex run ${preview} test:populateUsers`, { stdio: 'inherit' });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant portion of the file
ls -la test || true
sed -n '1,120p' test/globalSetup.ts | nl -ba

# Also grep for cleanup/populate invocations to see if gating exists elsewhere
rg -n "test:cleanUp|test:populateUsers|CONVEX_PREVIEW_NAME|execSync|execFileSync" -S test/globalSetup.ts test -g'*.ts' || true

# Show where convexUrl is defined (to determine whether it could be non-local)
rg -n "convexUrl" -S test/globalSetup.ts test -g'*.ts' || true

Repository: ianpaschal/combat-command

Length of output: 580


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print first ~220 lines with line numbers using awk
sed -n '1,220p' test/globalSetup.ts | awk '{printf "%5d %s\n", NR, $0}'

# Search for execSync usage and any gating related to preview/CI elsewhere
rg -n "execSync\\(|execFileSync\\(|CONVEX_PREVIEW_NAME|test:cleanUp|test:populateUsers|globalSetup" -S . -g'*.ts'

# Show where convexUrl is sourced (in case gating depends on it)
rg -n "convexUrl" -S . -g'*.ts'

Repository: ianpaschal/combat-command

Length of output: 2809


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where the CLI actions are defined
rg -n "test:cleanUp|test:populateUsers|action.*cleanUp|action.*populateUsers" -S . -g'*.ts' -g'*.js' -g'*.tsx'

# Inspect the convex actions implementation(s), if present
fd -t f "test.*(cleanUp|populateUsers)" . || true
fd -t f "cleanUp" . || true
fd -t f "populateUsers" . || true

Repository: ianpaschal/combat-command

Length of output: 726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show cleanUp implementation
if [ -f convex/_model/_test/mutations/cleanUp.ts ]; then
  echo "---- convex/_model/_test/mutations/cleanUp.ts ----"
  sed -n '1,220p' convex/_model/_test/mutations/cleanUp.ts | awk '{printf "%4d %s\n", NR, $0}'
else
  echo "Missing: convex/_model/_test/mutations/cleanUp.ts"
fi

# Show populateUsers implementation
if [ -f convex/_model/_test/actions/populateUsers.ts ]; then
  echo "---- convex/_model/_test/actions/populateUsers.ts ----"
  sed -n '1,260p' convex/_model/_test/actions/populateUsers.ts | awk '{printf "%4d %s\n", NR, $0}'
else
  echo "Missing: convex/_model/_test/actions/populateUsers.ts"
fi

# Check test scaffolding / fixtures that might show intended deployment targeting
echo "---- test/fixtures.ts (relevant bits) ----"
sed -n '1,120p' test/fixtures.ts | awk '{printf "%4d %s\n", NR, $0}'

echo "---- playwright.config.ts (relevant bits) ----"
sed -n '1,120p' playwright.config.ts | awk '{printf "%4d %s\n", NR, $0}'

Repository: ianpaschal/combat-command

Length of output: 6646


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect package.json for scripts / convex config usage
ls -la
cat package.json 2>/dev/null || true

# Search for convex config files and deployment env vars used in repo
fd -t f "convex.*" . || true
fd -t f ".env*" . || true
rg -n "CONVEX_DEPLOYMENT|CONVEX_DEPLOY_KEY|CONVEX_PREVIEW_NAME|--preview-name|convex run|convex dev" -S . -g'*.ts' -g'*.js' -g'*.json' -g'*.md' -g'*.yml' -g'*.yaml' || true

# Show any convex.json / convex.config.* if present
fd -t f "convex.json" . || true
fd -t f "convex.config.*" . || true

Repository: ianpaschal/combat-command

Length of output: 28806


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show convex config
if [ -f convex/convex.config.ts ]; then
  echo "---- convex/convex.config.ts ----"
  sed -n '1,220p' convex/convex.config.ts | awk '{printf "%4d %s\n", NR, $0}'
fi

# Show getEnvironment helper
if [ -f convex/_model/common/_helpers/getEnvironment.ts ]; then
  echo "---- convex/_model/common/_helpers/getEnvironment.ts ----"
  sed -n '1,220p' convex/_model/common/_helpers/getEnvironment.ts | awk '{printf "%4d %s\n", NR, $0}'
fi

# Show test README that may document env vars
if [ -f test/README.md ]; then
  echo "---- test/README.md ----"
  sed -n '1,200p' test/README.md | awk '{printf "%4d %s\n", NR, $0}'
fi

# Search for how CONVEX_DEPLOYMENT is used in scripts/workflows
rg -n "CONVEX_DEPLOYMENT" -S . -g'*.ts' -g'*.js' -g'*.md' -g'*.yml' -g'*.yaml' || true

# Search for CONVEX_PREVIEW_NAME usage
rg -n "CONVEX_PREVIEW_NAME" -S . -g'*.ts' -g'*.js' -g'*.md' -g'*.yml' -g'*.yaml' || true

Repository: ianpaschal/combat-command

Length of output: 2256


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "getEnvironment\\(" -S convex test -g'*.ts'
rg -n "CONVEX_DEPLOY_KEY" -S convex test -g'*.ts'

Repository: ianpaschal/combat-command

Length of output: 348


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect convex/auth.ts around the getEnvironment check
if [ -f convex/auth.ts ]; then
  sed -n '80,170p' convex/auth.ts | awk '{printf "%4d %s\n", NR+79, $0}'
else
  echo "Missing convex/auth.ts"
fi

# Inspect CI workflows for env vars related to e2e/playwright/convex
fd -t f ".github/workflows" . || true
fd -t f ".github/workflows/*" . || true
fd -t f ".github/workflows" . -e "*.yml" -e "*.yaml" || true

# Grep for playwright/e2e invocation and convex-related env vars
rg -n "playwright test|test:e2e|test:e2e:slow|CONVEX_PREVIEW_NAME|CONVEX_DEPLOY_KEY|CONVEX_DEPLOYMENT|JWT_PRIVATE_KEY|VITE_CONVEX_URL" .github/workflows -S || true

# Also search for any mention in other CI config files
rg -n "playwright test|test:e2e|CONVEX_PREVIEW_NAME|CONVEX_DEPLOY_KEY|CONVEX_DEPLOYMENT" .github -S || true

Repository: ianpaschal/combat-command

Length of output: 3927


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' .github/workflows/ci.yml | awk '{printf "%4d %s\n", NR, $0}'
sed -n '120,260p' .github/workflows/ci.yml | awk '{printf "%4d %s\n", NR, $0}'

Repository: ianpaschal/combat-command

Length of output: 2721


Harden cleanup execution: avoid shell interpolation and block destructive cleanup without an explicit preview target.

  • test/globalSetup.ts executes destructive cleanup via npx convex run ... test:cleanUp, and the cleanUp mutation deletes all non-preserved tables.
  • The current execSync builds a shell command by interpolating CONVEX_PREVIEW_NAME, which risks command injection and can target a different deployment than the one used by VITE_CONVEX_URL (since npx convex run relies on its default target when --preview-name is omitted).
Suggested patch
-import { execSync } from 'child_process';
+import { execFileSync } from 'child_process';
@@
 export default async function globalSetup(): Promise<void> {
-  const preview = process.env.CONVEX_PREVIEW_NAME ? `--preview-name ${process.env.CONVEX_PREVIEW_NAME}` : '';
+  const previewName = process.env.CONVEX_PREVIEW_NAME;
+  const previewArgs = previewName ? ['--preview-name', previewName] : [];
+  if (process.env.CI && !previewName) {
+    throw new Error('CONVEX_PREVIEW_NAME must be set in CI before running test cleanup');
+  }
+  if (!previewName && !/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/.test(convexUrl)) {
+    throw new Error('Refusing to run destructive cleanup without preview name on a non-local deployment');
+  }
@@
-  execSync( `npx convex run ${preview} test:cleanUp '${JSON.stringify(cleanUpArgs)}'`, { stdio: 'inherit' });
+  execFileSync('npx', ['convex', 'run', ...previewArgs, 'test:cleanUp', JSON.stringify(cleanUpArgs)], {
+    stdio: 'inherit',
+  });
@@
   if (!usersExist) {
-    execSync(`npx convex run ${preview} test:populateUsers`, { stdio: 'inherit' });
+    execFileSync('npx', ['convex', 'run', ...previewArgs, 'test:populateUsers'], { stdio: 'inherit' });
   }
 }
📝 Committable suggestion

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

Suggested change
const preview = process.env.CONVEX_PREVIEW_NAME ? `--preview-name ${process.env.CONVEX_PREVIEW_NAME}` : '';
const client = new ConvexHttpClient(convexUrl!);
const { page: users } = await client.query(api.users.getUsers, {
paginationOpts: { numItems: 1, cursor: null },
});
const usersExist = users.length > 0;
const cleanUpArgs = {
preservedTables: usersExist ? [
'authAccounts',
'authSessions',
'authRefreshTokens',
'authVerificationCodes',
'authVerifiers',
'authRateLimits',
'users',
'userPreferences',
] : [],
};
execSync( `npx convex run ${preview} test:cleanUp '${JSON.stringify(cleanUpArgs)}'`, { stdio: 'inherit' });
if (!usersExist) {
execSync(`npx convex run ${preview} test:populateUsers`, { stdio: 'inherit' });
}
const previewName = process.env.CONVEX_PREVIEW_NAME;
const previewArgs = previewName ? ['--preview-name', previewName] : [];
if (process.env.CI && !previewName) {
throw new Error('CONVEX_PREVIEW_NAME must be set in CI before running test cleanup');
}
if (!previewName && !/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/.test(convexUrl)) {
throw new Error('Refusing to run destructive cleanup without preview name on a non-local deployment');
}
const client = new ConvexHttpClient(convexUrl!);
const { page: users } = await client.query(api.users.getUsers, {
paginationOpts: { numItems: 1, cursor: null },
});
const usersExist = users.length > 0;
const cleanUpArgs = {
preservedTables: usersExist ? [
'authAccounts',
'authSessions',
'authRefreshTokens',
'authVerificationCodes',
'authVerifiers',
'authRateLimits',
'users',
'userPreferences',
] : [],
};
execFileSync('npx', ['convex', 'run', ...previewArgs, 'test:cleanUp', JSON.stringify(cleanUpArgs)], {
stdio: 'inherit',
});
if (!usersExist) {
execFileSync('npx', ['convex', 'run', ...previewArgs, 'test:populateUsers'], { stdio: 'inherit' });
}
🧰 Tools
🪛 OpenGrep (1.21.0)

[ERROR] 39-39: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 42-42: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

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

In `@test/globalSetup.ts` around lines 18 - 43, The cleanup invocation is
vulnerable to shell injection and may target the wrong deployment because it
interpolates CONVEX_PREVIEW_NAME into a shell string; update the globalSetup to
(1) require an explicit preview target before running destructive cleanup (throw
or skip if process.env.CONVEX_PREVIEW_NAME is missing or doesn't match the
convexUrl host), (2) stop using execSync with a single interpolated command and
use a spawn/execFile variant that accepts an argv array (e.g.,
child_process.execFileSync or spawnSync) to call "npx" with arguments
["convex","run","--preview-name", previewName, "test:cleanUp",
JSON.stringify(cleanUpArgs")] so arguments are not shell-interpolated, and (3)
pass the JSON payload as a single safe argument (the existing cleanUpArgs
variable) and similarly invoke "test:populateUsers" with argv to avoid shells;
reference ConvexHttpClient, convexUrl, process.env.CONVEX_PREVIEW_NAME,
cleanUpArgs, execSync, and the "test:cleanUp"/"test:populateUsers" commands when
making these changes.

@ianpaschal ianpaschal merged commit ab7b482 into ian/cc-2-improve-pairing-workflow May 22, 2026
4 checks passed
@ianpaschal ianpaschal deleted the ian/cc-64-add-first-playwright-tests branch May 22, 2026 17:38
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