Skip to content

Update deps 2026-03#1665

Open
nygrenh wants to merge 32 commits intomasterfrom
update-deps-2026-03
Open

Update deps 2026-03#1665
nygrenh wants to merge 32 commits intomasterfrom
update-deps-2026-03

Conversation

@nygrenh
Copy link
Member

@nygrenh nygrenh commented Mar 3, 2026

Summary by CodeRabbit

  • New Features

    • Responsive fit-to-width text for headings and paragraphs; image focal-point positioning; draft course visibility controls.
  • Bug Fixes

    • Chapter locking initialization and complete-and-lock behavior made consistent; timestamp text formatting adjusted; improved block-attribute validation and tighter component typings.
  • Tests

    • Added and expanded UI/e2e coverage for notifications, quizzes, consent flows, exercise resets and various course scenarios.
  • Chores

    • Node target updated to 24.14.0, pnpm to 10.30.3, Rust toolchain bumped and wide dependency upgrades across services.

@github-actions github-actions bot added dependencies Pull requests that update a dependency file quizzes shared-module labels Mar 3, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updated Node base images to Node 24, bumped pnpm/workspace and many JS/Rust dependencies, updated Rust toolchain to 1.93, refactored headless-lms chapter‑locking and RNG usage, expanded Gutenberg block attributes/guards, adjusted type import/generation scripts, and added/updated Playwright system tests.

Changes

