Skip to content

fix(ws-server): score submit-answer from the server clock and only for the open question - #233

Merged
TheCodeHeist-Coder merged 1 commit into
TheCodeHeist-Coder:mainfrom
Anshuu2004:fix/issue-232-submit-answer-server-time
Nov 3, 2026
Merged

TheCodeHeist-Coder merged 1 commit into
TheCodeHeist-Coder:mainfrom
Anshuu2004:fix/issue-232-submit-answer-server-time

Conversation

@Anshuu2004

Copy link
Copy Markdown
Contributor

What does this PR do?

quiz:submit-answer in apps/ws-server/src/utils/handleMessages.ts now measures the time itself and only accepts an answer for the question that is open right now. #157 added the clamp and the rescale, but the number still came from the client and nothing checked that the question was still live.

  • It reads quiz:deadline:<sid> and quiz:current-q:<sid> from Redis (the keys timeManager.ts already writes). If there is no deadline, the question index doesn't match the one in the message, or now > deadline, the answer is dropped.
  • elapsedMs = limitMs - (deadline - now), clamped to [0, limitMs]. The timeMs in the payload is not read anymore. The measured value is also what gets stored as the answer's timeMs.
  • An unknown questionId is dropped too (before it was buffered with 0 points).
  • quiz:end and the "no more questions" branch of quiz:next-question now call a new closeQuestionWindow() in timeManager.ts, which deletes the deadline and current-q keys. Without that, when the host ends the quiz mid-question the deadline key lives on for up to timeLimit + 60s and answers would still get buffered after the final board. It runs before drainSession.
  • ARCHITECTURE.md: the scoring notes said the client sends timeMs and still showed the old 1000 - timeMs formula, so I updated both.

Related issue

Closes #232

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance
  • Tests
  • Build / CI / deployment

Which services does this touch?

  • apps/frontend
  • apps/http-server
  • apps/ws-server
  • apps/genAI
  • packages/db (schema or Prisma changes)
  • Docker / CI

How did you test this?

Run in apps/ws-server, baseline is clean main (976d5d8):

clean main this branch
pnpm test (vitest) 2 files, 42 tests pass 3 files, 55 tests pass
pnpm check-types passes passes
pnpm build (tsc -b) passes passes

No pre-existing failures. (ws-server has no lint script, so there was nothing to run there. I generated the Prisma client first with pnpm db:generate, otherwise check-types fails on main as well.)

New handleMessages.submitAnswer.test.ts uses a fake clock (vi.useFakeTimers on Date) and a mocked Redis get:

  • timeMs: 0, -500, 19999 and "nope" all give the same server-measured elapsed time and points
  • answer at the deadline is accepted and floors at 1 point, 1ms after it is rejected
  • deadline key gone (question closed) is rejected and doesn't trigger the early finish
  • answer for a question other than the current one is rejected, the current one is accepted
  • unknown questionId, missing current-q marker and a garbage deadline value are rejected

I also added an assertion to the existing quiz:end test that closeQuestionWindow runs before drainSession. As a sanity check I put the old handleMessages.ts back with the new tests and 10 of them fail, with the new handler all 55 pass.

I did not run the full stack with Docker and a real Redis or play a quiz with several participants. The Redis reads are covered with mocks only.

Notes

  • Honest players: the clock now starts at the server's deadline start, not at the moment the browser got the question, so a correct answer can score about one latency worth less than before. The scale is 10 to 1 points over the whole limit, so this rarely moves the score by a point.
  • quiz:next-question broadcasts the question before startQuestionTimer writes the deadline keys. An answer landing in that few-ms gap would be dropped. A human can't click that fast, but flagging it. I did not reorder those calls in this PR.
  • Rejected answers are dropped silently (same as the existing if (!participantId) break), the client gets no error message. I kept it that way since the issue says "ignore".
  • The frontend still sends timeMs; it is just ignored now, so I left LiveQuiz.tsx alone.
  • participantId in the message is still taken from the payload rather than from the socket. That is a separate problem from this issue and I left it out.

…r the open question

submit-answer used the timeMs the client sent (only clamped) and never
checked that the question was still open. Now it reads quiz:deadline and
quiz:current-q from Redis, drops the answer if the question is not the
live one or the deadline has passed, and computes elapsedMs from
the deadline instead of using the payload value. The measured value is
also what gets stored as the answer's timeMs.

quiz:end and the "no more questions" path now clear the deadline keys, so
answers arriving after the quiz ended are not buffered again.

Adds handleMessages.submitAnswer.test.ts (fake clock) and updates the
auth test mock for the new timeManager exports. ARCHITECTURE.md scoring
notes updated to match.
@TheCodeHeist-Coder
TheCodeHeist-Coder merged commit 19334fe into TheCodeHeist-Coder:main Nov 3, 2026
10 checks passed
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.

fix: submit-answer still trusts client timeMs and accepts answers for closed questions (follow-up to #157)

2 participants