Appearance
Local Development Runbook
Start
bash
cd go-platform
make devThe compose stack starts:
- PostgreSQL with service databases
- Redis
- NATS
- MinIO
- Go Formula DOCX parser runtime on
localhost:8095 - API Gateway on
localhost:8085 - selected core service skeletons
Stop
bash
make dev-downLogs
bash
make dev-logsSmoke Test
bash
BASE_URL=http://localhost:8085 scripts/test/smoke.shStorage MinIO E2E Smoke
scripts/test/storage-e2e.sh verifies the complete document-service object path:
- request
/presigned-upload PUTbytes to the returned signed URL- create
/media-assetsmetadata - read
/media-assets/{id}/content - compare downloaded bytes with the uploaded payload
Against Docker Compose document-service:
bash
cd go-platform
STORAGE_BASE_URL=http://localhost:8088 \
STORAGE_API_PREFIX=/v1/storage \
scripts/test/storage-e2e.shAgainst API Gateway with routes.storage-native-localhost-example.json:
bash
STORAGE_BASE_URL=http://localhost:8085 \
STORAGE_API_PREFIX=/api/storage \
scripts/test/storage-e2e.shscripts/test/storage-media-parity-smoke.sh compares a real legacy media asset read with the native document-service read path without mutating data:
bash
STORAGE_PARITY_MEDIA_ASSET_ID=<legacy-media-asset-id> \
STORAGE_PARITY_AUTHORIZATION='Bearer <token>' \
STORAGE_PARITY_ORGANIZATION_ID=<org-id> \
STORAGE_PARITY_NATIVE_MODE=auto \
scripts/test/storage-media-parity-smoke.shUse STORAGE_PARITY_NATIVE_MODE=storage-key before native media metadata is backfilled, or STORAGE_PARITY_NATIVE_MODE=asset-id plus STORAGE_PARITY_VARIANTS=original,trim,formula when the same media asset id is available in document-service.
DOCX Media Materialization Runtime Check
scripts/test/docx-materialization-parity.sh verifies that the live DOCX import path can materialize real corpus images through document-service:
- health-check Go Formula DOCX,
docx-import-service, anddocument-service - import the Physics 28Q corpus DOCX through
/v1/import/docx/qas - assert
28questions and7renderable images - assert all
7images are rewritten to/api/storage/media-assets/{id}/content - read each materialized asset back from
document-service
Against Docker Compose services, start the import/storage pair and the internal Go Formula DOCX runtime with a container-facing presigned upload endpoint. The host-facing default http://localhost:9902 is correct for storage-e2e.sh, but docx-import-service needs the signed URL to resolve from inside the Compose network:
bash
cd go-platform
S3_PRESIGNED_UPLOAD_ENDPOINT=http://minio:9000 \
docker compose -f deploy/docker-compose.yml up -d --build go-formula-docx document-service docx-import-service
# One-time for a fresh local hoctapaz_document_db when goose is unavailable.
awk '/^-- \+goose Down/{exit} {print}' services/document-service/migrations/000001_init.sql \
| docker compose -f deploy/docker-compose.yml exec -T postgres-local \
psql -U hoctapaz -d hoctapaz_document_db
awk '/^-- \+goose Down/{exit} {print}' services/document-service/migrations/000002_media_assets.sql \
| docker compose -f deploy/docker-compose.yml exec -T postgres-local \
psql -U hoctapaz -d hoctapaz_document_db
GO_FORMULA_DOCX_URL=http://localhost:8095 \
DOCX_IMPORT_SERVICE_URL=http://localhost:8087 \
DOCUMENT_SERVICE_URL=http://localhost:8088 \
HOCTAPAZ_DOCX_CORPUS_DIR=/Users/velikho/Desktop/test-hoctapaz \
make test-docx-materializationCompose sets docx-import-service GO_FORMULA_DOCX_URL to http://go-formula-docx:8080 by default. Override it only when testing an external parser process.
DOCX Import Realtime Fanout
docx-import-service uses in-process SSE fanout when REDIS_URL is empty. In Docker Compose and local K8s, REDIS_URL is set and the service also publishes redacted job lifecycle updates to Redis pub/sub so multiple service processes can share job.updated events.
Native DOCX job uploads are parsed by a bounded in-process worker queue. While a job is waiting or active, the legacy-compatible SSE/list summary includes job.queue fields such as active, waiting, waitingPosition, and jobsAhead.
Queued uploads are written to DOCX_IMPORT_PAYLOAD_DIR before the job is persisted. The worker stores only an opaque payload reference on the job row and deletes the file after a terminal outcome. On startup, the service re-enqueues stored PENDING/PROCESSING jobs if the payload still exists, or fails them with DOCX_IMPORT_PAYLOAD_MISSING if it does not. Docker Compose uses a named volume. The offline K8s manifest and Helm chart mount a docx-import-payloads PersistentVolumeClaim by default so queued payload refs survive pod replacement in local clusters that provide dynamic storage.
Default settings:
bash
DOCX_IMPORT_EVENTS_CHANNEL=docx-import:jobs
DOCX_IMPORT_WORKER_CONCURRENCY=2
DOCX_IMPORT_PAYLOAD_DIR=/var/lib/hoctapaz/docx-import-payloads
GO_FORMULA_DOCX_URL=http://go-formula-docx:8080Validate the payload persistence deployment wiring with:
bash
make test-docx-payload-persistenceWhen comparing native DOCX status reads against a legacy worker stack, enable the read-only BullMQ bridge so pending native summaries can report the legacy exam-import-algorithm queue snapshot if the job is not in the native in-process queue:
bash
DOCX_IMPORT_BULLMQ_BRIDGE_ENABLED=1
DOCX_IMPORT_BULLMQ_REDIS_URL=redis://host.docker.internal:6388
DOCX_IMPORT_BULLMQ_QUEUE_NAME=exam-import-algorithm
DOCX_IMPORT_BULLMQ_QUEUE_PREFIX=bullThe bridge reads BullMQ Redis keys only. It does not enqueue, remove, retry, or claim legacy jobs. Validate the deployment wiring with:
bash
make test-docx-bullmq-bridgeValidate a live pending/processing job row through the non-default status route table with:
bash
DOCX_BULLMQ_STATUS_JOB_ID=<job-id> \
DOCX_BULLMQ_STATUS_AUTHORIZATION='Bearer <token>' \
DOCX_BULLMQ_STATUS_ORGANIZATION_ID=<org-id> \
DOCX_BULLMQ_STATUS_EXPECT_QUEUE_STATUS=waiting \
make test-docx-bullmq-statusThe live smoke reads GET /api/exam-import/teacher-library?q=<jobId> and asserts the row exposes legacy-shaped BullMQ queue fields. It does not mutate Redis or the import job.
Validate the real browser route with:
bash
DOCX_IMPORT_LIBRARY_BROWSER_AUTHORIZATION='Bearer <token>' \
DOCX_IMPORT_LIBRARY_BROWSER_ORGANIZATION_ID=<org-id> \
make test-docx-import-library-browserValidate the static import-status route-table safety rules with:
bash
make test-import-status-routesValidate the non-default DOCX Fast create route through a live gateway only when a DOCX has already been uploaded to document-service/MinIO and the gateway is running with routes.import-create-native-localhost-example.json:
bash
IMPORT_CREATE_LIVE_CONFIRM=create-native \
IMPORT_CREATE_LIVE_STORAGE_KEY=<uploaded-docx-storage-key> \
IMPORT_CREATE_LIVE_FILE_NAME=<file-name.docx> \
IMPORT_CREATE_LIVE_AUTHORIZATION='Bearer <token>' \
IMPORT_CREATE_LIVE_ORGANIZATION_ID=<org-id> \
make test-import-create-liveThe live smoke writes one native DOCX Fast job, validates gateway route headers, checks the legacy success envelope, and then confirms the sibling job detail path still routes through the broad legacy import route. It does not run without IMPORT_CREATE_LIVE_CONFIRM=create-native.
Validate the materialized DOCX Fast temp-draft read branch through the same non-default route table only when the target native DOCX Fast job is completed and has stored parse output:
bash
IMPORT_TEMP_DRAFT_JOB_ID=<completed-native-docx-fast-job-id> \
IMPORT_TEMP_DRAFT_AUTHORIZATION='Bearer <token>' \
IMPORT_TEMP_DRAFT_ORGANIZATION_ID=<org-id> \
make test-import-temp-draft-liveThe live smoke validates /v1/routes, confirms a sibling temp-draft asset metadata path still routes through the broad legacy import route, reads the temp-draft, checks gateway route headers, and asserts the legacy materialized draft envelope with token materialized-{id}.
Validate materialized DOCX Fast temp asset content through the same non-default route table only when the target native DOCX Fast job is completed and the asset id is present in the stored parse result:
bash
IMPORT_TEMP_ASSET_JOB_ID=<completed-native-docx-fast-job-id> \
IMPORT_TEMP_ASSET_ID=<materialized-temp-asset-id> \
IMPORT_TEMP_ASSET_AUTHORIZATION='Bearer <token>' \
IMPORT_TEMP_ASSET_ORGANIZATION_ID=<org-id> \
make test-import-temp-asset-liveThe live smoke validates /v1/routes, confirms a sibling temp-draft asset metadata path still routes through the broad legacy import route, streams the asset content, checks gateway route headers, and asserts non-empty binary content with Content-Type plus Cache-Control: private, max-age=300.
Validate DOCX Fast reprocess through the same non-default route table only when the target native DOCX Fast job has a stored source DOCX and is not active in the native worker queue:
bash
IMPORT_REPROCESS_CONFIRM=reprocess-native \
IMPORT_REPROCESS_JOB_ID=<native-docx-fast-job-id> \
IMPORT_REPROCESS_AUTHORIZATION='Bearer <token>' \
IMPORT_REPROCESS_ORGANIZATION_ID=<org-id> \
make test-import-reprocess-liveThe live smoke validates /v1/routes, confirms a sibling import job detail path still routes through the broad legacy import route, posts the reprocess request, checks gateway route headers, and asserts the legacy success envelope preserves the same job id with message DOCX Fast import reprocessed.
For browser-route evidence from the real import surface, start the web app with NEXT_PUBLIC_API_URL pointing at the gateway and run either UI-upload mode:
bash
IMPORT_CREATE_BROWSER_AUTHORIZATION='Bearer <token>' \
IMPORT_CREATE_BROWSER_ORGANIZATION_ID=<org-id> \
IMPORT_CREATE_BROWSER_DOCX_FILE=/Users/velikho/Desktop/test-hoctapaz/sample.docx \
make test-import-create-browseror storage-key mode when the DOCX object already exists:
bash
IMPORT_CREATE_BROWSER_AUTHORIZATION='Bearer <token>' \
IMPORT_CREATE_BROWSER_ORGANIZATION_ID=<org-id> \
IMPORT_CREATE_BROWSER_STORAGE_KEY=<uploaded-docx-storage-key> \
IMPORT_CREATE_BROWSER_FILE_NAME=sample.docx \
make test-import-create-browserUse IMPORT_CREATE_BROWSER_PATH=/teacher/questions/import plus IMPORT_CREATE_BROWSER_AUTO_APPROVE=1 to validate the question-bank bulk import surface without changing frontend source.
To search for a specific BullMQ-backed job from the page:
bash
DOCX_IMPORT_LIBRARY_BROWSER_AUTHORIZATION='Bearer <token>' \
DOCX_IMPORT_LIBRARY_BROWSER_ORGANIZATION_ID=<org-id> \
DOCX_IMPORT_LIBRARY_BROWSER_JOB_ID=<job-id> \
DOCX_IMPORT_LIBRARY_BROWSER_EXPECT_QUEUE_STATUS=waiting \
make test-docx-import-library-browserRollback for the Redis layer is removing REDIS_URL or pointing DOCX_IMPORT_EVENTS_CHANNEL at an unused channel. Stored-job replay and local process fanout still work.
Legacy Proxy
Gateway proxies legacy routes to LEGACY_API_BASE_URL, defaulting to http://localhost:4001.
Storage Native Route Smoke
The default route table keeps /api/storage/* on legacy. To test the strangler path locally without frontend changes, run the gateway with the storage native example:
bash
cd go-platform
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.storage-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverRun document-service separately on :8097, or in Docker Compose use the container-facing target in deploy/gateway/routes.storage-native-example.json.
Rollback is changing GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json or setting the storage route state to legacy_proxy.
Question Type DB Read Smoke
For a fresh local hoctapaz_question_bank_db, apply the service migrations and run the native read endpoint with Postgres enabled:
bash
cd go-platform
awk '/^-- \+goose Down/{exit} {print}' services/question-bank-service/migrations/000001_init.sql \
| docker compose -f deploy/docker-compose.yml exec -T postgres-local \
psql -U hoctapaz -d hoctapaz_question_bank_db
awk '/^-- \+goose Down/{exit} {print}' services/question-bank-service/migrations/000002_question_type_definitions.sql \
| docker compose -f deploy/docker-compose.yml exec -T postgres-local \
psql -U hoctapaz -d hoctapaz_question_bank_db
awk '/^-- \+goose Down/{exit} {print}' services/question-bank-service/migrations/000003_question_core.sql \
| docker compose -f deploy/docker-compose.yml exec -T postgres-local \
psql -U hoctapaz -d hoctapaz_question_bank_db
DATABASE_URL=postgres://hoctapaz:hoctapaz@localhost:5433/hoctapaz_question_bank_db?sslmode=disable \
HTTP_ADDR=:8098 \
go run ./services/question-bank-service/cmd/server
curl -sS -H 'X-Organization-Id: org_local' \
'http://localhost:8098/v1/question-types?summary=1'
curl -sS -X POST \
-H 'Content-Type: application/json' \
-H 'X-Organization-Id: org_local' \
-H 'X-User-Id: teacher_local' \
-H 'X-User-Role: TEACHER' \
--data '{"code":"LOCAL_CUSTOM","name":"Loại local","baseType":"ESSAY"}' \
'http://localhost:8098/v1/question-types'
curl -sS -X POST \
-H 'Content-Type: application/json' \
-H 'X-Organization-Id: org_local' \
-H 'X-User-Id: teacher_local' \
--data '{"document_id":"doc_local","questions":[{"id":"q1","type":"SINGLE_CHOICE","stem_text":"Question","options":[{"id":"A","content":"A"},{"id":"B","content":"B"}],"correct_answer":{"option_id":"A"},"solution_html":"Solution","media":[],"formulas":[{"id":"f1","latex":"x^2","html":"<span>x</span>"}]}]}' \
'http://localhost:8098/v1/questions/import-docx-output'
curl -sS -H 'X-Organization-Id: org_local' \
'http://localhost:8098/v1/questions?page=1&limit=5&q=Question&type=SINGLE_CHOICE&unclassifiedOnly=1'The default gateway route table keeps /api/question-types on legacy. Shadow question-type route tables match only exact GET /api/question-types. Native question-type route tables match exact GET plus POST/PATCH/DELETE write methods and keep unlisted methods plus nested GET manager routes on the broad legacy fallback.
Run the static route-table preflight before local gateway rehearsal:
bash
cd go-platform
make test-question-types-routes
QUESTION_TYPES_SELF_TEST=1 make test-question-types-liveUse deploy/gateway/routes.question-types-shadow-localhost-example.json for shadow-read comparison, or deploy/gateway/routes.question-types-native-localhost-example.json for native read/write rehearsal with gateway auth enabled. Full default cutover remains blocked until /teacher/questions/types, import-editor consumers, and browser parity are proven.
After starting the gateway with the native localhost route table and preparing a valid legacy auth token or session cookie, run the opt-in live write smoke:
bash
QUESTION_TYPES_LIVE_CONFIRM=write-native \
QUESTION_TYPES_AUTHORIZATION='Bearer <token>' \
QUESTION_TYPES_ORGANIZATION_ID=org_local \
GATEWAY_BASE_URL=http://localhost:8085 \
make test-question-types-liveThe script creates a unique custom question type, updates it, archives it via bulk status, hard-deletes it via bulk delete, and stores request/response artifacts in a temporary directory unless QUESTION_TYPES_ARTIFACT_DIR is set.
AI Classification Apply Gateway Smoke
The default gateway route table keeps /api/questions/ai-classify/* on legacy. Use deploy/gateway/routes.question-classification-apply-native-localhost-example.json only for the non-default apply-route rehearsal. This route table carves out exact PATCH /api/questions/ai-classify/apply while keeping job/status/SSE classification routes on the broad legacy questions fallback.
Run static route-table coverage before any live write rehearsal:
bash
cd go-platform
make test-question-classification-apply-routes
make test-ai-classification-job-route-guardRun the hermetic assertion self-test first:
bash
cd go-platform
QUESTION_CLASSIFICATION_APPLY_SELF_TEST=1 make test-question-classification-apply-liveAfter starting the gateway with the native apply route table and preparing a native question row plus legacy auth token or session cookie, run:
bash
QUESTION_CLASSIFICATION_APPLY_CONFIRM=apply-native \
QUESTION_CLASSIFICATION_APPLY_QUESTION_ID=<native-question-id> \
QUESTION_CLASSIFICATION_APPLY_AUTHORIZATION='Bearer <token>' \
QUESTION_CLASSIFICATION_APPLY_ORGANIZATION_ID=<org-id> \
GATEWAY_BASE_URL=http://localhost:8085 \
make test-question-classification-apply-liveThe smoke validates /v1/routes, confirms GET /api/questions/ai-classify/jobs?limit=1 still routes through broad legacy questions, applies the requested classification suggestion through the gateway, and checks the legacy-shaped success envelope.
Keep the job guard in place until ai-classifier-service proves BullMQ worker, taxonomy-rich prompt context, provider runtime, gateway RBAC, and browser parity. The guard allows the exact apply route above, but blocks public native routes for /api/questions/ai-classify/suggestions, /api/questions/ai-classify/jobs*, job SSE, job error history, and cancel.
Question Read Shadow Parity Smoke
scripts/test/question-read-shadow-parity.sh verifies that the gateway question-read shadow route table keeps the client response identical to legacy while also checking the native question-bank-service compatibility adapter at /v1/legacy/questions.
Run the static route-table preflight first:
bash
cd go-platform
make test-question-read-routesStart question-bank-service with Postgres on :8098 and start the gateway with the question read shadow route table:
bash
cd go-platform
DATABASE_URL=postgres://hoctapaz:hoctapaz@localhost:5433/hoctapaz_question_bank_db?sslmode=disable \
HTTP_ADDR=:8098 \
go run ./services/question-bank-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.questions-read-shadow-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThen run:
bash
QUESTION_READ_PARITY_QUESTION_ID=<id-present-in-legacy-and-native> \
QUESTION_READ_PARITY_ORGANIZATION_ID=<org-id> \
QUESTION_READ_PARITY_USER_ID=<teacher-id> \
QUESTION_READ_PARITY_USER_ROLE=TEACHER \
make test-question-read-shadowUse QUESTION_READ_PARITY_REQUIRE_NATIVE=0 only for a route-table/client-response smoke before native data has been backfilled. Cutover evidence should keep native checks enabled. The smoke expects shadow routes to rewrite to /v1/legacy/questions by default; set QUESTION_READ_PARITY_SHADOW_TARGET_PREFIX=/v1/questions only for older raw-native route-table experiments.
Rollback is changing GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json or setting questions-list and questions-detail to legacy_proxy.
To rehearse native question reads through the gateway after native data has been backfilled, use the non-default native route table instead:
bash
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.questions-read-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThis routes only authenticated GET /api/questions and GET /api/questions/:id to the compatibility adapter in question-bank-service. POST /api/questions, AI classification job routes, folders, groups, and other nested question routes still use the broad legacy fallback. Rollback is switching back to deploy/gateway/routes.json.
Then compare gateway native responses with direct adapter responses:
bash
QUESTION_READ_NATIVE_AUTHORIZATION='Bearer <access-token>' \
QUESTION_READ_NATIVE_ORGANIZATION_ID=<org-id> \
QUESTION_READ_NATIVE_USER_ID=<same-user-id-as-token> \
QUESTION_READ_NATIVE_USER_ROLE=TEACHER \
QUESTION_READ_NATIVE_QUESTION_ID=<id-present-in-native> \
make test-question-read-nativeThe native parity smoke also checks that writes and nested question routes still report the broad legacy-proxy route.
For browser-route evidence, start the web app with NEXT_PUBLIC_API_URL pointing at the gateway and run:
bash
QUESTION_READ_BROWSER_AUTHORIZATION='Bearer <teacher-or-admin-token>' \
QUESTION_READ_BROWSER_ORGANIZATION_ID=<org-id> \
make test-question-read-browserSet QUESTION_READ_BROWSER_DETAIL_ID=auto and QUESTION_READ_BROWSER_REQUIRE_ITEMS=1 when the local question read model has real rows and the editor-detail path must be verified through the browser.
For admin all-organization browser evidence, use an admin token:
bash
QUESTION_READ_BROWSER_AUTHORIZATION='Bearer <admin-token>' \
QUESTION_READ_BROWSER_ORGANIZATION_ID=<org-id> \
make test-question-read-browser-adminAuth Route Rehearsal
The default gateway route table keeps /api/auth/* on legacy. Use deploy/gateway/routes.auth-native-localhost-example.json only for a non-default native session rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-auth-routesStart auth-service on :8081 and the gateway with the non-default route table:
bash
cd go-platform
AUTH_JWT_SECRET=<shared-local-secret> \
HTTP_ADDR=:8081 \
go run ./services/auth-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.auth-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses only register, login, refresh, logout, and me. Google auth, test-login, forgot/reset password, profile, KYC, email-change, and other auth siblings stay on broad legacy auth routing. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Organization Read Route Rehearsal
The default gateway route table keeps /api/organizations* on legacy. Use deploy/gateway/routes.organizations-read-native-localhost-example.json only for a non-default read-only organization rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-organization-routesStart school-service on :8083 and the gateway with the non-default route table:
bash
cd go-platform
HTTP_ADDR=:8083 go run ./services/school-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.organizations-read-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses only GET /api/organizations, GET /api/organizations/:id, GET /api/organizations/:id/units, and GET /api/organizations/:id/members. Organization writes, purge, unit/member mutations, student-imports, broad /api/organizations, and fallback stay on legacy. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Classroom Route Guard
The default gateway route table keeps /api/classrooms* and /api/admin/classrooms* on legacy. There is no non-default classroom public route-table rehearsal yet because legacy classroom surfaces still require teacher, organization, subject, user, lesson, assignment, attempt, and deferred counter hydration outside classroom-service.
Run the static no-cutover guard before adding or changing gateway route tables:
bash
cd go-platform
make test-classroom-route-guardThis guard scans deploy/gateway/routes.json and current non-default deploy/gateway/routes*.json examples. It fails if /api/classrooms* or /api/admin/classrooms* is promoted away from legacy_proxy. Future classroom adapters should update the guard only after response hydration, cross-service validation, auth/RBAC, and deferred count contracts are explicit.
Student Course Route Rehearsal
The default gateway route table keeps /api/student/courses*, /api/courses*, and /api/public* on legacy. Use deploy/gateway/routes.student-courses-read-native-localhost-example.json only for a non-default student course read rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-student-course-routesStart course-service on :8086 and the gateway with the non-default route table:
bash
cd go-platform
HTTP_ADDR=:8086 go run ./services/course-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.student-courses-read-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses only GET /api/student/courses and GET /api/student/courses/{courseId} with gateway auth, global STUDENT role checks, and organization header injection. Recommendation, purchase, lesson progress, video progress, material view writes, teacher/admin course management, public catalog, broad student-course/course/public routes, and fallback stay on legacy. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Student Course Progress Route Rehearsal
The default gateway route table keeps /api/student/courses*, /api/courses*, and /api/public* on legacy. Use deploy/gateway/routes.student-course-progress-native-localhost-example.json only for a non-default student course progress write rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-student-course-progress-routesStart course-service on :8086 and the gateway with the non-default route table:
bash
cd go-platform
HTTP_ADDR=:8086 go run ./services/course-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.student-course-progress-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses only POST /api/student/courses/{courseId}/lessons/{lessonId}/progress, POST /api/student/courses/{courseId}/lessons/{lessonId}/video-progress, and POST /api/student/courses/{courseId}/materials/{materialId}/view with gateway auth, global STUDENT role checks, and organization header injection. Student course list/detail are covered by the separate read rehearsal and remain legacy in this progress-only table. Mastery, recommendation, purchase, teacher/admin course management, public catalog, broad student-course/course routes, and fallback stay on legacy. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Notification Route Rehearsal
The default gateway route table keeps /api/notifications*, /api/parent/alerts*, /api/alerts*, and /api/admin/inbox* on legacy. Use deploy/gateway/routes.notifications-native-localhost-example.json only for a non-default inbox/preferences rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-notification-routesStart notification-service on :8092 and the gateway with the non-default route table:
bash
cd go-platform
HTTP_ADDR=:8092 go run ./services/notification-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.notifications-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses notification inbox list/detail, mark-read, delete, read-all/delete-all, and both preferences aliases: /api/notifications/preferences and /api/alerts/preferences. Notification create/batch/snapshot, parent alerts, weak-topic alerts, admin inbox, broad notification/alert/admin routes, and fallback stay on legacy. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Parent Alert Route Rehearsal
The default gateway route table keeps /api/parent/alerts*, /api/notifications*, /api/alerts*, and /api/admin* on legacy. Use deploy/gateway/routes.parent-alerts-native-localhost-example.json only for a non-default parent alert read rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-parent-alert-routesStart notification-service on :8092 and the gateway with the non-default route table:
bash
cd go-platform
HTTP_ADDR=:8092 go run ./services/notification-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.parent-alerts-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses only GET /api/parent/alerts, POST /api/parent/alerts/{notificationId}/read, and POST /api/parent/alerts/read-all with gateway auth plus global PARENT role checks. Weak-topic alert creation, parent-child relationship lookup, notification inbox/preferences, admin inbox, broad parent-alert/alert/admin routes, and fallback stay on legacy. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Feature Maintenance Route Rehearsal
The default gateway route table keeps /api/feature-maintenance*, /api/admin/feature-maintenance*, /api/admin/operations*, broad /api/admin*, and broad /api/questions* on legacy. Use deploy/gateway/routes.feature-maintenance-native-localhost-example.json only for a non-default feature-maintenance public/admin rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-feature-maintenance-routesStart admin-service on :8094 and the gateway with the non-default route table:
bash
cd go-platform
HTTP_ADDR=:8094 go run ./services/admin-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.feature-maintenance-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses only GET /api/feature-maintenance/public, GET /api/admin/feature-maintenance, and PATCH /api/admin/feature-maintenance/{key}. The public route stays unauthenticated; admin routes require a bearer token with global ADMIN role. Broad feature-maintenance/admin routes, AI classification job reads, admin operations/audit, users/settings/wallet/support/dashboard, and fallback stay on legacy. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Admin Audit Route Rehearsal
The default gateway route table keeps /api/admin/operations* and broad /api/admin* on legacy. Use deploy/gateway/routes.admin-audit-native-localhost-example.json only for a non-default audit-log read rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-admin-audit-routesStart admin-service on :8094 and the gateway with the non-default route table:
bash
cd go-platform
HTTP_ADDR=:8094 go run ./services/admin-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.admin-audit-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses only GET /api/admin/operations/audit. It requires a bearer token with global ADMIN role. Admin operations health, queues, retry/clear commands, metrics, import jobs, outbox, AI settings, feature-maintenance routes not included in the table, and broad admin routes stay on legacy. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Analytics Read Route Rehearsal
The default gateway route table keeps /api/analytics*, /api/exams*, /api/classrooms*, /api/students*, /api/parents*, and fallback on legacy. Use deploy/gateway/routes.analytics-read-native-localhost-example.json only for a non-default analytics result/weak-topic read rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-analytics-routesStart analytics-service on :8093 and the gateway with the non-default route table:
bash
cd go-platform
HTTP_ADDR=:8093 go run ./services/analytics-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.analytics-read-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses only GET /api/analytics/results and GET /api/analytics/weak-topics. Both require a bearer token with global ADMIN or TEACHER role and an organization id from X-Organization-Id, organizationId, or the token default organization. Snapshot/event writes, exam/classroom/student/parent analytics, classroom export, and broad route families stay on legacy. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Attempt Route Rehearsal
The default gateway route table keeps /api/exams/* and /api/attempts/* on legacy. Use deploy/gateway/routes.attempt-native-localhost-example.json only for a non-default attempt runtime rehearsal.
Run the static route-table preflight first:
bash
cd go-platform
make test-attempt-routesStart attempt-service on :8091 and the gateway with the non-default route table:
bash
cd go-platform
HTTP_ADDR=:8091 go run ./services/attempt-service/cmd/server
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.attempt-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
AUTH_JWT_SECRET=<same-secret-as-legacy-token> \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThe route table rehearses only POST /api/exams/:examId/start, GET /api/attempts/:attemptId, answer save, submit, event read/write, and result read. Broad /api/exams, broad /api/attempts, and fallback stay on legacy. Rollback is switching GATEWAY_ROUTE_TABLE back to deploy/gateway/routes.json.
Import Review Save/Detail Live Smoke
scripts/test/import-review-roundtrip-live-smoke.sh exercises the opt-in P4-020 gateway route table with a real import job:
- fetch
GET /api/exam-import/jobs/{id} - save a reviewed parse payload through
PATCH /api/exam-import/jobs/{id}/review - reload
GET /api/exam-import/jobs/{id} - verify
questionTypeManualOverrideandquestionTypeReviewed
Start the gateway with the combined route table:
bash
cd go-platform
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.import-review-roundtrip-native-localhost-example.json \
LEGACY_API_BASE_URL=http://localhost:4001 \
HTTP_ADDR=:8085 \
go run ./services/api-gateway/cmd/serverThen run the smoke with a completed native import job id and a valid teacher or admin session. By default the script restores the original parse payload after verification:
bash
IMPORT_REVIEW_ROUNDTRIP_JOB_ID=<native-job-id> \
IMPORT_REVIEW_ROUNDTRIP_AUTHORIZATION='Bearer <access-token>' \
IMPORT_REVIEW_ROUNDTRIP_ORGANIZATION_ID=<org-id> \
make test-import-review-roundtrip-liveSet IMPORT_REVIEW_ROUNDTRIP_RESTORE=0 only when intentionally leaving the reviewed payload in place for follow-up browser verification. Rollback is returning the gateway to deploy/gateway/routes.json; the default route table does not include these native review/detail routes.
Import Approval Route Preflight
scripts/test/import-approval-route-coverage.sh statically validates the non-default import approval route-table examples without executing approval or mutating data:
bash
cd go-platform
make test-import-approval-routesThe check verifies only POST /api/exam-import/jobs/{id}/approve is carved out to docx-import-service, while sibling import job actions stay on the broad legacy /api/exam-import route. Live approval parity still requires a completed import job, native question-bank/exam dependencies, and explicit target-write approval.
To exercise the approval write through the gateway, start the gateway with deploy/gateway/routes.import-approval-native-localhost-example.json and run:
bash
cd go-platform
IMPORT_APPROVAL_LIVE_CONFIRM=approve-native \
IMPORT_APPROVAL_JOB_ID=<completed-job-id> \
IMPORT_APPROVAL_AUTHORIZATION='Bearer <token>' \
IMPORT_APPROVAL_ORGANIZATION_ID=<org-id> \
make test-import-approval-liveOptional payload overrides:
bash
IMPORT_APPROVAL_BODY_JSON='{"target":"QUESTION_BANK","questionMetadata":{"subjectId":"math","gradeLevel":12}}'
IMPORT_APPROVAL_BODY_FILE=/path/to/approval-body.jsonThe live smoke first validates /v1/routes, checks GET /api/exam-import/jobs/{id} still uses the broad legacy route, then posts /api/exam-import/jobs/{id}/approve and asserts route headers plus the native approval response. It exits before any HTTP request unless IMPORT_APPROVAL_LIVE_CONFIRM=approve-native is set.