Cohort / File(s) Summary
Node / Docker / Build scripts
Dockerfile.node.base.dockerfile, bin/build-dockerfile-node-base, bin/build-dockerfile-node-cache
Bumped Node base image from node:22-bookworm-slimnode:24-bookworm-slim.
Workspace / pnpm configs
pnpm-workspace.yaml, services/*/pnpm-workspace.yaml, shared-module/*/pnpm-workspace.yaml, storybook/pnpm-workspace.yaml, system-tests/pnpm-workspace.yaml
Introduced/updated workspace flags (engineStrict, linkWorkspacePackages), set Node to 24.14.0, added overrides, switched to time-based resolution, enabled strictDepBuilds and related workspace controls.
Top-level & service package manifests
package.json, services/*/package.json, shared-module/*/package.json, storybook/package.json, system-tests/package.json
Bumped packageManager to [email protected] and performed widespread dependency/devDependency version upgrades.
Rust toolchain & Cargo
rust-toolchain.toml, services/headless-lms/*/Cargo.toml, services/headless-lms/server/Cargo.toml, services/headless-lms/utils/Cargo.toml
Toolchain channel → 1.93; many crate version/feature bumps (tokio, sqlx, reqwest → rustls, uuid, chrono, etc.); rustls/init added.
Chapter locking (headless-lms)
services/headless-lms/models/src/user_chapter_locking_statuses.rs, services/headless-lms/models/src/user_course_settings.rs, services/headless-lms/server/src/controllers/.../chapters.rs, .../courses.rs, .../main_frontend/courses/mod.rs
Refactor: complete_chaptercomplete_and_lock_chapter (adds course_id), added get_or_init_all_for_course and ensure_not_unlocked_yet_status; auto-initialize NotUnlockedYet rows and updated callers.
RNG / random APIs
services/headless-lms/*/src/*.rs, services/headless-lms/server/src/prelude.rs, services/headless-lms/utils/src/strings.rs
Switched imports from rand::Rngrand::RngExt, adopted SampleString API for random strings, re-exported RNG traits in prelude.
Gutenberg types & generation
bin/extract-gutenberg-types, bin/extract-gutenberg-types (script), services/cms/scripts/extractGutenbergAttributeTypes.ts, services/main-frontend/types/..., services/main-frontend/types/.../*.guard.ts
Changed module resolution and output paths for generated types, made formatting conditional on guard files, added/expanded attributes (fitText, focalPoint, background/text/gradient colors), and updated guards to validate new fields.
Frontend components & styles
services/main-frontend/src/.../HeadingBlock.tsx, ImageBlock.tsx, Paragraph/index.tsx, Paragraph/styles.ts, VerseBlock.tsx
Added fitText responsive sizing (clamp) for headings/paragraphs; added focalPoint support for images (object-position); updated component prop types to include extra attributes.
CMS & editor tweaks
services/cms/next-env.d.ts, services/example-exercise/next-env.d.ts, services/quizzes/next-env.d.ts, services/tmc/next-env.d.ts, services/cms/next.config.js, services/cms/src/components/editors/GutenbergEditor.tsx, services/cms/src/blocks/Aside/*
Switched triple-slash references to imports in next-env files, removed eslint.ignoreDuringBuilds, changed outputFileTracingRoot to __dirname, adjusted Gutenberg imports and Aside component typings.
Test infra & tests
services/quizzes/tests/api/utils/appRouterTestClient.ts, system-tests/src/tests/**, system-tests/src/utils/dialogs.ts
appRouterTestClient now uses raw listener + Web API Request conversion for SuperTest; many new/updated Playwright tests and minor locator/formatting refinements.
Misc / small edits
shared-module/..., storybook/*, various next-env.d.ts, SQLx metadata files, services/headless-lms/models/src/courses.rs, services/headless-lms/server/src/domain/system_health/kubernetes.rs
Formatting, packageManager bumps, added Default derive to CourseUpdate, SQLx hash/whitespace tweak, timestamp formatting changed to default to_string(), and other small type/format edits.

Sequence Diagram(s)

sequenceDiagram
  participant Client as "Client"
  participant Controller as "API Controller"
  participant Model as "user_chapter_locking_statuses\n(Model)"
  participant DB as "Database"

  Client->>Controller: GET /courses/:id/locking-statuses
  Controller->>Model: get_or_init_all_for_course(conn, user_id, course_id)
  Model->>DB: query course locking_enabled
  DB-->>Model: course_locking_enabled
  alt locking enabled and no statuses
    Model->>DB: insert NotUnlockedYet rows for chapters
    DB-->>Model: inserted rows
    Model->>DB: fetch statuses for user & course
  else statuses exist
    Model->>DB: fetch statuses for user & course
  end
  Model-->>Controller: Vec<UserChapterLockingStatus>
  Controller-->>Client: 200 OK + statuses
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

chore

Poem

"🐰
I hopped from twenty-two to twenty-four with glee,
pnpm and Cargo gleamed, dependencies set free.
fitText stretches lines and focalPoints find the sight,
chapters wake from NotUnlockedYet to light.
A joyful thump — the codebase hums tonight!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Update deps 2026-03' is vague and generic, using non-descriptive terms that don't convey meaningful information about the specific changes. Consider a more descriptive title that indicates the scope of updates, such as 'Update Node.js, Rust toolchain, and dependencies to 2026-03 versions' or specifying major changes like 'Upgrade Node 24, Rust 1.93, and pnpm ecosystem dependencies'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch update-deps-2026-03

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.

@coderabbitai coderabbitai bot added the chore label Mar 3, 2026
Copy link
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: 4

Caution

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

⚠️ Outside diff range comments (10)
system-tests/src/tests/mark-as-spam.spec.ts (1)

76-77: ⚠️ Potential issue | 🔴 Critical

Missing await on click action causes race condition.

The .click() on line 76 is not awaited, so the expect on line 77 may execute before the click completes, causing flaky test behavior.

🐛 Proposed fix
-    student2Page.getByLabel("Report Answer").getByRole("button", { name: "Submit" }).click()
+    await student2Page.getByLabel("Report Answer").getByRole("button", { name: "Submit" }).click()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@system-tests/src/tests/mark-as-spam.spec.ts` around lines 76 - 77, Add an
await before the click action to avoid the race condition: await the call on
student2Page.getByLabel("Report Answer").getByRole("button", { name: "Submit"
}).click() so the subsequent await expect(student2Page.getByText("Success", {
exact: true })).toBeVisible() runs only after the click completes; ensure the
click promise is awaited (and optionally await any resulting navigation or
network idle if applicable).
system-tests/src/tests/draft.spec.ts (1)

86-103: ⚠️ Potential issue | 🟠 Major

Wrap mutation clicks with success-notification waits to avoid flakiness.

Line 86 (Create) and Line 102 (Update) trigger backend mutations but proceed without waitForSuccessNotification, so subsequent steps can race ahead.

Suggested fix
-    await page.getByRole("dialog").getByRole("button", { name: "Create" }).click()
+    await waitForSuccessNotification(page, async () => {
+      await page.getByRole("dialog").getByRole("button", { name: "Create" }).click()
+    })
@@
-    await page.getByRole("dialog").getByRole("button", { name: "Update" }).click()
-    await page.getByRole("button", { name: "Update", exact: true }).waitFor({ state: "hidden" })
+    await waitForSuccessNotification(page, async () => {
+      await page.getByRole("dialog").getByRole("button", { name: "Update" }).click()
+    })

As per coding guidelines, "For Playwright system tests, make sure each test that triggers a backend mutation ... waits for a UI confirmation element ... Prefer using waitForSuccessNotification for success toasts."

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

In `@system-tests/src/tests/draft.spec.ts` around lines 86 - 103, The Create and
Update button clicks in the test (the calls that invoke
page.getByRole("dialog").getByRole("button", { name: "Create" }).click() and
page.getByRole("dialog").getByRole("button", { name: "Update" }).click() /
page.getByRole("button", { name: "Update", exact: true }).waitFor(...)) must be
wrapped with waits for the backend mutation success toast: after triggering each
mutation, call the shared helper waitForSuccessNotification (or equivalent) to
wait for the success notification before continuing; update the flows around the
"Create" click and the "Update" click to wait for waitForSuccessNotification()
(or its Promise) so later assertions and interactions are not raced.
system-tests/src/tests/reset-exercises.spec.ts (3)

88-95: ⚠️ Potential issue | 🟡 Minor

Missing wait after "Start peer review" mutation.

Clicking "Start peer review" triggers a backend mutation, but the test immediately navigates to a different URL without waiting for confirmation. This can cause flaky tests if the operation hasn't completed before navigation.

Add a wait for a UI confirmation element (e.g., a success notification or state change) after clicking the button before proceeding with the goto.

As per coding guidelines: "make sure each test that triggers a backend mutation waits for a UI confirmation element that proves the request completed successfully before proceeding."

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

In `@system-tests/src/tests/reset-exercises.spec.ts` around lines 88 - 95, After
clicking the "Start peer review" button via student1Page.getByRole("button", {
name: "Start peer review" }), wait for a UI confirmation (for example a visible
success notification, the appearance/disappearance of a specific toast, or a
change in the button/state such as the "Try again" or a peer-review status
element) before calling student1Page.goto(...). Locate the click in the test and
insert an explicit wait (e.g., waitFor on the confirmation element or
waitForSelector for the toast/state change) to ensure the backend mutation
completed before navigation.

112-114: ⚠️ Potential issue | 🟡 Minor

Missing wait after Submit mutation before navigation.

The Submit click on line 112 triggers a backend mutation, but the test proceeds to navigate to the next page without waiting for confirmation that the submission completed.

Add a wait for a confirmation element (similar to the pattern used elsewhere in this test with waitFor({ state: "visible" })).

Proposed fix
   await student1Page.getByRole("button", { name: "Submit" }).click()
+  await student1Page.getByRole("button", { name: "Try again" }).waitFor({ state: "visible" })

   await navigateToNextPageInMaterial(student1Page)

As per coding guidelines: "make sure each test that triggers a backend mutation waits for a UI confirmation element that proves the request completed successfully before proceeding."

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

In `@system-tests/src/tests/reset-exercises.spec.ts` around lines 112 - 114, After
calling student1Page.getByRole("button", { name: "Submit" }).click(), add a wait
for the submission confirmation UI (use the same pattern as elsewhere by calling
waitFor({ state: "visible" }) on the confirmation element) before calling
navigateToNextPageInMaterial; locate the confirmation element the test uses
elsewhere (e.g., the "submission saved" / success text or specific selector used
with waitFor) and await it to ensure the backend mutation completed before
navigation.

203-203: ⚠️ Potential issue | 🟡 Minor

Typo: "resest" → "resets".

Proposed fix
-  // Admin resest exercises that have a peer review
+  // Admin resets exercises that have a peer review
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@system-tests/src/tests/reset-exercises.spec.ts` at line 203, Update the
inline comment "Admin resest exercises that have a peer review" to correct the
typo: replace "resest" with "resets" so the comment reads "Admin resets
exercises that have a peer review"; locate this string in
system-tests/src/tests/reset-exercises.spec.ts (the comment near the test
describing admin behavior) and make the single-word correction.
system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-column.spec.ts (1)

77-77: ⚠️ Potential issue | 🟠 Major

Fix iframe scoping: use consistent frame references instead of generic selectors.

Lines 77, 108, 136–137 use page.frameLocator("iframe") which may target the wrong iframe when multiple exist. On page-3 (lines 77 and nearby), use frame. On page-4 (after line 96), lines 108, 118, 131, 136–137, and 146 must use frame2.

Additionally, line 14's comment // Go to http://project-331.local/ is redundant—it restates what the code does; remove it.

Proposed fixes
-  await page.frameLocator("iframe").locator('button:has-text("This is second option")').click()
+  await frame.locator('button:has-text("This is second option")').click()

-  await page.frameLocator("iframe").locator('button:has-text("This is first option")').click()
+  await frame2.locator('button:has-text("This is first option")').click()

-      frame.getByText(`Only first option can be correct because it is the first one.`),
+      frame2.getByText(`Only first option can be correct because it is the first one.`),

-    waitForTheseToBeVisibleAndStable: [frame.locator(`text="This is second option"`)],
+    waitForTheseToBeVisibleAndStable: [frame2.locator(`text="This is second option"`)],

-  await page.frameLocator("iframe").locator('button:has-text("This is first option")').click()
-  await page.frameLocator("iframe").locator('button:has-text("This is second option")').click()
+  await frame2.locator('button:has-text("This is first option")').click()
+  await frame2.locator('button:has-text("This is second option")').click()

-      frame.getByText(`Second option can't be the right answer because it isn't the first one.`),
+      frame2.getByText(`Second option can't be the right answer because it isn't the first one.`),

-  // Go to http://project-331.local/
-  await page.goto("http://project-331.local/organizations")
+  await page.goto("http://project-331.local/organizations")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-column.spec.ts`
at line 77, Replace generic page.frameLocator("iframe") calls with the
appropriate named frame variables to avoid targeting the wrong iframe: on page-3
use frame (e.g., replace usages like
page.frameLocator("iframe").locator(...).click() with
frame.locator(...).click()), and on page-4 replace those calls with frame2 (use
frame2.locator(...).click() for lines noted in the review such as the selectors
around "This is second option" and other button/locator interactions). Also
remove the redundant comment "// Go to http://project-331.local/" at the top of
the test file. Ensure you update every instance that currently uses
page.frameLocator("iframe") to the correct frame variable (frame or frame2) so
selectors consistently target the intended iframe.
system-tests/src/tests/query-mutation-notifications/success-notification.spec.ts (1)

17-28: ⚠️ Potential issue | 🟠 Major

Ensure toast mode is always restored, even on assertion failures.

Line 17 changes toast behavior globally, but Line 28 won’t run if Lines 20-27 throw. This can leak state and make later tests flaky. Wrap the mutation/assertion block in try/finally so showToastsNormally(page) always executes.

Proposed fix
   await showNextToastsInfinitely(page)
-  await page.click(`button:text-is("Save") >> visible=true`)
-
-  await expectScreenshotsToMatchSnapshots({
-    screenshotTarget: page,
-    headless,
-    testInfo,
-    snapshotName: "success-notification-test",
-    waitForTheseToBeVisibleAndStable: [page.getByText("Success").first().first()],
-    dontWaitForSpinnersToDisappear: true,
-  })
-  await showToastsNormally(page)
+  try {
+    await page.click(`button:text-is("Save") >> visible=true`)
+
+    await expectScreenshotsToMatchSnapshots({
+      screenshotTarget: page,
+      headless,
+      testInfo,
+      snapshotName: "success-notification-test",
+      waitForTheseToBeVisibleAndStable: [page.getByText("Success").first().first()],
+      dontWaitForSpinnersToDisappear: true,
+    })
+  } finally {
+    await showToastsNormally(page)
+  }
As per coding guidelines, "If someone write a tests that depends on a toast staying on the screen, make sure to use the `showNextToastsInfinitely`, and `showToastsNormally` functions."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@system-tests/src/tests/query-mutation-notifications/success-notification.spec.ts`
around lines 17 - 28, The test currently calls showNextToastsInfinitely(page)
then runs actions and assertions (page.click and
expectScreenshotsToMatchSnapshots) but if those throw the final call
showToastsNormally(page) is skipped; wrap the block from page.click(...) through
expectScreenshotsToMatchSnapshots(...) in a try/finally so
showToastsNormally(page) is always executed to restore toast behavior (refer to
showNextToastsInfinitely, page.click, expectScreenshotsToMatchSnapshots, and
showToastsNormally).
services/quizzes/tests/api/utils/appRouterTestClient.ts (3)

40-41: ⚠️ Potential issue | 🟠 Major

Use binary-safe response forwarding instead of response.text().

The current code converts all responses to text via response.text(), which corrupts binary payloads. Since the handler accepts a generic Web API Response that can contain any data type (not just JSON), forwarding binary data as text loses integrity. Use arrayBuffer() + Buffer for proper byte-safe forwarding.

Suggested fix
-      const responseBody = await response.text()
-      res.end(responseBody)
+      const responseBody = Buffer.from(await response.arrayBuffer())
+      res.end(responseBody)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/quizzes/tests/api/utils/appRouterTestClient.ts` around lines 40 -
41, Replace the text-based forwarding with binary-safe forwarding: call
response.arrayBuffer() instead of response.text(), convert it to a Node Buffer
with Buffer.from(await response.arrayBuffer()), and pass that Buffer to
res.end(...) so binary payloads are preserved; make sure to use the existing
Response object (response) and the same status/headers handling around where
responseBody and res.end(...) are used (e.g., in appRouterTestClient.ts where
responseBody is created and res.end is called).

18-22: ⚠️ Potential issue | 🟠 Major

Normalize incoming Node headers to handle multi-value headers like Set-Cookie correctly.

Line 20 casts req.headers as HeadersInit directly, but Node.js headers contain string | string[] | undefined values. The Fetch API's RequestInit silently concatenates array values with commas (e.g., ["a", "b"] becomes "a,b"), which breaks multi-value headers like Set-Cookie that require separate entries. Additionally, undefined values become the string "undefined" rather than being omitted.

The suggested fix normalizes headers using the Headers object, which correctly handles arrays via append():

Suggested fix
+      const headers = new Headers()
+      for (const [key, value] of Object.entries(req.headers)) {
+        if (value === undefined) continue
+        if (Array.isArray(value)) {
+          for (const v of value) headers.append(key, v)
+        } else {
+          headers.set(key, value)
+        }
+      }
+
       const requestObj = new Request(url, {
         method: req.method,
-        headers: req.headers as HeadersInit,
+        headers,
         body,
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/quizzes/tests/api/utils/appRouterTestClient.ts` around lines 18 -
22, The request creation currently casts req.headers to HeadersInit (the Request
instantiation at requestObj) which concatenates arrays and turns undefined into
"undefined"; instead, normalize Node headers by creating a new Headers()
instance, iterate over Object.entries(req.headers) and for each key: if value is
an array call headers.append for each element, if value is a string call
headers.set, and if value is undefined skip it, then pass that Headers instance
into the Request constructor (replace the direct cast in the Request(url, {...})
call).

34-37: ⚠️ Potential issue | 🟠 Major

Remove unconditional JSON Content-Type fallback.

Lines 34-37 force application/json whenever the handler omits Content-Type. This masks bugs in handler implementations and mislabels responses that are plain text, HTML, binary, or empty. The fallback should be removed; handlers should be responsible for setting appropriate Content-Type headers.

Suggested fix
-      // Ensure Content-Type is set for JSON responses
-      if (!res.getHeader("Content-Type")) {
-        res.setHeader("Content-Type", "application/json")
-      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/quizzes/tests/api/utils/appRouterTestClient.ts` around lines 34 -
37, Remove the unconditional fallback that sets Content-Type to application/json
in the test client: delete the block that checks res.getHeader("Content-Type")
and calls res.setHeader("Content-Type", "application/json") (the lines shown in
appRouterTestClient.ts). Let handlers themselves set appropriate Content-Type
values and update any tests that relied on the fallback to explicitly set
expected headers where needed; keep the rest of the response handling in the
test client unchanged.
🧹 Nitpick comments (17)
system-tests/src/tests/mark-as-spam.spec.ts (2)

44-56: Same pattern: submission without confirmation wait.

Line 55 also submits without waiting for confirmation. If the submission hasn't completed before Student2's test step begins, it could affect the peer review availability.

Consider adding a confirmation wait or using waitForSuccessNotification for consistency with the coding guidelines.

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

In `@system-tests/src/tests/mark-as-spam.spec.ts` around lines 44 - 56, The
Student1 submission step (inside the test.step "Student1 submits answer") clicks
the Submit button on student1Page but doesn't wait for completion; after calling
student1Page.getByRole("button", { name: "Submit" }).click(), wait for the
confirmation by either using the existing helper
waitForSuccessNotification(student1Page) or an equivalent assertion (e.g., wait
for the success toast or the post-submission state) to ensure the submission is
finished before proceeding to Student2's steps.

69-71: Consider waiting for submission confirmation before proceeding.

The submit button click triggers a backend mutation but the test immediately proceeds to click "Start peer review" without verifying the submission completed. This could cause flaky behavior if the submission hasn't finished processing.

As per coding guidelines: "make sure each test that triggers a backend mutation (e.g., clicking a save button) waits for a UI confirmation element that proves the request completed successfully before proceeding."

♻️ Suggested approach
     await student2Page.getByRole("button", { name: "Submit" }).click()
+    // Wait for submission confirmation before starting peer review
+    await expect(student2Page.getByText(/submitted/i)).toBeVisible()

     await student2Page.getByRole("button", { name: "Start peer review" }).click()

Alternatively, if there's a success toast, use waitForSuccessNotification:

await waitForSuccessNotification(student2Page, async () => {
  await student2Page.getByRole("button", { name: "Submit" }).click()
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@system-tests/src/tests/mark-as-spam.spec.ts` around lines 69 - 71, The test
clicks the Submit button then immediately clicks "Start peer review", risking
flakiness because the backend mutation may not have completed; update the flow
around student2Page.getByRole("button", { name: "Submit" }).click() to wait for
a UI confirmation before proceeding—either wrap the click with the helper
waitForSuccessNotification(student2Page, async () => { ... }) or explicitly
await a success toast or confirmation element (e.g., await
student2Page.locator(...).waitFor()) before calling
student2Page.getByRole("button", { name: "Start peer review" }).click(); ensure
the chosen confirmation is deterministic and related to the submission action.
system-tests/src/tests/user-research-consent.spec.ts (1)

68-69: Consider replacing networkidle with a more reliable wait.

While the ESLint disable comment acknowledges the issue, waitForLoadState("networkidle") can be flaky. Consider waiting for a specific UI element that indicates the page is ready instead.

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

In `@system-tests/src/tests/user-research-consent.spec.ts` around lines 68 - 69,
Replace the flaky waitForLoadState("networkidle") with an explicit wait for a
specific UI element that signals readiness: locate the element that shows the
consent UI (e.g., the consent banner/form button) and change the call to await
page.waitForSelector('<CONSENT_SELECTOR>') instead of
page.waitForLoadState("networkidle"); update the test in
user-research-consent.spec.ts where page.waitForLoadState is used so the test
waits deterministically for that selector before interacting with the page.
services/headless-lms/doc-macro/Cargo.toml (1)

19-19: Use local #[allow(clippy::expect_used)] instead of global allow for better intent clarity.

The crate has only 4 expect() callsites, all with clear invariant-checking rationale in parsing/generation logic. Allowing expect_used globally while denying unwrap_used creates policy asymmetry; consider adding targeted #[allow(clippy::expect_used)] at each callsite instead to make the intent explicit and maintain consistent panic-prevention policy.

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

In `@services/headless-lms/doc-macro/Cargo.toml` at line 19, Remove the global
expect allowance by deleting the expect_used = "allow" entry from Cargo.toml and
instead add targeted #[allow(clippy::expect_used)] attributes on the specific
functions/methods that call expect() (the four parsing/generation callsites) so
each callsite is annotated (place the attribute on the enclosing fn or impl
block and add a short comment documenting the invariant that justifies the
expect). Make sure no other global clippy allow for expect remains and run cargo
clippy to verify only those annotated locations suppress the lint.
system-tests/src/tests/reset-exercises.spec.ts (1)

146-149: Prefer semantic selectors over index-based checkbox selection.

These lines use fragile index-based selectors (nth(3), nth(4), etc.) that will break if the checkbox order changes. Compare to the better pattern used later at lines 171-172:

await adminPage.getByRole("row", { name: "Multiple task exercise Page" }).getByLabel("").check()

Consider refactoring to use row names or other semantic identifiers to make the test more resilient to UI changes.

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

In `@system-tests/src/tests/reset-exercises.spec.ts` around lines 146 - 149, The
test uses fragile index-based checkbox selection via
adminPage.locator('input[type="checkbox"]').nth(3/4/5/6'); replace each nth(...)
call with semantic selectors—e.g., find the specific row by its visible name
using adminPage.getByRole("row", { name: "<row name>" }) and then call
.getByLabel("<checkbox label>" or "")/ .locator('input[type="checkbox"]') on
that row before .check(); update the four checkbox lines to target their rows or
labels so the assertions use stable, descriptive selectors instead of numeric
indexes.
system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-column.spec.ts (1)

14-14: Drop this redundant navigation comment.

Line 14 repeats the intent already obvious from Line 15.

As per coding guidelines, "Prefer comment quality over comment quantity. Do not add comments that simply restate what the code already says, narrate control flow, or explain obvious syntax."

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

In
`@system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-column.spec.ts`
at line 14, Remove the redundant inline comment "// Go to
http://project-331.local/" that merely repeats the next statement; simply delete
that comment so the test reads directly with the navigation call (e.g., the
cy.visit or equivalent on the following line) without the duplicate explanatory
line.
system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-row.spec.ts (2)

17-19: Remove the pseudo-parallel Promise.all wrapper.

Line 17 executes sequentially because of the inner await, so Promise.all adds noise without benefit.

♻️ Proposed cleanup
-  await Promise.all([
-    await selectOrganization(page, "University of Helsinki, Department of Computer Science"),
-  ])
+  await selectOrganization(page, "University of Helsinki, Department of Computer Science")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-row.spec.ts`
around lines 17 - 19, Remove the unnecessary Promise.all wrapper around a single
awaited call: replace the current await Promise.all([ await
selectOrganization(page, "University of Helsinki, Department of Computer
Science") ]) by directly awaiting the selectOrganization call (await
selectOrganization(page, "University of Helsinki, Department of Computer
Science")); if you intended true parallelism with multiple calls, instead pass
raw promises to Promise.all (e.g., Promise.all([selectOrganization(...),
otherCall(...)]) ) but do not use an inner await.

14-14: Drop this redundant navigation comment.

Line 14 restates exactly what Line 15 already expresses and adds no extra rationale.

As per coding guidelines, "Prefer comment quality over comment quantity. Do not add comments that simply restate what the code already says, narrate control flow, or explain obvious syntax."

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

In
`@system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-row.spec.ts`
at line 14, Remove the redundant navigation comment "// Go to
http://project-331.local/" in the
multiple-choice-course-material-widget-row.spec.ts test file (it duplicates the
next line). Delete that comment line so the test only contains the actual
navigation code, or replace it with a concise, non-redundant explanation only if
additional context is necessary.
services/headless-lms/server/src/test_helper.rs (1)

169-195: LGTM - Functional but verbose.

The fully-qualified trait syntax works correctly for generating random strings in macros where import hygiene matters. The pattern is repeated three times (lines 169-173, 180-184, 191-195).

If desired, the verbosity could be reduced by importing the trait at the macro expansion site or using a helper function, but the current approach is functionally correct for test code.

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

In `@services/headless-lms/server/src/test_helper.rs` around lines 169 - 195, The
repeated fully-qualified calls to ::rand::distr::Alphanumeric and
::rand::distr::SampleString in the macro arms (the `@inner` patterns creating rs)
are verbose; simplify by centralizing randomness: either add a small helper
function (e.g., sample_random_alphanumeric(len) used by the macro) or import the
trait/type at the macro expansion site so you can call
Alphanumeric::sample_string(&Alphanumeric, &mut rand::rng(), 8) or
SampleString::sample_string(...) with shorter names; update the macro arms that
reference ::rand::distr::Alphanumeric, SampleString and rand::rng() to call the
helper or the imported names to remove duplication.
services/headless-lms/models/src/user_chapter_locking_statuses.rs (1)

268-291: Nested async function is unconventional but functional.

Defining get_statuses_for_user_and_course as a nested async function inside get_or_init_all_for_course works but is unusual. Consider extracting it as a private module-level function for better readability and potential reuse.

♻️ Optional: Extract as module-level function
+async fn get_statuses_for_user_and_course(
+    conn: &mut PgConnection,
+    user_id: Uuid,
+    course_id: Uuid,
+) -> ModelResult<Vec<UserChapterLockingStatus>> {
+    let rows = sqlx::query_as!(
+        DatabaseRow,
+        r#"
+SELECT id, created_at, updated_at, deleted_at, user_id, chapter_id, course_id, status::text as "status!"
+FROM user_chapter_locking_statuses
+WHERE user_id = $1
+  AND course_id = $2
+  AND deleted_at IS NULL
+        "#,
+        user_id,
+        course_id
+    )
+    .fetch_all(&mut *conn)
+    .await?;
+
+    rows.into_iter()
+        .map(|r| r.try_into())
+        .collect::<ModelResult<Vec<_>>>()
+}
+
 pub async fn get_or_init_all_for_course(
     // ... remove the nested function definition
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/headless-lms/models/src/user_chapter_locking_statuses.rs` around
lines 268 - 291, Extract the nested async function
get_statuses_for_user_and_course out of get_or_init_all_for_course into a
private module-level async fn with the same signature (async fn
get_statuses_for_user_and_course(conn: &mut PgConnection, user_id: Uuid,
course_id: Uuid) -> ModelResult<Vec<UserChapterLockingStatus>>) and move its SQL
query body unchanged; update the call site inside get_or_init_all_for_course to
call the new module-level function and ensure required types (DatabaseRow,
UserChapterLockingStatus, ModelResult, Uuid, PgConnection) are in scope (add use
statements if needed) and remove the nested function definition.
services/quizzes/tests/api/utils/appRouterTestClient.ts (1)

13-39: Trim redundant inline comments in the listener body.

Several comments here restate obvious operations (status/header/body setting), which makes the helper noisier to scan. Keep only intent/rationale comments where behavior is non-obvious.

As per coding guidelines, "Prefer comment quality over comment quantity... Do not add comments that simply restate what the code already says..."

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

In `@services/quizzes/tests/api/utils/appRouterTestClient.ts` around lines 13 -
39, Remove redundant, self‑evident inline comments in the listener body and keep
only concise intent comments: delete comments that simply restate the code
(e.g., "Convert Node.js IncomingMessage to Web API Request", "Create a proper
Request object using the Web API", "Set response status", "Set response
headers", "Send response body"), and retain a short explanatory comment where
behavior is non‑obvious (for example keep one line explaining why you set a
default Content-Type for JSON). Locate the block around req, getBody, new
Request(...), handler(requestObj), response, and res in appRouterTestClient.ts
and simplify to a single intent comment plus the Content-Type rationale.
shared-module/packages/common/pnpm-workspace.yaml (1)

21-24: Deduplicate overlapping minimatch override ranges.

Line 22 already covers Line 21, and Line 24 already covers Line 23. Removing redundant entries keeps the override set easier to audit.

♻️ Proposed simplification
 overrides:
   glob@>=10.2.0 <10.5.0: '>=10.5.0'
   js-yaml@<3.14.2: '>=3.14.2'
   js-yaml@>=4.0.0 <4.1.1: '>=4.1.1'
-  minimatch@<3.1.3: '>=3.1.3'
   minimatch@<3.1.4: '>=3.1.4'
-  minimatch@>=9.0.0 <9.0.6: '>=9.0.6'
   minimatch@>=9.0.0 <9.0.7: '>=9.0.7'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared-module/packages/common/pnpm-workspace.yaml` around lines 21 - 24,
Remove the redundant minimatch overrides: delete the entries "minimatch@<3.1.3:
'>=3.1.3'" and "minimatch@>=9.0.0 <9.0.6: '>=9.0.6'" because they are subsumed
by "minimatch@<3.1.4: '>=3.1.4'" and "minimatch@>=9.0.0 <9.0.7: '>=9.0.7'";
after removing them, ensure the pnpm-workspace.yaml remains valid YAML (correct
indentation/quotes) and keep the remaining minimatch override entries intact.
services/main-frontend/pnpm-workspace.yaml (1)

20-23: Remove overlapping minimatch overrides to reduce config noise.

Line 21 subsumes Line 20, and Line 23 subsumes Line 22. Keeping only the stricter ranges preserves behavior and avoids ambiguity during maintenance.

♻️ Proposed simplification
 overrides:
   glob@>=10.2.0 <10.5.0: '>=10.5.0'
   js-yaml@>=4.0.0 <4.1.1: '>=4.1.1'
-  minimatch@<3.1.3: '>=3.1.3'
   minimatch@<3.1.4: '>=3.1.4'
-  minimatch@>=9.0.0 <9.0.6: '>=9.0.6'
   minimatch@>=9.0.0 <9.0.7: '>=9.0.7'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/main-frontend/pnpm-workspace.yaml` around lines 20 - 23, Remove the
overlapping minimatch overrides and keep only the stricter selectors: delete the
minimatch@<3.1.3: '>=3.1.3' entry and the minimatch@>=9.0.0 <9.0.6: '>=9.0.6'
entry, leaving minimatch@<3.1.4: '>=3.1.4' and minimatch@>=9.0.0 <9.0.7:
'>=9.0.7' so behavior is preserved and duplicate/ambiguous overrides are
eliminated.
bin/extract-gutenberg-types (1)

26-30: Optional: avoid double-formatting the same type files.

These new conditional blocks are valid, but combined with the earlier formatter call they can format the same .ts files twice per run. Consider formatting once at the end per target pair.

Also applies to: 33-37

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

In `@bin/extract-gutenberg-types` around lines 26 - 30, The script currently calls
run_command pnpm exec prettier twice for the same targets
(services/main-frontend/types/GutenbergBlockAttributes.ts and
GutenbergBlockAttributes.guard.ts) which can double-format; update the logic in
the extract-gutenberg-types script to collect the target files (e.g.,
GutenbergBlockAttributes.ts and GutenbergBlockAttributes.guard.ts) into a unique
list and invoke a single run_command prettier call per target pair (or run one
final prettier invocation for all unique files) instead of calling prettier in
both the earlier formatter call and again in the conditional blocks—modify the
sections that call run_command pnpm exec prettier so they use the deduplicated
list or a single post-processing prettier step.
services/main-frontend/src/components/course-material/ContentRenderer/core/common/Image/ImageBlock.tsx (2)

108-108: Remove unnecessary fragment wrapper.

The fragment around renderImage() is redundant when disableInteractivity is false.

♻️ Simplify rendering
-          {disableInteractivity ? renderImage() : <>{renderImage()}</>}
+          {renderImage()}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/main-frontend/src/components/course-material/ContentRenderer/core/common/Image/ImageBlock.tsx`
at line 108, The JSX contains a redundant fragment: in the conditional
expression inside ImageBlock the false branch wraps renderImage() in an
unnecessary <>...</>; replace the ternary expression so both branches render the
component directly (use disableInteractivity ? renderImage() : renderImage() or
simply renderImage() with conditional props) — update the usage around
disableInteractivity and renderImage() to remove the fragment wrapper.

68-70: Use VisuallyHidden from react-aria instead of CSS class.

Per coding guidelines, screen-reader-only text should use VisuallyHidden from react-aria rather than custom CSS classes.

♻️ Proposed refactor
+import { VisuallyHidden } from "react-aria"
 // ...
       {linkTarget && linkTarget.includes("_blank") && (
-        <span className="screen-reader-only">{t("screen-reader-opens-in-new-tab")}</span>
+        <VisuallyHidden>{t("screen-reader-opens-in-new-tab")}</VisuallyHidden>
       )}

As per coding guidelines: "For screen-reader-only text, use VisuallyHidden from react-aria instead of custom sr-only / visually-hidden CSS."

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

In
`@services/main-frontend/src/components/course-material/ContentRenderer/core/common/Image/ImageBlock.tsx`
around lines 68 - 70, Replace the custom screen-reader-only span in ImageBlock
(the conditional that renders {linkTarget && linkTarget.includes("_blank") &&
<span
className="screen-reader-only">{t("screen-reader-opens-in-new-tab")}</span>})
with react-aria's VisuallyHidden: import VisuallyHidden from
'@react-aria/visually-hidden' (or the project-specific react-aria entry) at the
top of the file and render
<VisuallyHidden>{t("screen-reader-opens-in-new-tab")}</VisuallyHidden> in place
of the span so the translation string from t(...) is preserved and accessibility
follows the coding guideline.
services/main-frontend/src/components/course-material/ContentRenderer/core/formatting/VerseBlock.tsx (1)

12-24: Consider validating textAlign values before interpolation.

The textAlign value is directly interpolated into CSS. While Gutenberg typically provides sanitized values, consider constraining to known valid values (left, center, right, justify) for defense-in-depth, consistent with how align is validated in other block types.

🛡️ Optional: Constrain textAlign to valid CSS values
 interface ExtraAttributes {
-  textAlign?: string
+  textAlign?: "left" | "center" | "right" | "justify"
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/main-frontend/src/components/course-material/ContentRenderer/core/formatting/VerseBlock.tsx`
around lines 12 - 24, The code interpolates data.attributes.textAlign directly
into the CSS inside VerseBlock (ExtraAttributes → data.attributes → textAlign);
validate textAlign against a whitelist (e.g., "left","center","right","justify")
before using it in the className interpolation and fall back to a safe default
or omit the rule if invalid. Add a small guard in VerseBlock that
normalizes/checks textAlign (e.g., const safeTextAlign = allowed.has(textAlign)
? textAlign : undefined) and then use safeTextAlign in the template instead of
the raw value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pnpm-workspace.yaml`:
- Around line 15-17: The onlyBuiltDependencies entry currently has a single
malformed scalar "esbuild unrs-resolver"; split it into two distinct list items
so each package is its own entry under onlyBuiltDependencies (e.g., create
separate items for esbuild and unrs-resolver) so strictDepBuilds can correctly
exclude each package; update the onlyBuiltDependencies list to standard YAML
list syntax ensuring "esbuild" and "unrs-resolver" appear as separate entries.

In `@services/cms/pnpm-workspace.yaml`:
- Around line 7-10: The minimumReleaseAgeExclude array (key name
minimumReleaseAgeExclude) is missing "react-dom"; update the list that currently
contains "next", "react", and "@next/*" to also include "react-dom" so React and
ReactDOM are both excluded from release-age gating and remain
version-synchronized with other workspaces.

In `@services/headless-lms/utils/Cargo.toml`:
- Line 40: Replace the deprecated reqwest builder call .use_rustls_tls() with
.tls_backend_rustls() in the HTTP client construction (locate the
reqwest::Client::builder() usage in services/headless-lms/utils/src/http.rs),
and ensure the Cargo.toml has reqwest compiled with the rustls backend feature
so the new tls_backend_rustls() method is available; leave rand = "0.10.0" as-is
since no changes are required for its usage.

In `@shared-module/packages/common/package.json`:
- Line 77: Update the `@types/node` dependency in package.json to match the
runtime major (Node 24) so type definitions align with the pinned runtime;
locate the "@types/node" entry in shared-module/packages/common/package.json and
change its version spec (currently "^25.3.3") to a 24.x range (for example
"^24.0.0" or a more specific 24.y.z you prefer), then run install/typecheck to
verify no type mismatches.

---

Outside diff comments:
In `@services/quizzes/tests/api/utils/appRouterTestClient.ts`:
- Around line 40-41: Replace the text-based forwarding with binary-safe
forwarding: call response.arrayBuffer() instead of response.text(), convert it
to a Node Buffer with Buffer.from(await response.arrayBuffer()), and pass that
Buffer to res.end(...) so binary payloads are preserved; make sure to use the
existing Response object (response) and the same status/headers handling around
where responseBody and res.end(...) are used (e.g., in appRouterTestClient.ts
where responseBody is created and res.end is called).
- Around line 18-22: The request creation currently casts req.headers to
HeadersInit (the Request instantiation at requestObj) which concatenates arrays
and turns undefined into "undefined"; instead, normalize Node headers by
creating a new Headers() instance, iterate over Object.entries(req.headers) and
for each key: if value is an array call headers.append for each element, if
value is a string call headers.set, and if value is undefined skip it, then pass
that Headers instance into the Request constructor (replace the direct cast in
the Request(url, {...}) call).
- Around line 34-37: Remove the unconditional fallback that sets Content-Type to
application/json in the test client: delete the block that checks
res.getHeader("Content-Type") and calls res.setHeader("Content-Type",
"application/json") (the lines shown in appRouterTestClient.ts). Let handlers
themselves set appropriate Content-Type values and update any tests that relied
on the fallback to explicitly set expected headers where needed; keep the rest
of the response handling in the test client unchanged.

In `@system-tests/src/tests/draft.spec.ts`:
- Around line 86-103: The Create and Update button clicks in the test (the calls
that invoke page.getByRole("dialog").getByRole("button", { name: "Create"
}).click() and page.getByRole("dialog").getByRole("button", { name: "Update"
}).click() / page.getByRole("button", { name: "Update", exact: true
}).waitFor(...)) must be wrapped with waits for the backend mutation success
toast: after triggering each mutation, call the shared helper
waitForSuccessNotification (or equivalent) to wait for the success notification
before continuing; update the flows around the "Create" click and the "Update"
click to wait for waitForSuccessNotification() (or its Promise) so later
assertions and interactions are not raced.

In `@system-tests/src/tests/mark-as-spam.spec.ts`:
- Around line 76-77: Add an await before the click action to avoid the race
condition: await the call on student2Page.getByLabel("Report
Answer").getByRole("button", { name: "Submit" }).click() so the subsequent await
expect(student2Page.getByText("Success", { exact: true })).toBeVisible() runs
only after the click completes; ensure the click promise is awaited (and
optionally await any resulting navigation or network idle if applicable).

In
`@system-tests/src/tests/query-mutation-notifications/success-notification.spec.ts`:
- Around line 17-28: The test currently calls showNextToastsInfinitely(page)
then runs actions and assertions (page.click and
expectScreenshotsToMatchSnapshots) but if those throw the final call
showToastsNormally(page) is skipped; wrap the block from page.click(...) through
expectScreenshotsToMatchSnapshots(...) in a try/finally so
showToastsNormally(page) is always executed to restore toast behavior (refer to
showNextToastsInfinitely, page.click, expectScreenshotsToMatchSnapshots, and
showToastsNormally).

In
`@system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-column.spec.ts`:
- Line 77: Replace generic page.frameLocator("iframe") calls with the
appropriate named frame variables to avoid targeting the wrong iframe: on page-3
use frame (e.g., replace usages like
page.frameLocator("iframe").locator(...).click() with
frame.locator(...).click()), and on page-4 replace those calls with frame2 (use
frame2.locator(...).click() for lines noted in the review such as the selectors
around "This is second option" and other button/locator interactions). Also
remove the redundant comment "// Go to http://project-331.local/" at the top of
the test file. Ensure you update every instance that currently uses
page.frameLocator("iframe") to the correct frame variable (frame or frame2) so
selectors consistently target the intended iframe.

In `@system-tests/src/tests/reset-exercises.spec.ts`:
- Around line 88-95: After clicking the "Start peer review" button via
student1Page.getByRole("button", { name: "Start peer review" }), wait for a UI
confirmation (for example a visible success notification, the
appearance/disappearance of a specific toast, or a change in the button/state
such as the "Try again" or a peer-review status element) before calling
student1Page.goto(...). Locate the click in the test and insert an explicit wait
(e.g., waitFor on the confirmation element or waitForSelector for the
toast/state change) to ensure the backend mutation completed before navigation.
- Around line 112-114: After calling student1Page.getByRole("button", { name:
"Submit" }).click(), add a wait for the submission confirmation UI (use the same
pattern as elsewhere by calling waitFor({ state: "visible" }) on the
confirmation element) before calling navigateToNextPageInMaterial; locate the
confirmation element the test uses elsewhere (e.g., the "submission saved" /
success text or specific selector used with waitFor) and await it to ensure the
backend mutation completed before navigation.
- Line 203: Update the inline comment "Admin resest exercises that have a peer
review" to correct the typo: replace "resest" with "resets" so the comment reads
"Admin resets exercises that have a peer review"; locate this string in
system-tests/src/tests/reset-exercises.spec.ts (the comment near the test
describing admin behavior) and make the single-word correction.

---

Nitpick comments:
In `@bin/extract-gutenberg-types`:
- Around line 26-30: The script currently calls run_command pnpm exec prettier
twice for the same targets
(services/main-frontend/types/GutenbergBlockAttributes.ts and
GutenbergBlockAttributes.guard.ts) which can double-format; update the logic in
the extract-gutenberg-types script to collect the target files (e.g.,
GutenbergBlockAttributes.ts and GutenbergBlockAttributes.guard.ts) into a unique
list and invoke a single run_command prettier call per target pair (or run one
final prettier invocation for all unique files) instead of calling prettier in
both the earlier formatter call and again in the conditional blocks—modify the
sections that call run_command pnpm exec prettier so they use the deduplicated
list or a single post-processing prettier step.

In `@services/headless-lms/doc-macro/Cargo.toml`:
- Line 19: Remove the global expect allowance by deleting the expect_used =
"allow" entry from Cargo.toml and instead add targeted
#[allow(clippy::expect_used)] attributes on the specific functions/methods that
call expect() (the four parsing/generation callsites) so each callsite is
annotated (place the attribute on the enclosing fn or impl block and add a short
comment documenting the invariant that justifies the expect). Make sure no other
global clippy allow for expect remains and run cargo clippy to verify only those
annotated locations suppress the lint.

In `@services/headless-lms/models/src/user_chapter_locking_statuses.rs`:
- Around line 268-291: Extract the nested async function
get_statuses_for_user_and_course out of get_or_init_all_for_course into a
private module-level async fn with the same signature (async fn
get_statuses_for_user_and_course(conn: &mut PgConnection, user_id: Uuid,
course_id: Uuid) -> ModelResult<Vec<UserChapterLockingStatus>>) and move its SQL
query body unchanged; update the call site inside get_or_init_all_for_course to
call the new module-level function and ensure required types (DatabaseRow,
UserChapterLockingStatus, ModelResult, Uuid, PgConnection) are in scope (add use
statements if needed) and remove the nested function definition.

In `@services/headless-lms/server/src/test_helper.rs`:
- Around line 169-195: The repeated fully-qualified calls to
::rand::distr::Alphanumeric and ::rand::distr::SampleString in the macro arms
(the `@inner` patterns creating rs) are verbose; simplify by centralizing
randomness: either add a small helper function (e.g.,
sample_random_alphanumeric(len) used by the macro) or import the trait/type at
the macro expansion site so you can call
Alphanumeric::sample_string(&Alphanumeric, &mut rand::rng(), 8) or
SampleString::sample_string(...) with shorter names; update the macro arms that
reference ::rand::distr::Alphanumeric, SampleString and rand::rng() to call the
helper or the imported names to remove duplication.

In `@services/main-frontend/pnpm-workspace.yaml`:
- Around line 20-23: Remove the overlapping minimatch overrides and keep only
the stricter selectors: delete the minimatch@<3.1.3: '>=3.1.3' entry and the
minimatch@>=9.0.0 <9.0.6: '>=9.0.6' entry, leaving minimatch@<3.1.4: '>=3.1.4'
and minimatch@>=9.0.0 <9.0.7: '>=9.0.7' so behavior is preserved and
duplicate/ambiguous overrides are eliminated.

In
`@services/main-frontend/src/components/course-material/ContentRenderer/core/common/Image/ImageBlock.tsx`:
- Line 108: The JSX contains a redundant fragment: in the conditional expression
inside ImageBlock the false branch wraps renderImage() in an unnecessary
<>...</>; replace the ternary expression so both branches render the component
directly (use disableInteractivity ? renderImage() : renderImage() or simply
renderImage() with conditional props) — update the usage around
disableInteractivity and renderImage() to remove the fragment wrapper.
- Around line 68-70: Replace the custom screen-reader-only span in ImageBlock
(the conditional that renders {linkTarget && linkTarget.includes("_blank") &&
<span
className="screen-reader-only">{t("screen-reader-opens-in-new-tab")}</span>})
with react-aria's VisuallyHidden: import VisuallyHidden from
'@react-aria/visually-hidden' (or the project-specific react-aria entry) at the
top of the file and render
<VisuallyHidden>{t("screen-reader-opens-in-new-tab")}</VisuallyHidden> in place
of the span so the translation string from t(...) is preserved and accessibility
follows the coding guideline.

In
`@services/main-frontend/src/components/course-material/ContentRenderer/core/formatting/VerseBlock.tsx`:
- Around line 12-24: The code interpolates data.attributes.textAlign directly
into the CSS inside VerseBlock (ExtraAttributes → data.attributes → textAlign);
validate textAlign against a whitelist (e.g., "left","center","right","justify")
before using it in the className interpolation and fall back to a safe default
or omit the rule if invalid. Add a small guard in VerseBlock that
normalizes/checks textAlign (e.g., const safeTextAlign = allowed.has(textAlign)
? textAlign : undefined) and then use safeTextAlign in the template instead of
the raw value.

In `@services/quizzes/tests/api/utils/appRouterTestClient.ts`:
- Around line 13-39: Remove redundant, self‑evident inline comments in the
listener body and keep only concise intent comments: delete comments that simply
restate the code (e.g., "Convert Node.js IncomingMessage to Web API Request",
"Create a proper Request object using the Web API", "Set response status", "Set
response headers", "Send response body"), and retain a short explanatory comment
where behavior is non‑obvious (for example keep one line explaining why you set
a default Content-Type for JSON). Locate the block around req, getBody, new
Request(...), handler(requestObj), response, and res in appRouterTestClient.ts
and simplify to a single intent comment plus the Content-Type rationale.

In `@shared-module/packages/common/pnpm-workspace.yaml`:
- Around line 21-24: Remove the redundant minimatch overrides: delete the
entries "minimatch@<3.1.3: '>=3.1.3'" and "minimatch@>=9.0.0 <9.0.6: '>=9.0.6'"
because they are subsumed by "minimatch@<3.1.4: '>=3.1.4'" and
"minimatch@>=9.0.0 <9.0.7: '>=9.0.7'"; after removing them, ensure the
pnpm-workspace.yaml remains valid YAML (correct indentation/quotes) and keep the
remaining minimatch override entries intact.

In `@system-tests/src/tests/mark-as-spam.spec.ts`:
- Around line 44-56: The Student1 submission step (inside the test.step
"Student1 submits answer") clicks the Submit button on student1Page but doesn't
wait for completion; after calling student1Page.getByRole("button", { name:
"Submit" }).click(), wait for the confirmation by either using the existing
helper waitForSuccessNotification(student1Page) or an equivalent assertion
(e.g., wait for the success toast or the post-submission state) to ensure the
submission is finished before proceeding to Student2's steps.
- Around line 69-71: The test clicks the Submit button then immediately clicks
"Start peer review", risking flakiness because the backend mutation may not have
completed; update the flow around student2Page.getByRole("button", { name:
"Submit" }).click() to wait for a UI confirmation before proceeding—either wrap
the click with the helper waitForSuccessNotification(student2Page, async () => {
... }) or explicitly await a success toast or confirmation element (e.g., await
student2Page.locator(...).waitFor()) before calling
student2Page.getByRole("button", { name: "Start peer review" }).click(); ensure
the chosen confirmation is deterministic and related to the submission action.

In
`@system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-column.spec.ts`:
- Line 14: Remove the redundant inline comment "// Go to
http://project-331.local/" that merely repeats the next statement; simply delete
that comment so the test reads directly with the navigation call (e.g., the
cy.visit or equivalent on the following line) without the duplicate explanatory
line.

In
`@system-tests/src/tests/quizzes/widget/multiple-choice-course-material-widget-row.spec.ts`:
- Around line 17-19: Remove the unnecessary Promise.all wrapper around a single
awaited call: replace the current await Promise.all([ await
selectOrganization(page, "University of Helsinki, Department of Computer
Science") ]) by directly awaiting the selectOrganization call (await
selectOrganization(page, "University of Helsinki, Department of Computer
Science")); if you intended true parallelism with multiple calls, instead pass
raw promises to Promise.all (e.g., Promise.all([selectOrganization(...),
otherCall(...)]) ) but do not use an inner await.
- Line 14: Remove the redundant navigation comment "// Go to
http://project-331.local/" in the
multiple-choice-course-material-widget-row.spec.ts test file (it duplicates the
next line). Delete that comment line so the test only contains the actual
navigation code, or replace it with a concise, non-redundant explanation only if
additional context is necessary.

In `@system-tests/src/tests/reset-exercises.spec.ts`:
- Around line 146-149: The test uses fragile index-based checkbox selection via
adminPage.locator('input[type="checkbox"]').nth(3/4/5/6'); replace each nth(...)
call with semantic selectors—e.g., find the specific row by its visible name
using adminPage.getByRole("row", { name: "<row name>" }) and then call
.getByLabel("<checkbox label>" or "")/ .locator('input[type="checkbox"]') on
that row before .check(); update the four checkbox lines to target their rows or
labels so the assertions use stable, descriptive selectors instead of numeric
indexes.

In `@system-tests/src/tests/user-research-consent.spec.ts`:
- Around line 68-69: Replace the flaky waitForLoadState("networkidle") with an
explicit wait for a specific UI element that signals readiness: locate the
element that shows the consent UI (e.g., the consent banner/form button) and
change the call to await page.waitForSelector('<CONSENT_SELECTOR>') instead of
page.waitForLoadState("networkidle"); update the test in
user-research-consent.spec.ts where page.waitForLoadState is used so the test
waits deterministically for that selector before interacting with the page.

Comment on lines +15 to +17
onlyBuiltDependencies:
- esbuild unrs-resolver

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "pnpm-workspace.yaml" -type f | head -5

Repository: rage/secret-project-331

Length of output: 255


🏁 Script executed:

cat -n pnpm-workspace.yaml | sed -n '10,20p'

Repository: rage/secret-project-331

Length of output: 325


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import pathlib, re

for p in pathlib.Path('.').rglob('pnpm-workspace.yaml'):
    lines = p.read_text(encoding='utf-8').splitlines()
    in_block = False
    for i, line in enumerate(lines, start=1):
        if re.match(r'^\s*onlyBuiltDependencies:\s*$', line):
            in_block = True
            continue
        if in_block:
            if re.match(r'^\S', line):
                in_block = False
                continue
            m = re.match(r'^\s*-\s*(.+?)\s*$', line)
            if not m:
                continue
            item = m.group(1).strip("'\"")
            if ' ' in item:
                print(f"{p}:{i}: suspicious onlyBuiltDependencies item -> {item!r}")
PY

Repository: rage/secret-project-331

Length of output: 155


Fix malformed onlyBuiltDependencies entry.

Line 16 combines two package names into one scalar (esbuild unrs-resolver). With strictDepBuilds enabled, this prevents the intended packages from being excluded from dependency builds.

Suggested patch
 onlyBuiltDependencies:
-  - esbuild unrs-resolver
+  - esbuild
+  - unrs-resolver
📝 Committable suggestion

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

Suggested change
onlyBuiltDependencies:
- esbuild unrs-resolver
onlyBuiltDependencies:
- esbuild
- unrs-resolver
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pnpm-workspace.yaml` around lines 15 - 17, The onlyBuiltDependencies entry
currently has a single malformed scalar "esbuild unrs-resolver"; split it into
two distinct list items so each package is its own entry under
onlyBuiltDependencies (e.g., create separate items for esbuild and
unrs-resolver) so strictDepBuilds can correctly exclude each package; update the
onlyBuiltDependencies list to standard YAML list syntax ensuring "esbuild" and
"unrs-resolver" appear as separate entries.

Comment on lines +7 to +10
minimumReleaseAgeExclude:
- next
- react
- '@next/*'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, let's check if the file exists and see its current content
echo "=== Checking services/cms/pnpm-workspace.yaml ==="
if [ -f services/cms/pnpm-workspace.yaml ]; then
  echo "File found. Lines 7-10:"
  sed -n '7,10p' services/cms/pnpm-workspace.yaml
  echo ""
  echo "Full minimumReleaseAgeExclude section:"
  sed -n '/minimumReleaseAgeExclude:/,/^[^ -]/p' services/cms/pnpm-workspace.yaml | head -20
else
  echo "File not found at services/cms/pnpm-workspace.yaml"
fi

echo ""
echo "=== Running verification script to find all pnpm-workspace.yaml files ==="

python - <<'PY'
import pathlib, re

def parse_min_release_exclude(text: str):
    vals = []
    in_block = False
    for line in text.splitlines():
        if re.match(r'^\s*minimumReleaseAgeExclude:\s*$', line):
            in_block = True
            continue
        if in_block:
            if re.match(r'^\S', line):
                break
            m = re.match(r'^\s*-\s*(.+?)\s*$', line)
            if m:
                vals.append(m.group(1).strip("'\""))
    return vals

print("Checking all pnpm-workspace.yaml files:")
for p in sorted(pathlib.Path('.').rglob('pnpm-workspace.yaml')):
    vals = parse_min_release_exclude(p.read_text(encoding='utf-8'))
    if vals:
        print(f"\n{p}:")
        print(f"  minimumReleaseAgeExclude: {vals}")
        if 'react' in vals:
            has_react = 'react' in vals
            has_react_dom = 'react-dom' in vals
            print(f"  react={has_react}, react-dom={has_react_dom}")
            if has_react and not has_react_dom:
                print(f"  ⚠️  react present but react-dom missing")
PY

Repository: rage/secret-project-331

Length of output: 1434


Include react-dom in the minimumReleaseAgeExclude list.

The react package is excluded from release-age gating, but react-dom is missing. All other workspace files in the repository (services/example-exercise, services/main-frontend, services/quizzes, services/tmc, shared-module/packages/common, and the root workspace) include both packages in their exclusion lists to maintain version synchronization between React and ReactDOM.

Suggested patch
 minimumReleaseAgeExclude:
   - next
   - react
+  - react-dom
   - '@next/*'
📝 Committable suggestion

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

Suggested change
minimumReleaseAgeExclude:
- next
- react
- '@next/*'
minimumReleaseAgeExclude:
- next
- react
- react-dom
- '@next/*'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/cms/pnpm-workspace.yaml` around lines 7 - 10, The
minimumReleaseAgeExclude array (key name minimumReleaseAgeExclude) is missing
"react-dom"; update the list that currently contains "next", "react", and
"@next/*" to also include "react-dom" so React and ReactDOM are both excluded
from release-age gating and remain version-synchronized with other workspaces.

bytes = "1.11.1"
# Random number generators and other randomness functionality.
rand = "0.9.2"
rand = "0.10.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check rand and reqwest declarations in Cargo.toml ==="
cat -n services/headless-lms/utils/Cargo.toml | grep -E "(rand|reqwest)" || echo "Not found in snippet"

echo ""
echo "=== Full Cargo.toml around lines 40 and 77 ==="
wc -l services/headless-lms/utils/Cargo.toml
head -85 services/headless-lms/utils/Cargo.toml | tail -50

Repository: rage/secret-project-331

Length of output: 2047


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find .query() and .form() usages in services/headless-lms ==="
rg -n --type rust '\.query\s*\(|\.form\s*\(' services/headless-lms -A 2 -B 2

echo ""
echo "=== Find use_rustls_tls usages ==="
rg -n --type rust 'use_rustls_tls' services/headless-lms -A 2 -B 2

echo ""
echo "=== Find rand call sites ==="
rg -n --type rust '(rand::thread_rng|thread_rng\(|rand::rng\(|gen_range\(|gen_bool\(|sample_iter\()' services/headless-lms -A 1 -B 1

Repository: rage/secret-project-331

Length of output: 6858


🌐 Web query:

reqwest 0.13.2 feature flags query form methods

💡 Result:

In reqwest 0.13.2, the RequestBuilder helper methods .query(...) and .form(...) are feature-gated and not enabled by default:

  • .query(...) → requires crate feature query (pulls in serde + serde_urlencoded) [1], and the method is “Available on crate feature query only.” [2]
  • .form(...) → requires crate feature form (pulls in serde + serde_urlencoded) [1], and the method is “Available on crate feature form only.” [2]

Enable them in Cargo.toml like:

[dependencies]
reqwest = { version = "0.13.2", features = ["query", "form"] }

Reqwest 0.13 introduced this as a breaking-ish change: “query and form are now crate features, disabled by default.” [3]

Feature flag inventory (0.13.2): query and form are among the non-default feature flags listed on docs.rs. [4]

Sources: [1] [2] [3] [4]


🌐 Web query:

rand 0.10.0 breaking changes from 0.9

💡 Result:

Key breaking changes in rand 0.10.0 (released 2026-02-08) vs 0.9.x (from the 0.10.0 changelog): [1]

  • OsRng renamed to SysRng, and OsErrorSysError; feature os_rngsys_rng. [1]
  • Trait naming shift: rand::Rngrand::RngExt, because upstream rand_core renamed RngCoreRng. [1]
  • Sequence API renames (breaking):
    • IndexedRandom::choose_multiplesample
    • choose_multiple_arraysample_array
    • choose_multiple_weightedsample_weighted
    • SliceChooseIterIndexedSamples
    • IteratorRandom::choose_multiplesample
    • choose_multiple_fillsample_fill [1]
  • Removed APIs:
    • SeedableRng::{from_os_rng, try_from_os_rng} removed. [1]
    • ReseedingRng removed. [1]
    • Clone removed for StdRng and ReseedingRng (and ReseedingRng is gone anyway). [1]
  • Feature removals: small_rng feature removed (and “nightly” feature removed). [1]
  • Toolchain bump: now Edition 2024 with MSRV 1.85. [1]
  • StdRng backend dependency change: rand_chacha dependency replaced by chacha20 (may break direct ChaCha type usage, though StdRng output is intended to remain the same). [1]

Source: rand CHANGELOG.md for 0.10.0. [1]


🏁 Script executed:

#!/bin/bash
# Check the actual http.rs file to see context around use_rustls_tls
cat -n services/headless-lms/utils/src/http.rs | head -25

Repository: rage/secret-project-331

Length of output: 842


🌐 Web query:

reqwest 0.13.2 use_rustls_tls method ClientBuilder

💡 Result:

In reqwest 0.13.2, ClientBuilder::use_rustls_tls() still exists, but it’s deprecated. Use ClientBuilder::tls_backend_rustls() instead. [1]

let client = reqwest::Client::builder()
    .tls_backend_rustls() // preferred in 0.13.x
    .build()?;

Requirements / notes:

  • The builder methods are only available when the rustls Cargo feature is enabled. In 0.13.2, rustls is enabled by default (unless you turned off default features). [2]
  • If you did disable default features, enable rustls explicitly:
reqwest = { version = "0.13.2", default-features = false, features = ["rustls"] }

Background: in 0.13, reqwest made TLS changes (including “many TLS-related methods renamed” while keeping older names as soft-deprecated). [3]

Sources: ClientBuilder docs (shows use_rustls_tls deprecated → tls_backend_rustls) [1], reqwest 0.13.2 feature list (shows rustls default) [2], 0.13.0 changelog notes about TLS method renames [3].


Replace deprecated use_rustls_tls() with tls_backend_rustls() in reqwest 0.13.2.

In services/headless-lms/utils/src/http.rs line 15, .use_rustls_tls() is deprecated in reqwest 0.13.2. Use .tls_backend_rustls() instead:

reqwest::Client::builder()
    .tls_backend_rustls()
    .https_only(!http_test_mode)
    .build()

The rand 0.10.0 usage in the codebase (.sample_iter(), .sample_string(), .random_range(), .shuffle()) is compatible with 0.10.0 and requires no changes. The .query() method found in the codebase is from actix-web, not reqwest, so reqwest's feature-gating of .query() and .form() does not apply here.

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

In `@services/headless-lms/utils/Cargo.toml` at line 40, Replace the deprecated
reqwest builder call .use_rustls_tls() with .tls_backend_rustls() in the HTTP
client construction (locate the reqwest::Client::builder() usage in
services/headless-lms/utils/src/http.rs), and ensure the Cargo.toml has reqwest
compiled with the rustls backend feature so the new tls_backend_rustls() method
is available; leave rand = "0.10.0" as-is since no changes are required for its
usage.

"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.3.3",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import json, pathlib, re

pkg_path = pathlib.Path("shared-module/packages/common/package.json")
root_ws = pathlib.Path("pnpm-workspace.yaml")

pkg = json.loads(pkg_path.read_text(encoding="utf-8"))
types_node = pkg.get("devDependencies", {}).get("@types/node", "")
m_types = re.match(r'^\^?(\d+)', types_node)

node_major = None
for line in root_ws.read_text(encoding="utf-8").splitlines():
    m = re.match(r'^\s*nodeVersion:\s*([0-9]+)\.', line)
    if m:
        node_major = m.group(1)
        break

print(f"@types/node: {types_node}")
print(f"workspace node major: {node_major}")

if m_types and node_major and m_types.group(1) != node_major:
    print("MISMATCH: `@types/node` major does not match workspace Node major")
PY

Repository: rage/secret-project-331

Length of output: 176


🏁 Script executed:

# Also check the actual files to see their current state
echo "=== pnpm-workspace.yaml ==="
cat pnpm-workspace.yaml

echo -e "\n=== package.json (lines around 77) ==="
sed -n '70,85p' shared-module/packages/common/package.json

Repository: rage/secret-project-331

Length of output: 1425


Align @types/node major with the pinned runtime major.

The @types/node dependency at line 77 specifies version ^25.3.3 (major 25), but the workspace is pinned to Node 24.14.0. This major version mismatch can hide runtime incompatibilities during type-checking.

Suggested patch
-    "@types/node": "^25.3.3",
+    "@types/node": "^24.14.0",
📝 Committable suggestion

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

Suggested change
"@types/node": "^25.3.3",
"@types/node": "^24.14.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared-module/packages/common/package.json` at line 77, Update the
`@types/node` dependency in package.json to match the runtime major (Node 24) so
type definitions align with the pinned runtime; locate the "@types/node" entry
in shared-module/packages/common/package.json and change its version spec
(currently "^25.3.3") to a 24.x range (for example "^24.0.0" or a more specific
24.y.z you prefer), then run install/typecheck to verify no type mismatches.

Copy link
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 (3)
system-tests/src/tests/exams/exam-instructions.spec.ts (2)

40-44: Normalize the empty-block selector; avoid escaped duplicate variant.

The escaped aria-label selector at Lines 41-43 is hard to read and maintain. Reuse one canonical selector string for both interactions.

Suggested change
+  const emptyBlockSelector =
+    `[aria-label="Empty block; start writing or type forward slash to choose a block"]`
+
   await page
-    .locator(`[aria-label="Empty block; start writing or type forward slash to choose a block"]`)
+    .locator(emptyBlockSelector)
     .fill("These are the instructions")
   await page.getByText("These are the instructions").press("Enter")
   await page
-    .locator(
-      `[aria-label="Empty\\ block\\;\\ start\\ writing\\ or\\ type\\ forward\\ slash\\ to\\ choose\\ a\\ block"]`,
-    )
+    .locator(emptyBlockSelector)
     .pressSequentially("/")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@system-tests/src/tests/exams/exam-instructions.spec.ts` around lines 40 - 44,
The aria-label selector used in the locator call is duplicated with hard-to-read
escaping; extract the canonical selector string into a single constant (e.g.,
const EMPTY_BLOCK_SELECTOR = `[aria-label="Empty block; start writing or type
forward slash to choose a block"]`) and replace the inline duplicated escaped
selector in the locator(...) call and any other uses (like the
pressSequentially("/") interaction) with that constant (update the locator
invocation that calls .locator(EMPTY_BLOCK_SELECTOR) and then
.pressSequentially("/")). Ensure the constant uses the unescaped, human-readable
text so both locator and subsequent interactions reuse the same selector.

39-39: Use keyboard Enter instead of text-locator Enter to avoid focus ambiguity.

At Line 39, getByText("These are the instructions").press("Enter") can become flaky if multiple matches appear or focus shifts in the editor. Prefer pressing Enter on the active editor focus.

Suggested change
-  await page.getByText("These are the instructions").press("Enter")
+  await page.keyboard.press("Enter")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@system-tests/src/tests/exams/exam-instructions.spec.ts` at line 39, The
current call page.getByText("These are the instructions").press("Enter") is
flaky due to focus ambiguity; instead ensure the editor has focus and use
keyboard input: focus the editor locator (e.g., page.getByRole("textbox") or the
specific editor locator) via its .focus() method, then invoke
page.keyboard.press("Enter") to send the Enter keystroke; update the test to
replace the getByText(...).press("Enter") usage with the two-step focus +
keyboard.press flow to avoid ambiguous text-locator pressing.
services/cms/package.json (1)

30-30: Make @next/bundle-analyzer versioning consistent with next.

Line 30 pins @next/bundle-analyzer to 16.1.6 while line 65 allows next to float with ^16.1.6. These should use the same versioning strategy to avoid unintended version drift.

♻️ Proposed fix
-    "@next/bundle-analyzer": "16.1.6",
+    "@next/bundle-analyzer": "^16.1.6",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/cms/package.json` at line 30, The `@next/bundle-analyzer` dependency
is pinned to "16.1.6" while the "next" dependency uses a caret range; make their
versioning strategy consistent by updating the "@next/bundle-analyzer" entry in
package.json to use the same semver style as "next" (e.g., change
"@next/bundle-analyzer": "16.1.6" to "@next/bundle-analyzer": "^16.1.6"), or
alternatively pin both to exact versions—ensure you update the
"@next/bundle-analyzer" line to match whichever approach is used for "next".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@services/cms/package.json`:
- Line 30: The `@next/bundle-analyzer` dependency is pinned to "16.1.6" while the
"next" dependency uses a caret range; make their versioning strategy consistent
by updating the "@next/bundle-analyzer" entry in package.json to use the same
semver style as "next" (e.g., change "@next/bundle-analyzer": "16.1.6" to
"@next/bundle-analyzer": "^16.1.6"), or alternatively pin both to exact
versions—ensure you update the "@next/bundle-analyzer" line to match whichever
approach is used for "next".

In `@system-tests/src/tests/exams/exam-instructions.spec.ts`:
- Around line 40-44: The aria-label selector used in the locator call is
duplicated with hard-to-read escaping; extract the canonical selector string
into a single constant (e.g., const EMPTY_BLOCK_SELECTOR = `[aria-label="Empty
block; start writing or type forward slash to choose a block"]`) and replace the
inline duplicated escaped selector in the locator(...) call and any other uses
(like the pressSequentially("/") interaction) with that constant (update the
locator invocation that calls .locator(EMPTY_BLOCK_SELECTOR) and then
.pressSequentially("/")). Ensure the constant uses the unescaped, human-readable
text so both locator and subsequent interactions reuse the same selector.
- Line 39: The current call page.getByText("These are the
instructions").press("Enter") is flaky due to focus ambiguity; instead ensure
the editor has focus and use keyboard input: focus the editor locator (e.g.,
page.getByRole("textbox") or the specific editor locator) via its .focus()
method, then invoke page.keyboard.press("Enter") to send the Enter keystroke;
update the test to replace the getByText(...).press("Enter") usage with the
two-step focus + keyboard.press flow to avoid ambiguous text-locator pressing.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9371d4 and 23a2e5e.

⛔ Files ignored due to path filters (7)
  • services/cms/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • services/headless-lms/Cargo.lock is excluded by !**/*.lock
  • system-tests/src/__screenshots__/quizzes/widget/multiple-choice-clickable.spec.ts/widget-multiple-choice-clickable-answered-desktop-regular.png is excluded by !**/*.png
  • system-tests/src/__screenshots__/quizzes/widget/multiple-choice-row-widget.spec.ts/widget-multiple-choice-row-initial-desktop-regular.png is excluded by !**/*.png
  • system-tests/src/__screenshots__/sidebar-toggle.spec.ts/block-list-desktop-regular.png is excluded by !**/*.png
  • system-tests/src/__screenshots__/sidebar-toggle.spec.ts/block-menu-desktop-regular.png is excluded by !**/*.png
  • system-tests/src/__screenshots__/sidebar-toggle.spec.ts/block-properties-desktop-regular.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • services/cms/package.json
  • services/cms/src/components/editors/GutenbergEditor.tsx
  • services/headless-lms/Cargo.toml
  • services/headless-lms/server/Cargo.toml
  • services/headless-lms/server/src/programs/start_server.rs
  • system-tests/src/tests/cms/converting-blocks.spec.ts
  • system-tests/src/tests/course-export.spec.ts
  • system-tests/src/tests/exams/exam-instructions.spec.ts
  • system-tests/src/tests/exams/grade-exams-manually.spec.ts
  • system-tests/src/tests/partners-block.spec.ts
  • system-tests/src/tests/research-form.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • services/headless-lms/server/Cargo.toml

