Skip to content

Add grading assignment defaults and reminder automation#722

Open
jon-bell wants to merge 17 commits into
stagingfrom
cursor-grading-defaults-reminders-fb5d
Open

Add grading assignment defaults and reminder automation#722
jon-bell wants to merge 17 commits into
stagingfrom
cursor-grading-defaults-reminders-fb5d

Conversation

@jon-bell
Copy link
Copy Markdown
Contributor

@jon-bell jon-bell commented Apr 18, 2026

Summary

  • address all current PR feedback for grading defaults/reminders by hardening form behavior, migration safety, and concurrency semantics
  • prevent assignment edit form value clobbering by seeding grading automation fields only during initial reset(...) and keeping self-review effect scoped to self-review fields
  • stabilize profile application in assignment create form via memoized profile map, switch profile select values to string-safe bindings, and continue honoring manual overrides when auto-apply is disabled
  • harden grading defaults management page with invalid-course guardrails, delete confirmation, and generated database type usage without duplicated row-shape declarations
  • update assignment create payload fallbacks to use nullish semantics and a single remindersEnabled read
  • strengthen migration safety: class-scoped profile FK, RLS/policy for automation state table, overdue-only reminder recipient filtering, atomic FOR UPDATE state claim in cron runner, auto-assignment completion gating on function result, and pg_cron schedule exception handling
  • fix auto-assignment retry semantics for misconfiguration branches: missing rubric, missing instructor actor, and empty assignee pool now emit warnings and return -1 so auto_assigned_at remains unset and cron retries after configuration is corrected
  • add and extend targeted Playwright E2E coverage in tests/e2e/grading-assignment-defaults.test.tsx

Targeted E2E Coverage Added

tests/e2e/grading-assignment-defaults.test.tsx now covers two workflow tracks:

  1. Profile management + assignment create/edit wiring

    • create grading default profile with auto-assign/reminder settings
    • verify delete confirmation prompt and cancel behavior
    • apply saved profile on assignment create form and assert fields populate
    • assert manual override preservation when "Apply profile settings on selection" is disabled
    • assert re-apply behavior when toggle is re-enabled
    • verify edit form hydration and DB persistence after saving updated values
  2. Deadline automation execution path (submissions → deadline hit → assignment + reminders)

    • seed lab leaders and students in distinct lab sections
    • create assignment + seed submissions for those students
    • set assignment due date in the past and enable automation settings
    • trigger run_assignment_grading_automation()
    • assert review assignments are created for the seeded submissions and assigned to grader profiles (lab leaders in this setup)
    • assert automation state row was updated (auto_assigned_at, last_reminder_sent_at, recipient count)
    • assert reminder emails were queued with expected reply_to, cc_emails, and intended recipient users

Validation

  • npm run format
  • npx tsc --noEmit
  • npx eslint tests/e2e/grading-assignment-defaults.test.tsx
  • attempted: TMPDIR=/dev/shm npx playwright test tests/e2e/grading-assignment-defaults.test.tsx --project=chromium --reporter=line --output=/dev/shm/pw-results --workers=1

Notes

  • local Playwright execution remains environment-blocked because the local Supabase DB container repeatedly exits during initdb with No space left on device (ENOSPC), which prevents test fixture creation (createClass) and end-to-end runtime execution in this VM.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Instructor-only "Grading Assignment Defaults" nav entry and management page for reusable grading profiles.
    • Assignment create/edit UI: profile selector with optional "apply on select", auto-assign-at-deadline, assignee-pool and optional grader subset, review-due offset, and late-grading reminder controls (enable, interval, reply-to, CC) with input normalization.
    • Background automation: auto-assign reviews (deadline & submission-triggered) and scheduled late-grading reminder queuing/sending.
  • Tests

    • Expanded E2E suite covering profile CRUD, form integration/apply-on-select, auto-assignment scenarios, reminder cadence, idempotency, and queued email verification.

Co-authored-by: Jonathan Bell <jon@jonbell.net>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Warning

Rate limit exceeded

@jon-bell has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 25 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a711db40-3ec7-459e-9ca5-e47ab76eb86e

📥 Commits

Reviewing files that changed from the base of the PR and between e3215bb and 3e1ae94.

📒 Files selected for processing (7)
  • app/course/[course_id]/dynamicCourseNav.tsx
  • app/course/[course_id]/manage/assignments/new/form.tsx
  • app/course/[course_id]/manage/assignments/new/page.tsx
  • supabase/functions/_shared/SupabaseTypes.d.ts
  • tests/e2e/grading-assignment-defaults.test.tsx
  • utils/supabase/DatabaseTypes.d.ts
  • utils/supabase/SupabaseTypes.d.ts

Walkthrough

Adds class-scoped grading-assignment default profiles and per-assignment overrides, DB schema and RPCs for auto-assignment/late-reminder scheduling, UI to manage/apply profiles and form integration, TypeScript Supabase type updates, and E2E tests validating automation and email queuing.

Changes

Grading automation (single cohesive change DAG)

Layer / File(s) Summary
DB schema & migrations
supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql
Create grading_assignment_default_profiles and assignment_grading_automation_state; add grading-automation & reminder columns to assignments; constraints, RLS, triggers, and pg_cron schedule.
DB functions (RPCs)
public.auto_assign_grading_reviews_for_assignment, public.queue_late_grading_reminders_for_assignment, public.run_assignment_grading_automation (in migration)
Add SECURITY DEFINER RPCs to perform deadline auto-assignment, queue late-grading reminder emails, and a runner that iterates due assignments and upserts automation state.
Supabase server/client types
supabase/functions/_shared/SupabaseTypes.d.ts, utils/supabase/SupabaseTypes.d.ts, utils/supabase/DatabaseTypes.d.ts
Add table typings for new tables, extend assignments Row/Insert/Update with automation/reminder fields, add RPC TypeScript signatures and related relationship changes.
Assignment form model & helpers
app/course/[course_id]/manage/assignments/new/form.tsx
Introduce exported AssignmentFormValues type; add helpers normalizeCcEmails, numberInputValueAs, normalizeProfileIdSubset; update many subforms to use AssignmentFormValues; add GradingAutomationSubform rendering profile selector, apply-on-select behavior, auto-assign and late-reminder controls.
Assignment pages wiring (create/edit)
app/course/[course_id]/manage/assignments/new/page.tsx, app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx
Switch useForm generic to AssignmentFormValues, initialize defaultValues for automation fields, normalize reset payload from query data, extend create/update payloads to include grading-default, auto-assign, and late-reminder fields, and ensure normalization on submit.
Grading defaults management UI
app/course/[course_id]/manage/course/grading-assignment-defaults/page.tsx
Add page component to list/create/edit/delete class-scoped grading default profiles with react-hook-form, Refine useList/useCreate/useUpdate/useDelete, CC parsing/serialization, validation, and deletion safety check against assignments.
Navigation
app/course/[course_id]/dynamicCourseNav.tsx
Add “Grading Assignment Defaults” entry under Course Settings (instructors only) linking to /course/${courseID}/manage/course/grading-assignment-defaults.
E2E tests
tests/e2e/grading-assignment-defaults.test.tsx
Add Playwright tests: profile CRUD, apply-on-select behavior in assignment create/edit, RPC execution that auto-assigns reviews, and queued late-reminder email assertions.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser as Client
    participant AssignmentForm
    participant GradingAutomationSubform
    participant RefineAPI
    participant Database
    rect rgba(120,160,200,0.5)
    User->>Browser: Open assignment create/edit page
    Browser->>AssignmentForm: Render form
    AssignmentForm->>GradingAutomationSubform: Render profile selector & controls
    GradingAutomationSubform->>RefineAPI: List grading default profiles
    RefineAPI->>Database: SELECT grading_assignment_default_profiles
    Database-->>RefineAPI: Return profiles
    RefineAPI-->>GradingAutomationSubform: Profiles loaded
    User->>GradingAutomationSubform: Select profile / toggle apply
    GradingAutomationSubform->>AssignmentForm: setValue(apply profile values)
    User->>AssignmentForm: Submit
    AssignmentForm->>AssignmentForm: Normalize grading fields
    AssignmentForm->>RefineAPI: Create/Update assignment (include grading fields)
    RefineAPI->>Database: INSERT/UPDATE assignments row
    Database-->>RefineAPI: Confirm
    RefineAPI-->>AssignmentForm: Success
    AssignmentForm-->>User: Saved
    end
