feat(ai-resources): add timeout to AI calls and cache warming for resources#163
Conversation
- Prevents indefinite hangs when AI providers are slow or unresponsive. - Default 5 min timeout with per-caller override via timeoutMs parameter.
- 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.
- 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.
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.
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.
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.
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
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughTwo independent changes: AI resource generation strengthens lock semantics in warmCache and getResourcesForUser to prevent concurrent generation, refactors inline timeout handling, and improves error propagation during background-generation races; a database migration adds reversible data backfill for job_search_status with explicit restoration on rollback. ChangesAI Resource Generation Lock Management
Database Migration Data Backfill
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/modules/ai-resources/ai-resources.service.ts`:
- Around line 45-80: warmCache currently stores a long-running background
promise into generationLocks so getResourcesForUser may await a 5-minute
background timeout; change generationLocks to store metadata (e.g., { promise,
isBackground, startedAt, timeoutMs }) when warmCache calls
generateAndSaveResources using AI_RESOURCE_CONSTANTS.BACKGROUND_TIMEOUT_MS and
isBackground=true, then update getResourcesForUser to detect an existing lock
with isBackground=true and avoid awaiting the full background promise: either
(a) race the background promise with a short INLINE_TIMEOUT_MS timeout (reject
or fall through if timeout hits) so the user request uses its own inline
deadline, or (b) skip joining the background lock and start its own
generateAndSaveResources call with AI_RESOURCE_CONSTANTS.INLINE_TIMEOUT_MS;
ensure generateAndSaveResources remains unchanged, only adapt how
generationLocks entries are created and consumed in warmCache and
getResourcesForUser so inline requests never inherit the 5-minute background
timeout.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c32fa69f-348b-44a0-a921-be5f422e6443
📒 Files selected for processing (8)
src/modules/ai-report/ai-report.service.tssrc/modules/ai-resources/ai-resources.constants.tssrc/modules/ai-resources/ai-resources.service.tssrc/modules/ai/openrouter.service.tssrc/modules/ai/resource-generation.service.tssrc/modules/talent/talent.module.tssrc/modules/talent/talent.service.settings.spec.tssrc/modules/talent/talent.service.ts
…into fix/ai-res-gen
… 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.
…ill-bridge-api into fix/ai-res-gen
…ill-bridge-api into fix/verified-profile
…idge-api into fix/ai-res-gen
…into fix/ai-res-gen
…into fix/verified-profile
…fety - 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
…ill-bridge-api into fix/verified-profile
…into fix/ai-res-gen
…ill-bridge-api into fix/ai-res-gen
* 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>
Description
Problem
The
/talent/resourcesand/talent/ai-report/guidance-reportendpoints call an LLM (OpenRouter/Anthropic/Gemini) with no timeout. If the AI provider is slow, the HTTP request hangs indefinitely and the user sees a page that never loads.What I fixed
1. Added a timeout to all AI calls
File: openrouter.service.ts
Before:
generateObject()could hang forever.After: Every AI call has an
AbortSignal.timeout(). If the AI doesn't respond in time, it throws a 503 instead of hanging.2. Made the timeout configurable per caller
Files: openrouter.service.ts, resource-generation.service.ts, ai-resources.service.ts
chat()accepts an optionaltimeoutMsparamgenerate()passes it through3. Added cache warming when a user picks their track
Files: talent.service.ts, ai-resources.service.ts, talent.module.ts
When a user selects their track during onboarding (
saveTrackStep), we immediately fire off a background job to generate resources for their track. By the time they navigate to the Resources page, the data is already cached in the DB, which is an instant load.Related Issue (Link to Github issue)
Motivation and Context
The Resources page (
/talent/resources) and AI Report page (/talent/ai-report/guidance-report) were hanging indefinitely when the AI provider was slow (~3 min response times observed). Users saw a page that never loaded. This PR adds safety timeouts and proactively generates resources in the background during onboarding so users never have to wait.How Has This Been Tested?
Screenshots
Types of changes
Checklist:
Summary by CodeRabbit
New Features
Bug Fixes
Chores