# Conflicts:
#	services/headless-lms/Cargo.lock
@socket-security
Copy link

socket-security bot commented Mar 3, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm entities is 91.0% likely obfuscated

Confidence: 0.91

Location: Package overview

From: services/main-frontend/pnpm-lock.yamlnpm/@types/[email protected]npm/@svgr/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm entities is 91.0% likely obfuscated

Confidence: 0.91

Location: Package overview

From: services/main-frontend/pnpm-lock.yamlnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Copy link
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.

Caution

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

⚠️ Outside diff range comments (1)
system-tests/src/tests/user-research-consent.spec.ts (1)

57-59: ⚠️ Potential issue | 🟠 Major

Assert dialog absence explicitly after re-login.

At Line 58, checking the “Organizations” heading does not prove the consent dialog is gone; this can pass as a false positive. Add an explicit negative assertion for the dialog.

Suggested patch
     await page.locator("id=login-button").click()
     await expect(page.getByRole("heading", { name: "Organizations" })).toBeVisible()
+    await expect(page.getByTestId("research-consent-dialog")).not.toBeVisible()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@system-tests/src/tests/user-research-consent.spec.ts` around lines 57 - 59,
After re-login (after interacting with "id=login-button" and asserting the
"Organizations" heading), add an explicit negative assertion that the
user-research consent dialog is not present (instead of relying on the heading
check). Locate the consent dialog using the same selector used elsewhere in the
test (e.g., the dialog role or its unique text/label) and assert its absence
with a negative expectation (for example using page.getByRole('dialog', { name:
/consent/i }) or the project's consent dialog selector and .not.toBeVisible() or
.toHaveCount(0)); update the test around the "id=login-button" / "Organizations"
assertions to include this explicit negative check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@system-tests/src/tests/user-research-consent.spec.ts`:
- Around line 57-59: After re-login (after interacting with "id=login-button"
and asserting the "Organizations" heading), add an explicit negative assertion
that the user-research consent dialog is not present (instead of relying on the
heading check). Locate the consent dialog using the same selector used elsewhere
in the test (e.g., the dialog role or its unique text/label) and assert its
absence with a negative expectation (for example using page.getByRole('dialog',
{ name: /consent/i }) or the project's consent dialog selector and
.not.toBeVisible() or .toHaveCount(0)); update the test around the
"id=login-button" / "Organizations" assertions to include this explicit negative
check.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23a2e5e and 3b8e64d.