Loading
sequenceDiagram
    participant Scheduler as pg_cron
    participant DB
    participant Runner as run_assignment_grading_automation
    participant AutoAssign as auto_assign_grading_reviews_for_assignment
    participant QueueEmails as queue_late_grading_reminders_for_assignment
    participant bulkAssign as bulk_assign_reviews
    rect rgba(200,160,120,0.5)
    Scheduler->>DB: Trigger run_assignment_grading_automation()
    DB->>Runner: Execute
    Runner->>DB: Find due/non-archived assignments with automation enabled
    alt Auto-assign needed
        Runner->>AutoAssign: Call with assignment_id
        AutoAssign->>DB: Select submissions & eligible staff
        AutoAssign->>bulkAssign: Call bulk_assign_reviews(...)
        bulkAssign-->>AutoAssign: Assigned count
        AutoAssign-->>Runner: Return count
    end
    alt Late reminders needed
        Runner->>QueueEmails: Call with assignment_id
        QueueEmails->>DB: Find recipients, insert emails & batch
        DB-->>QueueEmails: Return queued count
        QueueEmails-->>Runner: Return count
    end
    Runner->>DB: Upsert automation state (timestamps/counts)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

enhancement, backend, database, frontend, testing

Poem

Profiles saved where instructors decide,
Cron hums softly, reminders glide,
Forms tidy defaults, subsets pruned,
Assignments routed, emails queued,
A little automation, grading simplified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature additions: grading assignment defaults and reminder automation. It directly reflects the core changes across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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.

Co-authored-by: Jonathan Bell <jon@jonbell.net>
Co-authored-by: Jonathan Bell <jon@jonbell.net>
@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented Apr 18, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ⚠️ Changes detected (Review) 20 changed, 6 removed, 4 failures May 22, 2026, 8:48 PM

@jon-bell jon-bell marked this pull request as ready for review April 18, 2026 22:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (1)
app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx (1)

37-75: ⚠️ Potential issue | 🟠 Major

Edits to grading-automation fields can be clobbered when the self-review query resolves.

This effect depends on selfReviewSetting?.data.*, so it re-runs when the self-review query resolves — which is typically after the initial reset(queryData) in the effect above. Whenever that happens, all nine form.setValue(...) calls here re-fire and re-seed the grading-automation fields from queryData, overwriting any edits the user made in the meantime. The previous code only set three self-review fields, so the race was benign; now that seven auto-assign/reminder fields are driven from this same effect, the blast radius is much larger (e.g., a user toggles "Auto assign at deadline" off right after the page loads, and it flips back on when selfReviewSetting lands).

Two reasonable options:

  1. Move the grading-automation seeding into the first effect (which only depends on queryData/reset) by passing the full shape to reset(...).
  2. Guard this effect so the grading-automation branch only runs once per queryData load (e.g., a ref or a separate effect keyed only on queryData).
🛡️ Sketch of option 1
 useEffect(() => {
   if (queryData) {
-    reset(queryData);
+    const qd = queryData as AssignmentFormValues;
+    reset({
+      ...qd,
+      grading_default_profile_id: qd.grading_default_profile_id ?? null,
+      auto_assign_at_deadline: qd.auto_assign_at_deadline ?? false,
+      auto_assign_assignee_pool: qd.auto_assign_assignee_pool ?? "graders",
+      auto_assign_review_due_hours: qd.auto_assign_review_due_hours ?? 72,
+      late_grading_reminders_enabled: qd.late_grading_reminders_enabled ?? false,
+      late_grading_reminder_interval_hours: qd.late_grading_reminder_interval_hours ?? 12,
+      late_grading_reply_to: qd.late_grading_reply_to ?? null,
+      late_grading_cc_emails: qd.late_grading_cc_emails ?? { emails: [] }
+    });
   }
 }, [queryData, reset]);

Then drop the grading-automation setValue calls from the self-review effect.

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

In `@app/course/`[course_id]/manage/assignments/[assignment_id]/edit/page.tsx
around lines 37 - 75, The effect that reads selfReviewSetting is re-seeding
grading-automation fields (multiple form.setValue calls) whenever
selfReviewSetting resolves and clobbers user edits; move the grading-automation
seeding into the initial queryData-driven reset so those fields are set only
when queryData loads (i.e., include grading-automation keys in the object passed
to reset(queryData) in the earlier effect that handles queryData) and remove the
grading-automation form.setValue calls from the selfReviewSetting useEffect;
alternatively, if you prefer minimal change, guard the selfReviewSetting
useEffect with a ref or a secondary flag keyed only on queryData so the
grading-automation branch runs at most once per queryData load.
🧹 Nitpick comments (4)
app/course/[course_id]/manage/assignments/new/form.tsx (2)

43-70: Duplicate local type — prefer the newly-added generated type from DatabaseTypes.d.ts.

GradingAssignmentDefaultProfile is now exported from @/utils/supabase/DatabaseTypes. Re-declaring a hand-maintained shape here (and again in grading-assignment-defaults/page.tsx) invites drift as columns are added/renamed. Swap to the generated alias and keep the narrow GradingAssigneePool/GradingCcEmails helpers if they're handy for form typing.

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

In `@app/course/`[course_id]/manage/assignments/new/form.tsx around lines 43 - 70,
The local GradingAssignmentDefaultProfile type declaration duplicates the
generated type and should be replaced with an import from the generated
DatabaseTypes; update this file's type references (the
GradingAssignmentDefaultProfile mentioned above and any usages inside
AssignmentFormValues) to use the exported alias from
"@/utils/supabase/DatabaseTypes" instead of re-declaring it, remove the
duplicate declarations here and in grading-assignment-defaults/page.tsx, and
keep/continue using the narrow helper types GradingAssigneePool and
GradingCcEmails where needed for form typing.

666-679: Minor: controlled <select> value should be coerced to string.

value={selectedProfileId ?? ""} passes a number (or "") to the DOM <select>. React tolerates this but the matching <option value={profile.id}> also yields a number, and under Strict Mode hydration with mismatched types you can see "Selected options mismatch" warnings. Cast both sides to string (String(profile.id) and String(selectedProfileId)) for consistency, and parse back in the onChange. Not breaking, just tidier.

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

In `@app/course/`[course_id]/manage/assignments/new/form.tsx around lines 666 -
679, The select is passing mixed types—number and ""—causing potential hydration
warnings; update the value prop on NativeSelectField to use
String(selectedProfileId ?? "") and set each option's value to
String(profile.id) so both sides are strings, and in the onChange handler parse
the selected string back to a number (or null) before calling
setValue("grading_default_profile_id", ...); locate symbols: NativeSelectField,
selectedProfileId, profiles, onChange, and setValue to make these changes.
app/course/[course_id]/manage/course/grading-assignment-defaults/page.tsx (1)

31-45: Prefer the generated type over a hand-rolled duplicate.

This PR adds GradingAssignmentDefaultProfile to utils/supabase/DatabaseTypes.d.ts; importing from there keeps this page in lockstep with the database schema (and catches future column additions/renames at compile time). The local GradingAssigneePool/GradingCcEmails aliases can stay as narrowed helpers, but the row shape shouldn't be maintained in two places.

♻️ Proposed refactor
+import type { GradingAssignmentDefaultProfile as DbGradingAssignmentDefaultProfile } from "@/utils/supabase/DatabaseTypes";
+
 type GradingAssigneePool = "graders" | "instructors" | "instructors_and_graders";
 type GradingCcEmails = { emails: string[] };

-type GradingAssignmentDefaultProfile = {
-  id: number;
-  class_id: number;
-  name: string;
-  description: string | null;
-  auto_assign_at_deadline: boolean;
-  auto_assign_assignee_pool: GradingAssigneePool;
-  auto_assign_review_due_hours: number;
-  late_grading_reminders_enabled: boolean;
-  late_grading_reminder_interval_hours: number | null;
-  late_grading_reply_to: string | null;
-  late_grading_cc_emails: GradingCcEmails;
-};
+type GradingAssignmentDefaultProfile = DbGradingAssignmentDefaultProfile;

(Same suggestion applies to the duplicate block in app/course/[course_id]/manage/assignments/new/form.tsx.)

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

