mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 17:57:06 +00:00
refactor(sidebar): rebuild footer as priority-driven Ops Pulse strip (#1178)
* refactor(sidebar): rebuild footer as priority-driven Ops Pulse strip Replace the simple notification ticker with a derived activity summary that picks one of six states (active-op, failure, automation, recent-event, quiet-live, disconnected) and routes per-state clicks to logs, schedules, or activity. The hook owns the cascade; the component is pure presentation; EditorLayout owns wiring. Failure detection covers unread errors in the last 24h; recent-event is limited to non-error stack notifications in the last hour; automation reads the next /scheduled-tasks?action=update run and a debounced state-invalidate listener; the deploy-panel composite key is used for elapsed-time tracking so close-then-immediately-reopen counts as a new session. * refactor(sidebar): apply Ops Pulse audit fixes - countEnabledAutoUpdates now defaults missing autoUpdateSettings entries to enabled, matching the backend's getStackAutoUpdateSettingsForNode contract. Previously the automation state could not render even with the documented per-row default-true. - findFailure now requires a stack_name so the sidebar does not select a system-level error whose click would no-op through navigateToNotification. System errors continue to surface via the top-bar NotificationPanel. - DeployPanelState gains a monotonic sessionId sourced from the existing internal counter, and the new usePanelSessionStartedAt hook keys the elapsed-time tracker off it so a same-stack rerun always resets even when isOpen stays true across succeeded then preparing. - buildConfig splits quiet-live out of the default and adds an exhaustiveness guard so future SidebarActivitySummary variants fail to compile. - New unit tests cover the default-true aggregation, the same-stack session reset, the non-stack failure guard, and the useNextAutoUpdateRun debounce and cleanup paths. Frontend suite: 276 / 276 pass.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { usePanelSessionStartedAt } from '../usePanelSessionStartedAt';
|
||||
import type { DeployPanelState } from '@/context/DeployFeedbackContext';
|
||||
|
||||
function panel(over: Partial<DeployPanelState> = {}): DeployPanelState {
|
||||
return {
|
||||
isOpen: false,
|
||||
stackName: '',
|
||||
action: 'deploy',
|
||||
status: 'preparing',
|
||||
sessionId: 0,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
describe('usePanelSessionStartedAt', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns null while the panel is closed', () => {
|
||||
const { result } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
|
||||
initialProps: panel(),
|
||||
});
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it('captures Date.now() when the panel opens', () => {
|
||||
const { result, rerender } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
|
||||
initialProps: panel(),
|
||||
});
|
||||
expect(result.current).toBeNull();
|
||||
const opened = Date.now();
|
||||
rerender(panel({ isOpen: true, stackName: 'web', sessionId: 1 }));
|
||||
expect(result.current).toBe(opened);
|
||||
});
|
||||
|
||||
it('resets the timestamp when sessionId changes even if isOpen stays true (same stack rerun)', () => {
|
||||
const { result, rerender } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
|
||||
initialProps: panel({ isOpen: true, stackName: 'web', sessionId: 1 }),
|
||||
});
|
||||
const first = result.current;
|
||||
expect(first).not.toBeNull();
|
||||
|
||||
// Status flows to succeeded but the panel stays visible.
|
||||
rerender(panel({ isOpen: true, stackName: 'web', status: 'succeeded', sessionId: 1 }));
|
||||
expect(result.current).toBe(first);
|
||||
|
||||
// Advance the clock and trigger a same-stack rerun under a new sessionId.
|
||||
vi.advanceTimersByTime(5_000);
|
||||
rerender(panel({ isOpen: true, stackName: 'web', status: 'preparing', sessionId: 2 }));
|
||||
|
||||
expect(result.current).not.toBe(first);
|
||||
expect(result.current).toBe((first ?? 0) + 5_000);
|
||||
});
|
||||
|
||||
it('does not flap when the same panel session re-renders with no changes', () => {
|
||||
const { result, rerender } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
|
||||
initialProps: panel({ isOpen: true, stackName: 'web', sessionId: 1 }),
|
||||
});
|
||||
const captured = result.current;
|
||||
|
||||
vi.advanceTimersByTime(10_000);
|
||||
rerender(panel({ isOpen: true, stackName: 'web', sessionId: 1, status: 'streaming' }));
|
||||
expect(result.current).toBe(captured);
|
||||
});
|
||||
|
||||
it('clears the timestamp when the panel closes', () => {
|
||||
const { result, rerender } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
|
||||
initialProps: panel({ isOpen: true, stackName: 'web', sessionId: 1 }),
|
||||
});
|
||||
expect(result.current).not.toBeNull();
|
||||
rerender(panel({ isOpen: false, sessionId: 1 }));
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user