diff --git a/apps/web/src/pages/routine-detail-page.tsx b/apps/web/src/pages/routine-detail-page.tsx index 161f6e5b0..b81e1b0b0 100644 --- a/apps/web/src/pages/routine-detail-page.tsx +++ b/apps/web/src/pages/routine-detail-page.tsx @@ -365,14 +365,26 @@ export function RoutineDetailPage({ subtitle={row.tenantName} actions={ <> - - + {!row.routine.enabled || health.state === "paused" ? ( + + ) : ( + <> + + + + )} diff --git a/apps/web/src/pages/routines-page.tsx b/apps/web/src/pages/routines-page.tsx index 8fd0d2f60..c36de50f7 100644 --- a/apps/web/src/pages/routines-page.tsx +++ b/apps/web/src/pages/routines-page.tsx @@ -351,7 +351,7 @@ export function GlobalRoutinesList({ onToggleEnabled(row, enabled)} /> diff --git a/apps/web/test/routine-detail-page.test.tsx b/apps/web/test/routine-detail-page.test.tsx index da56c0100..cf0307db5 100644 --- a/apps/web/test/routine-detail-page.test.tsx +++ b/apps/web/test/routine-detail-page.test.tsx @@ -173,6 +173,32 @@ describe("RoutineDetailPage", () => { expect(actions).toContain("Run now"); expect(actions).toContain("Pause"); }); + + function topBarActions(markup: string): string { + const start = markup.indexOf('data-testid="stage-top-bar-actions"'); + const end = markup.indexOf("", start); + return markup.slice(start, end === -1 ? undefined : end); + } + + test("hides Run now when the routine is off, and offers Resume only", () => { + const actions = topBarActions( + renderPage({ routine: { ...routine, enabled: false } }), + ); + expect(actions).toContain("Resume"); + expect(actions).not.toContain("Run now"); + expect(actions).not.toContain("Pause"); + }); + + test("hides Run now when the routine is dead-lettered, and offers Resume only", () => { + const actions = topBarActions( + renderPage({ + routine: { ...routine, deadLetteredAt: "2026-01-02T00:00:00.000Z" }, + }), + ); + expect(actions).toContain("Resume"); + expect(actions).not.toContain("Run now"); + expect(actions).not.toContain("Pause"); + }); }); describe("RoutineDetailPage lifecycle actions", () => { @@ -237,6 +263,21 @@ describe("RoutineDetailPage lifecycle actions", () => { } }); + test("Resume on a dead-lettered routine asks for it to be enabled again", () => { + const calls: boolean[] = []; + const { container, root } = mount( + { onToggleEnabled: (enabled: boolean) => calls.push(enabled) }, + { routine: { ...routine, deadLetteredAt: "2026-01-02T00:00:00.000Z" } }, + ); + try { + clickButton(container, "Resume"); + expect(calls).toEqual([true]); + } finally { + act(() => root.unmount()); + container.remove(); + } + }); + test("Run now triggers the run-now mutation", () => { let runs = 0; const { container, root } = mount({ @@ -254,6 +295,37 @@ describe("RoutineDetailPage lifecycle actions", () => { } }); + test("Run now reads as starting while the request is in flight", async () => { + let resolveRun: () => void = () => undefined; + const pending = new Promise((resolve) => { + resolveRun = resolve; + }); + const { container, root } = mount({ + onRunNow: () => pending, + }); + try { + clickButton(container, "Run now"); + const button = [...container.querySelectorAll("button")].find( + (candidate) => candidate.textContent?.includes("Starting") === true, + ); + expect(button).not.toBeUndefined(); + expect(button?.disabled).toBe(true); + + await act(async () => { + resolveRun(); + await pending; + }); + expect( + [...container.querySelectorAll("button")].some( + (candidate) => candidate.textContent?.includes("Starting") === true, + ), + ).toBe(false); + } finally { + act(() => root.unmount()); + container.remove(); + } + }); + test("Edit opens the routine's editor", () => { let edits = 0; const { container, root } = mount({ diff --git a/apps/web/test/routines-page.test.tsx b/apps/web/test/routines-page.test.tsx index 62e306bff..44b8f675e 100644 --- a/apps/web/test/routines-page.test.tsx +++ b/apps/web/test/routines-page.test.tsx @@ -262,6 +262,20 @@ describe("GlobalRoutinesList", () => { expect(markup).toContain("—"); }); + test("the On switch is named On, not Pause, while the routine is enabled", () => { + const markup = renderList([row()]); + expect(markup).toContain('aria-label="On Morning brief"'); + expect(markup).not.toContain('aria-label="Pause Morning brief"'); + }); + + test("the On switch is named Off when the routine is disabled", () => { + const markup = renderList([ + row({ routine: { ...routine, enabled: false } }), + ]); + expect(markup).toContain('aria-label="Off Morning brief"'); + expect(markup).not.toContain('aria-label="Resume Morning brief"'); + }); + test("Run now calls onRunNow with the row", async () => { const calls: GlobalRoutineRow[] = []; const container = document.createElement("div"); @@ -371,6 +385,16 @@ describe("GlobalRoutinesList", () => { } }); + test("a dead-lettered routine's action reads Resume, not Run now", () => { + const markup = renderList([ + row({ + routine: { ...routine, deadLetteredAt: "2026-01-02T00:00:00.000Z" }, + }), + ]); + expect(markup).toContain("Resume"); + expect(markup).not.toContain("Run now"); + }); + test("Edit calls onEditRoutine with the row", () => { const calls: GlobalRoutineRow[] = []; const container = document.createElement("div");