In `@app/course/`[course_id]/manage/course/grading-assignment-defaults/page.tsx
around lines 31 - 45, Replace the locally defined
GradingAssignmentDefaultProfile with the generated DB row type from
utils/supabase/DatabaseTypes.d.ts and import that type; keep the local
GradingAssigneePool and GradingCcEmails helpers as-is, then redefine FormValues
as Omit<ImportedGradingAssignmentDefaultProfile, "id" | "class_id"> so the page
(and the similar block in the new assignment form) stays synchronized with the
database schema.
app/course/[course_id]/manage/assignments/new/page.tsx (1)

115-124: Minor: prefer ?? over || for nullable coercions and hoist repeated getValues reads.

|| null coerces falsy but valid values (e.g., a numeric 0) to null. While profile ids and hour counts aren't expected to be 0, ?? more accurately expresses "fallback only on null/undefined" and is idiomatic for this kind of payload construction. Also, late_grading_reminders_enabled is read three times here — grab it once into a local so the ternary is clearer and you can't accidentally de-sync the branches.

♻️ Proposed refactor
+            grading_default_profile_id: form.getValues("grading_default_profile_id") ?? null,
-            grading_default_profile_id: form.getValues("grading_default_profile_id") || null,
-            auto_assign_at_deadline: form.getValues("auto_assign_at_deadline") || false,
-            auto_assign_assignee_pool: form.getValues("auto_assign_assignee_pool") || "graders",
+            auto_assign_at_deadline: form.getValues("auto_assign_at_deadline") ?? false,
+            auto_assign_assignee_pool: form.getValues("auto_assign_assignee_pool") ?? "graders",
             auto_assign_review_due_hours: form.getValues("auto_assign_review_due_hours") ?? 72,
-            late_grading_reminders_enabled: form.getValues("late_grading_reminders_enabled") || false,
-            late_grading_reminder_interval_hours: form.getValues("late_grading_reminders_enabled")
-              ? (form.getValues("late_grading_reminder_interval_hours") ?? 12)
-              : null,
-            late_grading_reply_to: form.getValues("late_grading_reply_to") || null,
+            late_grading_reminders_enabled: remindersEnabled,
+            late_grading_reminder_interval_hours: remindersEnabled
+              ? (form.getValues("late_grading_reminder_interval_hours") ?? 12)
+              : null,
+            late_grading_reply_to: form.getValues("late_grading_reply_to") || null,
             late_grading_cc_emails: form.getValues("late_grading_cc_emails") ?? { emails: [] }

(with const remindersEnabled = form.getValues("late_grading_reminders_enabled") ?? false; hoisted just above the insert.)

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

