import { act, screen, waitFor, within } from '@testing-library/react'; import React from 'react'; import type { Mock } from 'vitest'; import { AccountService, InstanceReader, ManifestReader, RecentHistoryService, SETTINGS } from '@spinnaker/core'; import type { IKubernetesInstanceDetailsProps } from './KubernetesInstanceDetails'; import { KubernetesInstanceDetailsComponent as KubernetesInstanceDetails } from './KubernetesInstanceDetails'; import { renderWithRouter } from '../../../../core/src/utils/testUtils/rtl'; import { setupUser } from '../../../../core/src/utils/testUtils/userEvent'; import { findKubernetesInstanceIdentifier } from './kubernetesInstanceDetails.utils'; import { KubernetesManifestCommandBuilder } from '../../manifest/manifestCommandBuilder.service'; import { ManifestWizard } from '../../manifest/wizard/ManifestWizard'; describe('findKubernetesInstanceIdentifier', () => { it('finds pod instances under server groups and records server group recent-history data', () => { const addRecentHistory = vi.fn(); const identifier = findKubernetesInstanceIdentifier( appWithInfrastructure({ serverGroups: [ instanceManager({ category: 'serverGroup', name: 'replicaSet backend-abc123', instances: [{ id: 'pod-uid', name: 'pod backend-abc123-def45' }], }), ], }), 'pod-uid', addRecentHistory, ); expect(identifier).toEqual({ account: 'k8s-local', id: 'pod-uid', name: 'pod backend-abc123-def45', namespace: 'dev', }); expect(addRecentHistory).toHaveBeenCalledWith({ account: 'k8s-local', region: 'dev', serverGroup: 'replicaSet backend-abc123', }); }); it('finds pod instances under load balancers without server group recent-history data', () => { const addRecentHistory = vi.fn(); const identifier = findKubernetesInstanceIdentifier( appWithInfrastructure({ loadBalancers: [ instanceManager({ category: 'loadBalancer', name: 'service backend', instances: [{ id: 'service-pod-uid', name: 'pod backend-from-service' }], }), ], }), 'service-pod-uid', addRecentHistory, ); expect(identifier).toEqual({ account: 'k8s-local', id: 'service-pod-uid', name: 'pod backend-from-service', namespace: 'dev', }); expect(addRecentHistory).toHaveBeenCalledWith({ account: 'k8s-local', region: 'dev', }); }); it('finds pod instances under load balancer server groups and records server group recent-history data', () => { const addRecentHistory = vi.fn(); const identifier = findKubernetesInstanceIdentifier( appWithInfrastructure({ loadBalancers: [ { serverGroups: [ instanceManager({ category: 'serverGroup', name: 'replicaSet backend-from-service', instances: [{ id: 'nested-pod-uid', name: 'pod backend-from-nested-service' }], }), ], }, ], }), 'nested-pod-uid', addRecentHistory, ); expect(identifier).toEqual({ account: 'k8s-local', id: 'nested-pod-uid', name: 'pod backend-from-nested-service', namespace: 'dev', }); expect(addRecentHistory).toHaveBeenCalledWith({ account: 'k8s-local', region: 'dev', serverGroup: 'replicaSet backend-from-service', }); }); it('returns null for missing instances without recording recent-history data', () => { const addRecentHistory = vi.fn(); expect( findKubernetesInstanceIdentifier( appWithInfrastructure({ serverGroups: [instanceManager({ instances: [{ id: 'other-pod', name: 'pod other' }] })], }), 'missing-pod', addRecentHistory, ), ).toBeNull(); expect(addRecentHistory).not.toHaveBeenCalled(); }); }); describe('', () => { let originalAdHocInfraWritesEnabled: boolean; let props: IKubernetesInstanceDetailsProps; beforeEach(() => { originalAdHocInfraWritesEnabled = SETTINGS.kubernetesAdHocInfraWritesEnabled; SETTINGS.kubernetesAdHocInfraWritesEnabled = true; props = { app: appWithInfrastructure({ serverGroups: [ instanceManager({ category: 'serverGroup', name: 'replicaSet backend-abc123', instances: [{ id: 'pod-uid', name: 'pod backend-abc123-def45' }], }), ], }), environment: 'test', instance: { instanceId: 'pod-uid' }, moniker: { app: 'kubernetesapp', cluster: 'deployment backend' }, } as IKubernetesInstanceDetailsProps; vi.spyOn(InstanceReader, 'getInstanceDetails').mockReturnValue(Promise.resolve(instanceDetails()) as any); vi.spyOn(ManifestReader, 'getManifest').mockReturnValue(Promise.resolve(manifestDetails()) as any); vi.spyOn(RecentHistoryService, 'addExtraDataToLatest').mockReturnValue(undefined); vi.spyOn(AccountService, 'challengeDestructiveActions').mockResolvedValue(false); }); afterEach(() => { SETTINGS.kubernetesAdHocInfraWritesEnabled = originalAdHocInfraWritesEnabled; }); it('loads instance and manifest details before rendering the React sections', async () => { const user = setupUser(); const loadedInstance = instanceDetails(); const loadedManifest = manifestDetails({ events: [ { apiVersion: 'v1', count: 1, kind: 'Event', lastTimestamp: '2025-07-24T01:27:29Z', message: 'Created pod: backend-abc123-def45', reason: 'SuccessfulCreate', type: 'Normal', }, ], }); loadedManifest.manifest.metadata.annotations = { 'custom.details.spinnaker.io/resource': '{{account}} {{name}} {{provider}}', }; const builtCommand = { command: 'edit-pod' } as any; (InstanceReader.getInstanceDetails as Mock).mockResolvedValue(loadedInstance); (ManifestReader.getManifest as Mock).mockResolvedValue(loadedManifest); const buildCommand = vi .spyOn(KubernetesManifestCommandBuilder, 'buildNewManifestCommand') .mockResolvedValue(builtCommand); const showWizard = vi.spyOn(ManifestWizard, 'show').mockReturnValue(undefined); renderWithRouter(); expect(await screen.findByRole('heading', { name: 'backend-abc123-def45' })).toBeInTheDocument(); expect(InstanceReader.getInstanceDetails).toHaveBeenCalledWith('k8s-local', 'dev', 'pod backend-abc123-def45'); expect(ManifestReader.getManifest).toHaveBeenCalledWith('k8s-local', 'dev', 'pod backend-abc123-def45'); expect(RecentHistoryService.addExtraDataToLatest).toHaveBeenCalledWith('instances', { account: 'k8s-local', region: 'dev', serverGroup: 'replicaSet backend-abc123', }); ['Information', 'Status', 'Events', 'Resources', 'Labels', 'Instance links'].forEach((heading) => { expect(screen.getByRole('heading', { name: heading })).toBeInTheDocument(); }); expect(screen.getByText('k8s-local')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'custom' })).toBeInTheDocument(); expect(screen.getByText('k8s-local pod backend-abc123-def45 kubernetes')).toBeInTheDocument(); expect(screen.getByText('BestEffort')).toBeInTheDocument(); expect(screen.getByText('Ready')).toBeInTheDocument(); expect(screen.getByText('CPU')).toBeInTheDocument(); expect(screen.getByText(/app:\s*backend/)).toBeInTheDocument(); expect(screen.getByText('SuccessfulCreate')).toBeInTheDocument(); expect(screen.getByText('Created pod: backend-abc123-def45')).toBeInTheDocument(); await user.click(screen.getByRole('heading', { name: 'Instance links' })); expect(screen.getByRole('link', { name: 'Backend endpoint' })).toHaveAttribute( 'href', 'http://backend.example.com/test', ); expect(screen.getByRole('button', { name: 'Console Output (Raw)' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Pod Actions' })).toBeInTheDocument(); expect(screen.queryByText('Node IP')).not.toBeInTheDocument(); expect(screen.queryByText('Pod IP')).not.toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'Pod Actions' })); await user.click(screen.getByText('Edit')); expect(buildCommand).toHaveBeenCalledWith( props.app, loadedManifest.manifest, loadedInstance.moniker, loadedInstance.account, ); await waitFor(() => expect(showWizard).toHaveBeenCalledWith({ application: props.app, command: builtCommand, title: 'Edit Manifest', }), ); }); it('renders an Images section with copy-to-clipboard for each manifest artifact', async () => { (ManifestReader.getManifest as Mock).mockReturnValue( Promise.resolve( manifestDetails({ artifacts: [ { id: '1', type: 'docker/image', reference: 'gcr.io/project/backend@sha256:abc123' }, { id: '2', type: 'docker/image', reference: 'gcr.io/project/worker@sha256:def456' }, ], }), ) as any, ); renderWithRouter(); const imagesHeading = await screen.findByRole('heading', { name: 'Images' }); const imagesSection = within(imagesHeading.closest('.collapsible-section') as HTMLElement); const items = imagesSection.getAllByRole('listitem'); expect(items).toHaveLength(2); expect(imagesSection.getAllByRole('button', { name: 'Copy to clipboard' })).toHaveLength(2); expect(imagesSection.getAllByRole('textbox').map((node) => (node as HTMLTextAreaElement).value)).toEqual([ 'gcr.io/project/backend@sha256:abc123', 'gcr.io/project/worker@sha256:def456', ]); expect(within(items[0]).getByText('gcr.io/project/backend@sha256:abc123', { selector: 'i' })).toBeInTheDocument(); expect(within(items[1]).getByText('gcr.io/project/worker@sha256:def456', { selector: 'i' })).toBeInTheDocument(); expect(imagesSection.getAllByText('docker/image', { exact: false, selector: 'b' })).toHaveLength(2); }); it('omits the Images section when the manifest has no artifacts', async () => { renderWithRouter(); expect(await screen.findByRole('heading', { name: 'backend-abc123-def45' })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Labels' })).toBeInTheDocument(); expect(screen.queryByRole('heading', { name: 'Images' })).not.toBeInTheDocument(); }); it('auto-closes when the instance cannot be found in application infrastructure', async () => { const autoClose = vi.fn(); renderWithRouter( , ); await waitFor(() => expect(autoClose).toHaveBeenCalled()); expect(InstanceReader.getInstanceDetails).not.toHaveBeenCalled(); expect(ManifestReader.getManifest).not.toHaveBeenCalled(); }); it('replaces missing instance details through the injected state service', () => { const stateService = { go: vi.fn(), params: {} }; const component = new KubernetesInstanceDetails({ ...props, router: {}, stateParams: {}, stateService, } as any); (component as any).autoClose(); expect(stateService.params.allowModalToStayOpen).toBe(true); expect(stateService.go).toHaveBeenCalledWith('^', null, { location: 'replace' }); }); it('waits for application data before loading changed instance props', async () => { const autoClose = vi.fn(); const ready = deferred(); const serverGroups: any[] = []; const app = appWithInfrastructure({ serverGroups }); app.ready = () => ready.promise; const rendered = renderWithRouter(); rendered.rerender( , ); await act(async () => Promise.resolve()); expect(autoClose).not.toHaveBeenCalled(); expect(InstanceReader.getInstanceDetails).not.toHaveBeenCalled(); expect(ManifestReader.getManifest).not.toHaveBeenCalled(); serverGroups.push( instanceManager({ instances: [{ id: 'new-pod-uid', name: 'pod backend-new' }], }), ); await act(async () => ready.resolve()); await waitFor(() => expect(InstanceReader.getInstanceDetails).toHaveBeenCalledWith('k8s-local', 'dev', 'pod backend-new'), ); }); it('keeps the newer pod details when an older load resolves last', async () => { const oldInstance = deferred(); const oldManifest = deferred(); const newInstance = deferred(); const newManifest = deferred(); props = { ...props, app: appWithInfrastructure({ serverGroups: [ instanceManager({ instances: [ { id: 'pod-uid', name: 'pod backend-old' }, { id: 'new-pod-uid', name: 'pod backend-new' }, ], }), ], }), }; (InstanceReader.getInstanceDetails as Mock).mockImplementation( (_account: string, _namespace: string, name: string) => name === 'pod backend-new' ? newInstance.promise : oldInstance.promise, ); (ManifestReader.getManifest as Mock).mockImplementation((_account: string, _namespace: string, name: string) => name === 'pod backend-new' ? newManifest.promise : oldManifest.promise, ); const rendered = renderWithRouter(); await waitFor(() => expect(InstanceReader.getInstanceDetails).toHaveBeenCalled()); rendered.rerender(); await act(async () => { newInstance.resolve(instanceDetails({ displayName: 'backend-new', humanReadableName: 'pod backend-new' })); newManifest.resolve(manifestDetails()); }); expect(await screen.findByRole('heading', { name: 'backend-new' })).toBeInTheDocument(); await act(async () => { oldInstance.resolve(instanceDetails({ displayName: 'backend-old', humanReadableName: 'pod backend-old' })); oldManifest.resolve(manifestDetails()); }); expect(screen.getByRole('heading', { name: 'backend-new' })).toBeInTheDocument(); expect(screen.queryByRole('heading', { name: 'backend-old' })).not.toBeInTheDocument(); }); it('ignores stale load failures after a newer pod has rendered', async () => { const autoClose = vi.fn(); const oldInstance = deferred(); const oldManifest = deferred(); const newInstance = deferred(); const newManifest = deferred(); props = { ...props, autoClose, app: appWithInfrastructure({ serverGroups: [ instanceManager({ instances: [ { id: 'pod-uid', name: 'pod backend-old' }, { id: 'new-pod-uid', name: 'pod backend-new' }, ], }), ], }), }; (InstanceReader.getInstanceDetails as Mock).mockImplementation( (_account: string, _namespace: string, name: string) => name === 'pod backend-new' ? newInstance.promise : oldInstance.promise, ); (ManifestReader.getManifest as Mock).mockImplementation((_account: string, _namespace: string, name: string) => name === 'pod backend-new' ? newManifest.promise : oldManifest.promise, ); const rendered = renderWithRouter(); await waitFor(() => expect(InstanceReader.getInstanceDetails).toHaveBeenCalled()); rendered.rerender(); await act(async () => { oldInstance.resolve(instanceDetails({ displayName: 'backend-old', humanReadableName: 'pod backend-old' })); newInstance.resolve(instanceDetails({ displayName: 'backend-new', humanReadableName: 'pod backend-new' })); newManifest.resolve(manifestDetails()); }); expect(await screen.findByRole('heading', { name: 'backend-new' })).toBeInTheDocument(); await act(async () => oldManifest.reject(new Error('stale load failed'))); expect(autoClose).not.toHaveBeenCalled(); expect(screen.getByRole('heading', { name: 'backend-new' })).toBeInTheDocument(); }); }); function deferred() { let resolve!: (value: T | PromiseLike) => void; let reject!: (reason?: any) => void; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject }; } const instanceManager = (overrides: any = {}) => ({ account: 'k8s-local', region: 'dev', category: 'serverGroup', name: 'replicaSet backend', instances: [] as any[], ...overrides, }); const appWithInfrastructure = ({ serverGroups = [], loadBalancers = [], }: { serverGroups?: any[]; loadBalancers?: any[]; }) => ({ isStandalone: true, ready: () => Promise.resolve(), onRefresh: () => () => null, getDataSource: (key: string) => ({ data: key === 'serverGroups' ? serverGroups : loadBalancers, }), serverGroups: { refresh: vi.fn(), }, attributes: { instanceLinks: [ { title: 'Instance links', links: [{ title: 'Backend endpoint', path: 'http://{{ipAddress}}/{{environment}}' }], }, ], }, } as any); const instanceDetails = (overrides: any = {}) => ({ account: 'k8s-local', apiVersion: 'v1', cloudProvider: 'kubernetes', createdTime: 1753320449000, displayName: 'backend-abc123-def45', healthState: 'Up', humanReadableName: 'pod backend-abc123-def45', kind: 'pod', moniker: { app: 'kubernetesapp', cluster: 'deployment backend' }, namespace: 'dev', publicDnsName: 'backend.example.com', zone: 'dev', ...overrides, } as any); const manifestDetails = (overrides: any = {}) => ({ account: 'k8s-local', metrics: [{ containerName: 'backend', metrics: { 'CPU(cores)': '1', 'MEMORY(bytes)': '1Gi' } }], manifest: { kind: 'Pod', metadata: { labels: { app: 'backend', }, name: 'backend-abc123-def45', }, spec: { containers: [{ name: 'backend' }], nodeName: 'worker.dev.example', }, status: { conditions: [{ lastTransitionTime: '2025-07-24T01:27:29Z', message: 'ready', status: 'True', type: 'Ready' }], hostIP: '10.0.0.1', podIP: '10.0.0.2', qosClass: 'BestEffort', }, }, events: [], ...overrides, } as any);