From d572ee192dc98f9f31ee596e7108c876e6e135a9 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Fri, 9 Oct 2026 21:21:34 -0400 Subject: [PATCH] Require Home minutes_total and Supply run label for every list (review H1) parseTonight now rejects a tonight dinner without the minutes_total key instead of reading it as null; an explicit null is still valid. The Home API always emits the key. parseShoppingList checks the label against weekStart for unready lists too, since the API supplies it regardless of readiness. Co-Authored-By: Claude Opus 5.5 --- web/src/home/HomePage.test.tsx | 31 +++++++++++++++++++++++++++++++ web/src/home/api.ts | 21 ++++++++++----------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/web/src/home/HomePage.test.tsx b/web/src/home/HomePage.test.tsx index 59fe8ef..5a2f838 100644 --- a/web/src/home/HomePage.test.tsx +++ b/web/src/home/HomePage.test.tsx @@ -227,6 +227,16 @@ describe('HomePage', () => { expect(screen.queryByText(/min$/)).not.toBeInTheDocument() }) + it('reports tonight without a minutes_total rather than showing it as unknown', async () => { + const { minutes_total: _, ...tonight } = HOME.tonight + renderHome({ ...HOME, tonight }) + + expect(await screen.findByRole('alert')).toHaveTextContent('tonight has no minutes_total') + expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument() + expect(screen.queryByRole('region', { name: 'Tonight' })).not.toBeInTheDocument() + expect(screen.queryByText('Regular')).not.toBeInTheDocument() + }) + it.each([ ['a missing', undefined], ['an empty', ''], @@ -248,6 +258,27 @@ describe('HomePage', () => { expect(screen.queryByRole('link', { name: /Shopping list ready/ })).not.toBeInTheDocument() }) + it.each([ + ['a missing', undefined], + ['an empty', ''], + ])('reports an unready Supply run with %s label rather than showing the home', async (_, label) => { + renderHome({ ...HOME, shoppingList: { ...NOTHING_YET.shoppingList, label } }) + + expect(await screen.findByRole('alert')).toHaveTextContent('the unready shoppingList has no label') + expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument() + expect(screen.queryByRole('list', { name: 'Status' })).not.toBeInTheDocument() + }) + + it('reports an unready Supply run whose label is not its week rather than showing the home', async () => { + renderHome({ ...HOME, shoppingList: { ...NOTHING_YET.shoppingList, label: 'Week 41 · Oct 5 – 11' } }) + + expect(await screen.findByRole('alert')).toHaveTextContent( + 'shoppingList label "Week 41 · Oct 5 – 11" is not the week of 2026-09-28', + ) + expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument() + expect(screen.queryByRole('list', { name: 'Status' })).not.toBeInTheDocument() + }) + it('reports a Supply run whose weekStart is not a date rather than showing it', async () => { renderHome({ ...HOME, shoppingList: { ...HOME.shoppingList, weekStart: 'Oct 5' } }) diff --git a/web/src/home/api.ts b/web/src/home/api.ts index ef5b7cd..fb836cc 100644 --- a/web/src/home/api.ts +++ b/web/src/home/api.ts @@ -39,7 +39,8 @@ function parseTonight(where: string, body: unknown): Tonight | null { if (!isRecord(body) || typeof body.id !== 'string' || typeof body.name !== 'string') { throw new Error(`${where}: tonight is not a dinner`) } - const minutes = body.minutes_total ?? null + if (!('minutes_total' in body)) throw new Error(`${where}: tonight has no minutes_total`) + const minutes = body.minutes_total if (minutes !== null && (!Number.isInteger(minutes) || (minutes as number) <= 0)) { throw new Error(`${where}: tonight's minutes_total ${String(minutes)} is not a positive whole number`) } @@ -52,21 +53,19 @@ function parseTonight(where: string, body: unknown): Tonight | null { } } -// parseShoppingList checks the shoppingList of a GET /api/v1/home body. A -// ready list must carry the API's label for its week, which must be the -// label weekStart has, since the status row shows dates from weekStart. +// parseShoppingList checks the shoppingList of a GET /api/v1/home body. The +// list, ready or not, must carry the API's label for its week, which must be +// the label weekStart has, since the status row shows dates from weekStart. function parseShoppingList(where: string, list: unknown): ShoppingListStatus { if (!isRecord(list) || typeof list.weekStart !== 'string' || typeof list.ready !== 'boolean') { throw new Error(`${where}: shoppingList is not a week's Supply run status`) } const week = weekLabel(list.weekStart) - if (list.ready) { - if (typeof list.label !== 'string' || list.label === '') { - throw new Error(`${where}: the ready shoppingList has no label`) - } - if (list.label !== `${week.week} · ${week.dates}`) { - throw new Error(`${where}: shoppingList label "${list.label}" is not the week of ${list.weekStart}`) - } + if (typeof list.label !== 'string' || list.label === '') { + throw new Error(`${where}: the ${list.ready ? 'ready' : 'unready'} shoppingList has no label`) + } + if (list.label !== `${week.week} · ${week.dates}`) { + throw new Error(`${where}: shoppingList label "${list.label}" is not the week of ${list.weekStart}`) } return { weekStart: list.weekStart, dates: week.dates.replace(' – ', '–'), ready: list.ready } }