diff --git a/.claude/memory.md b/.claude/memory.md index 853229ab..a1629155 100644 --- a/.claude/memory.md +++ b/.claude/memory.md @@ -1329,3 +1329,16 @@ System User token before go-live` (warn). Best-effort, never throws, never - GOTCHA: `npm run build` runs a prebuild env check (`scripts/check-env.ts`) that fails without auth secrets; bypass locally with `SKIP_ENV_VALIDATION=true` (as CI does) to exercise the real Next.js compile. + +## 2026-06-12 (overnight orchestration) — git-proxy pushes + gate plumbing +- The remote-exec harness proxies git (127.0.0.1 local_proxy): a branch's + FIRST push succeeds; follow-up pushes can 403 / "remote end hung up" + while `git push` misreports "Everything up-to-date". Recurred tonight on + feature/43 and feature/41. Working recovery: push the commit via GitHub + MCP `push_files`, then `git fetch` + verify empty diff vs remote, then + `git reset --hard` to the remote tip. ALWAYS verify pushes via + ls-remote/API, never trust push output text. +- Two gate-runner bugs tonight were pipes swallowing exit codes + (`cmd | tail -1`, `prisma validate | grep`). Use pipefail or unpiped + gates. Local `npm run build` needs SKIP_ENV_VALIDATION=true; prisma + validate needs any DATABASE_URL set. diff --git a/__tests__/unit/services/reschedule.service.test.ts b/__tests__/unit/services/reschedule.service.test.ts index aa05565e..685b7938 100644 --- a/__tests__/unit/services/reschedule.service.test.ts +++ b/__tests__/unit/services/reschedule.service.test.ts @@ -456,6 +456,19 @@ describe('rescheduleService', () => { expect(result.cancelled).toBe(true); expect(result.gapFill).toBeUndefined(); }); + + it('rescheduleAppointment carries the gap-fill report through to its result (Bugbot #252)', async () => { + armTransaction('CONFIRMED', new Date(Date.now() + 5 * DAY_MS)); + mockProposeGapFill.mockResolvedValue({ taskCreated: true, taskId: 'task-9', candidateCount: 3 }); + // Post-cancel acknowledgement lookup — returning null skips the send + // path, which is exercised by its own tests above. + mockAppointmentFindUnique.mockResolvedValueOnce(null); + + const result = await rescheduleService.rescheduleAppointment('appt-gf', 'new date soon'); + + expect(result.gapFill).toEqual({ taskCreated: true, taskId: 'task-9', candidateCount: 3 }); + expect(result.note).toBe('new date soon'); + }); }); describe('cancelAppointment actor attribution', () => { diff --git a/app/[locale]/appointments/[id]/page.tsx b/app/[locale]/appointments/[id]/page.tsx index 9aae32f6..827d96d3 100644 --- a/app/[locale]/appointments/[id]/page.tsx +++ b/app/[locale]/appointments/[id]/page.tsx @@ -179,12 +179,10 @@ export default function AppointmentDetailPage({ params }: { params: Promise<{ id setRequestReview(false); }; - const handleCancel = async () => { - if (!confirm(t('cancelConfirm'))) return; - const reason = prompt(t('cancelReason')); - const payload = await handleAction('cancel', { reason: reason ?? undefined }); - // Phase 42 — the cancel response reports whether a gap-fill proposal - // task was raised; surface a small non-blocking pointer to it. + // Phase 42 — cancel AND reschedule free the slot via the same service + // path, so either response may report a raised gap-fill proposal task; + // surface the same non-blocking pointer for both. + const surfaceGapFillNotice = (payload: Awaited>) => { const gapFill = payload?.gapFill as | { taskCreated: boolean; taskId: string | null; candidateCount: number } | undefined; @@ -193,6 +191,18 @@ export default function AppointmentDetailPage({ params }: { params: Promise<{ id } }; + const handleCancel = async () => { + if (!confirm(t('cancelConfirm'))) return; + const reason = prompt(t('cancelReason')); + const payload = await handleAction('cancel', { reason: reason ?? undefined }); + surfaceGapFillNotice(payload); + }; + + const handleReschedule = async () => { + const payload = await handleAction('reschedule', {}); + surfaceGapFillNotice(payload); + }; + const handleNoShow = async () => { if (!confirm(t('noShowConfirm'))) return; await handleAction('no-show'); @@ -466,7 +476,7 @@ export default function AppointmentDetailPage({ params }: { params: Promise<{ id