In `@app/course/`[course_id]/manage/assignments/new/page.tsx around lines 115 -
124, The object construction uses || which wrongly treats falsy-but-valid values
as absent and repeatedly calls form.getValues("late_grading_reminders_enabled");
change each fallback to null/typed defaults using the nullish coalescing
operator (??) for fields like grading_default_profile_id,
auto_assign_at_deadline, auto_assign_assignee_pool,
auto_assign_review_due_hours, late_grading_reply_to, and late_grading_cc_emails,
and hoist form.getValues("late_grading_reminders_enabled") into a local constant
(e.g., const remindersEnabled = form.getValues("late_grading_reminders_enabled")
?? false) so you use remindersEnabled in the ternary for
late_grading_reminder_interval_hours and the boolean field
late_grading_reminders_enabled to avoid repeated reads and ensure correct
null/undefined fallback semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/course/`[course_id]/manage/assignments/new/form.tsx:
- Around line 617-651: The profileMap is rebuilt on every render causing the
useEffect that applies profile settings to re-run and overwrite user edits;
memoize profiles and profileMap using React's useMemo (import useMemo) so
profileMap is stable, and update the useEffect dependency array to remove
profileMap and include only selectedProfileId, profileExists,
applyProfileOnSelect, setValue (and any stable helpers like normalizeCcEmails if
needed); locate symbols profiles, profileMap, selectedProfileId,
applyProfileOnSelect, setValue, and the useEffect block and implement useMemo
around profiles.reduce to prevent continuous re-application.

In `@app/course/`[course_id]/manage/course/grading-assignment-defaults/page.tsx:
- Around line 68-94: GradingAssignmentDefaultsPage currently converts course_id
to classId with Number(course_id) and only gates useList with
Number.isFinite(classId); you must also guard form submission and payload
creation (onSubmit and any create/update calls that include class_id) so they
never send class_id: NaN. Fix by validating classId early in the component
(e.g., if (!Number.isFinite(classId)) return a small "Invalid course" message or
render the form disabled) and add a runtime check in the submit handler
(onSubmit) to abort if classId is not finite before building create/update
payloads, ensuring all references to classId in payloads use the validated
value.
- Around line 163-177: The handleDelete function currently calls deleteProfile
immediately; add a user confirmation step before invoking deleteProfile (e.g.,
use a Chakra Dialog or a window.confirm prompt) so destructive deletes are
gated—update the handleDelete handler to first show/await the confirmation, only
call deleteProfile({ resource: "grading_assignment_default_profiles", id }) and
proceed with toaster.success, clearForm (if editingId === id), and refetch()
when the user confirms, and abort with no-op when the user cancels.

In
`@supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql`:
- Around line 297-300: The code sets auto_assigned_at even when
bulk_assign_reviews failed; change the flow so auto_assigned_at is only set when
bulk_assign_reviews succeeded by checking (v_result->>'success')::boolean (same
check used with v_result and v_assignment) before updating the assignment.
Concretely, move or guard the update that writes auto_assigned_at (the block
that sets auto_assigned_at for v_assignment) so it executes only on success of
bulk_assign_reviews (i.e., when COALESCE((v_result->>'success')::boolean, false)
IS TRUE), and ensure the RETURN 0 path on failure does not reach the
auto_assigned_at update. Ensure references to bulk_assign_reviews, v_result,
v_assignment, and auto_assigned_at are used to locate and modify the logic.
- Around line 352-365: The reminder_recipients CTE currently selects all
incomplete review_assignments (ra) regardless of whether the individual review's
p_due_date is past; to only remind graders for overdue reviews, add a condition
in the WHERE of the CTE to filter by the review due datetime (e.g. AND
COALESCE(ra.p_due_date, v_assignment.due_date) < now() or simply AND
ra.p_due_date < now() if p_due_date is always set). Update the WHERE clause in
the reminder_recipients query (referencing reminder_recipients,
review_assignments/ra, and v_assignment) to include that overdue check so
auto-assigned reviews with future p_due_date are not included.
- Around line 414-456: The loop currently reads stale LEFT JOIN values then
upserts and performs side effects, allowing races; after the INSERT ... ON
CONFLICT (assignment_id) DO NOTHING into
public.assignment_grading_automation_state, re-select the state row for the
current v_assignment.id with SELECT ... FROM
public.assignment_grading_automation_state WHERE assignment_id = v_assignment.id
FOR UPDATE to atomically claim the work and refresh fields (auto_assigned_at,
last_reminder_sent_at, last_reminder_recipient_count) into local variables
before calling public.auto_assign_grading_reviews_for_assignment or
public.queue_late_grading_reminders_for_assignment so the subsequent IF checks
use the locked, up-to-date state to prevent duplicate work.
- Line 34: The foreign key on the assignment default profile currently only
references profiles.id, allowing cross-class profile links; update the FK to be
class-scoped by replacing the single-column FK on profile_id with a composite FK
(class_id, profile_id) that references profiles(class_id, id) (and ensure
profiles has class_id), and apply the same change for the other occurrence
(lines 104–108) so both constraints enforce tenant isolation; locate the
constraint(s) named or in the table(s) created in this migration (e.g.,
grading_assignment_default_profiles and the other similar constraint block) and
modify the reference to use (class_id, profile_id) -> profiles(class_id, id).
- Around line 154-162: The table assignment_grading_automation_state is created
without RLS which may allow clients to read/mutate automation timestamps; enable
row-level security on assignment_grading_automation_state and add restrictive
policies (e.g., enable RLS with ALTER TABLE ... ENABLE ROW LEVEL SECURITY and
then create policies that only allow access from server-side roles or functions
such as the supabase service_role or specific authenticated backend role) and
remove any PUBLIC grants so only the intended server/service roles and trusted
DB functions can SELECT/INSERT/UPDATE/DELETE; reference the table name
assignment_grading_automation_state and ensure policies explicitly permit only
the server/service context used by your backend.
- Around line 473-484: Wrap the pg_cron scheduling/unscheduling calls inside an
exception handler so failures don't abort the migration: inside the existing DO
$$ block surrounding the checks for pg_extension and cron.job, add a BEGIN ...
EXCEPTION WHEN OTHERS THEN ... END block around the PERFORM cron.unschedule(...)
and PERFORM cron.schedule(...) calls (the cron.unschedule and cron.schedule
invocations are the unique symbols to modify) and in the EXCEPTION branch log or
RAISE NOTICE with the error message so the migration continues when scheduling
fails due to privileges or misconfiguration.

In `@utils/supabase/SupabaseTypes.d.ts`:
- Around line 1016-1144: The PR contains manual edits to the auto-generated
Supabase types (e.g., assignment_grading_automation_state and
grading_assignment_default_profiles) — instead of hand-editing
SupabaseTypes.d.ts, revert your manual changes and regenerate the file by
running the local client generator (run npm run client-local against the current
DB/schema), verify the regenerated SupabaseTypes.d.ts includes the intended
table entries, and commit the regenerated file so the auto-generated types
remain in sync.

---

Outside diff comments:
In `@app/course/`[course_id]/manage/assignments/[assignment_id]/edit/page.tsx:
- Around line 37-75: The effect that reads selfReviewSetting is re-seeding
grading-automation fields (multiple form.setValue calls) whenever
selfReviewSetting resolves and clobbers user edits; move the grading-automation
seeding into the initial queryData-driven reset so those fields are set only
when queryData loads (i.e., include grading-automation keys in the object passed
to reset(queryData) in the earlier effect that handles queryData) and remove the
grading-automation form.setValue calls from the selfReviewSetting useEffect;
alternatively, if you prefer minimal change, guard the selfReviewSetting
useEffect with a ref or a secondary flag keyed only on queryData so the
grading-automation branch runs at most once per queryData load.

---

Nitpick comments:
In `@app/course/`[course_id]/manage/assignments/new/form.tsx:
- Around line 43-70: The local GradingAssignmentDefaultProfile type declaration
duplicates the generated type and should be replaced with an import from the
generated DatabaseTypes; update this file's type references (the
GradingAssignmentDefaultProfile mentioned above and any usages inside
AssignmentFormValues) to use the exported alias from
"@/utils/supabase/DatabaseTypes" instead of re-declaring it, remove the
duplicate declarations here and in grading-assignment-defaults/page.tsx, and
keep/continue using the narrow helper types GradingAssigneePool and
GradingCcEmails where needed for form typing.
- Around line 666-679: The select is passing mixed types—number and ""—causing
potential hydration warnings; update the value prop on NativeSelectField to use
String(selectedProfileId ?? "") and set each option's value to
String(profile.id) so both sides are strings, and in the onChange handler parse
the selected string back to a number (or null) before calling
setValue("grading_default_profile_id", ...); locate symbols: NativeSelectField,
selectedProfileId, profiles, onChange, and setValue to make these changes.

In `@app/course/`[course_id]/manage/assignments/new/page.tsx:
- Around line 115-124: The object construction uses || which wrongly treats
falsy-but-valid values as absent and repeatedly calls
form.getValues("late_grading_reminders_enabled"); change each fallback to
null/typed defaults using the nullish coalescing operator (??) for fields like
grading_default_profile_id, auto_assign_at_deadline, auto_assign_assignee_pool,
auto_assign_review_due_hours, late_grading_reply_to, and late_grading_cc_emails,
and hoist form.getValues("late_grading_reminders_enabled") into a local constant
(e.g., const remindersEnabled = form.getValues("late_grading_reminders_enabled")
?? false) so you use remindersEnabled in the ternary for
late_grading_reminder_interval_hours and the boolean field
late_grading_reminders_enabled to avoid repeated reads and ensure correct
null/undefined fallback semantics.

In `@app/course/`[course_id]/manage/course/grading-assignment-defaults/page.tsx:
- Around line 31-45: Replace the locally defined GradingAssignmentDefaultProfile
with the generated DB row type from utils/supabase/DatabaseTypes.d.ts and import
that type; keep the local GradingAssigneePool and GradingCcEmails helpers as-is,
then redefine FormValues as Omit<ImportedGradingAssignmentDefaultProfile, "id" |
"class_id"> so the page (and the similar block in the new assignment form) stays
synchronized with the database schema.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8dc8c29c-c4d3-4057-a337-f2dca088f751

📥 Commits

Reviewing files that changed from the base of the PR and between 801869b and f97abc4.

📒 Files selected for processing (9)
  • app/course/[course_id]/dynamicCourseNav.tsx
  • app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx
  • app/course/[course_id]/manage/assignments/new/form.tsx
  • app/course/[course_id]/manage/assignments/new/page.tsx
  • app/course/[course_id]/manage/course/grading-assignment-defaults/page.tsx
  • supabase/functions/_shared/SupabaseTypes.d.ts
  • supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql
  • utils/supabase/DatabaseTypes.d.ts
  • utils/supabase/SupabaseTypes.d.ts

Comment thread app/course/[course_id]/manage/assignments/new/form.tsx Outdated
Comment thread supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql Outdated
Comment thread supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql Outdated
Comment thread utils/supabase/SupabaseTypes.d.ts Outdated
Co-authored-by: Jonathan Bell <jon@jonbell.net>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (9)
app/course/[course_id]/manage/course/grading-assignment-defaults/page.tsx (3)

130-182: Referenced assignments aren't surfaced on delete.

handleDelete prompts before useDelete, but the migration defines the FK with ON DELETE SET NULL, meaning silently detaching the profile from any assignment currently pointing at it. That's probably desired behaviour, but instructors might be surprised to see their assignment forms show "No profile selected" the next morning. Consider showing the referenced assignment count in the confirm (or post-delete toast) — a cheap useList with filter grading_default_profile_id eq id before confirmation would do it.

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

In `@app/course/`[course_id]/manage/course/grading-assignment-defaults/page.tsx
around lines 130 - 182, The delete flow currently prompts and calls useDelete
(handleDelete) without surfacing how many assignments reference the profile (FK
ON DELETE SET NULL) so instructors are surprised when assignments lose the
profile; before showing the confirmation (in handleDelete) call useList (or the
existing list hook) with filter "grading_default_profile_id eq {id}" to count
referencing assignments and include that count in the confirm message (or, if
you prefer, fetch after delete and show a post-delete toaster with the count),
ensuring you reference the profile id passed to handleDelete and keep behavior
with useDelete unchanged.

55-76: Duplicate helpers — import from the form module instead.

normalizeCcEmails here is a copy of the one in app/course/[course_id]/manage/assignments/new/form.tsx. If the CC-shape contract ever evolves (multi-field, display name + address, etc.), having two definitions will drift. Easy win to export from form.tsx and import both here and in the assignment pages.

As per coding guidelines, use the @/* path alias when resolving the import path (e.g. @/app/course/[course_id]/manage/assignments/new/form).

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

In `@app/course/`[course_id]/manage/course/grading-assignment-defaults/page.tsx
around lines 55 - 76, The file duplicates the normalizeCcEmails helper (and
related parseCcEmails) already defined in form.tsx; remove the local definitions
(normalizeCcEmails and parseCcEmails) and instead export the canonical helper(s)
from app/course/[course_id]/manage/assignments/new/form.tsx and import them into
this page using the `@/`* path alias (e.g. import { normalizeCcEmails,
parseCcEmails } from '@/app/course/[course_id]/manage/assignments/new/form').
Ensure the exported names match the originals and update any call sites in this
file to use the imported functions.

184-203: window.confirm blocks the main thread and isn't stylable.

Functionally fine and addresses the prior destructive-action feedback. For parity with the rest of the app's Chakra surface, a Dialog would render inside the SSR/hydration boundary and is easier to e2e-test than a native browser prompt. Not a blocker.

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

In `@app/course/`[course_id]/manage/course/grading-assignment-defaults/page.tsx
around lines 184 - 203, Replace the blocking window.confirm call in handleDelete
with a Chakra UI Dialog: create a local state like pendingDeleteId (or
pendingDeleteProfile) and open the Dialog when user initiates delete (instead of
calling handleDelete directly), move the destructive call into a confirm handler
(e.g., confirmDelete which calls deleteProfile, toaster, clearForm/refetch and
closes the dialog) and add a cancel handler that clears pendingDeleteId to close
the dialog; update UI buttons that currently call handleDelete(id) to instead
set pendingDeleteId = id and render the Dialog with accessible title/description
and Confirm/Cancel buttons wired to the confirmDelete and cancel handlers so
deletion is non-blocking and stylable while preserving the existing
error/toaster/refetch behavior found in handleDelete.
app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx (2)

29-40: Normalize late_grading_cc_emails through the shared helper on load.

values.late_grading_cc_emails ?? { emails: [] } trusts the DB payload. The check constraint guarantees { emails: [...] } shape, but element types aren't constrained to strings (JSONB arrays can mix types), and the form's GradingCcEmails is typed as string[]. form.tsx exports normalizeCcEmails for precisely this reason and is already used both in the profile-apply effect there and in the defaults page. Using it here keeps the edit-form input consistent with the create path.

♻️ Suggested fix
-import AssignmentForm, { AssignmentFormValues } from "../../new/form";
+import AssignmentForm, { AssignmentFormValues, normalizeCcEmails } from "../../new/form";
@@
-        late_grading_cc_emails: values.late_grading_cc_emails ?? { emails: [] }
+        late_grading_cc_emails: normalizeCcEmails(values.late_grading_cc_emails)

(Requires exporting normalizeCcEmails from app/course/[course_id]/manage/assignments/new/form.tsx.)

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

In `@app/course/`[course_id]/manage/assignments/[assignment_id]/edit/page.tsx
around lines 29 - 40, The edit page currently sets late_grading_cc_emails
directly from the DB payload (values.late_grading_cc_emails ?? { emails: [] })
without normalizing types; import and call the shared normalizeCcEmails used in
form.tsx (export it if not already exported) and pass
values.late_grading_cc_emails through normalizeCcEmails before calling reset
(i.e., use normalizeCcEmails(values.late_grading_cc_emails) or fallback to
normalizeCcEmails({ emails: [] })) so the form's GradingCcEmails input always
receives a string[] shape.

62-134: useCallback dep list references form.refineCore without form.

React-Hooks exhaustive-deps will flag this — form is accessed via form.refineCore.onFinish(values) inside the callback, but only form.refineCore is listed. Since form is stable from useForm() anyway, adding it (and dropping form.refineCore) is the idiomatic fix. Non-blocking.

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

In `@app/course/`[course_id]/manage/assignments/[assignment_id]/edit/page.tsx
around lines 62 - 134, The callback passed to useCallback references
form.refineCore.onFinish but the dependency array includes form.refineCore
instead of form, which triggers exhaustive-deps; update the dependency array for
the onFinish callback to include form (and remove form.refineCore) so it depends
on the stable form object returned by useForm(), keeping other deps
(assignment_id, course_id, data?.data.self_review_setting_id, update) unchanged.
app/course/[course_id]/manage/assignments/new/form.tsx (2)

787-795: valueAsNumber on an optional hour field produces NaN, not null.

When late_grading_reminders_enabled is toggled on, then off, then on again, an empty input coerces to NaN. The create/edit pages normalize to ?? 12 before submit, but NaN ?? 12 === NaN. Consider setValueAs: (v) => (v === "" || v === null ? null : Number(v)) or similar, so the downstream nullish-coalescing default actually kicks in.

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

In `@app/course/`[course_id]/manage/assignments/new/form.tsx around lines 787 -
795, The numeric input for late_grading_reminder_interval_hours currently uses
valueAsNumber which coerces empty strings to NaN and breaks the downstream
nullish-coalescing default; update the react-hook-form registration for
late_grading_reminder_interval_hours (the register call) to use setValueAs that
returns null for empty or null values (e.g., setValueAs: v => (v === "" || v ===
null ? null : Number(v))) so that when late_grading_reminders_enabled is toggled
off/on the field normalizes to null and the existing ?? 12 fallback works as
intended.

58-71: Consider exporting normalizeCcEmails for reuse.

Two sibling files (edit/page.tsx, new/page.tsx) and the grading-assignment-defaults/page.tsx each re-derive or duplicate this logic. Exporting this helper keeps the CC-email contract in one place.

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

In `@app/course/`[course_id]/manage/assignments/new/form.tsx around lines 58 - 71,
The normalizeCcEmails helper is duplicated across multiple files; export it from
this module so other pages can import the single source of truth: convert the
local function normalizeCcEmails into a named export (export const
normalizeCcEmails) and update callers in edit/page.tsx, new/page.tsx and
grading-assignment-defaults/page.tsx to import { normalizeCcEmails } from this
file, ensuring the exported type GradingCcEmails is also exported if needed so
the contract remains consistent.
supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql (2)

454-464: Log message implies success even when nothing was assigned.

When v_auto_assigned_count is 0 (no submissions / no-op) the RAISE LOG still reads 'Auto-assigned grading for assignment %, created/updated=%'. Minor phrasing nit; consider distinguishing the "nothing to do" path from the "actually created assignments" path so operator logs are easier to triage.

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

In
`@supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql`
around lines 454 - 464, The RAISE LOG always reports "Auto-assigned grading..."
even when v_auto_assigned_count = 0; update the logic after calling
public.auto_assign_grading_reviews_for_assignment(v_assignment.id) to emit
distinct messages: if v_auto_assigned_count > 0, log the existing success
message with v_assignment.id and v_auto_assigned_count; if v_auto_assigned_count
= 0, log a no-op message like "No grading assignments created for assignment %
(count=0)"; and if v_auto_assigned_count < 0 (error path), log an error/warning
indicating the function failed. Adjust the RAISE LOG block near the call and
keep the existing update to assignment_grading_automation_state unchanged.

354-410: Queue function always creates an email_batches row, even on rubric-mismatch / empty recipient runs.

INSERT INTO public.email_batches at line 354 runs before the recipient CTE is evaluated, and the compensating DELETE on line 409 only fires when zero emails were queued. On every cron tick after deadline where there are still some (but stable) overdue reviewers, you will happily recreate a batch. That is correct — I only want to flag that the batch-delete compensation is racy if concurrent cron runs both insert batches; since run_assignment_grading_automation now holds FOR UPDATE on the state row (line 452) the outer serialization protects you, so this is a nit. Worth a short comment explaining that invariant so future refactors don't drop the lock.

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

In
`@supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql`
around lines 354 - 410, The INSERT into public.email_batches currently happens
before computing recipients which can lead to transient duplicate batches if
concurrent cron runs insert at the same time; add a brief explanatory comment
next to the INSERT (or at the top of this block) stating that concurrency is
protected by the outer FOR UPDATE lock on the state row held by
run_assignment_grading_automation (the state row lock at the FOR UPDATE in
run_assignment_grading_automation prevents concurrent inserts and makes the
later DELETE safe), so future refactors know this invariant must be preserved or
replaced with equivalent serialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql`:
- Around line 194-316: The function auto_assign_grading_reviews_for_assignment
currently returns 0 for misconfiguration cases (no rubric, no instructor actor,
or no eligible staff) which the caller run_assignment_grading_automation treats
as success and marks auto_assigned_at; change the behavior so that when
v_assignment.grading_rubric_id IS NULL, when v_actor_user_id IS NULL, or when
v_staff_count = 0 the function returns -1 (or RAISE WARNING/EXCEPTION) instead
of 0, leaving auto_assigned_at NULL and allowing retries, while keeping the
existing 0 return for the legitimate "no submissions" case; update any RAISE
WARNING messages to include context (assignment id and reason) to aid debugging.

