Answers can still fail silently — #60 fixed two of four call sites
- Dominant language
- TypeScript
- Stars
- 1
- Forks
- 0
- Avg merge
- 1h 21m
- Merged PRs (30d)
- 41
Description
## Evidence
#60 fixed `Queue.tsx` and `PhoneUnblock.tsx`, which is what its acceptance criteria named. There are **four** places that post an answer, and the two it did not name are the desktop path — the one used most.
### 1. `answerIntake()` reports success on failure and destroys the answer
`src/client/components/App.tsx:187-199`:
```ts
async function answerIntake() {
if (!row || !decision || !bar || bar.blocked) return;
setError(null);
await postJson(`/api/sessions/${row.id}/answer`, {
answers: intakePayload(decision, answers),
text: note.trim(),
});
dismiss();
setSheetOpen(false);
setNote("");
await reload();
}
```
No `res.ok` check and no `catch`. On any failure — a rejected POST or a non-2xx — it still calls `dismiss()`, closes the sheet and clears the note. The developer watches the intake sheet close, which is the app's signal for *sent*, and their typed answers are gone.
It is invoked as `void answerIntake()` (`App.tsx:255`, `App.tsx:457`), so a rejection is discarded exactly as in #60.
This is strictly worse than the bug #60 fixed: `Queue.tsx` at least left the text alone.
### 2. `submit()` checks the status but not the rejection
`src/client/components/App.tsx:201-235`:
```ts
const res = await postJson(`/api/sessions/${row.id}/answer`, { optionId: choice, text: said, images: attachments });
if (!res.ok) {
setError((await res.json()).error ?? "could not send");
return;
}
```
Two holes:
- **No `try`/`catch`.** A network rejection — offline, daemon down, relay down — never reaches the `!res.ok` branch. Called as `void submit()` (`App.tsx:255`, `App.tsx:403`), so it is silent.
- **`await res.json()` on a non-JSON error body throws**, inside the error path itself. A 502 from the relay with an HTML body produces no message at all — the failure handler fails.
The same `/api/sessions/:id/message` branch at `App.tsx:226` has the identical `res.json()` hole.
### 3. Two more unguarded `loadArtifact` calls
Same shape as the `useReportFrame.ts` bug #60 fixed:
- `src/client/components/ArtifactCard.tsx:35` — `void loadArtifact(...).then(...)`, no `.catch()`
- `src/client/components/ArtifactDialog.tsx:43` — same
A failed relayed load leaves an empty box forever and an unhandled rejection. `useReportFrame.ts` now returns a `failed` flag for exactly this; these two predate it.
## Acceptance criteria
- [ ] `answerIntake()` does not dismiss, close the sheet or clear the note unless the POST actually succeeded
- [ ] A failed intake answer shows the developer that it failed, and the typed answers survive it
- [ ] `submit()` surfaces a rejected POST, not only a non-2xx one
- [ ] A non-JSON error body produces a readable message rather than throwing inside the error path — both the `/answer` and `/message` branches
- [ ] `ArtifactCard.tsx` and `ArtifactDialog.tsx` render something honest on a failed load and leave no unhandled rejection
- [ ] A test for each of: intake POST rejects (sheet stays open, note kept), `submit()` POST rejects, 502 with an HTML body, artifact load rejects
- [ ] `pnpm typecheck` and `pnpm test` clean against the #50 baseline of 4 known failures
## Out of scope
- Automatic retry or an offline queue — same call as #60: tell the truth first.
- Any change to `Queue.tsx`, `PhoneUnblock.tsx` or `useReportFrame.ts`. Those are done and merged at `816d91d`; follow their pattern, do not revisit them.
- Refactoring the four answer call sites into one shared helper. Tempting and probably right eventually, but it is a bigger change than this and would make the fix hard to review.
## Verification
```
pnpm typecheck
pnpm test # 4 failures expected: dispatch-modal, model-costs, model-picker, settings-ui (#50)
```
Manual: with the daemon stopped, answer an intake from the cockpit and confirm the sheet stays open with the answers still in it.
## Related
- #60 (fixed the other two sites; merged at `816d91d`)
- #50 (the 4-failure baseline)
- #65 (re-run any fifth failure alone before trusting it)
Contributor guide
Research direction
Start with the four call sites named in src/client/components/App.tsx, ArtifactCard.tsx, and ArtifactDialog.tsx, comparing the completed pattern in useReportFrame.ts. Add tests for rejected POSTs, non-JSON errors, and failed artifact loads, then run pnpm typecheck and pnpm test against the documented four-failure baseline. Done means failures remain visible, entered answers are preserved, and no unhandled rejection occurs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100