test(advanced-assessment): fix MCQ gate test for 8-MCQ composition#168
Conversation
… — 2/8 correct needed to clear 75% threshold
|
Warning Review limit reached
More reviews will be available in 4 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
* feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal --------- Co-authored-by: Joy <joykenneth605@gmail.com>
* feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com>
* feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com>
* feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename * fix(personal-assessment): send min/max lengths and preserve input_type for all questions (#177) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(personal-assessment): add maxLength to all text questions and input_type to AI response schema * feat(ai-resources): add timeout to AI calls and cache warming for resources (#163) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * fix: separate job search status from availability & fix verified profile tags (#164) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * perf(personal-assessment): use instant fallback instead of slow AI generation (#178) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * perf(personal-assessment): skip AI generation, use instant deterministic fallback --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com>
* feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename * fix(personal-assessment): send min/max lengths and preserve input_type for all questions (#177) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(personal-assessment): add maxLength to all text questions and input_type to AI response schema * feat(ai-resources): add timeout to AI calls and cache warming for resources (#163) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * fix: separate job search status from availability & fix verified profile tags (#164) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * perf(personal-assessment): use instant fallback instead of slow AI generation (#178) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * perf(personal-assessment): skip AI generation, use instant deterministic fallback * fix(talent): validate roleTrack to prevent wrong-track assessment questions (#181) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): validate roleTrack against TALENT_ROLE_TRACKS and set profile.track on onboarding --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com>
* test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * chore: promote dev to staging (#167) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal --------- Co-authored-by: Joy <joykenneth605@gmail.com> * chore: resolve dev→staging merge conflict (#174) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * update staging (#176) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * chore: promote dev to staging (resolved conflicts) (#180) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename * fix(personal-assessment): send min/max lengths and preserve input_type for all questions (#177) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(personal-assessment): add maxLength to all text questions and input_type to AI response schema * feat(ai-resources): add timeout to AI calls and cache warming for resources (#163) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * fix: separate job search status from availability & fix verified profile tags (#164) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * perf(personal-assessment): use instant fallback instead of slow AI generation (#178) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * perf(personal-assessment): skip AI generation, use instant deterministic fallback --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * fix(dashboard): count all skill attempts (started + completed) so 3/3 shows during active session --------- Co-authored-by: Felix Gogodae <67784572+Trojanhorse7@users.noreply.github.com> Co-authored-by: Aaron <52609237+kenneropia@users.noreply.github.com> Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com>
* feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename * fix(personal-assessment): send min/max lengths and preserve input_type for all questions (#177) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(personal-assessment): add maxLength to all text questions and input_type to AI response schema * feat(ai-resources): add timeout to AI calls and cache warming for resources (#163) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * fix: separate job search status from availability & fix verified profile tags (#164) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * perf(personal-assessment): use instant fallback instead of slow AI generation (#178) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * perf(personal-assessment): skip AI generation, use instant deterministic fallback * fix(talent): validate roleTrack to prevent wrong-track assessment questions (#181) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): validate roleTrack against TALENT_ROLE_TRACKS and set profile.track on onboarding * fix(dashboard): show 3/3 attempts used during active 3rd session (#184) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * chore: promote dev to staging (#167) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal --------- Co-authored-by: Joy <joykenneth605@gmail.com> * chore: resolve dev→staging merge conflict (#174) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * update staging (#176) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * chore: promote dev to staging (resolved conflicts) (#180) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename * fix(personal-assessment): send min/max lengths and preserve input_type for all questions (#177) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(personal-assessment): add maxLength to all text questions and input_type to AI response schema * feat(ai-resources): add timeout to AI calls and cache warming for resources (#163) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * fix: separate job search status from availability & fix verified profile tags (#164) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * perf(personal-assessment): use instant fallback instead of slow AI generation (#178) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * perf(personal-assessment): skip AI generation, use instant deterministic fallback --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * fix(dashboard): count all skill attempts (started + completed) so 3/3 shows during active session --------- Co-authored-by: Felix Gogodae <67784572+Trojanhorse7@users.noreply.github.com> Co-authored-by: Aaron <52609237+kenneropia@users.noreply.github.com> Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> Co-authored-by: Felix Gogodae <67784572+Trojanhorse7@users.noreply.github.com> Co-authored-by: Aaron <52609237+kenneropia@users.noreply.github.com>
* feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename * fix(personal-assessment): send min/max lengths and preserve input_type for all questions (#177) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(personal-assessment): add maxLength to all text questions and input_type to AI response schema * feat(ai-resources): add timeout to AI calls and cache warming for resources (#163) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * fix: separate job search status from availability & fix verified profile tags (#164) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * perf(personal-assessment): use instant fallback instead of slow AI generation (#178) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * perf(personal-assessment): skip AI generation, use instant deterministic fallback * fix(talent): validate roleTrack to prevent wrong-track assessment questions (#181) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): validate roleTrack against TALENT_ROLE_TRACKS and set profile.track on onboarding * fix(dashboard): show 3/3 attempts used during active 3rd session (#184) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * chore: promote dev to staging (#167) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal --------- Co-authored-by: Joy <joykenneth605@gmail.com> * chore: resolve dev→staging merge conflict (#174) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * update staging (#176) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * chore: promote dev to staging (resolved conflicts) (#180) * feat(auth): add change-password DTO and shared messages * feat(auth): implement changePassword service method * feat(auth): add POST /auth/change-password endpoint * test(auth): unit-test changePassword service method * test(auth): controller tests for change-password — cookie clearing and delegation * feat(talent): rename SetProfileDto fields to snake_case (education_level, linkedin_url, avatar_url) * feat(talent): rename SaveTalentProfileDto fields to snake_case (education_level, linkedin_url) * feat(auth): add linkedin_url and is_job_ready to /auth/me response * test(auth): cover linkedin_url and is_job_ready in getProfile * Revert "feat(auth/talent): add linkedin_url + is_job_ready to /me; snake_case onboarding profile DTOs" * chore: add a new error message for conflict submission * fix(advanced-assessment): prevent starting a new attempt while submit is processing * test(advanced-assessment): assert processing lock blocks duplicate starts * fix(advanced-assessment): stop showing 90-minute timer after session submit * test(advanced-assessment): add getSession timer state coverage * chore: run pnpm format * fix(verified-profile): omit experience label from about_tags when validated level is present (#165) * test(verified-profile): add regression test for contradictory seniority + experience tags * fix(verified-profile): omit experience label from about_tags when validated level is present * test(verified-profile): update existing about_tags assertion to match corrected behavior * test(verified-profile): confirm experience label shown when no validated level * docs(verified-profile): document seniority/experience mutual-exclusion invariant * test(verified-profile): remove unreachable no-validated-level test case * fix(verified-profile): gate experience label suppression on hasValidatedLevel not seniorityBadge * test(verified-profile): fix incorrect seniority badge assertions in no-validated-level test * fix(advanced-assessment): correct question composition and add pending_lt3 session signal (#166) * test(advanced-assessment): add pending_lt3 assertion for 14-question session start * feat(advanced-assessment): add pending_lt3 flag to session response * test(advanced-assessment): add regression guard for 8 MCQ / 2 short-text composition * fix(advanced-assessment): correct question composition to 8 MCQ / 2 short-text per spec * docs(advanced-assessment): annotate question composition constants with spec source * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold (#168) * test(advanced-assessment): fix pending_lt3 REFLECTION filter case (#169) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * test(advanced-assessment): fix pending_lt3 filter — use SlotType.REFLECTION not string literal * fix(talent): persist resume filename and add delete endpoint (#171) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(talent): persist resume filename on upload and add DELETE /resume endpoint * feat(ai): resolve AI-generated resource URLs via YouTube & Serper APIs (#172) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * feat(ai): add URL resolution service for external link verification The AI model was generating hallucinated/broken URLs for learning resources. Instead of relying on the model to produce valid links, we now: - Generate only keywords, title, and description from the AI - Resolve video URLs via YouTube Data API v3 (search by title+description) - Resolve article URLs via Serper.dev Google Search API (excludes youtube.com) - Reclassify article items that resolve to YouTube URLs as type 'video' New env vars: YOUTUBE_API_KEY (optional), SERPER_API_KEY (optional) Removed deprecated: GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID (Google Custom Search API is closed to new customers) * feat(ai-resources): replace AI-generated URLs with verified external links Integrates UrlResolutionService into the resource generation pipeline: - After AI generates resource metadata, resolve all URLs via YouTube/Serper - Disable OpenRouter web search plugin (was causing timeouts without improving URL quality on the free model) - Relax Zod url validation from z.string().url() to z.string() to prevent costly retry loops when the model outputs placeholder URLs that will be replaced anyway - Increase INLINE_TIMEOUT_MS from 60s to 120s to accommodate the free model's slower response times - Articles that resolve to YouTube URLs are reclassified as videos and moved to the videos array * feat(ai-report): resolve URLs in guidance report resources Guidance reports also contained hallucinated resource URLs. Now: - After generating a guidance report, resolve recommended_resources URLs through Serper (articles) and YouTube API (videos) - Strip leading ": " prefix the model sometimes prepends to ai_summary, growth_insight, and summary fields - Add .min(20) constraint to ai_summary, growth_insight, and summary in the Zod schema so generateObject retries when the model produces garbage output (e.g. ": " or empty strings) instead of silently accepting it * perf(ai-report): parallelize report generation and limit on-demand scope The guidance-report endpoint was taking 160+ seconds because skill and advanced reports were generated sequentially. - Run both buildEnvelope calls in Promise.all (cuts total time ~50%) - Only trigger on-demand AI generation for advanced assessment reports; skill reports return cached data or empty envelope without blocking on AI generation This prevents the endpoint from timing out on slow free-tier models when both reports need generation simultaneously. * test(ai-report): update spec for advanced-only on-demand generation The "generates guidance report on demand" test was asserting on skill_guidance_report. Updated to reflect that only advanced reports trigger on-demand generation: - Mock returns null for skill result, result with null guidance_report for advanced - Assertion now checks advanced_guidance_report * refactor(verified-profile): simplify availability resolution based on job search status * fix(auth): account data export now emails attachment and returns download URL (#175) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(auth): send data export as email attachment and return base64 download_url * fix(auth): include user's name in data export filename * fix(personal-assessment): send min/max lengths and preserve input_type for all questions (#177) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(personal-assessment): add maxLength to all text questions and input_type to AI response schema * feat(ai-resources): add timeout to AI calls and cache warming for resources (#163) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * fix: separate job search status from availability & fix verified profile tags (#164) * feat(ai): add configurable timeout to OpenRouter chat calls - Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter. * feat(ai-resources): warm resource cache on track selection - Trigger background resource generation when a user selects their track during onboarding. Uses longer timeout for background work (5 min) and shorter timeout (60s) for inline user-facing fallback. * fix(ai-report): clarify fallback behavior for guidance report generation * fix: use live availability_status for verified profile tags - The about_tags on the verified profile were reading job_search_status from the frozen personal assessment answers instead of the current settings value (TalentProfile.availability_status). - Also guards against EmployerPoolProfile.availability being overwritten with job search status values by the settings endpoint. * fix: use live availability_status for verified profile about tags Previously buildAboutTags read job_search_status from the frozen personal_assessment_answers JSON, ignoring live changes made via settings. Now reads profile.availability_status directly, with fallback to assessment answer. Availability timing tag is hidden when user is not_looking. * feat: separate job_search_status from availability on employer pool Add job_search_status column to employer_pool_profiles so job search intent (actively_looking, open_to_opportunities, not_looking) is stored separately from availability timing (immediately_available, etc.). Migration backfills existing rows by moving job search values out of the availability column into the new field. Settings endpoint now writes to job_search_status instead of availability. * feat: exclude not_looking candidates from employer discovery Candidates with job_search_status = 'not_looking' no longer appear in employer browse results. They are NOT removed from an employer's saved candidates if already there. * feat: seed job_search_status from personal assessment on pool creation When an EmployerPoolProfile is created/updated after assessment, job_search_status is now populated from the personal assessment answer. Maps assessment values to TalentAvailabilityStatus: - actively_looking -> actively_looking - open_to_right_opportunity -> open_to_opportunities - just_exploring -> not_looking * fix(ai-resources): prevent inline requests from inheriting background timeout generationLocks now stores metadata (isBackground flag) so getResourcesForUser can detect a background warmCache lock and race it against the 60s inline timeout instead of awaiting the full 5-minute background deadline. * feat: add job_search_status resolution to employer pool profile service * fix: improve resource generation resilience and migration rollback safety - Migration down() now restores job_search_status values back into availability before dropping the column, making rollback reversible - ai-resources: background generation failures no longer propagate to the user; they fall through to inline generation with a warning log - ai-resources: lock ownership check prevents a stale finally block from deleting a newer lock set by a subsequent request - ai-resources: clearTimeout on the inline deadline timer when the background promise wins the race, preventing leaked timers * fix merge conflicts * perf(personal-assessment): use instant fallback instead of slow AI generation (#178) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * perf(personal-assessment): skip AI generation, use instant deterministic fallback --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * fix(dashboard): count all skill attempts (started + completed) so 3/3 shows during active session --------- Co-authored-by: Felix Gogodae <67784572+Trojanhorse7@users.noreply.github.com> Co-authored-by: Aaron <52609237+kenneropia@users.noreply.github.com> Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> * fix(ai-resources): more videos, verified URLs, no broken links (#186) * test(advanced-assessment): update MCQ gate test for 8-MCQ composition — 2/8 correct needed to clear 75% threshold * fix(ai-resources): generate more resources/videos, verify URLs are live, drop dead links --------- Co-authored-by: Joy <joykenneth605@gmail.com> Co-authored-by: Deborah Anyachukwu <108271834+Zubbee18@users.noreply.github.com> Co-authored-by: Felix Gogodae <67784572+Trojanhorse7@users.noreply.github.com> Co-authored-by: Aaron <52609237+kenneropia@users.noreply.github.com>
With 8 MCQs, 1/8 correct = 73.75% total (below the 75% job_ready threshold). Updated test to use 2/8 correct MCQs = 77.5%, which clears the threshold. Test name updated to match.