import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BlocksService } from '@services/blocks/blocks'; import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog'; import { FieldRetriever } from '@services/retriever/field-retriever'; import { EditorStateHolder } from '@stores/flow-editor'; import { vi } from 'vitest'; import { of } from 'rxjs'; import { GenericNodeComponent } from './generic-node'; describe('GenericNodeComponent', () => { let component: GenericNodeComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [GenericNodeComponent], providers: [ { provide: NodeSettingsDialogService, useValue: { open: vi.fn().mockResolvedValue(null) } }, { provide: EditorStateHolder, useValue: { currentFlow: vi.fn().mockReturnValue(null), flowValidationErrors: vi.fn().mockReturnValue([]), isBlockSelected: vi.fn().mockReturnValue(false), isValidationNodeHighlighted: vi.fn().mockReturnValue(false), updateData: vi.fn(), stopDraggingSelectedBlocks: vi.fn() } }, { provide: FieldRetriever, useValue: { retrieveSchema: vi.fn(), retrieveStructuredData: vi.fn(), retrieveText: vi.fn() } }, { provide: BlocksService, useValue: { peekBlockType: vi.fn().mockReturnValue(null), getBlockType: vi.fn().mockResolvedValue(null), biasAnnotationsDescriptor: vi.fn().mockReturnValue(null), updateBlock: vi.fn() } } ] }) .compileComponents(); fixture = TestBed.createComponent(GenericNodeComponent); component = fixture.componentInstance; fixture.componentRef.setInput('data', { id: 'node-1', inputs: {}, outputs: {}, selected: false, data: { id: 'node-1', typeName: '', name: 'Node 1', inputs: [], outputs: [], specificConfiguration: { name: 'Node 1' } } }); fixture.componentRef.setInput('emit', vi.fn()); fixture.componentRef.setInput('rendered', vi.fn()); fixture.detectChanges(); await fixture.whenStable(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('renders every input and output after the Rete node payload is updated', () => { fixture.componentRef.setInput('data', { ...component.data, inputs: { existing: { socket: { name: 'ANY' } }, new: { socket: { name: 'ANY' } } }, outputs: { noAssessment: { socket: { name: 'ANY' } }, excluded: { socket: { name: 'ANY' } }, continued: { socket: { name: 'ANY' } } }, data: { ...component.data.data, inputs: [ { name: 'existing', type: 'ANY', multiple: false }, { name: 'new', type: 'ANY', multiple: false } ], outputs: [ { name: 'noAssessment', type: 'ANY', multiple: false }, { name: 'excluded', type: 'ANY', multiple: false }, { name: 'continued', type: 'ANY', multiple: false } ] } }); fixture.detectChanges(); const host = fixture.nativeElement as HTMLElement; const inputLabels = Array.from(host.querySelectorAll('.llm-row-input .llm-pill-name')) .map((element) => element.textContent?.trim()); const outputLabels = Array.from(host.querySelectorAll('.llm-row-output .llm-pill-name')) .map((element) => element.textContent?.trim()); expect(inputLabels).toEqual(['existing', 'new']); expect(outputLabels).toEqual(['noAssessment', 'excluded', 'continued']); }); it('restores and syncs persisted expanded-mode state', () => { const nodeData = component.data.data as Record; nodeData['__focusOpen'] = true; (component as any).restorePersistedFocusState(); expect(component.focusOpen).toBe(true); expect(nodeData['__focusOpen']).toBe(true); component.toggleFocus(); expect(component.focusOpen).toBe(false); expect(nodeData['__focusOpen']).toBe(false); }); it('has no bias annotation badge when the block has no annotations', () => { expect(component.biasAnnotationBadge).toBeNull(); }); it('has no lane badge when the block has no laneId', () => { expect(component.laneBadge).toBeNull(); }); it('does not render a lane badge while swimlanes are disabled', () => { const editorState = TestBed.inject(EditorStateHolder) as any; editorState.currentFlow.mockReturnValue({ data: { lanes: [{ id: 'lane-hr', name: 'HR', order: 0, color: '#F59F00' }] } }); component.data.data = { ...component.data.data, laneId: 'lane-hr' }; expect(component.laneBadge).toBeNull(); }); it('computes the bias annotation badge from the node annotations and the severity catalog', () => { const blocks = TestBed.inject(BlocksService) as any; blocks.biasAnnotationsDescriptor.mockReturnValue({ options: { severity: [ { value: 'LOW', label: 'Low' }, { value: 'HIGH', label: 'High' } ] } }); component.data.data = { ...component.data.data, biasAnnotations: [ { id: 'a1', severity: 'LOW' }, { id: 'a2', severity: 'HIGH', behavioralProbe: { activationMode: 'PROMPT_DIRECTIVE', instruction: 'do it' } } ] }; expect(component.biasAnnotationBadge).toEqual({ count: 2, hasExecutableProbe: true, maxSeverityLabel: 'High' }); }); it('preserves id, position and bias annotations during block regeneration', async () => { const blocks = TestBed.inject(BlocksService) as any; const replacement = vi.fn().mockResolvedValue(undefined); component.data.data = { ...component.data.data, id: 'old-id', typeName: 'LLMBlock', position: { x: 10, y: 20 }, biasAnnotations: [{ id: 'bias-1', category: 'DYNAMIC', issue: 'keep me' }], __needsServerCreate: true, replaceWithCreatedNode: replacement }; blocks.updateBlock.mockReturnValue(of({ id: 'generated-id', name: 'Generated', typeName: 'LLMBlock', inputs: [], outputs: [], specificConfiguration: {}, position: { x: 99, y: 99 }, biasAnnotations: [] })); (component as any).maybeCreateBlockOnServer(); await fixture.whenStable(); expect(replacement).toHaveBeenCalledWith(expect.objectContaining({ id: 'old-id', position: { x: 10, y: 20 }, biasAnnotations: [{ id: 'bias-1', category: 'DYNAMIC', issue: 'keep me' }] })); }); });