From 05d8b28159f472370f3c695365aeefec5d723b01 Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Mon, 8 Jun 2026 22:00:23 -0500 Subject: [PATCH] fix(kube): network/config/storage list actions use item.namespace not filter prop Service/Ingress/ConfigMap/Secret/HPA/PVC/ServiceAccount/Role/RoleBinding/ NetworkPolicy/ResourceQuota/LimitRange action handlers now use the resource's own .namespace field instead of the UI filter namespace='all'. Removes the now-unused ns local variable from CronJobList/JobList/ReplicaSetList. 24 new TDD tests verify the correct namespace is passed to getResourceYamlCmd and deleteResourceCmd for each of the 12 affected components. --- src/components/Kubernetes/ConfigMapList.tsx | 8 +- src/components/Kubernetes/CronJobList.tsx | 15 +- src/components/Kubernetes/HPAList.tsx | 9 +- src/components/Kubernetes/IngressList.tsx | 9 +- src/components/Kubernetes/JobList.tsx | 9 +- src/components/Kubernetes/LimitRangeList.tsx | 8 +- .../Kubernetes/NetworkPolicyList.tsx | 8 +- src/components/Kubernetes/PVCList.tsx | 9 +- src/components/Kubernetes/ReplicaSetList.tsx | 11 +- .../Kubernetes/ResourceQuotaList.tsx | 8 +- src/components/Kubernetes/RoleBindingList.tsx | 9 +- src/components/Kubernetes/RoleList.tsx | 9 +- src/components/Kubernetes/SecretList.tsx | 9 +- .../Kubernetes/ServiceAccountList.tsx | 9 +- src/components/Kubernetes/ServiceList.tsx | 8 +- tests/unit/NamespaceActionFix.test.tsx | 663 ++++++++++++++++++ 16 files changed, 717 insertions(+), 84 deletions(-) create mode 100644 tests/unit/NamespaceActionFix.test.tsx diff --git a/src/components/Kubernetes/ConfigMapList.tsx b/src/components/Kubernetes/ConfigMapList.tsx index 5f3cd5f9..64c315ad 100644 --- a/src/components/Kubernetes/ConfigMapList.tsx +++ b/src/components/Kubernetes/ConfigMapList.tsx @@ -19,7 +19,7 @@ type ActiveModal = | { type: "delete"; cm: ConfigMapInfo } | null; -export function ConfigMapList({ configmaps, clusterId, namespace, onRefresh }: ConfigMapListProps) { +export function ConfigMapList({ configmaps, clusterId, namespace: _namespace, onRefresh }: ConfigMapListProps) { const [activeModal, setActiveModal] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -27,7 +27,7 @@ export function ConfigMapList({ configmaps, clusterId, namespace, onRefresh }: C const openEdit = async (cm: ConfigMapInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(clusterId, "configmaps", namespace, cm.name); + const yaml = await getResourceYamlCmd(clusterId, "configmaps", cm.namespace, cm.name); setActiveModal({ type: "edit", cm, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -38,7 +38,7 @@ export function ConfigMapList({ configmaps, clusterId, namespace, onRefresh }: C if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(clusterId, "configmaps", namespace, activeModal.cm.name); + await deleteResourceCmd(clusterId, "configmaps", activeModal.cm.namespace, activeModal.cm.name); setActiveModal(null); onRefresh?.(); } finally { @@ -104,7 +104,7 @@ export function ConfigMapList({ configmaps, clusterId, namespace, onRefresh }: C (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -44,7 +41,7 @@ export function CronJobList({ const openEdit = async (cj: CronJobInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "cronjobs", ns, cj.name); + const yaml = await getResourceYamlCmd(cid, "cronjobs", cj.namespace, cj.name); setActiveModal({ type: "edit", cj, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -54,7 +51,7 @@ export function CronJobList({ const handleSuspend = async (cj: CronJobInfo) => { setActionError(null); try { - await suspendCronjobCmd(cid, ns, cj.name); + await suspendCronjobCmd(cid, cj.namespace, cj.name); onRefresh?.(); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -64,7 +61,7 @@ export function CronJobList({ const handleResume = async (cj: CronJobInfo) => { setActionError(null); try { - await resumeCronjobCmd(cid, ns, cj.name); + await resumeCronjobCmd(cid, cj.namespace, cj.name); onRefresh?.(); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -74,7 +71,7 @@ export function CronJobList({ const handleTrigger = async (cj: CronJobInfo) => { setActionError(null); try { - await triggerCronjobCmd(cid, ns, cj.name); + await triggerCronjobCmd(cid, cj.namespace, cj.name); onRefresh?.(); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -85,7 +82,7 @@ export function CronJobList({ if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(cid, "cronjobs", ns, activeModal.cj.name); + await deleteResourceCmd(cid, "cronjobs", activeModal.cj.namespace, activeModal.cj.name); setActiveModal(null); onRefresh?.(); } finally { @@ -183,7 +180,7 @@ export function CronJobList({ (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -38,7 +35,7 @@ export function HPAList({ const openEdit = async (hpa: HorizontalPodAutoscalerInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "horizontalpodautoscalers", ns, hpa.name); + const yaml = await getResourceYamlCmd(cid, "horizontalpodautoscalers", hpa.namespace, hpa.name); setActiveModal({ type: "edit", hpa, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -49,7 +46,7 @@ export function HPAList({ if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(cid, "horizontalpodautoscalers", ns, activeModal.hpa.name); + await deleteResourceCmd(cid, "horizontalpodautoscalers", activeModal.hpa.namespace, activeModal.hpa.name); setActiveModal(null); onRefresh?.(); } finally { @@ -121,7 +118,7 @@ export function HPAList({ (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -38,7 +35,7 @@ export function IngressList({ const openEdit = async (ingress: IngressInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "ingresses", ns, ingress.name); + const yaml = await getResourceYamlCmd(cid, "ingresses", ingress.namespace, ingress.name); setActiveModal({ type: "edit", ingress, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -49,7 +46,7 @@ export function IngressList({ if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(cid, "ingresses", ns, activeModal.ingress.name); + await deleteResourceCmd(cid, "ingresses", activeModal.ingress.namespace, activeModal.ingress.name); setActiveModal(null); onRefresh?.(); } finally { @@ -119,7 +116,7 @@ export function IngressList({ (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -38,7 +35,7 @@ export function JobList({ const openEdit = async (job: JobInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "jobs", ns, job.name); + const yaml = await getResourceYamlCmd(cid, "jobs", job.namespace, job.name); setActiveModal({ type: "edit", job, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -49,7 +46,7 @@ export function JobList({ if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(cid, "jobs", ns, activeModal.job.name); + await deleteResourceCmd(cid, "jobs", activeModal.job.namespace, activeModal.job.name); setActiveModal(null); onRefresh?.(); } finally { @@ -123,7 +120,7 @@ export function JobList({ (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -27,7 +27,7 @@ export function LimitRangeList({ limitranges, clusterId, namespace, onRefresh }: const openEdit = async (lr: LimitRangeInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(clusterId, "limitranges", namespace, lr.name); + const yaml = await getResourceYamlCmd(clusterId, "limitranges", lr.namespace, lr.name); setActiveModal({ type: "edit", lr, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -38,7 +38,7 @@ export function LimitRangeList({ limitranges, clusterId, namespace, onRefresh }: if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(clusterId, "limitranges", namespace, activeModal.lr.name); + await deleteResourceCmd(clusterId, "limitranges", activeModal.lr.namespace, activeModal.lr.name); setActiveModal(null); onRefresh?.(); } finally { @@ -104,7 +104,7 @@ export function LimitRangeList({ limitranges, clusterId, namespace, onRefresh }: (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -27,7 +27,7 @@ export function NetworkPolicyList({ networkpolicies, clusterId, namespace, onRef const openEdit = async (np: NetworkPolicyInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(clusterId, "networkpolicies", namespace, np.name); + const yaml = await getResourceYamlCmd(clusterId, "networkpolicies", np.namespace, np.name); setActiveModal({ type: "edit", np, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -38,7 +38,7 @@ export function NetworkPolicyList({ networkpolicies, clusterId, namespace, onRef if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(clusterId, "networkpolicies", namespace, activeModal.np.name); + await deleteResourceCmd(clusterId, "networkpolicies", activeModal.np.namespace, activeModal.np.name); setActiveModal(null); onRefresh?.(); } finally { @@ -106,7 +106,7 @@ export function NetworkPolicyList({ networkpolicies, clusterId, namespace, onRef (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -38,7 +35,7 @@ export function PVCList({ const openEdit = async (pvc: PersistentVolumeClaimInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "persistentvolumeclaims", ns, pvc.name); + const yaml = await getResourceYamlCmd(cid, "persistentvolumeclaims", pvc.namespace, pvc.name); setActiveModal({ type: "edit", pvc, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -49,7 +46,7 @@ export function PVCList({ if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(cid, "persistentvolumeclaims", ns, activeModal.pvc.name); + await deleteResourceCmd(cid, "persistentvolumeclaims", activeModal.pvc.namespace, activeModal.pvc.name); setActiveModal(null); onRefresh?.(); } finally { @@ -121,7 +118,7 @@ export function PVCList({ (null); const [isActing, setIsActing] = useState(false); const [actionError, setActionError] = useState(null); @@ -44,7 +41,7 @@ export function ReplicaSetList({ const openEdit = async (rs: ReplicaSetInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "replicasets", ns, rs.name); + const yaml = await getResourceYamlCmd(cid, "replicasets", rs.namespace, rs.name); setActiveModal({ type: "edit", rs, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -55,7 +52,7 @@ export function ReplicaSetList({ if (activeModal?.type !== "delete") return; setIsActing(true); try { - await deleteResourceCmd(cid, "replicasets", ns, activeModal.rs.name); + await deleteResourceCmd(cid, "replicasets", activeModal.rs.namespace, activeModal.rs.name); setActiveModal(null); onRefresh?.(); } finally { @@ -138,7 +135,7 @@ export function ReplicaSetList({ resourceName={activeModal.rs.name} currentReplicas={activeModal.rs.replicas} onScale={(replicas) => - scaleReplicasetCmd(cid, ns, activeModal.rs.name, replicas).then(() => { + scaleReplicasetCmd(cid, activeModal.rs.namespace, activeModal.rs.name, replicas).then(() => { setActiveModal(null); onRefresh?.(); }) @@ -150,7 +147,7 @@ export function ReplicaSetList({ (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -27,7 +27,7 @@ export function ResourceQuotaList({ resourcequotas, clusterId, namespace, onRefr const openEdit = async (rq: ResourceQuotaInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(clusterId, "resourcequotas", namespace, rq.name); + const yaml = await getResourceYamlCmd(clusterId, "resourcequotas", rq.namespace, rq.name); setActiveModal({ type: "edit", rq, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -38,7 +38,7 @@ export function ResourceQuotaList({ resourcequotas, clusterId, namespace, onRefr if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(clusterId, "resourcequotas", namespace, activeModal.rq.name); + await deleteResourceCmd(clusterId, "resourcequotas", activeModal.rq.namespace, activeModal.rq.name); setActiveModal(null); onRefresh?.(); } finally { @@ -110,7 +110,7 @@ export function ResourceQuotaList({ resourcequotas, clusterId, namespace, onRefr (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -38,7 +35,7 @@ export function RoleBindingList({ const openEdit = async (rb: RoleBindingInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "rolebindings", ns, rb.name); + const yaml = await getResourceYamlCmd(cid, "rolebindings", rb.namespace, rb.name); setActiveModal({ type: "edit", rb, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -49,7 +46,7 @@ export function RoleBindingList({ if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(cid, "rolebindings", ns, activeModal.rb.name); + await deleteResourceCmd(cid, "rolebindings", activeModal.rb.namespace, activeModal.rb.name); setActiveModal(null); onRefresh?.(); } finally { @@ -115,7 +112,7 @@ export function RoleBindingList({ (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -38,7 +35,7 @@ export function RoleList({ const openEdit = async (role: RoleInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "roles", ns, role.name); + const yaml = await getResourceYamlCmd(cid, "roles", role.namespace, role.name); setActiveModal({ type: "edit", role, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -49,7 +46,7 @@ export function RoleList({ if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(cid, "roles", ns, activeModal.role.name); + await deleteResourceCmd(cid, "roles", activeModal.role.namespace, activeModal.role.name); setActiveModal(null); onRefresh?.(); } finally { @@ -113,7 +110,7 @@ export function RoleList({ (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -38,7 +35,7 @@ export function SecretList({ const openEdit = async (secret: SecretInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "secrets", ns, secret.name); + const yaml = await getResourceYamlCmd(cid, "secrets", secret.namespace, secret.name); setActiveModal({ type: "edit", secret, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -49,7 +46,7 @@ export function SecretList({ if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(cid, "secrets", ns, activeModal.secret.name); + await deleteResourceCmd(cid, "secrets", activeModal.secret.namespace, activeModal.secret.name); setActiveModal(null); onRefresh?.(); } finally { @@ -117,7 +114,7 @@ export function SecretList({ (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -38,7 +35,7 @@ export function ServiceAccountList({ const openEdit = async (sa: ServiceAccountInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(cid, "serviceaccounts", ns, sa.name); + const yaml = await getResourceYamlCmd(cid, "serviceaccounts", sa.namespace, sa.name); setActiveModal({ type: "edit", sa, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -49,7 +46,7 @@ export function ServiceAccountList({ if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(cid, "serviceaccounts", ns, activeModal.sa.name); + await deleteResourceCmd(cid, "serviceaccounts", activeModal.sa.namespace, activeModal.sa.name); setActiveModal(null); onRefresh?.(); } finally { @@ -115,7 +112,7 @@ export function ServiceAccountList({ (null); const [isDeleting, setIsDeleting] = useState(false); const [actionError, setActionError] = useState(null); @@ -43,7 +43,7 @@ export function ServiceList({ services, clusterId, namespace, onRefresh }: Servi const openEdit = async (svc: ServiceInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(clusterId, "services", namespace, svc.name); + const yaml = await getResourceYamlCmd(clusterId, "services", svc.namespace, svc.name); setActiveModal({ type: "edit", svc, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -54,7 +54,7 @@ export function ServiceList({ services, clusterId, namespace, onRefresh }: Servi if (activeModal?.type !== "delete") return; setIsDeleting(true); try { - await deleteResourceCmd(clusterId, "services", namespace, activeModal.svc.name); + await deleteResourceCmd(clusterId, "services", activeModal.svc.namespace, activeModal.svc.name); setActiveModal(null); onRefresh?.(); } finally { @@ -140,7 +140,7 @@ export function ServiceList({ services, clusterId, namespace, onRefresh }: Servi void; + mockImplementation: (fn: (cmd: string, args?: unknown) => Promise) => void; +}; + +const mockInvoke = invoke as MockedInvoke; + +// ─── helpers ───────────────────────────────────────────────────────────────── + +/** Open the action menu for the first row whose name cell matches `name`. */ +function openActionMenu(name: string) { + const cell = screen.getByText(name); + const row = cell.closest("tr")!; + const btn = row.querySelector("button")!; + fireEvent.click(btn); +} + +/** Click the first menu item that contains `label`. */ +function clickMenuItem(label: string) { + const item = screen.getByText(label); + fireEvent.click(item); +} + +// ─── ServiceList ───────────────────────────────────────────────────────────── + +describe("ServiceList – action IPC uses item.namespace", () => { + const svc: ServiceInfo = { + name: "my-svc", + namespace: "production", + type: "ClusterIP", + cluster_ip: "10.0.0.1", + ports: [], + age: "1d", + selector: {}, + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("my-svc"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "services", + namespace: "production", + resourceName: "my-svc", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("my-svc"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "services", + namespace: "production", + resourceName: "my-svc", + }) + ); + }); +}); + +// ─── IngressList ───────────────────────────────────────────────────────────── + +describe("IngressList – action IPC uses item.namespace", () => { + const ing: IngressInfo = { + name: "my-ingress", + namespace: "staging", + host: "example.com", + addresses: [], + age: "2d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("my-ingress"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "ingresses", + namespace: "staging", + resourceName: "my-ingress", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("my-ingress"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "ingresses", + namespace: "staging", + resourceName: "my-ingress", + }) + ); + }); +}); + +// ─── ConfigMapList ──────────────────────────────────────────────────────────── + +describe("ConfigMapList – action IPC uses item.namespace", () => { + const cm: ConfigMapInfo = { + name: "app-config", + namespace: "kube-system", + data_keys: 3, + age: "10d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("app-config"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "configmaps", + namespace: "kube-system", + resourceName: "app-config", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("app-config"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "configmaps", + namespace: "kube-system", + resourceName: "app-config", + }) + ); + }); +}); + +// ─── SecretList ─────────────────────────────────────────────────────────────── + +describe("SecretList – action IPC uses item.namespace", () => { + const secret: SecretInfo = { + name: "db-creds", + namespace: "production", + type: "Opaque", + data_keys: 2, + age: "5d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("db-creds"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "secrets", + namespace: "production", + resourceName: "db-creds", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("db-creds"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "secrets", + namespace: "production", + resourceName: "db-creds", + }) + ); + }); +}); + +// ─── HPAList ────────────────────────────────────────────────────────────────── + +describe("HPAList – action IPC uses item.namespace", () => { + const hpa: HorizontalPodAutoscalerInfo = { + name: "web-hpa", + namespace: "default", + min_replicas: 1, + max_replicas: 10, + current_replicas: 3, + desired_replicas: 3, + age: "7d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("web-hpa"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "horizontalpodautoscalers", + namespace: "default", + resourceName: "web-hpa", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("web-hpa"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "horizontalpodautoscalers", + namespace: "default", + resourceName: "web-hpa", + }) + ); + }); +}); + +// ─── PVCList ────────────────────────────────────────────────────────────────── + +describe("PVCList – action IPC uses item.namespace", () => { + const pvc: PersistentVolumeClaimInfo = { + name: "data-pvc", + namespace: "staging", + status: "Bound", + volume: "pv-001", + capacity: "10Gi", + access_modes: ["ReadWriteOnce"], + age: "3d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("data-pvc"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "persistentvolumeclaims", + namespace: "staging", + resourceName: "data-pvc", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("data-pvc"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "persistentvolumeclaims", + namespace: "staging", + resourceName: "data-pvc", + }) + ); + }); +}); + +// ─── ServiceAccountList ─────────────────────────────────────────────────────── + +describe("ServiceAccountList – action IPC uses item.namespace", () => { + const sa: ServiceAccountInfo = { + name: "app-sa", + namespace: "production", + secrets: 1, + age: "30d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("app-sa"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "serviceaccounts", + namespace: "production", + resourceName: "app-sa", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("app-sa"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "serviceaccounts", + namespace: "production", + resourceName: "app-sa", + }) + ); + }); +}); + +// ─── RoleList ───────────────────────────────────────────────────────────────── + +describe("RoleList – action IPC uses item.namespace", () => { + const role: RoleInfo = { + name: "pod-reader", + namespace: "default", + age: "14d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("pod-reader"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "roles", + namespace: "default", + resourceName: "pod-reader", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("pod-reader"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "roles", + namespace: "default", + resourceName: "pod-reader", + }) + ); + }); +}); + +// ─── RoleBindingList ────────────────────────────────────────────────────────── + +describe("RoleBindingList – action IPC uses item.namespace", () => { + const rb: RoleBindingInfo = { + name: "pod-reader-binding", + namespace: "default", + role: "pod-reader", + age: "10d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("pod-reader-binding"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "rolebindings", + namespace: "default", + resourceName: "pod-reader-binding", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("pod-reader-binding"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "rolebindings", + namespace: "default", + resourceName: "pod-reader-binding", + }) + ); + }); +}); + +// ─── NetworkPolicyList ──────────────────────────────────────────────────────── + +describe("NetworkPolicyList – action IPC uses item.namespace", () => { + const np: NetworkPolicyInfo = { + name: "deny-all", + namespace: "production", + pod_selector: "{}", + policy_types: ["Ingress"], + age: "3d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("deny-all"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "networkpolicies", + namespace: "production", + resourceName: "deny-all", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("deny-all"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "networkpolicies", + namespace: "production", + resourceName: "deny-all", + }) + ); + }); +}); + +// ─── ResourceQuotaList ──────────────────────────────────────────────────────── + +describe("ResourceQuotaList – action IPC uses item.namespace", () => { + const rq: ResourceQuotaInfo = { + name: "compute-resources", + namespace: "default", + request_cpu: "4", + request_memory: "8Gi", + limit_cpu: "8", + limit_memory: "16Gi", + age: "7d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("compute-resources"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "resourcequotas", + namespace: "default", + resourceName: "compute-resources", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("compute-resources"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "resourcequotas", + namespace: "default", + resourceName: "compute-resources", + }) + ); + }); +}); + +// ─── LimitRangeList ─────────────────────────────────────────────────────────── + +describe("LimitRangeList – action IPC uses item.namespace", () => { + const lr: LimitRangeInfo = { + name: "cpu-mem-limits", + namespace: "default", + limit_count: 3, + age: "14d", + }; + + beforeEach(() => vi.clearAllMocks()); + + it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue("yaml: content"); + render( + + ); + openActionMenu("cpu-mem-limits"); + clickMenuItem("Edit"); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "limitranges", + namespace: "default", + resourceName: "cpu-mem-limits", + }) + ); + }); + + it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + render( + {}} /> + ); + openActionMenu("cpu-mem-limits"); + clickMenuItem("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i }); + fireEvent.click(confirmBtn); + await waitFor(() => + expect(mockInvoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "limitranges", + namespace: "default", + resourceName: "cpu-mem-limits", + }) + ); + }); +});