feat(KB-157): add soft-pause (enginePaused) alongside hard-stop (globalPause)

- Add enginePaused field to Settings type with soft-pause semantics (drain queue, don't kill agents)
- Gate scheduler, triage, auto-merge, and periodic merge retry on enginePaused
- Add Pause and Stop buttons to dashboard Header replacing single toggle
- Wire enginePaused state through App with optimistic toggle and unpause resume logic
- Add tests for scheduler, triage, dashboard CLI, Header, and App covering pause behavior
This commit is contained in:
Dustin Byrne
2026-03-28 03:50:42 -04:00
parent 3dc741c56e
commit cac10af5e1
14 changed files with 692 additions and 38 deletions

View File

@@ -133,7 +133,7 @@ describe("App auto-open Settings on unauthenticated", () => {
});
});
describe("App global pause", () => {
describe("App global pause (hard stop)", () => {
it("initializes global pause state from fetchSettings", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
@@ -142,13 +142,13 @@ describe("App global pause", () => {
render(<App />);
// When globally paused, the button should show "Resume AI engine"
// When globally paused, the stop button should show "Start AI engine"
await waitFor(() => {
expect(screen.getByTitle("Resume AI engine")).toBeTruthy();
expect(screen.getByTitle("Start AI engine")).toBeTruthy();
});
});
it("shows Pause button when globalPause is false", async () => {
it("shows Stop button when globalPause is false", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
globalPause: false,
@@ -157,7 +157,7 @@ describe("App global pause", () => {
render(<App />);
await waitFor(() => {
expect(screen.getByTitle("Pause AI engine")).toBeTruthy();
expect(screen.getByTitle("Stop AI engine")).toBeTruthy();
});
});
@@ -171,15 +171,15 @@ describe("App global pause", () => {
// Wait for initial render
await waitFor(() => {
expect(screen.getByTitle("Pause AI engine")).toBeTruthy();
expect(screen.getByTitle("Stop AI engine")).toBeTruthy();
});
// Click the pause button
fireEvent.click(screen.getByTitle("Pause AI engine"));
// Click the stop button
fireEvent.click(screen.getByTitle("Stop AI engine"));
// Should optimistically switch to "Resume" state
// Should optimistically switch to "Start" state
await waitFor(() => {
expect(screen.getByTitle("Resume AI engine")).toBeTruthy();
expect(screen.getByTitle("Start AI engine")).toBeTruthy();
});
// Should call updateSettings with globalPause: true
@@ -196,15 +196,68 @@ describe("App global pause", () => {
render(<App />);
await waitFor(() => {
expect(screen.getByTitle("Pause AI engine")).toBeTruthy();
expect(screen.getByTitle("Stop AI engine")).toBeTruthy();
});
// Click the pause button — will fail
fireEvent.click(screen.getByTitle("Pause AI engine"));
// Click the stop button — will fail
fireEvent.click(screen.getByTitle("Stop AI engine"));
// Should revert back to "Stop" state after failure
await waitFor(() => {
expect(screen.getByTitle("Stop AI engine")).toBeTruthy();
});
});
});
describe("App engine pause (soft pause)", () => {
it("initializes engine pause state from fetchSettings", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
enginePaused: true,
});
render(<App />);
// When engine is paused, the pause button should show "Resume AI engine"
await waitFor(() => {
expect(screen.getByTitle("Resume AI engine")).toBeTruthy();
});
});
it("shows Pause button when enginePaused is false", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
enginePaused: false,
});
render(<App />);
// Should revert back to "Pause" state after failure
await waitFor(() => {
expect(screen.getByTitle("Pause AI engine")).toBeTruthy();
});
});
it("toggles engine pause state and calls updateSettings", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
enginePaused: false,
});
render(<App />);
await waitFor(() => {
expect(screen.getByTitle("Pause AI engine")).toBeTruthy();
});
// Click the pause button
fireEvent.click(screen.getByTitle("Pause AI engine"));
// Should optimistically switch to "Resume" state
await waitFor(() => {
expect(screen.getByTitle("Resume AI engine")).toBeTruthy();
});
// Should call updateSettings with enginePaused: true
expect(updateSettings).toHaveBeenCalledWith({ enginePaused: true });
});
});