⛔ Files ignored due to path filters (1)
  • services/headless-lms/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • services/headless-lms/chatbot/Cargo.toml
  • services/headless-lms/models/src/courses.rs
  • services/headless-lms/server/src/controllers/course_material/chatbot.rs
  • system-tests/src/tests/exams/grade-exams-manually.spec.ts
  • system-tests/src/tests/user-research-consent.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • system-tests/src/tests/exams/grade-exams-manually.spec.ts
  • services/headless-lms/chatbot/Cargo.toml

@coderabbitai coderabbitai bot removed the chore label Mar 3, 2026
Copy link
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: 4

♻️ Duplicate comments (1)
shared-module/packages/common/package.json (1)

78-78: ⚠️ Potential issue | 🟠 Major

@types/node major is still ahead of the pinned Node runtime major.

At Line 78, @types/node is ^25.3.3 while workspace Node is pinned to 24.x in this PR context. This can type-check APIs that are unavailable at runtime.

#!/bin/bash
set -euo pipefail

echo "== Node version pins from workspace files =="
fd 'pnpm-workspace.yaml$' | while read -r f; do
  echo "-- $f"
  rg -n 'nodeVersion:' "$f" || true
done

echo
echo "== `@types/node` in shared-module/packages/common/package.json =="
rg -n '"@types/node"\s*:' shared-module/packages/common/package.json
Suggested patch
-    "@types/node": "^25.3.3",
+    "@types/node": "^24.14.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared-module/packages/common/package.json` at line 78, The `@types/node`
dependency in shared-module/packages/common/package.json is set to "^25.3.3"
which is ahead of the workspace Node runtime (pinned to 24.x); update the
"@types/node" version to a 24.x-compatible release (e.g., "^24.0.0") in that
package.json, run your install/type-check (pnpm install && pnpm -w -F common run
typecheck or equivalent) to verify no new type errors, and commit the change so
the dev typings match the pinned Node runtime.
🧹 Nitpick comments (2)
services/headless-lms/models/src/user_chapter_locking_statuses.rs (1)