---

Nitpick comments:
In `@app/course/`[course_id]/manage/assignments/[assignment_id]/edit/page.tsx:
- Around line 29-40: The edit page currently sets late_grading_cc_emails
directly from the DB payload (values.late_grading_cc_emails ?? { emails: [] })
without normalizing types; import and call the shared normalizeCcEmails used in
form.tsx (export it if not already exported) and pass
values.late_grading_cc_emails through normalizeCcEmails before calling reset
(i.e., use normalizeCcEmails(values.late_grading_cc_emails) or fallback to
normalizeCcEmails({ emails: [] })) so the form's GradingCcEmails input always
receives a string[] shape.
- Around line 62-134: The callback passed to useCallback references
form.refineCore.onFinish but the dependency array includes form.refineCore
instead of form, which triggers exhaustive-deps; update the dependency array for
the onFinish callback to include form (and remove form.refineCore) so it depends
on the stable form object returned by useForm(), keeping other deps
(assignment_id, course_id, data?.data.self_review_setting_id, update) unchanged.

In `@app/course/`[course_id]/manage/assignments/new/form.tsx:
- Around line 787-795: The numeric input for
late_grading_reminder_interval_hours currently uses valueAsNumber which coerces
empty strings to NaN and breaks the downstream nullish-coalescing default;
update the react-hook-form registration for late_grading_reminder_interval_hours
(the register call) to use setValueAs that returns null for empty or null values
(e.g., setValueAs: v => (v === "" || v === null ? null : Number(v))) so that
when late_grading_reminders_enabled is toggled off/on the field normalizes to
null and the existing ?? 12 fallback works as intended.
- Around line 58-71: The normalizeCcEmails helper is duplicated across multiple
files; export it from this module so other pages can import the single source of
truth: convert the local function normalizeCcEmails into a named export (export
const normalizeCcEmails) and update callers in edit/page.tsx, new/page.tsx and
grading-assignment-defaults/page.tsx to import { normalizeCcEmails } from this
file, ensuring the exported type GradingCcEmails is also exported if needed so
the contract remains consistent.

