mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 12:17:34 +00:00
ccad5c925b
* feat(nodes): hide hub-only views when active node is remote Fleet, Schedules, Audit, Logs, and Auto-Update operate on hub-owned state (node registry, fleet schedules, centralized audit, fleet-wide log aggregation, fleet-wide update preview). When the active node is remote, proxying those surfaces would show that remote's own disconnected state instead of the hub's. Hide them from the nav strip and force-redirect to Home if one was open during the node switch. Backend hubOnlyGuard middleware sits between nodeContextMiddleware and the remote proxy and rejects /api/scheduled-tasks, /api/audit-log, and /api/notification-routes with 403 + HUB_ONLY_ENDPOINT when nodeId resolves to a remote, closing the script-bypass path the UI gating cannot reach. Settings sub-sections were already gated via the hiddenOnRemote registry; this extends the same model to top-level views. * docs(nodes): note hub-only visibility on Fleet, Schedules, Audit, Logs, Auto-Update Each of the five hub-only feature pages now points readers to the canonical "What top-level views show when a remote node is active" section in multi-node.mdx, so users landing directly on a feature page understand why the nav item disappears when they switch to a remote node.
55 lines
1.6 KiB
TypeScript
55 lines
1.6 KiB
TypeScript
/**
|
|
* HubOnlyGate short-circuits to null when the active node is remote so the
|
|
* wrapped lazy chunk is never fetched. Locks the load-bearing behavior
|
|
* documented in HubOnlyGate.tsx.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen } from '@testing-library/react';
|
|
import * as NodeContext from '@/context/NodeContext';
|
|
import { HubOnlyGate } from '../HubOnlyGate';
|
|
|
|
vi.mock('@/context/NodeContext');
|
|
|
|
function mockActiveNode(type: 'local' | 'remote' | null) {
|
|
vi.mocked(NodeContext.useNodes).mockReturnValue({
|
|
activeNode: type === null ? null : { type, id: 1, name: 'n' },
|
|
} as unknown as ReturnType<typeof NodeContext.useNodes>);
|
|
}
|
|
|
|
describe('HubOnlyGate', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('renders children when active node is local', () => {
|
|
mockActiveNode('local');
|
|
render(
|
|
<HubOnlyGate>
|
|
<div data-testid="payload">hub content</div>
|
|
</HubOnlyGate>,
|
|
);
|
|
expect(screen.getByTestId('payload')).toBeTruthy();
|
|
});
|
|
|
|
it('renders children when active node is null (initial load)', () => {
|
|
mockActiveNode(null);
|
|
render(
|
|
<HubOnlyGate>
|
|
<div data-testid="payload">hub content</div>
|
|
</HubOnlyGate>,
|
|
);
|
|
expect(screen.getByTestId('payload')).toBeTruthy();
|
|
});
|
|
|
|
it('returns null when active node is remote', () => {
|
|
mockActiveNode('remote');
|
|
const { container } = render(
|
|
<HubOnlyGate>
|
|
<div data-testid="payload">hub content</div>
|
|
</HubOnlyGate>,
|
|
);
|
|
expect(container.firstChild).toBeNull();
|
|
expect(screen.queryByTestId('payload')).toBeNull();
|
|
});
|
|
});
|