236-307: Document side effects in get_or_init_all_for_course

This public function now does more than “get/init”: it inserts missing statuses and may unlock chapters. Please add a short doc comment describing those side effects and when they happen, so callers don’t treat it as a read-only fetch.

As per coding guidelines: "Require consistent, minimal function documentation... include a short, plain-language description of what the function is for... Comments should add meaning rather than paraphrasing the name or implementation."

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

In `@services/headless-lms/models/src/user_chapter_locking_statuses.rs` around
lines 236 - 307, Add a short doc comment above the public function
get_or_init_all_for_course describing its side effects: it will insert missing
user_chapter_locking_statuses for the course when course.chapter_locking_enabled
is true, and it may call crate::chapters::unlock_first_chapters_for_user to
unlock chapters (triggered when course locking is enabled and all existing
statuses are ChapterLockingStatus::NotUnlockedYet), so callers should not assume
this is a read-only fetch; mention the conditions under which each side effect
occurs.
services/main-frontend/package.json (1)

60-63: Keep react and react-dom locked to the same exact version.

At Lines 60-63, react is exact (19.2.4) while react-dom is ranged (^19.2.4). This can drift to different patch/minor builds over time.

Suggested patch
-    "react-dom": "^19.2.4",
+    "react-dom": "19.2.4",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/main-frontend/package.json` around lines 60 - 63, The package.json
currently pins "react" to an exact version and "react-dom" with a caret, which
can allow drift; update the dependency declaration for "react-dom" (the
react-dom entry in package.json) to use the exact same version string as "react"
(change "^19.2.4" to "19.2.4") so both "react" and "react-dom" are locked to
identical versions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/cms/package.json`:
- Line 86: The package.json dependency "@types/node" is pinned to ^25.3.3 which
mismatches the workspace Node runtime (nodeVersion 24.14.0); update the
"@types/node" entry in services/cms package.json to use the matching major/minor
version (e.g., ^24.14.0) so the types align with the pinned runtime and avoid
type/runtime incompatibilities.

