humainflow-web/src/app/shared/nodes/generic-node/generic-node.spec.ts

200 lines
6.6 KiB
TypeScript

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<GenericNodeComponent>;
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<string, unknown>;
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' }]
}));
});
});