--- Section A: pure/offline checks --- [PASS] DomainValidator::isValidSlug accepts "career-quiz-1" [PASS] DomainValidator::isValidSlug rejects "Bad Slug!" [PASS] DomainValidator::isValidTelegramId accepts negative channel id [PASS] DomainValidator::isValidTelegramId rejects non-numeric [PASS] DomainValidator::toIntOrNull returns null for non-numeric [PASS] DomainValidator::clampLength truncates correctly [PASS] AdminAuthService::isAdmin true for configured id [PASS] AdminAuthService::isAdmin false for unrelated id [PASS] StudentStates::IDLE constant is "idle" [PASS] AnalyticsEvents::TEST_STARTED constant is stable [PASS] User::fromRow hydrates id/telegramId [PASS] User::isAdminRole true for role=admin [PASS] User::fromRow does not throw on empty array (missing keys) [PASS] User::toArray round trips role [PASS] TestSession::isInProgress true for status=in_progress [PASS] TestSession::fromRow decodes meta_json safely [PASS] TestSession::fromRow handles malformed meta_json without throwing --- Section B: DB-backed checks --- [PASS] SettingsService::set + get round trips value [PASS] SettingsService cache invalidates on set (sees new value immediately) [PASS] SettingsService cache file was written [PASS] UserService upsert is idempotent (same id both times) [PASS] UserService upsert updates username on second call [PASS] UserService upsert keeps first ref_code (first-touch attribution) [PASS] StateService::set + get round trips state name [PASS] StateService::set + get round trips context [PASS] StateService::mergeContext preserves existing keys [PASS] StateService::mergeContext adds new keys [PASS] StateService::resetToIdle returns to idle with empty context [PASS] CampaignService::create returns a persisted row with id [PASS] CampaignService::findByCode retrieves the same campaign [PASS] TestService::create returns a persisted test with id [PASS] TestGraphService::getGraph returns the created profile [PASS] TestGraphService::getGraph returns the created question with its option [PASS] TestGraphService::getGraph attaches option->profile score [PASS] SessionService::startNew creates an in_progress session [PASS] SessionService::startNew abandons the previous session (policy: one in_progress per user/test) [PASS] Previous session status is now "abandoned" in DB [PASS] SessionService::saveAnswer + getSelectedOptionIds round trips the picked option [PASS] SessionService::saveAnswer is idempotent per question (no duplicate row) [PASS] Session is still in_progress before complete() [PASS] SessionService::complete clears in_progress lookup [PASS] AnalyticsService::record persists an event row PHASE 4 DOMAIN SMOKE RESULT: OK (0 failures)