In `@app/course/`[course_id]/manage/course/grading-assignment-defaults/page.tsx:
- Around line 130-182: The delete flow currently prompts and calls useDelete
(handleDelete) without surfacing how many assignments reference the profile (FK
ON DELETE SET NULL) so instructors are surprised when assignments lose the
profile; before showing the confirmation (in handleDelete) call useList (or the
existing list hook) with filter "grading_default_profile_id eq {id}" to count
referencing assignments and include that count in the confirm message (or, if
you prefer, fetch after delete and show a post-delete toaster with the count),
ensuring you reference the profile id passed to handleDelete and keep behavior
with useDelete unchanged.
- Around line 55-76: The file duplicates the normalizeCcEmails helper (and
related parseCcEmails) already defined in form.tsx; remove the local definitions
(normalizeCcEmails and parseCcEmails) and instead export the canonical helper(s)
from app/course/[course_id]/manage/assignments/new/form.tsx and import them into
this page using the `@/`* path alias (e.g. import { normalizeCcEmails,
parseCcEmails } from '@/app/course/[course_id]/manage/assignments/new/form').
Ensure the exported names match the originals and update any call sites in this
file to use the imported functions.
- Around line 184-203: Replace the blocking window.confirm call in handleDelete
with a Chakra UI Dialog: create a local state like pendingDeleteId (or
pendingDeleteProfile) and open the Dialog when user initiates delete (instead of
calling handleDelete directly), move the destructive call into a confirm handler
(e.g., confirmDelete which calls deleteProfile, toaster, clearForm/refetch and
closes the dialog) and add a cancel handler that clears pendingDeleteId to close
the dialog; update UI buttons that currently call handleDelete(id) to instead
set pendingDeleteId = id and render the Dialog with accessible title/description
and Confirm/Cancel buttons wired to the confirmDelete and cancel handlers so
deletion is non-blocking and stylable while preserving the existing
error/toaster/refetch behavior found in handleDelete.

In
`@supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql`:
- Around line 454-464: The RAISE LOG always reports "Auto-assigned grading..."
even when v_auto_assigned_count = 0; update the logic after calling
public.auto_assign_grading_reviews_for_assignment(v_assignment.id) to emit
distinct messages: if v_auto_assigned_count > 0, log the existing success
message with v_assignment.id and v_auto_assigned_count; if v_auto_assigned_count
= 0, log a no-op message like "No grading assignments created for assignment %
(count=0)"; and if v_auto_assigned_count < 0 (error path), log an error/warning
indicating the function failed. Adjust the RAISE LOG block near the call and
keep the existing update to assignment_grading_automation_state unchanged.
- Around line 354-410: The INSERT into public.email_batches currently happens
before computing recipients which can lead to transient duplicate batches if
concurrent cron runs insert at the same time; add a brief explanatory comment
next to the INSERT (or at the top of this block) stating that concurrency is
protected by the outer FOR UPDATE lock on the state row held by
run_assignment_grading_automation (the state row lock at the FOR UPDATE in
run_assignment_grading_automation prevents concurrent inserts and makes the
later DELETE safe), so future refactors know this invariant must be preserved or
replaced with equivalent serialization.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7148d0d3-4401-472b-ad34-ff34cd21df8d

📥 Commits

Reviewing files that changed from the base of the PR and between f97abc4 and 14e55b9.

📒 Files selected for processing (5)
  • app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx
  • app/course/[course_id]/manage/assignments/new/form.tsx
  • app/course/[course_id]/manage/assignments/new/page.tsx
  • app/course/[course_id]/manage/course/grading-assignment-defaults/page.tsx
  • supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql

Co-authored-by: Jonathan Bell <jon@jonbell.net>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/e2e/grading-assignment-defaults.test.tsx (2)

139-141: Replace the fixed 1.5s sleep with a deterministic wait so the clobber-regression guard doesn't go stale.

This block is specifically guarding the "don't re-seed automation fields after the initial reset(...)" fix called out in the PR objectives, which is great — but page.waitForTimeout(1500) is both flaky (CI can easily delay an effect past 1.5s) and silently permissive (a regression that clobbers after, say, 2s would still let this test pass). A deterministic signal (e.g. expect.poll the value for a short window, or wait on a data-form-ready / network-idle marker tied to the initial reset) would both reduce flakes and keep the regression guard honest.

♻️ Sketch of a poll-based guard
-  await editDueHoursInput.fill("77");
-  await page.waitForTimeout(1500);
-  await expect(editDueHoursInput).toHaveValue("77");
+  await editDueHoursInput.fill("77");
+  // Ensure no delayed reset() clobbers the edited value.
+  await expect
+    .poll(async () => editDueHoursInput.inputValue(), { timeout: 3000, intervals: [200, 300, 500] })
+    .toBe("77");
+  await expect(editDueHoursInput).toHaveValue("77");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/grading-assignment-defaults.test.tsx` around lines 139 - 141,
Replace the flaky fixed sleep (page.waitForTimeout(1500)) used after filling
editDueHoursInput with a deterministic wait: poll the element value (e.g., use
expect.poll or expect(editDueHoursInput).toHaveValue within a short timeout) or
wait on a reliable signal (network/form-ready event) that confirms the initial
reset has completed; update the block around editDueHoursInput so the assertion
only proceeds once the deterministic condition is met rather than using
page.waitForTimeout.

78-83: Delete coverage only hops over the fence — the "confirm and actually delete" path is never exercised.

The PR hardens the defaults page with a delete confirmation dialog, and this block nicely verifies the cancel/dismiss branch. However, the accept branch (dialog accepted → row disappears from the list → row removed from grading_assignment_default_profiles) is the one that actually mutates data, and it's currently untested here. Since this test already owns a throwaway profile (and later seeds a separate one via supabase.update on the assignment, so deletion of this profile wouldn't break the rest of the flow if reordered), it would be cheap to add a second page.once("dialog", d => d.accept()) pass and assert the row is gone.

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

In `@tests/e2e/grading-assignment-defaults.test.tsx` around lines 78 - 83, The
test currently only covers dismissing the delete confirmation dialog; add the
"accept" path by registering a second page.once("dialog", async dialog =>
dialog.accept()) before triggering the Delete button again (using the same
page.getByRole("button", { name: 'Delete' }).first().click()), then assert the
profile row identified by gradingProfileName is no longer visible via
expect(page.getByText(gradingProfileName)).not.toBeVisible() and optionally
verify the row was removed from the grading_assignment_default_profiles table;
ensure you reuse the same throwaway profile (or reorder seeding) so deleting it
won't break later steps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/e2e/grading-assignment-defaults.test.tsx`:
- Around line 139-141: Replace the flaky fixed sleep (page.waitForTimeout(1500))
used after filling editDueHoursInput with a deterministic wait: poll the element
value (e.g., use expect.poll or expect(editDueHoursInput).toHaveValue within a
short timeout) or wait on a reliable signal (network/form-ready event) that
confirms the initial reset has completed; update the block around
editDueHoursInput so the assertion only proceeds once the deterministic
condition is met rather than using page.waitForTimeout.
- Around line 78-83: The test currently only covers dismissing the delete
confirmation dialog; add the "accept" path by registering a second
page.once("dialog", async dialog => dialog.accept()) before triggering the
Delete button again (using the same page.getByRole("button", { name: 'Delete'
}).first().click()), then assert the profile row identified by
gradingProfileName is no longer visible via
expect(page.getByText(gradingProfileName)).not.toBeVisible() and optionally
verify the row was removed from the grading_assignment_default_profiles table;
ensure you reuse the same throwaway profile (or reorder seeding) so deleting it
won't break later steps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: eb8ac282-d207-4bbc-8c08-f232329339d4

