Show the view-skill button on ungrouped array rows too

The node draws array rows from two near-identical template blocks - one
inside a group fieldset, one for ungrouped fields - and the button was
added to only the first. An LLM node renders its Skills list through the
second, so the button never appeared where it was actually wanted.

The test that missed this only exercised the component class, so it adds
a rendering test covering both blocks, and clears the stored user between
service tests: left behind, it made a sign-in a no-op transition and the
cache-reset tests failed intermittently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-20 16:08:54 +02:00
parent 87f6e2564b
commit 9802f12203
4 changed files with 67 additions and 0 deletions

View File

@ -53,6 +53,10 @@ describe('FlowsService', () => {
createNewFlow: vi.fn()
};
// See the same line in projects.spec.ts: a user left in storage by an earlier test would make
// the sign-in below a no-op transition, and the cache would have no reason to reset.
localStorage.removeItem(Authorization.USER_STORAGE_KEY);
TestBed.configureTestingModule({});
service = TestBed.inject(FlowsService);
service.flowsCallService = callServiceSpy as unknown as FlowsCallServiceBase;

View File

@ -43,6 +43,10 @@ describe('ProjectsService', () => {
deleteProject: vi.fn()
};
// Authorization restores the signed-in user from storage when it is constructed, and storage
// outlives a TestBed. Left over, it makes "bob signs in" a no-op transition in the test below.
localStorage.removeItem(Authorization.USER_STORAGE_KEY);
TestBed.configureTestingModule({});
service = TestBed.inject(ProjectsService);
service.projectsCallService = callServiceSpy as unknown as ProjectsCallServiceBase;

View File

@ -548,6 +548,16 @@
<div class="llm-array-item">
<span class="llm-array-item-summary">{{ entry.summary }}</span>
<div class="llm-array-item-actions">
@if (entry.skillId; as skillId) {
<button
type="button"
class="llm-edit-btn"
title="View skill content"
(pointerdown)="$event.stopPropagation()"
(click)="viewSkillContent(skillId, $event)">
<i class="bi bi-eye"></i>
</button>
}
@if (!isReadonly) {
<button
type="button"

View File

@ -415,6 +415,23 @@ describe('GenericNodeComponent', () => {
expect(definitions).toEqual([expect.objectContaining({ path: 'skills', skillIdProperty: 'skillId' })]);
});
it('finds it through a $ref, which is the shape the server actually sends', () => {
// Regression: SkillBinding is used by exactly one block type, so the bundler leaves it in
// this schema's own `definitions` and the array references it by $ref rather than inlining
// it - unlike the synthetic schema in the test above.
const component = fixture.componentInstance as any;
const definitions = component.buildArrayFieldDefinitions({
type: 'object',
definitions: { SkillBinding: skillItemSchema },
properties: {
skills: { type: 'array', items: { $ref: '#/definitions/SkillBinding' } }
}
});
expect(definitions).toEqual([expect.objectContaining({ path: 'skills', skillIdProperty: 'skillId' })]);
});
it('leaves skillIdProperty null for an array field with no skills-catalog property', () => {
const component = fixture.componentInstance as any;
@ -440,6 +457,38 @@ describe('GenericNodeComponent', () => {
expect(items).toEqual([expect.objectContaining({ index: 0, skillId: 'mcp-context-economy' })]);
});
it('offers the button in every place the node renders an array row', () => {
// Regression: the node draws array rows from two near-identical template blocks - one for
// fields inside a group fieldset, one for ungrouped fields - and the button was first added
// to only one of them, so it never appeared on an LLM node's ungrouped Skills list.
const component = fixture.componentInstance as any;
const row = { index: 0, summary: 'mcp-context-economy', skillId: 'mcp-context-economy' };
const arrayField = { path: 'skills', label: 'Skills', items: [row] };
// The node recomputes its display model from the (absent) schema on every check, which would
// wipe the rows this test is here to render.
component.refreshParameterFields = () => {};
component.schemaReady = true;
component.parameterDisplaySections = [
{
key: 'grouped',
group: {
key: 'grouped',
legend: 'Group',
items: [{ path: 'skills', field: null, richContentField: null, arrayField, optionalGroupField: null }]
},
item: null
},
{
key: 'ungrouped',
group: null,
item: { path: 'skills', field: null, richContentField: null, arrayField, optionalGroupField: null }
}
];
component.cdr.detectChanges();
const buttons = fixture.nativeElement.querySelectorAll('[title="View skill content"]');
expect(buttons.length).toBe(2);
});
it('fetches the skill definition and opens it read-only', async () => {
const component = fixture.componentInstance as any;
const httpMock = TestBed.inject(HttpTestingController);