diff --git a/cluster-api/src/components/overview/ClusterDetailsErrorOverview.test.tsx b/cluster-api/src/components/overview/ClusterDetailsErrorOverview.test.tsx
new file mode 100644
index 0000000000..564ba3b221
--- /dev/null
+++ b/cluster-api/src/components/overview/ClusterDetailsErrorOverview.test.tsx
@@ -0,0 +1,60 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { InlineSolutionPanel } from './ClusterDetailsErrorOverview';
+import { ClusterPriorityError } from './clusterHealth';
+
+vi.mock('@iconify/react', () => ({
+ Icon: ({ icon }: { icon: string }) => ,
+}));
+
+vi.mock('./capiUtils', async importOriginal => ({
+ ...(await importOriginal()),
+ copyToClipboard: vi.fn().mockResolvedValue(undefined),
+}));
+
+const priorityError: ClusterPriorityError = {
+ errorDef: {
+ id: 'test-error',
+ title: 'Test error',
+ description: 'A test error',
+ severity: 'warning',
+ matcher: {},
+ solution: {
+ steps: ['do a thing'],
+ quickFixCommands: [
+ { description: 'Fix A', command: 'kubectl apply -f a.yaml' },
+ { description: 'Fix B', command: 'kubectl apply -f b.yaml' },
+ ],
+ },
+ },
+} as ClusterPriorityError;
+
+function copyIconFor(command: string): string {
+ const row = screen.getByText(command).closest('div')?.parentElement;
+ return row!.querySelector('[data-icon]')!.getAttribute('data-icon')!;
+}
+
+describe('InlineSolutionPanel copy feedback', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("does not clear a newer copy's checkmark when an older copy's timeout fires", async () => {
+ render();
+
+ const buttons = screen.getAllByRole('button');
+ fireEvent.click(buttons[0]); // copy "Fix A"
+ await vi.advanceTimersByTimeAsync(1000);
+ fireEvent.click(buttons[1]); // copy "Fix B", within A's 1800ms window
+
+ // A's original timeout fires now (1800ms after A's click); B's hasn't yet.
+ await vi.advanceTimersByTimeAsync(800);
+
+ expect(copyIconFor('kubectl apply -f a.yaml')).toBe('mdi:content-copy');
+ expect(copyIconFor('kubectl apply -f b.yaml')).toBe('mdi:check');
+ });
+});
diff --git a/cluster-api/src/components/overview/ClusterDetailsErrorOverview.tsx b/cluster-api/src/components/overview/ClusterDetailsErrorOverview.tsx
index b42168e178..6071fe09ea 100644
--- a/cluster-api/src/components/overview/ClusterDetailsErrorOverview.tsx
+++ b/cluster-api/src/components/overview/ClusterDetailsErrorOverview.tsx
@@ -70,7 +70,7 @@ function renderClusterHealthStatus(healthy: boolean, priorityError: ClusterPrior
* @param props.open - Whether the panel is currently expanded or collapsed.
* @returns A Collapse-wrapped React component with solution instructions.
*/
-function InlineSolutionPanel({
+export function InlineSolutionPanel({
priorityError,
open,
}: {
@@ -85,7 +85,9 @@ function InlineSolutionPanel({
try {
await copyToClipboard(text);
setCopiedCommand(text);
- setTimeout(() => setCopiedCommand(null), 1800);
+ // only clear if this timeout's command is still the one showing —
+ // otherwise it clobbers a more recent copy before its own 1800ms is up
+ setTimeout(() => setCopiedCommand(current => (current === text ? null : current)), 1800);
} catch {
setCopiedCommand(null);
}