mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32:52 +00:00
fix(resources): attribute free images to managed prune by repository family (#1769)
* fix(resources): attribute free images to managed prune by repository family After a stack update, unused previous tags lost compose labels and dropped out of managed prune. Match free image repositories still used by managed containers (excluding foreign Compose projects), and clarify the fleet dry-run unlock on the prune card footer. * fix(resources): omit stackName on repo-match managed prune items Repository sharing is not ownership; the confirm list must not show stack attribution for repo-matched free images. Pin repository-key normalization with exact Set assertions so the duplicated parser cannot drift silently. * fix(fleet): bind prune fingerprints to image tag sets RepoTag churn on an already-planned image Id no longer leaves the dry-run fingerprint unchanged, so fleet preflight rejects the whole execute when any node retags. Itemized delete also refuses on reference drift and reports multi-repository refuse without implying a clean no-op.
This commit is contained in:
@@ -72,6 +72,23 @@ it('keeps destructive prune disabled until an itemized dry run is reviewed', asy
|
||||
render(<FleetPruneCard nodes={nodes} />);
|
||||
await waitFor(() => expect(screen.getByText('~ 4 KB reclaimable')).toBeInTheDocument());
|
||||
expect(screen.getByRole('button', { name: 'Prune fleet' })).toBeDisabled();
|
||||
expect(screen.getByText(/Run Dry run to unlock Prune fleet/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('drops the unlock footer once dry run review is valid', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockedFetch.mockImplementation((url: string) => {
|
||||
if (url === '/fleet/prune/estimate') return Promise.resolve(estimateResponse());
|
||||
if (url === '/fleet/labels/fleet-prune') return Promise.resolve(jsonResponse(200, { results: [planResult] }));
|
||||
return Promise.resolve(jsonResponse(404, {}));
|
||||
});
|
||||
|
||||
render(<FleetPruneCard nodes={nodes} />);
|
||||
await waitFor(() => expect(screen.getByText(/Run Dry run to unlock Prune fleet/)).toBeInTheDocument());
|
||||
await user.click(screen.getByRole('button', { name: 'Dry run' }));
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Prune fleet' })).toBeEnabled());
|
||||
expect(screen.queryByText(/Run Dry run to unlock Prune fleet/)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/Reversible · no · reviewed across 1 node/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders item metadata and enables prune after a valid dry run', async () => {
|
||||
|
||||
@@ -315,7 +315,11 @@ export function FleetPruneCard({ nodes }: Props) {
|
||||
variant: 'destructive',
|
||||
disabled: running || !reviewValid,
|
||||
}}
|
||||
footerContext={`Reversible · no · reviewed across ${nodes.length} node${nodes.length === 1 ? '' : 's'}`}
|
||||
footerContext={
|
||||
targets.size > 0 && !reviewValid
|
||||
? `Reversible · no · reviewed across ${nodes.length} node${nodes.length === 1 ? '' : 's'} · Run Dry run to unlock Prune fleet`
|
||||
: `Reversible · no · reviewed across ${nodes.length} node${nodes.length === 1 ? '' : 's'}`
|
||||
}
|
||||
>
|
||||
<SheetSection title={`Targets · ${targets.size} / ${ALL_TARGETS.length}`} meta={targets.size === 0 ? 'pick at least one' : undefined}>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
|
||||
Reference in New Issue
Block a user