Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions __tests__/safeguarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@
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+',
Expand All @@ -754,7 +754,7 @@
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();
});

});
Expand Down Expand Up @@ -808,6 +808,18 @@
expect(result.stateUpdates.currentGate).not.toBe('B5_PROFILE_CHILDREN');
});

test('PNTS through B5_PROFILE_CHILDREN does not re-trigger (regression)', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed this new regression test looks like it's covering the same ground as the existing test at line 798 ("User who prefers not to say on children question progresses past the question"). Same session setup, same option selected, same assertion that the state doesn't come back to B5_PROFILE_CHILDREN.

Not a problem for correctness, but two identical tests means if the behaviour ever changes we'll get two failures for the same thing, and one of them will need updating anyway. Could we drop one? Either is fine, though I'd keep the new one if it has a clearer name tied to the fix in this PR.

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');
});

});

// =============================================================================
Expand Down Expand Up @@ -1016,16 +1028,16 @@

// Find the safeguarding-trigger log call
const safeguardingCalls = logSpy.mock.calls.filter(
(c) => typeof c[0] === 'string' && c[0].includes('SAFEGUARDING_TRIGGER')

Check warning on line 1031 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access [0] on an `any` value

Check warning on line 1031 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access [0] on an `any` value

Check warning on line 1031 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access [0] on an `any` value

Check warning on line 1031 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access [0] on an `any` value
);
expect(safeguardingCalls.length).toBeGreaterThan(0);

const payload = JSON.parse(safeguardingCalls[0][1]);

Check warning on line 1035 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access [1] on an `any` value

Check warning on line 1035 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe argument of type `any` assigned to a parameter of type `string`

Check warning on line 1035 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe assignment of an `any` value

Check warning on line 1035 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access [1] on an `any` value

Check warning on line 1035 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe argument of type `any` assigned to a parameter of type `string`

Check warning on line 1035 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe assignment of an `any` value
expect(payload.type).toBe('AGE_NUMERIC');

Check warning on line 1036 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .type on an `any` value

Check warning on line 1036 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .type on an `any` value
expect(payload.code).toBe('AGE_NUMERIC_14');

Check warning on line 1037 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .code on an `any` value

Check warning on line 1037 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .code on an `any` value
expect(payload.sessionId).toBe('test-session-abc-123');

Check warning on line 1038 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .sessionId on an `any` value

Check warning on line 1038 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .sessionId on an `any` value
expect(payload.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);

Check warning on line 1039 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .timestamp on an `any` value

Check warning on line 1039 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .timestamp on an `any` value
expect(payload.event).toBe('UNDER_16_DETECTED');

Check warning on line 1040 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .event on an `any` value

Check warning on line 1040 in __tests__/safeguarding.test.ts

View workflow job for this annotation

GitHub Actions / test

Unsafe member access .event on an `any` value
expect(payload.pathwayChange).toBe('TERMINATED_TO_UNDER_16_EXIT');

// Verify NO log call across the whole spy contains the raw user text
Expand Down
13 changes: 12 additions & 1 deletion lib/handlers/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down
4 changes: 2 additions & 2 deletions lib/phrasebank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`
Expand All @@ -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.`
Expand Down
25 changes: 14 additions & 11 deletions lib/stateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,15 @@ const needProfileRequirements: Record<string, string[]> = {
* 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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking, but worth considering: typing after as string | undefined means a typo at a call site (e.g. 'chilren') would silently do nothing and quietly re-introduce the bug this PR is fixing. If we constrain it to a union of the actual field names ('age' | 'gender' | 'lgbtq' | 'convictions' | 'nrpf' | 'children'), TypeScript will catch that at compile time.

Small change, and it protects the fix going forward. Up to you.

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 {
Expand Down Expand Up @@ -1292,26 +1295,26 @@ 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');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something, but it looks like this handler returns routeToNextProfileQuestion() directly, so the value the user just gave for hasChildren doesn't end up in stateUpdates and gets lost when route.ts merges back into the session.

I can see from the PR description you're already tracking this as backlog item 59. Would mind adding the backlog to tracking and commit it with the next PR? It would be helpful for me to see what's known and what isn't.


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),
stateUpdates: { currentGate: 'LGBTQ_SPECIALIST_ASK', lgbtq: true }
};
}
const sessionWithLgbtq = { ...session, lgbtq: lgbtqValue };
const lgbtqResult = routeToNextProfileQuestion(sessionWithLgbtq);
const lgbtqResult = routeToNextProfileQuestion(sessionWithLgbtq, 'lgbtq');
return {
...lgbtqResult,
stateUpdates: {
Expand All @@ -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: {
Expand All @@ -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<number, { status: string; funds: string | null }> = {
Expand All @@ -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: {
Expand All @@ -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;
Expand Down
Loading