updates on the issue and the another thing updates on the ai optimization

This commit is contained in:
2026-09-23 11:58:24 +05:30
parent c6dfa3cb8f
commit 7544a07066
17 changed files with 622 additions and 104 deletions

View File

@@ -0,0 +1,116 @@
// orderFlow imports geocodeAddress from a component that pulls in
// @astryxdesign ESM, which jest does not transform. Only the function matters
// here, and every assertion below supplies its own place, so the fallback
// geocode is stubbed to "found nothing" — which is also the case the last
// test covers.
jest.mock('components/nearle_components/AddressAutocomplete', () => ({
geocodeAddress: jest.fn().mockResolvedValue(null)
}));
import { ORDER_STEPS } from '@/lib/assistant/orderFlow';
/**
* The address step of the conversational order flow.
*
* It was reading Google's raw Places shape — `address_components` and
* `geometry.location.lat()` — from a value that never has either.
* `geocodeAddress` returns whatever `getAddressSuggestions` produced, and that
* goes through `standardizePlace`, which emits a FLAT place: formatted_address,
* city, postcode, latitude, longitude.
*
* So the forEach never ran and both coordinates came back undefined. Every
* order created through the assistant was missing its delivery coordinates —
* the thing the step's own comment says the dispatch optimiser routes on — and
* the separate "what's the delivery pincode?" step fired every time because
* nothing had ever set it.
*/
const addressStep = ORDER_STEPS.find((s) => s.id === 'deliveryaddress');
// What geocodingService.standardizePlace actually returns.
const flatPlace = {
formatted_address: '18 Race Course Road, Coimbatore, Tamil Nadu 641018',
name: '18 Race Course Road',
suburb: 'Race Course',
city: 'Coimbatore',
state: 'Tamil Nadu',
postcode: '641018',
latitude: 11.0038,
longitude: 76.9645
};
describe('the address step', () => {
it('is an address step, so the UI can render a search field for it', () => {
// A type the renderer does not know falls through to the plain chat
// composer — which is how this ended up asking for a full address as free
// text with no suggestions.
expect(addressStep.type).toBe('address');
});
it('reads coordinates off the FLAT place shape', async () => {
const { value } = await addressStep.resolve('race course road', { place: flatPlace });
expect(value.deliverylatitude).toBeCloseTo(11.0038);
expect(value.deliverylongitude).toBeCloseTo(76.9645);
});
it('reads city and pincode off the flat shape too', async () => {
const { value } = await addressStep.resolve('race course road', { place: flatPlace });
expect(value.deliverycity).toBe('Coimbatore');
expect(value.deliverypincode).toBe('641018');
});
it('skips the pincode question once the place supplied one', async () => {
// `when: (d) => !d.deliverypincode` — the step only exists for addresses
// that could not supply one. It used to fire every single time.
const pincodeStep = ORDER_STEPS.find((s) => s.id === 'deliverypincode');
const { value } = await addressStep.resolve('race course road', { place: flatPlace });
const draft = addressStep.apply({}, value);
expect(pincodeStep.when(draft)).toBe(false);
});
it('still understands a raw Google Places result', async () => {
// Kept as a fallback so a caller handing in an unstandardised result does
// not silently lose its coordinates the way this step used to.
const googlePlace = {
formatted_address: '5 Gandhipuram, Coimbatore',
geometry: { location: { lat: () => 11.0168, lng: () => 76.9558 } },
address_components: [
{ long_name: 'Coimbatore', types: ['locality'] },
{ long_name: '641012', types: ['postal_code'] }
]
};
const { value } = await addressStep.resolve('gandhipuram', { place: googlePlace });
expect(value.deliverylatitude).toBeCloseTo(11.0168);
expect(value.deliverycity).toBe('Coimbatore');
expect(value.deliverypincode).toBe('641012');
});
it('accepts plain lat/lng numbers, not only getter functions', async () => {
const { value } = await addressStep.resolve('somewhere', {
place: { formatted_address: 'X', geometry: { location: { lat: 12.5, lng: 77.5 } } }
});
expect(value.deliverylatitude).toBe(12.5);
expect(value.deliverylongitude).toBe(77.5);
});
it('never emits a coordinate pair with only one half', async () => {
// A booking with a latitude and no longitude is worse than one with
// neither: it looks located and routes to the wrong place.
const { value } = await addressStep.resolve('nowhere', {
place: { formatted_address: 'No coords', city: 'Coimbatore' }
});
expect(value.deliverylatitude).toBeUndefined();
expect(value.deliverylongitude).toBeUndefined();
// and it still keeps what it DID learn
expect(value.deliverycity).toBe('Coimbatore');
expect(value.deliveryaddress).toBe('No coords');
});
it('refuses rather than inventing when nothing resolves', async () => {
const result = await addressStep.resolve('qqqq', { place: null });
// With no option place it falls back to geocoding, which in a test
// environment resolves to nothing — the step must report that, not guess.
expect(result.error || result.value).toBeTruthy();
if (result.error) expect(result.error).toMatch(/couldn.t find that address/i);
});
});

View File

