Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9fc58d8
e2e: harden office-hours "Student can request help" against post-crea…
jon-bell May 23, 2026
030a5ff
e2e: revert office-hours DB fallback, use longer waitForURL
jon-bell May 23, 2026
88fc32d
e2e: harden two flakes surfaced by 10x local sweep
jon-bell May 23, 2026
cdabbe4
office-hours: shrink new-help-request critical path before router.push
jon-bell May 23, 2026
91c9f41
e2e: revert rubric "Preview paused" banner wait; add sweep script
jon-bell May 23, 2026
72398ca
e2e: bump office-hours "Student can request help" budget to 360s
jon-bell May 23, 2026
be8924b
e2e: lookup-by-description fallback for office-hours submit waits
jon-bell May 23, 2026
8c3211a
e2e: surface form error toasts + extend office-hours fallback to 180s
jon-bell May 23, 2026
cccb433
e2e: pre-submit wait for realtime to deliver queue staff assignment
jon-bell May 23, 2026
226ee65
debug: instrument office-hours form + test to root-cause CI no-op
jon-bell May 23, 2026
22ef1f5
debug: defensive re-fill + log submitted request text
jon-bell May 23, 2026
3772df0
e2e: explicit toBeEnabled wait + 600s budget for office-hours
jon-bell May 23, 2026
dd58924
debug: log the actual latest help_requests rows when fallback can't f…
jon-bell May 23, 2026
88228b8
debug: narrow fallback diagnostic to class_id + admin URL
jon-bell May 23, 2026
bfe7c89
debug: force-click the office-hours Submit Request button
jon-bell May 23, 2026
21dda31
office-hours: prevent /new page from unmounting the form mid-submit
jon-bell May 23, 2026
52c98d6
office-hours: stop unmounting form on realtime active-staff blip
jon-bell May 23, 2026
2abd46a
e2e: re-introduce force:true click for office-hours submit
jon-bell May 23, 2026
db434f7
office-hours: stop disabling Submit button on realtime-flapping hasEr…
jon-bell May 23, 2026
03d0a74
debug: verify help_requests row exists in DB right after create
jon-bell May 23, 2026
21b607c
e2e: dispatch office-hours form submit via requestSubmit() not click
jon-bell May 23, 2026
0a84f46
e2e: retry-loop requestSubmit() until onSubmit actually fires
jon-bell May 23, 2026
1caa747
e2e: extend submit retry-loop to 180s + alternate click/requestSubmit
jon-bell May 23, 2026
0387dff
e2e: reset office-hours.test.tsx to pre-PR baseline
jon-bell May 23, 2026
d2b906a
e2e: revert loginAsUser to networkidle waits
jon-bell May 23, 2026
49b2820
Remove OH-DEBUG diagnostic instrumentation from newRequestForm
jon-bell May 23, 2026
ce8b02b
e2e: retry rubric-editor mutex toggle until YAML parse-debounce settles
jon-bell May 23, 2026
9ead17a
Atomic help-request creation via SECURITY DEFINER RPC
jon-bell May 24, 2026
2a9ce48
e2e-sweep: validate iterations, log circuit-breaker errors, exit nonz…
jon-bell May 24, 2026
474397d
Prettier-format regenerated SupabaseTypes after RPC migration
jon-bell May 24, 2026
5ae7679
Warm helpRequests cache after RPC before navigating to chat page
jon-bell May 24, 2026
0092b4f
Address three CI-only e2e flakes uncovered after the office-hours RPC…
jon-bell May 24, 2026
80d5663
Don't block reply-composer close on optional watcher warm-up
jon-bell May 24, 2026
74bf169
Prettier-format discussion_thread.tsx warm-up call
jon-bell May 24, 2026
9f69e4e
Race office-hours cache-warm against 2s timeout so nav can't stall
jon-bell May 24, 2026
4b183ca
Move office-hours help-request cache warm-up to chat page (invalidate…
jon-bell May 24, 2026
ab8589f
Handle invalidate's resolved-but-missing path on help-request chat page
jon-bell May 24, 2026
95dbed3
Distinguish transient invalidate failures from missing row on chat page
jon-bell May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ axe-debug/
/logs/
/screenshots/
/screenshots-*/
/sweep-results-*/
/.e2e-*.done
/.build.done
/screenshot-diff-report.json
26 changes: 20 additions & 6 deletions app/course/[course_id]/discussion/discussion_thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function DiscussionThreadReply({
}
}, [visible]);
const { tableController } = useDiscussionThreadsController();
const courseController = useCourseController();

const sendMessage = useCallback(
async (message: string, profile_id: string, close = true) => {
Expand All @@ -64,16 +65,29 @@ export function DiscussionThreadReply({
body: message
});

// invalidate({
// resource: "discussion_threads",
// invalidates: ['detail'],
// id: thread.parent!
// });
if (close) {
setVisible(false);
}

// Replying auto-follows the thread via a server-side trigger that
// INSERTs into discussion_thread_watchers. The Follow→Unfollow
// button transition reads that row through a realtime-backed
// TableController, and on slow CI runs realtime delivery of the
// new watcher row lags the reply create by several seconds, during
// which the button keeps saying "Follow". Warm the watcher cache
// directly so the UI flips without waiting on the broadcast. Run
// this after the composer close because it's best-effort —
// realtime is the authoritative path — and we don't want a slow
// REST round-trip stalling the close transition.
void courseController.discussionThreadWatchers
.getOneByFilters([
{ column: "discussion_thread_root_id", operator: "eq", value: thread.root || thread.id }
])
.catch(() => {
// Realtime will catch up.
});
},
[tableController, setVisible, thread]
[tableController, courseController, setVisible, thread]
);
if (!visible) {
return <></>;
Expand Down
257 changes: 108 additions & 149 deletions app/course/[course_id]/office-hours/[queue_id]/new/newRequestForm.tsx

Large diffs are not rendered by default.

64 changes: 34 additions & 30 deletions app/course/[course_id]/office-hours/[queue_id]/new/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { useParams, useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useHelpQueue, useActiveHelpQueueAssignments } from "@/hooks/useOfficeHoursRealtime";
import { useCallback, useEffect, useRef, useState } from "react";
import { useHelpQueue } from "@/hooks/useOfficeHoursRealtime";
import { Box, Card, Container, Text, Button } from "@chakra-ui/react";

import HelpRequestForm from "./newRequestForm";
Expand All @@ -11,48 +11,51 @@ export default function NewRequestPage() {
const { queue_id, course_id } = useParams();
const router = useRouter();
const helpQueue = useHelpQueue(Number(queue_id));
// Use the specialized hook that subscribes to individual item changes
const activeHelpQueueAssignments = useActiveHelpQueueAssignments();

// Check if queue has an active assignment (staff is working)
const hasActiveAssignment = useMemo(() => {
if (!activeHelpQueueAssignments) return false;
// Demo queues don't require active staff
if (helpQueue?.is_demo) return true;
return activeHelpQueueAssignments.some((assignment) => assignment.help_queue_id === Number(queue_id));
}, [activeHelpQueueAssignments, queue_id, helpQueue?.is_demo]);
// The queue is "accepting new requests" iff the DB column says so (or
// it's a demo queue). We intentionally do NOT factor in active-staff
// here — that's a realtime-derived signal that flaps under load, and
// if we unmount the form based on it we strand in-flight click events
// (React can't dispatch a synthetic submit to a component that's no
// longer mounted). Tracked down via the office-hours CI flake on PR
// 785: failing attempts showed ZERO console.log events from inside
// the form's onSubmit despite Playwright's click() returning success.
// The form itself enforces the "active staff" guard via the submit
// button's `disabled` state, which is the right place for a realtime
// gate — it disables the button while data is in flux without
// unmounting the form.
const queueAcceptingRequests = helpQueue?.is_demo || (helpQueue?.available ?? false);

// Check if queue is available for new requests (both available flag AND has active staff, unless demo)
const isQueueOpen = helpQueue?.is_demo || (helpQueue?.available && hasActiveAssignment);

// Set while the form is submitting/navigating to its newly created request, so the
// redirect below stands down and can't race (and swallow) the form's router.push.
// Track whether the form is mid-submit. Use both a ref (so the
// useEffect below can read it without rerunning on every toggle) and
// useState (so the render-time guard below actually picks up the flip
// — refs don't trigger renders).
const isSubmittingRef = useRef(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmittingChange = useCallback((submitting: boolean) => {
isSubmittingRef.current = submitting;
setIsSubmitting(submitting);
}, []);

useEffect(() => {
// Don't redirect while a submission is in flight — the form navigates to the new
// request itself, and a competing router.replace here gets swallowed under load,
// stranding the student on a URL without the request id.
if (isSubmittingRef.current) return;
// Don't act on indeterminate state: activeHelpQueueAssignments is undefined until the
// realtime data loads (and can blip undefined on re-subscribe), which would briefly make
// the queue look closed and fire a spurious redirect.
if (activeHelpQueueAssignments === undefined) return;
if (helpQueue && !isQueueOpen) {
// Redirect back to queue page if queue is not open for new requests
// Don't act on indeterminate state: helpQueue is undefined until the
// realtime data loads, which would briefly make the queue look closed
// and fire a spurious redirect.
if (helpQueue === undefined) return;
if (!queueAcceptingRequests) {
// Redirect back to queue page if the queue is closed at the
// DB-row level (helpQueue.available === false and not demo).
router.replace(`/course/${course_id}/office-hours/${queue_id}`);
}
}, [helpQueue, isQueueOpen, activeHelpQueueAssignments, router, course_id, queue_id]);

// Show error message if queue is not open for new requests
if (helpQueue && !isQueueOpen) {
const reason = !helpQueue.available
? "This queue is not currently accepting new requests."
: "This queue is not currently staffed.";
}, [helpQueue, queueAcceptingRequests, router, course_id, queue_id]);

// Show error message only when the queue's own row says it's closed.
// Don't unmount the form on a realtime blip.
if (helpQueue !== undefined && !queueAcceptingRequests && !isSubmitting) {
return (
<Container>
<Box py={8}>
Expand All @@ -62,7 +65,8 @@ export default function NewRequestPage() {
Queue Closed for New Requests
</Text>
<Text color="fg.muted" mb={4}>
{reason} You can still view existing requests and queue status.
This queue is not currently accepting new requests. You can still view existing requests and queue
status.
</Text>
<Button onClick={() => router.push(`/course/${course_id}/office-hours/${queue_id}`)} variant="outline">
Back to Queue
Expand Down
72 changes: 72 additions & 0 deletions scripts/e2e-sweep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash
# Run the full Playwright suite N times sequentially. Between iterations:
# - Clear the GitHub circuit breaker (otherwise the create-submission tests
# trip it on their first run and downstream submission flows fail fast).
# - Move that iteration's test-results aside so the next run starts clean
# and we keep traces/db-state for failure diagnosis.
# - Append a one-line summary to logs/sweep-summary.txt.
#
# Usage: ./scripts/e2e-sweep.sh <iterations>
# Outputs:
# logs/sweep-iN.log per-run Playwright output
# sweep-results-iN/ per-run test-results dir
# logs/sweep-summary.txt one line per run with pass/fail counts
# .e2e-sweep.done sentinel after all runs complete

set -uo pipefail

n="${1:?usage: $0 <iterations>}"
if ! [[ "$n" =~ ^[1-9][0-9]*$ ]]; then
echo "usage: $0 <iterations> (positive integer)" >&2
exit 2
fi
mkdir -p logs
rm -f .e2e-sweep.done logs/sweep-summary.txt
rm -rf sweep-results-i*

failed_iters=0
start_all=$(date +%s)
for ((i=1; i<=n; i++)); do
echo "[sweep] iter ${i}/${n} start=$(date -Is)" | tee -a logs/sweep-summary.txt

# Clear GitHub circuit breaker. Trip is from create-submission tests'
# cloneRepository hitting the dummy App; if previous iteration tripped it,
# this iteration's downstream tests would fast-fail without retry. We
# log (rather than silence) failures so environment drift is visible.
if ! docker exec -i supabase_db_pawtograder-platform psql -U postgres -d postgres -c \
"UPDATE public.github_circuit_breakers SET state='closed', open_until=now() WHERE state='open';" \
>> "logs/sweep-i${i}.log" 2>&1; then
echo "[sweep] iter ${i}/${n} warn=failed_to_clear_github_circuit_breaker" | tee -a logs/sweep-summary.txt
fi

rm -rf test-results playwright-report
start=$(date +%s)
BASE_URL=http://localhost:3001 \
npx playwright test --project=chromium \
>> "logs/sweep-i${i}.log" 2>&1
pw_exit=$?
end=$(date +%s)
if [ "$pw_exit" -ne 0 ]; then
failed_iters=$((failed_iters + 1))
fi

# Move per-run test-results aside so they don't get clobbered by the next run.
if [ -d test-results ]; then
mv test-results "sweep-results-i${i}"
fi

# Summary: tally passed/failed/skipped/did-not-run. Pull them from the
# tail of the log where Playwright prints the totals block.
summary=$(tail -40 "logs/sweep-i${i}.log" | grep -E "^\s*[0-9]+\s+(passed|failed|skipped|did not run)" | tr -d ' ' | tr '\n' ' ' || true)
echo "[sweep] iter ${i}/${n} exit=${pw_exit} elapsed=$((end - start))s ${summary}" | tee -a logs/sweep-summary.txt
done

end_all=$(date +%s)
echo "[sweep] DONE n=${n} total=$((end_all - start_all))s failed_iters=${failed_iters}" | tee -a logs/sweep-summary.txt

# Sentinel records the failed-iteration count, not a constant "0", so
# automation reading .e2e-sweep.done can branch on real outcomes.
echo "${failed_iters}" > .e2e-sweep.done
if [ "$failed_iters" -gt 0 ]; then
exit 1
fi
25 changes: 25 additions & 0 deletions supabase/functions/_shared/SupabaseTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11076,6 +11076,18 @@ export type Database = {
};
Returns: Json;
};
_materialize_bare_check_regrade_comment: {
Args: {
p_author: string;
p_line: number;
p_points: number;
p_regrade_request_id: number;
p_request: Database["public"]["Tables"]["submission_regrade_requests"]["Row"];
p_submission_artifact_id: number;
p_submission_file_id: number;
};
Returns: Record<string, unknown>;
};
_submission_review_is_completable: {
Args: { p_submission_review_id: number };
Returns: boolean;
Expand Down Expand Up @@ -11610,6 +11622,19 @@ export type Database = {
};
Returns: undefined;
};
create_help_request_with_participants: {
Args: {
p_file_references?: Json;
p_help_queue_id: number;
p_is_private?: boolean;
p_location_type?: Database["public"]["Enums"]["location_type"];
p_referenced_submission_id?: number;
p_request: string;
p_student_profile_ids?: string[];
p_template_id?: number;
};
Returns: number;
};
create_invitation: {
Args: {
p_class_id: number;
Expand Down
Loading
Loading