In `@services/headless-lms/models/src/user_chapter_locking_statuses.rs`:
- Around line 557-642: The test
get_or_init_all_for_course_unlocks_first_chapter_when_all_not_unlocked_yet is
misleading because no exercises are created so multiple chapters could
auto-unlock; fix by making the setup match the intent: create at least one
exercise tied to chapter1 (e.g., insert an exercise for chapter1 via the same
crate::exercises insert helper) so only chapter1 should be unlocked, or
alternatively strengthen the assertions after calling get_or_init_all_for_course
to assert chapter1 is Unlocked and chapter2 remains NotUnlockedYet (use
ChapterLockingStatus variants and the chapter1/chapter2 ids and functions
get_or_init_all_for_course and ensure_not_unlocked_yet_status to locate the
logic).

In `@services/main-frontend/package.json`:
- Line 88: The dev dependency "@types/node" is set to ^25.3.3 but the workspace
Node runtime is pinned to 24.14.0; update the entry for "@types/node" in
package.json to use a 24.x caret range (e.g., "^24.0.0" or "^24.x") so the type
definitions match the pinned Node major and prevent invalid API type-checking
against Node 25.

In `@services/quizzes/package.json`:
- Line 47: The package.json contains an unsupported "export": "next export" npm
script (and next.config.js currently uses output: "standalone"); either remove
the "export" script and any tooling invoking it if you don't need static
exports, or change next.config.js output to "export" and replace the script with
a build-based flow (use "next build" and then serve the generated static output)
so you stop calling the removed CLI; update the package.json scripts accordingly
and ensure next.config.js's output setting (the output property) and any
deployment steps align (refer to the "export" script in package.json and the
output property in next.config.js).

