Appearance
Exam Start Adapter Task
Agent workflow: follow README.md for Audit -> Investigate -> Code -> Test -> Fix; keep this pack's writable scope and verification commands authoritative.
Dispatch type: orchestrator
Owner: orchestrator with exam-service, attempt-service, api-gateway, and optional bff-service workers.
Goal:
Promote student exam start only through an adapter that calls exam-service runtime access first, then attempt-service start with the resolver-owned data.startInput payload.
Legacy source evidence:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/attempts/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/web
Current foundation:
exam-serviceexposesPOST /v1/exams/{id}/runtime-access.- The resolver returns
data.startInputshaped for attempt-service and denies missing/wrong password, unpublished/offline/closed/not-open exams, inactive links, unassigned students through verifier, and missing snapshots. - Default gateway routes must keep
/api/exams/:examId/starton legacy until this task has route proof.
Writable files:
deploy/gateway/routes.attempt-native-example.jsondeploy/gateway/routes.attempt-native-localhost-example.jsonservices/api-gateway/**services/bff-service/**only if BFF is explicitly chosen as the adapter hostservices/attempt-service/**only for trusted decision/idempotency hardeningcontracts/openapi/services/{api-gateway,bff-service,attempt-service}.yamldocs/api/api-gateway.mddocs/api/exam-service.mddocs/api/attempt-service.mddocs/qa/attempt-live-smoke.mddocs/qa/attempt-browser-smoke.mddocs/qa/exam-authoring-route-rehearsals.mdscripts/test/attempt-route-coverage.shscripts/test/attempt-live-smoke.shscripts/test/attempt-browser-smoke.shscripts/test/attempt-browser-smoke.mjs- root
Makefileonly fortest-attempt-routes,test-attempt-live, andtest-attempt-browser - this task file
Delegated gateway verification scope:
- Route examples:
deploy/gateway/routes.attempt-native-example.jsonanddeploy/gateway/routes.attempt-native-localhost-example.json. - Test scripts:
scripts/test/attempt-route-coverage.sh,scripts/test/attempt-live-smoke.sh,scripts/test/attempt-browser-smoke.sh, andscripts/test/attempt-browser-smoke.mjs. - QA docs:
docs/qa/attempt-live-smoke.md,docs/qa/attempt-browser-smoke.md, anddocs/qa/exam-authoring-route-rehearsals.md. - Make targets:
test-attempt-routes,test-attempt-live, andtest-attempt-browser.
Orchestrator-owned files:
- default gateway route promotion
- frontend public route callers and browser smoke artifacts
- production rollout/rollback notes
Must not edit:
exam-servicepolicy rules unless the resolver contract itself changes.- frontend public route callers until the non-default adapter rehearsal is passing.
Tasks:
- Choose adapter host: api-gateway aggregation or BFF write adapter. Do not split policy across both.
- Adapter request flow:
- forward actor/org/correlation headers to exam-service runtime access
- if
allowed=false, return a legacy-compatible denial without calling attempt-service - if
allowed=true, post onlydata.startInputto attempt-service - propagate idempotency/correlation context through both calls
- Add attempt-service hardening so public-compatible start aliases reject caller-supplied runtime/access bodies unless they are internal/trusted adapter calls.
- Done locally: both attempt-service start endpoints now require
X-Exam-Runtime-Decision-Source: exam-serviceplus idempotency/correlation context before acceptingdata.startInput. - Done locally: attempt-service usecase coverage proves attempt-owned retake/open-attempt/submitted count behavior after a trusted resolver decision, including access-link per-student limits, guest-link shared capacity, inactive/expired link rejection, overdue assignment rejection, deadline clamp to future assignment due times, and legacy expiry equality semantics.
- Done locally: attempt-service persists the selected start idempotency or correlation value per exam/student, exact replays return the original attempt after submit/grade, and a partial unique index prevents concurrent duplicate
IN_PROGRESSattempts for the same exam/student. - Done locally: api-gateway owns an
exam_start_adapterroute state forPOST /api/exams/:examId/start. The adapter calls exam-service/v1/exams/{id}/runtime-access, returns a denial without calling attempt-service whenallowed=false, and forwards onlydata.startInputto attempt-service/v1/exams/{id}/startwithX-Internal-Service: api-gateway, the shared internal token, and the trusted runtime decision marker whenallowed=true. Direct requests without that token are rejected by both native hops. - Done locally: non-default attempt route-table examples now use
exam_target+attempt_targetfor the start adapter instead of a direct attempt-service proxy target. - Done locally: focused gateway tests prove
/api/exams/:examId/startcalls exam-service before attempt-service and never reaches attempt-service on a runtime-access denial. - Done locally: named opt-in route/live/browser proof hooks exist for the adapter flow without promoting the default route table.
make test-attempt-routesvalidates the non-default route-table shape,make test-attempt-livechecks/v1/routesplus gateway route headers against a running gateway, andmake test-attempt-browserruns the same header checks from a Playwright browser page context. - Keep default
deploy/gateway/routes.jsonon legacy until browser/route smoke has been run with real disposable fixtures and rollback evidence is recorded.
Acceptance:
- Gateway/BFF cannot bypass exam-service link/password/window/assignment/snapshot decisions.
- Attempt-service owns attempt counts and copied attempt snapshots; it does not query exam DBs.
- Public start route has a rollback route table and a test command.
Verification:
GOTOOLCHAIN=go1.25.11 go test ./services/api-gateway/... ./services/attempt-service/...make test-attempt-routesATTEMPT_LIVE_SELF_TEST=1 bash scripts/test/attempt-live-smoke.shATTEMPT_BROWSER_SELF_TEST=1 bash scripts/test/attempt-browser-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_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