Skip to content

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.yaml
  • docs/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.json and deploy/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/result through scripts/test/attempt-route-coverage.sh, scripts/test/attempt-live-smoke.sh, scripts/test/attempt-browser-smoke.sh, and scripts/test/attempt-browser-smoke.mjs.
  • QA evidence lives in docs/qa/attempt-live-smoke.md and docs/qa/attempt-browser-smoke.md; Makefile targets are test-attempt-routes, test-attempt-live, and test-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 for IMMEDIATE, 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}/attempts and /v1/exams/{examId}/start now reject direct public-compatible start bodies unless a trusted adapter forwards X-Exam-Runtime-Decision-Source: exam-service and 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_REQUIRED access-link attempts are counted per student/link, GUEST_ALLOWED link capacity is shared across students, inactive/expired links are rejected, and overdue assignment decisions cannot be bypassed by the incoming startInput. 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, or X-Correlation-Id is persisted per exam/student, exact replays return the original attempt after submit or grading, and the database has a partial unique guard for one IN_PROGRESS attempt per exam/student.
  • Done: first successful native submit emits optional best-effort analytics-service producer events when ANALYTICS_SERVICE_URL or ANALYTICS_SERVICE_BASE_URL is configured. The events use sourceService=attempt-service, versioned event types attempt.submitted.v1 and attempt.graded.v1, stable sourceEventId values {attemptId}:submitted and {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 /api route 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.sql persists the copied owner snapshot; the first successful context-bound submit additionally emits attempt.graded.v2 with {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-contract slice: a first successful native submit/grade with a visible copied result policy writes optional attempt.grade_released.notification.v1 outbox rows in the same owner transaction when NOTIFICATION_SERVICE_URL or NOTIFICATION_SERVICE_BASE_URL is configured. A background dispatcher then delivers GRADE_RELEASED for 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/resolve only at dispatch time, before delivering GRADE_RELEASED_PARENT with {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-released consumes exam.results_released.v1 handoff payloads from exam-service and updates copied exam_results_released_at policy snapshots by examId and optional organizationId. This makes existing submitted MANUAL and pre-close AFTER_CLOSE attempts visible after release without reading exam-service databases. The receiver requires X-Internal-Service: exam-service plus a matching X-Internal-Token before 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.v1 student and parent descriptors only for newly updated, graded, submitted attempts whose copied MANUAL or pre-close AFTER_CLOSE policy was hidden before the release. The copied result visibility update and its PENDING outbox 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 release sourceEventId as payload provenance, retries transient delivery, and marks exhausted or malformed rows DEAD without 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-handoffs accepts versioned ai-classifier-service essay 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 records ESSAY_GRADE_HANDOFF with sourceEventId replay protection. This is not a public /api/attempts* promotion and does not call usage-service or wallet-service.
  • Done: GET /v1/admin/attempts/summary now 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-proof slice: HTTP and usecase coverage proves trusted start -> answer save -> event -> submit -> result, exact idempotency replay after grading, timeout/retake counting, and IMMEDIATE/AFTER_CLOSE/MANUAL result 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 real exam-service and 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-postgres adds 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 -> read GRADED, 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 rendered GRADED score. 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-workflow
  • make test-attempt-routes
  • ATTEMPT_LIVE_SELF_TEST=1 bash scripts/test/attempt-live-smoke.sh
  • ATTEMPT_LIVE_CONFIRM=attempt-native ATTEMPT_EXAM_ID=<published-exam-id> ATTEMPT_AUTHORIZATION='Bearer <student-token>' ATTEMPT_ORGANIZATION_ID=<org-id> make test-attempt-live
  • ATTEMPT_BROWSER_SELF_TEST=1 bash scripts/test/attempt-browser-smoke.sh
  • ATTEMPT_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-browser
  • DOCX_PHYSICS_GATEWAY_ATTEMPT_LOCAL_POSTGRES_CONFIRM=local-postgres HOCTAPAZ_DOCX_CORPUS_DIR=/Users/velikho/Desktop/test-hoctapaz make test-docx-physics-attempt-local-postgres

Go-platform documentation is generated from repository Markdown.