---

Duplicate comments:
In `@shared-module/packages/common/package.json`:
- Line 78: The `@types/node` dependency in
shared-module/packages/common/package.json is set to "^25.3.3" which is ahead of
the workspace Node runtime (pinned to 24.x); update the "@types/node" version to
a 24.x-compatible release (e.g., "^24.0.0") in that package.json, run your
install/type-check (pnpm install && pnpm -w -F common run typecheck or
equivalent) to verify no new type errors, and commit the change so the dev
typings match the pinned Node runtime.

---

Nitpick comments:
In `@services/headless-lms/models/src/user_chapter_locking_statuses.rs`:
- Around line 236-307: Add a short doc comment above the public function
get_or_init_all_for_course describing its side effects: it will insert missing
user_chapter_locking_statuses for the course when course.chapter_locking_enabled
is true, and it may call crate::chapters::unlock_first_chapters_for_user to
unlock chapters (triggered when course locking is enabled and all existing
statuses are ChapterLockingStatus::NotUnlockedYet), so callers should not assume
this is a read-only fetch; mention the conditions under which each side effect
occurs.

In `@services/main-frontend/package.json`:
- Around line 60-63: The package.json currently pins "react" to an exact version
and "react-dom" with a caret, which can allow drift; update the dependency
declaration for "react-dom" (the react-dom entry in package.json) to use the
exact same version string as "react" (change "^19.2.4" to "19.2.4") so both
"react" and "react-dom" are locked to identical versions.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8e64d and ce0f834.

⛔ Files ignored due to path filters (6)
  • services/cms/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • services/example-exercise/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • services/main-frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • services/quizzes/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • services/tmc/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • shared-module/packages/common/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • services/cms/package.json
  • services/example-exercise/package.json
  • services/headless-lms/models/src/user_chapter_locking_statuses.rs
  • services/main-frontend/package.json
  • services/quizzes/package.json
  • services/tmc/package.json
  • shared-module/packages/common/package.json
  • shared-module/packages/common/src/hooks/useConcurrencyThrottle.ts
  • shared-module/packages/common/tests/setup-jest.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • services/example-exercise/package.json

"@types/node": "^24.6.2",
"@types/jsdom": "^28.0.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.3.3",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Workspace configuration files =="
fd -e yaml -e yml | grep -E '(workspace|pnpm)' | head -20

echo
echo "== Content of pnpm-workspace.yaml if exists =="
if [ -f "pnpm-workspace.yaml" ]; then
  cat pnpm-workspace.yaml
fi

echo
echo "== services/cms/package.json - `@types/node` line =="
rg -n '"@types/node"' services/cms/package.json -A 1 -B 1

echo
echo "== services/cms/package.json line 86 context =="
sed -n '84,88p' services/cms/package.json

Repository: rage/secret-project-331

Length of output: 1923


Align @types/node major version with the pinned Node runtime.

Line 86 pins @types/node to ^25.3.3, but the workspace configuration (pnpm-workspace.yaml) specifies nodeVersion: 24.14.0. The types major version should match the runtime major version to prevent type incompatibilities and mask runtime issues. Change to ^24.14.0.

Suggested patch
-    "@types/node": "^25.3.3",
+    "@types/node": "^24.14.0",
📝 Committable suggestion

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

Suggested change
"@types/node": "^25.3.3",
"@types/node": "^24.14.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/cms/package.json` at line 86, The package.json dependency
"@types/node" is pinned to ^25.3.3 which mismatches the workspace Node runtime
(nodeVersion 24.14.0); update the "@types/node" entry in services/cms
package.json to use the matching major/minor version (e.g., ^24.14.0) so the
types align with the pinned runtime and avoid type/runtime incompatibilities.

