Answering a decision from the composer throws unhandled on a dead link, instead of failing like every other send
- Dominant language
- TypeScript
- Stars
- 1
- Forks
- 0
- Avg merge
- 1h 21m
- Merged PRs (30d)
- 41
Description
## What's wrong
`App.tsx`'s `submit()` has two send paths. The plain-message path (`src/client/components/App.tsx:317-329`, the `giveUp` flow) wraps its `postJson` in a `try`/`catch` so a rejected fetch (a dead link, not just a bad status) fails visibly - text restored, an error shown. `DecisionSheet.tsx`'s own `send()` (`src/client/components/DecisionSheet.tsx:201-217`) does the same with its own `try`/`catch`.
The composer's *other* send path - answering a decision without opening the sheet, `App.tsx:266-286` - has no `try`/`catch` around its `postJson` call:
```ts
if (decision && !intake) {
if (!choice && said === "" && attachments.length === 0) return;
setSendState("sending");
tap();
const res = await postJson(`/api/sessions/${row.id}/answer`, { optionId: choice, text: said, images: attachments });
if (!res.ok) { ... }
...
}
```
If `postJson` *rejects* (network down, dead link - `fetches.tsx`'s test helpers call this "reject", as opposed to a bad HTTP status) rather than resolving with a bad status, this throws inside an `async function` called as `void submit()` from `onSubmit` (`App.tsx:522`), which becomes an unhandled promise rejection. The send state is left stuck on `"sending"` forever, no error is shown, and nothing is restored - the exact bug #60 and #86 both fixed for the other two send paths.
Found while writing `tests/composer-haptics.test.tsx` for #95: a test using `answerFails: "reject"` against this path produced `Vitest caught 1 unhandled error during the test run` instead of exercising the failure branch. The test was changed to use a bad status (`answerFails: 502`) instead, which does exercise `!res.ok` correctly - but the reject path itself is still unhandled in the real app.
## Acceptance criteria
- [ ] Answering a decision from the composer (not the sheet) with a rejected fetch fails the same visible way a bad status does: `setError`, `failSend()`, no unhandled rejection.
- [ ] `sendState` does not get stuck on `"sending"` when this happens.
- [ ] A regression test using `answerFails: "reject"` against this exact path (composer, not sheet) passes without an unhandled rejection.
## Out of scope
- `DecisionSheet.tsx`'s own `send()` - already has a `try`/`catch`, not touched by this.
- The plain-message path - already has a `try`/`catch` (`giveUp`), not touched by this.
## Verification
```
pnpm test -- composer
```
Plus a new test exercising `answerFails: "reject"` against the composer's decision-answer path specifically.
## Related
Found during #95. Same shape of bug as #60 (the sheet) and #86 (the plain-message path), both already fixed - this is the one path that was missed.
Contributor guide
Research direction
Start in src/client/components/App.tsx:266-286 and compare the composer decision-answer path with the existing guarded send paths at lines 317-329 and in DecisionSheet.tsx:201-217. Run pnpm test -- composer, then add or update a regression test using answerFails: "reject" for the composer path. Done means the rejection shows the normal error, resets sending state, and produces no unhandled rejection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100