diff --git a/__tests__/safeguarding.test.ts b/__tests__/safeguarding.test.ts index 233c119..6a75092 100644 --- a/__tests__/safeguarding.test.ts +++ b/__tests__/safeguarding.test.ts @@ -743,7 +743,7 @@ describe('LGBTQ+ Specialist Follow-up', () => { expect(result.stateUpdates.lgbtq).toBe(false); }); - test('answering Prefer not to say to LGBTQ sets lgbtq false and skips specialist ask', () => { + test('answering Prefer not to say to LGBTQ sets lgbtq null and skips specialist ask', () => { const session = sessionAt('B5_PROFILE_LGBTQ', { supportNeed: 'Emergency Housing', ageCategory: '25+', @@ -754,7 +754,7 @@ describe('LGBTQ+ Specialist Follow-up', () => { const result = select(session, 3); // Prefer not to say expect(result.stateUpdates.currentGate).not.toBe('B5_PROFILE_LGBTQ'); expect(result.stateUpdates.currentGate).not.toBe('LGBTQ_SPECIALIST_ASK'); - expect(result.stateUpdates.lgbtq).toBe(false); + expect(result.stateUpdates.lgbtq).toBeNull(); }); }); @@ -808,6 +808,18 @@ describe('Null-Check Gate Fix', () => { expect(result.stateUpdates.currentGate).not.toBe('B5_PROFILE_CHILDREN'); }); + test('PNTS through B5_PROFILE_CHILDREN does not re-trigger (regression)', () => { + const session = sessionAt('B5_PROFILE_CHILDREN', { + supportNeed: 'Financial', + ageCategory: '25+', + localAuthority: 'Birmingham', + immigrationStatus: 'British', + publicFunds: 'Yes', + }); + const result = select(session, 3); // Prefer not to say + expect(result.stateUpdates.currentGate).not.toBe('B5_PROFILE_CHILDREN'); + }); + }); // ============================================================================= diff --git a/lib/handlers/shared.ts b/lib/handlers/shared.ts index 3361ee8..fb532de 100644 --- a/lib/handlers/shared.ts +++ b/lib/handlers/shared.ts @@ -60,8 +60,19 @@ export function buildUnder16Exit(session: SessionState): RoutingResult { // Local Children's Services — LA must be known by this point. // All call sites should route through CRISIS_UNDER16_LOCATION first // if LA is not yet set, so this is never reached without a valid LA. + // If it IS reached without one, log the error and recover by asking for area. if (!childServices) { - throw new Error(`buildUnder16Exit called without a valid local authority (got: ${JSON.stringify(session.localAuthority)}). Route to CRISIS_UNDER16_LOCATION first.`); + console.error(`[VA] buildUnder16Exit called without valid LA (got: ${JSON.stringify(session.localAuthority)}, sessionId: ${session.sessionId}). Recovering to CRISIS_UNDER16_LOCATION.`); + const locationPhrase = getPhrase('CRISIS_UNDER16_LOCATION', isSupporter); + return { + text: locationPhrase?.text || '', + options: locationPhrase?.options, + stateUpdates: { + currentGate: 'CRISIS_UNDER16_LOCATION' as GateType, + safeguardingTriggered: true, + safeguardingType: 'UNDER_16', + }, + }; } text += `CHILDREN'S SERVICES\n`; diff --git a/lib/phrasebank.ts b/lib/phrasebank.ts index 8de9d99..1f64730 100644 --- a/lib/phrasebank.ts +++ b/lib/phrasebank.ts @@ -1479,7 +1479,7 @@ Childline https://www.childline.org.uk You can call or chat online about anything — they'll listen and help you work out what to do next. -You can also talk to any trusted adult — a teacher, a family member, or your local council's Children's Services team. +You can also talk to any trusted adult — a teacher, a family member, or your local council's Children's Services team. You can find your local council at https://www.gov.uk/find-local-council --- If you are in immediate danger, call 999.` @@ -1500,7 +1500,7 @@ NSPCC Helpline (for adults) https://www.nspcc.org.uk/keeping-children-safe/reporting-abuse/ For adults who are worried about a child. -They can also be supported by a trusted adult — a teacher, a family member, or their local council's Children's Services team. +They can also be supported by a trusted adult — a teacher, a family member, or their local council's Children's Services team. You can find their local council at https://www.gov.uk/find-local-council --- If they are in immediate danger, call 999.` diff --git a/lib/stateMachine.ts b/lib/stateMachine.ts index 4a82373..eacfbbe 100644 --- a/lib/stateMachine.ts +++ b/lib/stateMachine.ts @@ -226,12 +226,15 @@ const needProfileRequirements: Record = { * and what profile data has already been collected. * Returns terminal output when all required fields are collected. */ -function routeToNextProfileQuestion(session: SessionState): RoutingResult { +function routeToNextProfileQuestion(session: SessionState, after?: string): RoutingResult { const need = session.supportNeed || ''; const required = needProfileRequirements[need] || []; - + // Check each required field in order for (const field of required) { + // Skip the field we just answered — prevents re-triggering on null PNTS values + if (after && field === after) continue; + // Age if (field === 'age' && !session.ageCategory) { return { @@ -1292,18 +1295,18 @@ export function processInput(session: SessionState, input: string): RoutingResul if (profAge === '25 or over') mappedProfAge = '25+'; const sessionWithAge = { ...session, ageCategory: mappedProfAge }; - return routeToNextProfileQuestion(sessionWithAge); + return routeToNextProfileQuestion(sessionWithAge, 'age'); case 'B5_PROFILE_GENDER': const profGenderOptions = ['Male', 'Female', 'Non-binary or other', 'Prefer not to say']; const profGender = choice ? profGenderOptions[choice - 1] : null; const sessionWithGender = { ...session, gender: profGender }; - return routeToNextProfileQuestion(sessionWithGender); + return routeToNextProfileQuestion(sessionWithGender, 'gender'); case 'B5_PROFILE_LGBTQ': { // 1 = Yes, 2 = No, 3 = Prefer not to say - const lgbtqValue = choice === 1 ? true : false; + const lgbtqValue = choice === 1 ? true : (choice === 2 ? false : null); if (lgbtqValue === true) { return { ...phrase('LGBTQ_SPECIALIST_ASK', session.isSupporter), @@ -1311,7 +1314,7 @@ export function processInput(session: SessionState, input: string): RoutingResul }; } const sessionWithLgbtq = { ...session, lgbtq: lgbtqValue }; - const lgbtqResult = routeToNextProfileQuestion(sessionWithLgbtq); + const lgbtqResult = routeToNextProfileQuestion(sessionWithLgbtq, 'lgbtq'); return { ...lgbtqResult, stateUpdates: { @@ -1324,7 +1327,7 @@ export function processInput(session: SessionState, input: string): RoutingResul case 'LGBTQ_SPECIALIST_ASK': { const lgbtqPref = choice === 1 ? 'Specialist first' : 'Show both'; const sessionWithSpec = { ...session, lgbtqServicePreference: lgbtqPref }; - const specResult = routeToNextProfileQuestion(sessionWithSpec); + const specResult = routeToNextProfileQuestion(sessionWithSpec, 'lgbtq'); return { ...specResult, stateUpdates: { @@ -1339,7 +1342,7 @@ export function processInput(session: SessionState, input: string): RoutingResul const convictions = choice ? convictionOptions[choice - 1] : null; const sessionWithConvictions = { ...session, criminalConvictions: convictions }; - return routeToNextProfileQuestion(sessionWithConvictions); + return routeToNextProfileQuestion(sessionWithConvictions, 'convictions'); case 'IMMIGRATION_STATUS_ASK': { const immigrationMap: Record = { @@ -1362,7 +1365,7 @@ export function processInput(session: SessionState, input: string): RoutingResul immigrationStatus, publicFunds, }; - const result = routeToNextProfileQuestion(sessionWithImmigration); + const result = routeToNextProfileQuestion(sessionWithImmigration, 'nrpf'); return { ...result, stateUpdates: { @@ -1375,10 +1378,10 @@ export function processInput(session: SessionState, input: string): RoutingResul case 'B5_PROFILE_CHILDREN': // 1 = Yes, 2 = No, 3 = Prefer not to say - const childrenValue = choice === 1 ? true : false; + const childrenValue = choice === 1 ? true : (choice === 2 ? false : null); const sessionWithChildren = { ...session, hasChildren: childrenValue }; - return routeToNextProfileQuestion(sessionWithChildren); + return routeToNextProfileQuestion(sessionWithChildren, 'children'); case 'B6_HOMELESSNESS_STATUS': const homeless = choice === 1;