Comment on lines +557 to +642
async fn get_or_init_all_for_course_unlocks_first_chapter_when_all_not_unlocked_yet() {
insert_data!(:tx, :user, :org, course: course, instance: _instance, :course_module);

let chapter1 = crate::chapters::insert(
tx.as_mut(),
PKeyPolicy::Generate,
&crate::chapters::NewChapter {
name: "Chapter 1".to_string(),
color: None,
course_id: course,
chapter_number: 1,
front_page_id: None,
opens_at: None,
deadline: None,
course_module_id: Some(course_module.id),
},
)
.await
.unwrap();

// insert a second chapter to ensure only the first is auto-unlocked
let chapter2 = crate::chapters::insert(
tx.as_mut(),
PKeyPolicy::Generate,
&crate::chapters::NewChapter {
name: "Chapter 2".to_string(),
color: None,
course_id: course,
chapter_number: 2,
front_page_id: None,
opens_at: None,
deadline: None,
course_module_id: Some(course_module.id),
},
)
.await
.unwrap();

// Enable chapter locking for the course
let existing_course = crate::courses::get_course(tx.as_mut(), course)
.await
.unwrap();

crate::courses::update_course(
tx.as_mut(),
course,
crate::courses::CourseUpdate {
name: existing_course.name,
description: existing_course.description,
is_draft: existing_course.is_draft,
is_test_mode: existing_course.is_test_mode,
can_add_chatbot: existing_course.can_add_chatbot,
is_unlisted: existing_course.is_unlisted,
is_joinable_by_code_only: existing_course.is_joinable_by_code_only,
ask_marketing_consent: existing_course.ask_marketing_consent,
flagged_answers_threshold: existing_course.flagged_answers_threshold.unwrap_or(1),
flagged_answers_skip_manual_review_and_allow_retry: existing_course
.flagged_answers_skip_manual_review_and_allow_retry,
closed_at: existing_course.closed_at,
closed_additional_message: existing_course.closed_additional_message,
closed_course_successor_id: existing_course.closed_course_successor_id,
chapter_locking_enabled: true,
},
)
.await
.unwrap();

// Ensure we start from a state where all chapters are not_unlocked_yet
let _ = ensure_not_unlocked_yet_status(tx.as_mut(), user, chapter1, course)
.await
.unwrap();
let _ = ensure_not_unlocked_yet_status(tx.as_mut(), user, chapter2, course)
.await
.unwrap();

let statuses = get_or_init_all_for_course(tx.as_mut(), user, course)
.await
.unwrap();

assert!(!statuses.is_empty());
assert!(
statuses
.iter()
.any(|s| s.chapter_id == chapter1 && s.status == ChapterLockingStatus::Unlocked)
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test intent is misleading and under-asserted

The test name/comment says only the first chapter should auto-unlock, but this setup creates no exercises, so unlock behavior can include multiple initial chapters. The assertion also only checks chapter1, which doesn’t validate the stated intent.

✅ Suggested test adjustment
-    async fn get_or_init_all_for_course_unlocks_first_chapter_when_all_not_unlocked_yet() {
+    async fn get_or_init_all_for_course_unlocks_initial_chapters_when_all_not_unlocked_yet() {
@@
-        // insert a second chapter to ensure only the first is auto-unlocked
+        // insert a second chapter to verify initial auto-unlock behavior
@@
         assert!(
             statuses
                 .iter()
                 .any(|s| s.chapter_id == chapter1 && s.status == ChapterLockingStatus::Unlocked)
         );
+        assert!(
+            statuses
+                .iter()
+                .any(|s| s.chapter_id == chapter2 && s.status == ChapterLockingStatus::Unlocked)
+        );
📝 Committable suggestion

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

Suggested change
async fn get_or_init_all_for_course_unlocks_first_chapter_when_all_not_unlocked_yet() {
insert_data!(:tx, :user, :org, course: course, instance: _instance, :course_module);
let chapter1 = crate::chapters::insert(
tx.as_mut(),
PKeyPolicy::Generate,
&crate::chapters::NewChapter {
name: "Chapter 1".to_string(),
color: None,
course_id: course,
chapter_number: 1,
front_page_id: None,
opens_at: None,
deadline: None,
course_module_id: Some(course_module.id),
},
)
.await
.unwrap();
// insert a second chapter to ensure only the first is auto-unlocked
let chapter2 = crate::chapters::insert(
tx.as_mut(),
PKeyPolicy::Generate,
&crate::chapters::NewChapter {
name: "Chapter 2".to_string(),
color: None,
course_id: course,
chapter_number: 2,
front_page_id: None,
opens_at: None,
deadline: None,
course_module_id: Some(course_module.id),
},
)
.await
.unwrap();
// Enable chapter locking for the course
let existing_course = crate::courses::get_course(tx.as_mut(), course)
.await
.unwrap();
crate::courses::update_course(
tx.as_mut(),
course,
crate::courses::CourseUpdate {
name: existing_course.name,
description: existing_course.description,
is_draft: existing_course.is_draft,
is_test_mode: existing_course.is_test_mode,
can_add_chatbot: existing_course.can_add_chatbot,
is_unlisted: existing_course.is_unlisted,
is_joinable_by_code_only: existing_course.is_joinable_by_code_only,
ask_marketing_consent: existing_course.ask_marketing_consent,
flagged_answers_threshold: existing_course.flagged_answers_threshold.unwrap_or(1),
flagged_answers_skip_manual_review_and_allow_retry: existing_course
.flagged_answers_skip_manual_review_and_allow_retry,
closed_at: existing_course.closed_at,
closed_additional_message: existing_course.closed_additional_message,
closed_course_successor_id: existing_course.closed_course_successor_id,
chapter_locking_enabled: true,
},
)
.await
.unwrap();
// Ensure we start from a state where all chapters are not_unlocked_yet
let _ = ensure_not_unlocked_yet_status(tx.as_mut(), user, chapter1, course)
.await
.unwrap();
let _ = ensure_not_unlocked_yet_status(tx.as_mut(), user, chapter2, course)
.await
.unwrap();
let statuses = get_or_init_all_for_course(tx.as_mut(), user, course)
.await
.unwrap();
assert!(!statuses.is_empty());
assert!(
statuses
.iter()
.any(|s| s.chapter_id == chapter1 && s.status == ChapterLockingStatus::Unlocked)
);
}
async fn get_or_init_all_for_course_unlocks_initial_chapters_when_all_not_unlocked_yet() {
insert_data!(:tx, :user, :org, course: course, instance: _instance, :course_module);
let chapter1 = crate::chapters::insert(
tx.as_mut(),
PKeyPolicy::Generate,
&crate::chapters::NewChapter {
name: "Chapter 1".to_string(),
color: None,
course_id: course,
chapter_number: 1,
front_page_id: None,
opens_at: None,
deadline: None,
course_module_id: Some(course_module.id),
},
)
.await
.unwrap();
// insert a second chapter to verify initial auto-unlock behavior
let chapter2 = crate::chapters::insert(
tx.as_mut(),
PKeyPolicy::Generate,
&crate::chapters::NewChapter {
name: "Chapter 2".to_string(),
color: None,
course_id: course,
chapter_number: 2,
front_page_id: None,
opens_at: None,
deadline: None,
course_module_id: Some(course_module.id),
},
)
.await
.unwrap();
// Enable chapter locking for the course
let existing_course = crate::courses::get_course(tx.as_mut(), course)
.await
.unwrap();
crate::courses::update_course(
tx.as_mut(),
course,
crate::courses::CourseUpdate {
name: existing_course.name,
description: existing_course.description,
is_draft: existing_course.is_draft,
is_test_mode: existing_course.is_test_mode,
can_add_chatbot: existing_course.can_add_chatbot,
is_unlisted: existing_course.is_unlisted,
is_joinable_by_code_only: existing_course.is_joinable_by_code_only,
ask_marketing_consent: existing_course.ask_marketing_consent,
flagged_answers_threshold: existing_course.flagged_answers_threshold.unwrap_or(1),
flagged_answers_skip_manual_review_and_allow_retry: existing_course
.flagged_answers_skip_manual_review_and_allow_retry,
closed_at: existing_course.closed_at,
closed_additional_message: existing_course.closed_additional_message,
closed_course_successor_id: existing_course.closed_course_successor_id,
chapter_locking_enabled: true,
},
)
.await
.unwrap();
// Ensure we start from a state where all chapters are not_unlocked_yet
let _ = ensure_not_unlocked_yet_status(tx.as_mut(), user, chapter1, course)
.await
.unwrap();
let _ = ensure_not_unlocked_yet_status(tx.as_mut(), user, chapter2, course)
.await
.unwrap();
let statuses = get_or_init_all_for_course(tx.as_mut(), user, course)
.await
.unwrap();
assert!(!statuses.is_empty());
assert!(
statuses
.iter()
.any(|s| s.chapter_id == chapter1 && s.status == ChapterLockingStatus::Unlocked)
);
assert!(
statuses
.iter()
.any(|s| s.chapter_id == chapter2 && s.status == ChapterLockingStatus::Unlocked)
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/headless-lms/models/src/user_chapter_locking_statuses.rs` around
lines 557 - 642, The test
get_or_init_all_for_course_unlocks_first_chapter_when_all_not_unlocked_yet is
misleading because no exercises are created so multiple chapters could
auto-unlock; fix by making the setup match the intent: create at least one
exercise tied to chapter1 (e.g., insert an exercise for chapter1 via the same
crate::exercises insert helper) so only chapter1 should be unlocked, or
alternatively strengthen the assertions after calling get_or_init_all_for_course
to assert chapter1 is Unlocked and chapter2 remains NotUnlockedYet (use
ChapterLockingStatus variants and the chapter1/chapter2 ids and functions
get_or_init_all_for_course and ensure_not_unlocked_yet_status to locate the
logic).

"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.3.3",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Node version pins =="
fd 'pnpm-workspace.yaml$' | while read -r f; do
  echo "-- $f"
  rg -n 'nodeVersion:' "$f" || true
done

echo
echo "== `@types/node` in services/main-frontend/package.json =="
rg -n '"@types/node"\s*:' services/main-frontend/package.json

Repository: rage/secret-project-331

Length of output: 807


@types/node major should match the pinned Node runtime major.

At line 88, @types/node is ^25.3.3 (Node 25.x) while the workspace config is pinned to Node 24.14.0; this can allow invalid runtime API usage during type-checking. Update @types/node to ^24.x or align the runtime version.

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

In `@services/main-frontend/package.json` at line 88, The dev dependency
"@types/node" is set to ^25.3.3 but the workspace Node runtime is pinned to
24.14.0; update the entry for "@types/node" in package.json to use a 24.x caret
range (e.g., "^24.0.0" or "^24.x") so the type definitions match the pinned Node
major and prevent invalid API type-checking against Node 25.

"katex": "^0.16.33",
"lodash": "^4.17.23",
"monaco-editor": "^0.55.1",
"next": "16.1.6",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify current package settings
echo "=== Checking Next.js version in package.json ==="
rg -n '"next"\s*:\s*"16\.1\.6"' services/quizzes/package.json || echo "Version not found"

echo -e "\n=== Checking for 'next export' in scripts ==="
rg -n '"export"\s*:\s*"next export"' services/quizzes/package.json || echo "Export script not found"

echo -e "\n=== Checking for next.config files ==="
fd -HI '^next\.config\.(js|mjs|cjs|ts)$' services/quizzes 2>/dev/null || echo "No next.config found in services/quizzes"

# Check if output: "export" is configured anywhere in the services directory
echo -e "\n=== Searching for output export configuration ==="
if [ -d "services/quizzes" ]; then
  find services/quizzes -maxdepth 2 -name "next.config.*" -exec cat {} + 2>/dev/null | head -50 || echo "No config files found"
fi

Repository: rage/secret-project-331

Length of output: 1474


🌐 Web query:

Next.js 16.1.x static export CLI command documentation - is "next export" still supported or deprecated

💡 Result:

In Next.js 16.1.x, the next export CLI command is not supported (it was deprecated in v13.3.0 and removed in v14.0.0). Instead, static exports are done by setting output: 'export' in next.config.js and then running next build, which produces the static output (default out/). [1]

If you still have scripts like next build && next export, you should remove next export and rely on output: 'export' + next build. [1]

Sources
[1] Next.js Docs – “Static Exports” (Last updated Feb 27, 2026), incl. version history for next export deprecation/removal: (nextjs.org)

Citations:


The next export command is not supported in Next.js 16.1.6 and will fail—this needs to be fixed.

Next.js removed the next export CLI command in v14.0.0. The "export": "next export" script on line 8 is incompatible with v16.1.6. Additionally, next.config.js is configured with output: "standalone" (for server deployments), not output: "export" (for static exports).

Either remove the export script if static exports aren't needed, or change output: "standalone" to output: "export" in next.config.js and use next build to generate static output.

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

In `@services/quizzes/package.json` at line 47, The package.json contains an
unsupported "export": "next export" npm script (and next.config.js currently
uses output: "standalone"); either remove the "export" script and any tooling
invoking it if you don't need static exports, or change next.config.js output to
"export" and replace the script with a build-based flow (use "next build" and
then serve the generated static output) so you stop calling the removed CLI;
update the package.json scripts accordingly and ensure next.config.js's output
setting (the output property) and any deployment steps align (refer to the
"export" script in package.json and the output property in next.config.js).

@coderabbitai coderabbitai bot added the chore label Mar 3, 2026
Copy link
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)
services/headless-lms/models/src/user_chapter_locking_statuses.rs (2)

209-307: Add rustdoc for the new public chapter-locking APIs.

complete_and_lock_chapter and get_or_init_all_for_course introduce important side effects (state mutation + initialization/unlock behavior) but are undocumented. Add short docs clarifying intent and side effects.

As per coding guidelines, “Require consistent, minimal function documentation… Public interfaces should have clearer and more comprehensive documentation.”

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

In `@services/headless-lms/models/src/user_chapter_locking_statuses.rs` around
lines 209 - 307, The two public functions complete_and_lock_chapter and
get_or_init_all_for_course lack rustdoc describing intent and side effects; add
concise /// comments above each function that state what the function does, its
parameters, the side effects (DB mutations, potential creation of rows and
unlocking behavior), the returned value (UserChapterLockingStatus or
Vec<UserChapterLockingStatus>), and error/ not-found behaviors (ModelError on
failure); ensure docs mention when course chapter locking must be enabled and
that get_or_init_all_for_course will insert missing rows and may call
unlock_first_chapters_for_user, and keep each docblock short (one to three
sentences) following project doc style.

245-260: Align ON CONFLICT target to use constraint name for consistency.

The insert paths at lines 245–260 and nearby use ON CONFLICT (user_id, chapter_id, deleted_at) while other mutations in the same file use ON CONFLICT ON CONSTRAINT idx_user_chapter_locking_statuses_user_chapter_active. The constraint definition is UNIQUE NULLS NOT DISTINCT (user_id, chapter_id, deleted_at), so both forms are functionally equivalent—PostgreSQL correctly infers the same constraint either way. However, using the explicit constraint name form consistently throughout improves code clarity and reduces reliance on inference.

Suggested changes
-ON CONFLICT (user_id, chapter_id, deleted_at) DO NOTHING
+ON CONFLICT ON CONSTRAINT idx_user_chapter_locking_statuses_user_chapter_active DO NOTHING

Apply the same change to all insert operations in this file (lines 245 and nearby).

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

In `@services/headless-lms/models/src/user_chapter_locking_statuses.rs` around
lines 245 - 260, Replace the positional ON CONFLICT target with the explicit
constraint name to match other mutations: in the INSERT query that currently
uses "ON CONFLICT (user_id, chapter_id, deleted_at) DO NOTHING" (the query that
inserts into user_chapter_locking_statuses and selects from chapters) change it
to "ON CONFLICT ON CONSTRAINT
idx_user_chapter_locking_statuses_user_chapter_active DO NOTHING"; apply the
same replacement to the other insert statements in this file so all inserts
consistently reference idx_user_chapter_locking_statuses_user_chapter_active
instead of the column list.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@services/headless-lms/models/src/user_chapter_locking_statuses.rs`:
- Around line 209-307: The two public functions complete_and_lock_chapter and
get_or_init_all_for_course lack rustdoc describing intent and side effects; add
concise /// comments above each function that state what the function does, its
parameters, the side effects (DB mutations, potential creation of rows and
unlocking behavior), the returned value (UserChapterLockingStatus or
Vec<UserChapterLockingStatus>), and error/ not-found behaviors (ModelError on
failure); ensure docs mention when course chapter locking must be enabled and
that get_or_init_all_for_course will insert missing rows and may call
unlock_first_chapters_for_user, and keep each docblock short (one to three
sentences) following project doc style.
- Around line 245-260: Replace the positional ON CONFLICT target with the
explicit constraint name to match other mutations: in the INSERT query that
currently uses "ON CONFLICT (user_id, chapter_id, deleted_at) DO NOTHING" (the
query that inserts into user_chapter_locking_statuses and selects from chapters)
change it to "ON CONFLICT ON CONSTRAINT
idx_user_chapter_locking_statuses_user_chapter_active DO NOTHING"; apply the
same replacement to the other insert statements in this file so all inserts
consistently reference idx_user_chapter_locking_statuses_user_chapter_active
instead of the column list.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce0f834 and 91baaec.

📒 Files selected for processing (1)
  • services/headless-lms/models/src/user_chapter_locking_statuses.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant