mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32:52 +00:00
81858a0bb0
* feat(mesh): refine the Routing tab and add per-route removal
Tighten the Fleet > Routing tab so its node cards and diagnostics read
honestly for proxy-connected fleets, and remove a couple of dead ends.
- Node cards drop the "pilot connected/offline" line, which was meaningless
for nodes that connect over the HTTP API proxy. The compact card drops the
matching agent cell and now reads stacks / aliases / bridge.
- Enabling mesh on a proxy node shows a transient "Connecting" state and
settles to meshed on its own, instead of flashing "Degraded" with a manual
refresh while the bridge finishes dialing.
- The alias detail sheet gains a "Remove from mesh" action that opts the
alias's owning stack out (the confirmation says how many aliases that
drops), and a "Topology" tab. The standalone topology sheet and its
opt-in-sheet shortcut are removed in favor of the tab.
- The "Add stack" affordance stays reachable on a meshed node, so a node
that already has aliases is no longer a dead end.
- Diagnostics and the alias detail sheet show a transport-aware line
("local", "API proxy bridge", or "Pilot tunnel" with its state) instead of
a fixed "Pilot tunnel" label.
Adds unit coverage for the new state derivation, transport descriptor, the
enable auto-converge, and the admin-gated removal, and updates the mesh docs.
* fix(mesh): harden Routing tab toggle and alias sheet against async races
Independent review surfaced two narrow async races in the new code.
- RoutingNodeCard: cancel the converge re-poll batch at the start of any
toggle (so a slow disable can't let a prior enable's re-polls fire), and
guard the toast, refresh, timer scheduling, and setToggling behind a mounted
ref so nothing runs after the card unmounts while the enable POST is still
in flight.
- MeshRouteDetailSheet: re-check the cancelled flag after reading the
diagnostic body, not just before it. Reading the body is itself async, so a
superseded alias's diagnostic could otherwise call setDiag and expose Remove
for the wrong stack.
Adds tests for unmount-before-enable-resolves and the deferred-diagnostic
alias switch.
70 lines
2.7 KiB
TypeScript
70 lines
2.7 KiB
TypeScript
/**
|
|
* Render-gate coverage for MeshOptInSheet's opt-in/out controls.
|
|
*
|
|
* Opting a stack in or out is admin-only on the backend
|
|
* (POST /api/mesh/nodes/:id/stacks/:stack/opt-in|opt-out require admin). This
|
|
* test locks the matching UI gate: a manager sees Add/Remove buttons, a
|
|
* non-manager sees the membership read-only with a hint. The read-only branch
|
|
* must never issue the admin-only mutation.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen } from '@testing-library/react';
|
|
import type { MeshStackEntry } from '@/types/mesh';
|
|
|
|
vi.mock('@/lib/api', () => ({ apiFetch: vi.fn() }));
|
|
vi.mock('@/components/ui/toast-store', () => ({
|
|
toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn(), info: vi.fn(), loading: vi.fn(), dismiss: vi.fn() },
|
|
}));
|
|
|
|
import { apiFetch } from '@/lib/api';
|
|
import { MeshOptInSheet } from './MeshOptInSheet';
|
|
|
|
const STACKS: MeshStackEntry[] = [
|
|
{ name: 'web', optedIn: true },
|
|
{ name: 'db', optedIn: false },
|
|
];
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(apiFetch).mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ stacks: STACKS }),
|
|
} as unknown as Response);
|
|
});
|
|
|
|
function renderSheet(canManage: boolean) {
|
|
return render(
|
|
<MeshOptInSheet
|
|
open={true}
|
|
onOpenChange={() => {}}
|
|
nodeId={1}
|
|
nodeName="node-alpha"
|
|
onChanged={() => {}}
|
|
canManage={canManage}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
describe('MeshOptInSheet canManage gate', () => {
|
|
it('shows opt-in/out controls for a manager', async () => {
|
|
renderSheet(true);
|
|
expect(await screen.findByText('web')).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: /Remove from mesh/i })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: /Add to mesh/i })).toBeInTheDocument();
|
|
expect(screen.queryByText(/Changing mesh membership requires an administrator/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('renders the membership read-only for a non-manager', async () => {
|
|
renderSheet(false);
|
|
expect(await screen.findByText('web')).toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: /Remove from mesh/i })).not.toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: /Add to mesh/i })).not.toBeInTheDocument();
|
|
expect(screen.getByText(/Changing mesh membership requires an administrator/i)).toBeInTheDocument();
|
|
// The read-only branch must never issue an opt-in/opt-out request.
|
|
expect(vi.mocked(apiFetch)).not.toHaveBeenCalledWith(
|
|
expect.stringContaining('/opt-'),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|