@@ -1,5 +1,8 @@
import { getPageContext, CHIP_LABELS } from '@/components/assistant/DoormileAI/pageContext';
import { resolveIntentId } from '@/components/assistant/intents';
// The LIVE catalogue. src/components/assistant/*.js are dead duplicates with
// no importers — AIPanel and Home both import from @/lib/assistant. Testing
// the copy that never runs is how a green suite hides a broken page.
import { resolveIntentId } from '@/lib/assistant/intents';
/**
* Every suggestion a page offers must resolve — and must resolve to the

View File

@@ -0,0 +1,119 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import dayjs from 'dayjs';
import { DateRangeFields } from '@/components/ds/DateRangeFields';
/**
* The two ranges that can only ever return nothing.
*
* Both were accepted silently by the report pages before this component
* existed: an inverted range (from 30 Sep, to 1 Sep) and a future one. Neither
* input reported invalid, no message appeared, and the table showed zero rows —
* which an operator reads as "no orders in this period" rather than "this
* filter is impossible". An empty result presented as a finding is the same
* class of defect as a silently truncated count.
*
* `min`/`max` cover the picker, which is how it is actually used. The clamp in
* onChange covers typing and pasting, which ignore those attributes — so both
* are asserted here.
*/
const today = dayjs().format('YYYY-MM-DD');
const yesterday = dayjs().subtract(1, 'day').format('YYYY-MM-DD');
const lastWeek = dayjs().subtract(7, 'day').format('YYYY-MM-DD');
const nextYear = dayjs().add(1, 'year').format('YYYY-MM-DD');
const setup = (value) => {
const onChange = jest.fn();
render(<DateRangeFields value={value} onChange={onChange} />);
return {
onChange,
from: screen.getByLabelText('From date'),
to: screen.getByLabelText('To date')
};
};
describe('bounds the picker itself', () => {
it('neither field offers a day after today', () => {
const { from, to } = setup({ from: lastWeek, to: yesterday });
expect(to).toHaveAttribute('max', today);
// from is capped by the to-date, which is tighter than today here
expect(from).toHaveAttribute('max', yesterday);
});
it('the to-field cannot go earlier than the from-field', () => {
const { to } = setup({ from: lastWeek, to: today });
expect(to).toHaveAttribute('min', lastWeek);
});
it('with no to-date yet, from is capped at today', () => {
const { from } = setup({ from: '', to: '' });
expect(from).toHaveAttribute('max', today);
});
});
describe('clamps what the picker cannot — typing and pasting', () => {
it('a future from-date is pulled back to today', () => {
const { from, onChange } = setup({ from: today, to: today });
fireEvent.change(from, { target: { value: nextYear } });
expect(onChange).toHaveBeenCalledWith({ from: today, to: today });
});
it('a future to-date is pulled back to today', () => {
const { to, onChange } = setup({ from: lastWeek, to: today });
fireEvent.change(to, { target: { value: nextYear } });
expect(onChange).toHaveBeenCalledWith({ from: lastWeek, to: today });
});
it('moving from PAST to drags to along, rather than inverting', () => {
// The edit is honoured — you have moved to a later period — and the other
// end follows. Rejecting the keystroke with no explanation is worse.
const { from, onChange } = setup({ from: lastWeek, to: yesterday });
fireEvent.change(from, { target: { value: today } });
expect(onChange).toHaveBeenCalledWith({ from: today, to: today });
});
it('moving to BEFORE from drags from back, rather than inverting', () => {
const { to, onChange } = setup({ from: yesterday, to: today });
fireEvent.change(to, { target: { value: lastWeek } });
expect(onChange).toHaveBeenCalledWith({ from: lastWeek, to: lastWeek });
});
it('never emits a range where from is after to', () => {
// The property, stated once. Every emission from any edit must hold it.
const cases = [
[{ from: lastWeek, to: yesterday }, 'From date', today],
[{ from: yesterday, to: today }, 'To date', lastWeek],
[{ from: today, to: today }, 'From date', nextYear],
[{ from: lastWeek, to: today }, 'To date', nextYear]
];
cases.forEach(([value, label, typed]) => {
const onChange = jest.fn();
const { unmount } = render(<DateRangeFields value={value} onChange={onChange} />);
fireEvent.change(screen.getByLabelText(label), { target: { value: typed } });
const next = onChange.mock.calls[0][0];
expect(next.from <= next.to).toBe(true);
expect(next.to <= today).toBe(true);
unmount();
});
});
});
describe('a cleared field is not an invalid one', () => {
it('passes an empty value through untouched', () => {
const { to, onChange } = setup({ from: lastWeek, to: today });
fireEvent.change(to, { target: { value: '' } });
expect(onChange).toHaveBeenCalledWith({ from: lastWeek, to: '' });
});
});
describe('the ceiling is overridable', () => {
it('max={null} allows a future date, for a picker that should look ahead', () => {
const onChange = jest.fn();
render(<DateRangeFields value={{ from: today, to: today }} onChange={onChange} max={null} />);
const from = screen.getByLabelText('From date');
fireEvent.change(from, { target: { value: nextYear } });
expect(onChange).toHaveBeenCalledWith({ from: nextYear, to: nextYear });
});
});