Appearance
attempt-service Agent Tasks
Agent workflow: follow README.md for Audit -> Investigate -> Code -> Test -> Fix; keep this pack's writable scope and verification commands authoritative.
Dispatch type: per-service
Dispatch ID: SVC-010
Owns: services/attempt-service.
Must read:
docs/agents/handoff-phase-7-attempt-*.md- legacy attempt save/submit/result modules:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/attempts/attempts.controller.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/exam-core.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/exam-core.types.ts.
Legacy source evidence:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/attempts/**- attempt/result routes in
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/** - attempt Prisma models in
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/prisma/schema.prisma.
Writable files:
services/attempt-service/**contracts/openapi/services/attempt-service.yamldocs/api/attempt-service.md
Orchestrator-owned files:
- exam runtime-access contract ownership, default route cutovers, and shared deploy manifests unless explicitly delegated.
Delegated gateway verification scope:
- Agents may maintain only non-default attempt route rehearsals in
deploy/gateway/routes.attempt-native-example.jsonanddeploy/gateway/routes.attempt-native-localhost-example.json; the default gateway route table remains orchestrator-owned. - Route proof must cover
/api/student/attempts,/api/exams/:examId/start,/api/attempts/:attemptId,/api/attempts/:attemptId/events, and/api/attempts/:attemptId/resultthroughscripts/test/attempt-route-coverage.sh,scripts/test/attempt-live-smoke.sh,scripts/test/attempt-browser-smoke.sh, andscripts/test/attempt-browser-smoke.mjs. - QA evidence lives in
docs/qa/attempt-live-smoke.mdanddocs/qa/attempt-browser-smoke.md; Makefile targets aretest-attempt-routes,test-attempt-live, andtest-attempt-browser. - The opt-in student UI candidate is documented in
docs/qa/student-exam-room-gateway-candidate.md. It validates browser request shaping only and cannot promote default routes or replace the IAM/classroom-backed browser proof.
Initial tasks:
- Add end-to-end tests for start -> save answers -> submit -> result.
- Verify timeout, conflict, grading, event ordering, and result visibility.
- Keep attempt snapshots independent from later question edits.
- Native student attempt list foundation is in place with
GET /v1/student/attempts, lightweight redacted summaries, route rehearsal, and focused HTTP coverage. Usecase coverage now also proves student-only ownership, explicit parent denial until a relationship adapter exists, unauthenticated detail/result denial, teacher/admin internal reads, and result visibility forIMMEDIATE,AFTER_CLOSE,MANUAL, released, and in-progress cases. HTTP coverage also denies unauthenticated detail reads and direct event writes from unauthenticated, non-owner student, parent, teacher, and admin actors. - Keep exam-room event writes student-owner only. Teacher/admin actors may read attempt detail/events/results as an internal foundation, but they must not write
QUESTION_VIEW, focus, network, or timeout events. - Add parent allow semantics only after profile-service supplies a verified parent-student relationship read model.
- Done locally: teacher detail/events/result reads require copied exam creator or managed-classroom teacher scope from the trusted start snapshot before any public teacher result route promotion. Admins remain allowed; unrelated teachers are denied.
- Add timeout/retake/public adapter result visibility coverage before public
/api/attempts*route promotion. - Keep native start blocked behind a hydrated exam runtime snapshot/access decision from exam-service or verified gateway aggregation. Direct public-compatible body hydration is treated as untrusted unless it carries a trusted resolver decision marker/idempotency context.
- Done locally:
/v1/exams/{examId}/attemptsand/v1/exams/{examId}/startnow reject direct public-compatible start bodies unless a trusted adapter forwardsX-Exam-Runtime-Decision-Source: exam-serviceand idempotency/correlation context. - Done locally: attempt start usecase coverage now proves attempt-owned start hardening after a trusted exam-service decision: open attempts are reused, submitted attempts count toward retake limits,
LOGIN_REQUIREDaccess-link attempts are counted per student/link,GUEST_ALLOWEDlink capacity is shared across students, inactive/expired links are rejected, and overdue assignment decisions cannot be bypassed by the incomingstartInput. Coverage also locks legacy boundary semantics: access links expiring at the current instant are expired, inactive links are denied before open-attempt reuse, and future assignment due times clamp the attempt deadline while due-at-now remains startable. - Done locally: attempt start idempotency is durable. The selected
Idempotency-Key,X-Idempotency-Key, orX-Correlation-Idis persisted per exam/student, exact replays return the original attempt after submit or grading, and the database has a partial unique guard for oneIN_PROGRESSattempt per exam/student. - Done: first successful native submit emits optional best-effort analytics-service producer events when
ANALYTICS_SERVICE_URLorANALYTICS_SERVICE_BASE_URLis configured. The events usesourceService=attempt-service, versioned event typesattempt.submitted.v1andattempt.graded.v1, stablesourceEventIdvalues{attemptId}:submittedand{attemptId}:graded,entity=ExamAttempt, owner organization/student/actor refs, occurred time, exam/result snapshots, and per-question grading metadata copied from attempt-owned snapshots. The core submit transaction does not require analytics-service, and public/apiroute promotion remains out of scope. - Done for the Course learning projection producer boundary: trusted Gateway starts may carry a Course owner decision only after Course validates active enrollment, topic-bound published lesson placement, and matching exam id. Attempt rejects missing
X-Course-Attempt-Context-Source: course-service, mismatched student/organization/exam values, incomplete contexts, and any other validator.000010_attempt_course_context.sqlpersists the copied owner snapshot; the first successful context-bound submit additionally emitsattempt.graded.v2with{attemptId}:graded:v2. The v2 payload contains no adapter-only exam id and generic v1 events keep their prior behavior. This remains an internal foundation, not a default Gateway promotion. - Done for the bounded
SVC-020-attempt-grade-notification-producer-contractslice: a first successful native submit/grade with a visible copied result policy writes optionalattempt.grade_released.notification.v1outbox rows in the same owner transaction whenNOTIFICATION_SERVICE_URLorNOTIFICATION_SERVICE_BASE_URLis configured. A background dispatcher then deliversGRADE_RELEASEDfor the student with{attemptId}:notification:grade_released. When the profile resolver is configured, the parent descriptor is queued as a separate durable row and parent account ids are resolved through profile-service/v1/internal/profiles/parent-recipients/resolveonly at dispatch time, before deliveringGRADE_RELEASED_PARENTwith{attemptId}:notification:grade_released_parent. Notification-service stays the replay/preference owner, payloads omit answers, access links, email, phone, and profile data, and a transient notifier or profile failure retries without rolling back a visible result. - Done: internal
POST /v1/events/exam-results-releasedconsumesexam.results_released.v1handoff payloads from exam-service and updates copiedexam_results_released_atpolicy snapshots byexamIdand optionalorganizationId. This makes existing submitted MANUAL and pre-close AFTER_CLOSE attempts visible after release without reading exam-service databases. The receiver requiresX-Internal-Service: exam-serviceplus a matchingX-Internal-Tokenbefore decoding or mutation. Replays are idempotent because attempts with an existing copied release timestamp are not updated again. - Done: the same release handoff queues existing
attempt.grade_released.notification.v1student and parent descriptors only for newly updated, graded, submitted attempts whose copiedMANUALor pre-closeAFTER_CLOSEpolicy was hidden before the release. The copied result visibility update and itsPENDINGoutbox rows commit atomically; attempts omitted by a replay produce neither new rows nor resolver calls. The dispatcher atomically claims due rows with a bounded lease before calling notification-service, so concurrent replicas do not issue the same normal delivery request. It keeps stable per-attempt notification source ids and the exam releasesourceEventIdas payload provenance, retries transient delivery, and marks exhausted or malformed rowsDEADwithout reversing visibility. A crash leaves the claimed row pending until its lease expires; notification-service source-event idempotency remains the final delivery boundary. - Done: internal
POST /v1/attempts/{attemptId}/essay-grade-handoffsaccepts versionedai-classifier-serviceessay grade handoffs after submit, validates question ownership and essay/manual scoring from copied attempt snapshots, updates the stored answer score and attempt totals inside attempt-service, and recordsESSAY_GRADE_HANDOFFwithsourceEventIdreplay protection. This is not a public/api/attempts*promotion and does not call usage-service or wallet-service. - Done:
GET /v1/admin/attempts/summarynow provides the owner API for admin dashboard/source-map attempt metrics. It reports only attempt-service local attempts, copied result-policy fields, question snapshot rows, answer rows, and event rows; weak-topic, result, progress, mastery, and dashboard rollups remain analytics-service ownership. - Done for the bounded
SVC-010-attempt-public-idempotency-proofslice: HTTP and usecase coverage proves trusted start -> answer save -> event -> submit -> result, exact idempotency replay after grading, timeout/retake counting, andIMMEDIATE/AFTER_CLOSE/MANUALresult visibility with redaction. A disposable non-default Gateway proof now covers start through submit, exact replay, Chromium native-read fetches, and a default-table rollback rehearsal. It uses a runtime-access fixture and Gateway health-page shell, so realexam-serviceand student exam-room parity remain gated; the default table stays legacy-proxied. - Done locally:
DOCX_PHYSICS_GATEWAY_ATTEMPT_LOCAL_POSTGRES_CONFIRM=local-postgres make test-docx-physics-attempt-local-postgresadds a fresh-service proof of the real Exam runtime access decision before Attempt creation. It imports the Physics fixture, publishes an online exam, creates two IAM identities in the same isolated organization, executes start -> save -> submit -> readGRADED, and checks all dynamic Gateway routes against the served candidate table. Its current runner also mounts the real Student Web candidate with an access-link code and observes exactly one each of start, answer-save, submit, and result-read, ending with the renderedGRADEDscore. This is non-default local browser evidence with in-memory document bytes; it is not provider, shared-runtime, default-route, or production promotion proof.
Acceptance:
- Attempt-service does not author exams or read live question rows.
- Student access rules are explicit in request/runtime snapshot inputs.
- Result visibility redaction matches legacy behavior.
- Attempt start route promotion cannot bypass assignment, password, link, online, open-window, or submitted/retake policy.
Verification:
GOTOOLCHAIN=go1.25.11 go test ./services/attempt-service/...ATTEMPT_RESULT_RELEASE_NOTIFICATION_SELF_TEST=1 make test-attempt-result-release-notification-workflowmake test-attempt-routesATTEMPT_LIVE_SELF_TEST=1 bash scripts/test/attempt-live-smoke.shATTEMPT_LIVE_CONFIRM=attempt-native ATTEMPT_EXAM_ID=<published-exam-id> ATTEMPT_AUTHORIZATION='Bearer <student-token>' ATTEMPT_ORGANIZATION_ID=<org-id> make test-attempt-liveATTEMPT_BROWSER_SELF_TEST=1 bash scripts/test/attempt-browser-smoke.shATTEMPT_BROWSER_CONFIRM=attempt-native ATTEMPT_BROWSER_EXAM_ID=<published-exam-id> ATTEMPT_BROWSER_AUTHORIZATION='Bearer <student-token>' ATTEMPT_BROWSER_ORGANIZATION_ID=<org-id> make test-attempt-browserDOCX_PHYSICS_GATEWAY_ATTEMPT_LOCAL_POSTGRES_CONFIRM=local-postgres HOCTAPAZ_DOCX_CORPUS_DIR=/Users/velikho/Desktop/test-hoctapaz make test-docx-physics-attempt-local-postgres