📥 Commits

Reviewing files that changed from the base of the PR and between 14e55b9 and 7f44d2e.

📒 Files selected for processing (1)
  • tests/e2e/grading-assignment-defaults.test.tsx

Co-authored-by: Jonathan Bell <jon@jonbell.net>
Co-authored-by: Jonathan Bell <jon@jonbell.net>
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 4, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/e2e/grading-assignment-defaults.test.tsx (2)

193-264: 💤 Low value

Test name suggests "lab leaders" pool but uses "graders" — verify this is intentional.

The test is named "deadline automation assigns grading to lab leaders" (line 193), but line 256 sets auto_assign_assignee_pool: "graders". The test passes because the lab leaders happen to have role: "grader", so they're included in the graders pool.

If the intent is to test the "lab_leaders" assignee pool feature specifically, consider updating line 256 to use that value. Otherwise, consider renaming the test to reflect that it's testing general grader assignment where graders happen to be lab leaders.

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

In `@tests/e2e/grading-assignment-defaults.test.tsx` around lines 193 - 264, The
test name says it targets lab leaders but the assignment config uses
auto_assign_assignee_pool: "graders"; decide whether to test the lab_leaders
pool or general graders. If you mean lab leaders, change the assignment update
to set auto_assign_assignee_pool: "lab_leaders" (refer to the autoAssignment
variable and the .update call that sets auto_assign_assignee_pool), otherwise
rename the test string ("deadline automation assigns grading to graders") to
match the current behavior. Ensure the test data (createLabSectionWithStudents /
lab_leaders variables) remains consistent with the chosen pool.

162-163: 💤 Low value

Consider replacing hardcoded wait with a condition-based assertion.

page.waitForTimeout(1500) can introduce flakiness if the input debounce timing changes. You could use expect(editDueHoursInput).toHaveValue("77") with a built-in retry instead, since Playwright's expect already polls.

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

In `@tests/e2e/grading-assignment-defaults.test.tsx` around lines 162 - 163,
Replace the hardcoded sleep by removing the page.waitForTimeout(1500) and rely
on Playwright's polling in the assertion: use await
expect(editDueHoursInput).toHaveValue("77") (optionally with a custom timeout)
so the test waits for the element value change instead of using a fixed delay;
locate the code around page.waitForTimeout and editDueHoursInput to make this
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/course/`[course_id]/manage/course/grading-assignment-defaults/page.tsx:
- Around line 116-118: The component references hooks useCourseController and
useListTableControllerValues (used in the gradersOnly and graderCourseRoles
logic) but they are not imported, causing a runtime ReferenceError; add import
statements for useCourseController and useListTableControllerValues at the top
of the file alongside the other hooks/modules so the symbols are defined when
gradersOnly and graderCourseRoles are evaluated. Ensure the imports match the
actual module exports in your codebase and place them with the other imports in
this file.

---

Nitpick comments:
In `@tests/e2e/grading-assignment-defaults.test.tsx`:
- Around line 193-264: The test name says it targets lab leaders but the
assignment config uses auto_assign_assignee_pool: "graders"; decide whether to
test the lab_leaders pool or general graders. If you mean lab leaders, change
the assignment update to set auto_assign_assignee_pool: "lab_leaders" (refer to
the autoAssignment variable and the .update call that sets
auto_assign_assignee_pool), otherwise rename the test string ("deadline
automation assigns grading to graders") to match the current behavior. Ensure
the test data (createLabSectionWithStudents / lab_leaders variables) remains
consistent with the chosen pool.
- Around line 162-163: Replace the hardcoded sleep by removing the
page.waitForTimeout(1500) and rely on Playwright's polling in the assertion: use
await expect(editDueHoursInput).toHaveValue("77") (optionally with a custom
timeout) so the test waits for the element value change instead of using a fixed
delay; locate the code around page.waitForTimeout and editDueHoursInput to make
this change.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e7852360-dfb5-4890-b373-82b2f322af1e

📥 Commits

Reviewing files that changed from the base of the PR and between ad9e3d6 and c35787e.

📒 Files selected for processing (10)
  • app/course/[course_id]/dynamicCourseNav.tsx
  • app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx
  • app/course/[course_id]/manage/assignments/new/form.tsx
  • app/course/[course_id]/manage/assignments/new/page.tsx
  • app/course/[course_id]/manage/course/grading-assignment-defaults/page.tsx
  • supabase/functions/_shared/SupabaseTypes.d.ts
  • supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql
  • tests/e2e/grading-assignment-defaults.test.tsx
  • utils/supabase/DatabaseTypes.d.ts
  • utils/supabase/SupabaseTypes.d.ts
✅ Files skipped from review due to trivial changes (3)
  • app/course/[course_id]/dynamicCourseNav.tsx
  • utils/supabase/DatabaseTypes.d.ts
  • supabase/functions/_shared/SupabaseTypes.d.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql
  • app/course/[course_id]/manage/assignments/new/form.tsx

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 4, 2026
…assign

- Fix CHECK-constraint subquery error (SQLSTATE 0A000) by wrapping the
  array-of-strings predicate in a new IMMUTABLE jsonb_is_string_array
  function and calling it from both grader_subset_ids CHECKs.
- Bound run_assignment_grading_automation to a recent-deadline window
  and only select rows with actionable work (unrun auto-assign or
  elapsed reminder interval) so cron cost stays proportional to recent
  work rather than the lifetime of the assignments table.
- Add submission-time auto-assignment for late submissions: AFTER
  INSERT trigger on submissions calls
  auto_assign_grading_review_for_submission for past-deadline
  insertions, with load-balanced staff picking and a re-submission
  guard. Errors are swallowed so a failed auto-assign cannot block
  the student's submission.
- Add E2E coverage for the assignee-pool branches (instructors,
  instructors_and_graders, graders subset, lab_leaders with conflict
  exclusion, group_mentors with conflict exclusion), cron idempotency,
  reminder cadence (backdating last_reminder_sent_at), reminder filter
  predicates, and the late-submission trigger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jon-bell and others added 3 commits May 16, 2026 01:15
…brittleness

Reproduced and fixed the CI failure (run 25948945436) locally against a fresh
Supabase + prod build.

SQL fix: auto_assign_grading_reviews_for_assignment and the new
auto_assign_grading_review_for_submission both did ORDER BY ur.created_at
on user_roles, which has no created_at column. Use ORDER BY ur.id, which
gives the same stable "earliest enrolled" semantics.

Product fix: GradingAutomationSubform applied the saved profile's values
to the form via a useEffect on selectedProfile every time selectedProfile
became defined — including the initial form reset from the assignment's
saved DB state. On edit, this clobbered per-assignment overrides with
profile defaults. Move the apply-profile-values call into the Saved-profile
select's onChange so it only fires on user-initiated profile changes.

UI cleanup: removed the duplicate <Toaster /> on the grading-defaults page
(layout.tsx already mounts one globally), which had caused every
toaster.success(...) to render twice in the DOM and tripped Playwright's
strict-mode locator.

E2E robustness fixes around real CI artifacts of the page:
- Chakra Checkbox.Control intercepts pointer events; click the label text
  instead (pattern used in gradebook.test.tsx).
- Lingering toast regions overlap the Delete buttons; use dispatchEvent("click")
  to bypass DOM-level stacking, and drive the two window.confirms with a
  single counter-based page.on("dialog") handler so order-of-arrival doesn't
  matter.
- ManageAssignmentNav renders its children twice (one desktop layout, one
  mobile layout) — both copies are in the DOM regardless of viewport, so
  scope every edit-page locator to the first <form> wrapper.
- The reminder-filter test now upserts submission_review rows (existing
  triggers may auto-create the grading-rubric one on submission insert)
  instead of unconditionally inserting and failing on the unique key.

Local validation: with the fixes applied, all 10 non-UI tests in
grading-assignment-defaults.test.tsx pass; the UI test progresses cleanly
through every assertion until the post-save "Assignment Updated" toast,
which requires a real GitHub webhook configure call (AGENTS.md documents
this as expected for dummy-credentials local runs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jon-bell
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@tests/e2e/grading-assignment-defaults.test.tsx`:
- Around line 209-211: The test clicks the Save button
(editForm.getByRole("button", { name: "Save" })) and immediately awaits the
"Assignment Updated" toaster without synchronizing the submission; add a
post-click synchronization step before asserting the toast by waiting for the
Save button to enter a loading/disabled state or for a network response that
indicates the update completed (e.g., waitForResponse matching the assignment
update endpoint), and also add an assertion branch that checks for the failure
toast title "Update Error" if the update doesn't finish; reference the existing
toaster title "Assignment Updated" (from
app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx) when
asserting success.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e5eba637-2625-43ce-8419-4fef8ef022df

📥 Commits

Reviewing files that changed from the base of the PR and between c35787e and e3215bb.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • app/course/[course_id]/manage/assignments/new/form.tsx
  • app/course/[course_id]/manage/course/grading-assignment-defaults/page.tsx
  • supabase/migrations/20260418100000_grading_assignment_defaults_and_reminders.sql
  • tests/e2e/grading-assignment-defaults.test.tsx

Comment on lines +209 to +211
await editForm.getByRole("button", { name: "Save" }).click();

await expect(page.getByText("Assignment Updated")).toBeVisible({ timeout: 20_000 });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the exact toast message text used in the assignment edit flow
rg -n -i "assignment updated" --type=ts --type=tsx app/course 2>/dev/null || \
rg -n -i "assignment updated" -g '*.ts' -g '*.tsx' app/course

Repository: pawtograder/platform

Length of output: 479


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="tests/e2e/grading-assignment-defaults.test.tsx"

echo "=== Test snippet around failing lines (190-225) ==="
nl -ba "$FILE" | sed -n '180,240p'

echo
echo "=== Locate toast/title strings in the test file ==="
rg -n --fixed-string "Assignment Updated" "$FILE" || true
rg -n -i "assignment updated" "$FILE" || true

echo
echo "=== Find where the UI string 'Assignment Updated' is defined/used ==="
rg -n --fixed-string "Assignment Updated" app tests || true
rg -n -i "assignment updated" app tests || true

echo
echo "=== Confirm whether a network/response wait exists near the Save click ==="
rg -n "waitForResponse|expect\\(.*response|waitUntil|route\\.|request" "$FILE" || true
rg -n "Save\"\\)\\.click|getByRole\\(\"button\", \\{ name: \"Save\"\\}\\)\\.click" "$FILE" || true

Repository: pawtograder/platform

Length of output: 177


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="tests/e2e/grading-assignment-defaults.test.tsx"

echo "=== Test snippet around failing lines (190-225) ==="
cat -n "$FILE" | sed -n '190,225p'

echo
echo "=== Locate toast/title strings in the test file ==="
rg -n --fixed-string "Assignment Updated" "$FILE" || true
rg -n -i "assignment updated" "$FILE" || true

echo
echo "=== Locate nearby Save click and subsequent waits/assertions ==="
rg -n 'getByRole\("button", \{ name: "Save"\}\)\.click\(\);' "$FILE" || true
rg -n 'getByText\("Assignment Updated"\)|getByText\(\x27Assignment Updated\x27\)|getByText' "$FILE" | head -n 50 || true

echo
echo "=== Confirm whether network/response waits exist near the Save click ==="
rg -n "waitForResponse|expect\\(.*response|waitUntil|route\\.|request|response" "$FILE" || true

Repository: pawtograder/platform

Length of output: 3793


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE_TEST="tests/e2e/grading-assignment-defaults.test.tsx"

echo "=== rg 'Assignment Updated' variants in test ==="
rg -n --fixed-strings "Assignment Updated" "$FILE_TEST" || true
rg -n -i "assignment updated" "$FILE_TEST" || true

echo
echo "=== rg Save click context: any loading/network waits nearby ==="
rg -n "getByRole\\(\"button\", \\{ name: \"Save\" \\}\\)\\.click\\(\\)" "$FILE_TEST"
rg -n "waitForResponse|route\\(|waitForURL|waitUntil|request|response|route\\.|supabase\\." "$FILE_TEST" || true
rg -n "toBeDisabled|loading|spinner|Saving|submit" "$FILE_TEST" || true

echo
echo "=== Show larger context around the failing assertion (200-220) ==="
sed -n '195,220p' "$FILE_TEST"

Repository: pawtograder/platform

Length of output: 3908


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Use the path we already saw from the earlier rg result.
FILE_APP="app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx"

echo "=== Context around title: \"Assignment Updated\" ==="
# Print a small window around the match
line=$(rg -n --fixed-strings "title: \"Assignment Updated\"" "$FILE_APP" | head -n 1 | cut -d: -f1)
echo "Matched line: ${line}"
start=$((line-25))
end=$((line+25))
if [ "$start" -lt 1 ]; then start=1; fi
sed -n "${start},${end}p" "$FILE_APP"

Repository: pawtograder/platform

Length of output: 1997


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE_TEST="tests/e2e/grading-assignment-defaults.test.tsx"

echo "=== All 'Assignment Updated' uses in the test file ==="
rg -n --fixed-strings "Assignment Updated" "$FILE_TEST" || true

echo
echo "=== Check for 'Update Error' toast assertions in the test file ==="
rg -n --fixed-strings "Update Error" "$FILE_TEST" || true

echo
echo "=== Search for 'toaster' / toast container usage in the test file ==="
rg -n "toaster|toast" "$FILE_TEST" || true

echo
echo "=== Show a bit more context around the Save click (280-360) to see other flows ==="
sed -n '240,360p' "$FILE_TEST"

Repository: pawtograder/platform

Length of output: 4938


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE_TEST="tests/e2e/grading-assignment-defaults.test.tsx"

echo "=== Find if this flow sets template_repo (GitHub webhook) ==="
rg -n "template_repo|template repo|webhook|githubRepoConfigureWebhook|Watch" "$FILE_TEST" || true

echo
echo "=== Find label/select changes in the same test section as the Save click ==="
# print a broader window around the failing Save click to see what fields are set
sed -n '140,225p' "$FILE_TEST"

Repository: pawtograder/platform

Length of output: 4576


Pipeline failure: add synchronization around Save before asserting the “Assignment Updated” toast

  • The expected toast text matches the app implementation: toaster.create({ title: "Assignment Updated", ... }) in app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx, so a capitalization/text mismatch is unlikely.
  • The test clicks Save and immediately waits up to 20s for the toast, with no assertion that the submission actually started/completed (no check for button loading/disabled, no network/response wait, and the existing waitForTimeout(1500) occurs earlier).
  • Add a post-click synchronization step (e.g., wait for the Save button to enter a loading/disabled state or for the persisted assignment update to land) and/or assert the failure toast (“Update Error”) when the update doesn’t finish.
🧰 Tools
🪛 GitHub Actions: E2E Tests / 0_e2e-local.txt

[error] 211-211: Playwright expect(toBeVisible) timed out (20s). Missing UI text: getByText('Assignment Updated')

🤖 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 `@tests/e2e/grading-assignment-defaults.test.tsx` around lines 209 - 211, The
test clicks the Save button (editForm.getByRole("button", { name: "Save" })) and
immediately awaits the "Assignment Updated" toaster without synchronizing the
submission; add a post-click synchronization step before asserting the toast by
waiting for the Save button to enter a loading/disabled state or for a network
response that indicates the update completed (e.g., waitForResponse matching the
assignment update endpoint), and also add an assertion branch that checks for
the failure toast title "Update Error" if the update doesn't finish; reference
the existing toaster title "Assignment Updated" (from
app/course/[course_id]/manage/assignments/[assignment_id]/edit/page.tsx) when
asserting success.

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.

2 participants