Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"deprioritized",
"gatekeep",
"ized",
"jaccard",
"llms",
"localappdata",
"midrun",
Expand Down
24 changes: 24 additions & 0 deletions src/__tests__/patternFly.getResources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,30 @@ describe('getPatternFlyMcpResources', () => {
expect(Array.from(result.hashIndex.keys()).some(key => /[A-Z]/.test(key))).toBe(false);
});

it('should index uriGroupId, unparameterized doc URIs, and schema URIs in uriIndex', async () => {
const result = await getPatternFlyMcpResources();
const buttonResource = result.resources.get('button');

expect(buttonResource).toBeDefined();
expect(buttonResource?.groupId).toBeDefined();

// Confirm uriIndex contains groupId and keys
const groupId = buttonResource?.groupId || '';

expect(result.uriIndex.get(`patternfly://docs/${groupId}`.toLowerCase())).toBe('button');
expect(result.uriIndex.get(`patternfly://schemas/${groupId}`.toLowerCase())).toBe('button');
expect(result.uriIndex.get('patternfly://docs/button')).toBe('button');
expect(result.uriIndex.get('patternfly://schemas/button')).toBe('button');

// Confirm hashIndex maps groupId
expect(result.hashIndex.get(groupId.toLowerCase())).toBe('button');

// Confirm entries have uriGroupId
const entries = buttonResource?.entries || [];

expect(entries.every(entry => (entry as any).uriGroupId.includes(groupId))).toBe(true);
});

it('should generate unique hash IDs for pathless component entries', async () => {
const result = await getPatternFlyMcpResources();
const entries = Array.from(result.resources.values()).flatMap(resource => resource.entries);
Expand Down
104 changes: 94 additions & 10 deletions src/__tests__/patternFly.search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('calculateRelevance', () => {
name: 'inline-alert-box',
entries: []
},
expected: 1
expected: 2
},
{
description: 'no match on name or displayNames',
Expand All @@ -98,7 +98,7 @@ describe('calculateRelevance', () => {
{ displayName: 'Primary Button' }
]
},
expected: 2
expected: 3
},
{
description: 'undefined entries',
Expand All @@ -107,7 +107,7 @@ describe('calculateRelevance', () => {
name: 'card',
entries: undefined
},
expected: 2
expected: 3
},
{
description: 'missing or empty displayName entries',
Expand All @@ -119,7 +119,7 @@ describe('calculateRelevance', () => {
{ displayName: undefined }
]
},
expected: 2
expected: 3
}
])('should create a relevance score, $description', ({ query, result, expected }) => {
const relevance = calculateRelevance(result as any, query);
Expand Down Expand Up @@ -361,12 +361,19 @@ describe('dynamicFilterPatternFly', () => {
expectedNames: ['button', 'button', 'modal', 'card']
},
{
description: 'fallback to original when using a broad category',
description: 'fallback to original when using a broad category and defined maxResultsLimit',
searchQuery: 'view',
filters: {},
options: {},
options: { maxResultsLimit: 1 },
expectedNames: ['button', 'button', 'modal', 'card']
},
{
description: 'do not fallback to original when using a broad category with dynamic maxResultsLimit',
searchQuery: 'view',
filters: {},
options: {},
expectedNames: ['modal', 'card']
},
{
description: 'skip iterative filter if useExistingFilters is true and filter is already set',
searchQuery: 'modal',
Expand Down Expand Up @@ -397,14 +404,28 @@ describe('dynamicFilterPatternFly', () => {
description: 'name filter wins and aborts sibling section scans',
searchQuery: 'modal',
filters: {},
options: { searchFilters: ['name', 'section'] as const },
options: { searchFilters: ['name', 'section'], maxResultsLimit: 1 },
expectedNames: ['modal']
},
{
description: 'name filter wins and aborts sibling section scans, dynamic maxResultsLimit',
searchQuery: 'modal',
filters: {},
options: { searchFilters: ['name', 'section'] },
expectedNames: ['modal']
},
{
description: 'section filter wins and aborts sibling name scans',
searchQuery: 'layouts',
filters: {},
options: { searchFilters: ['section', 'name'] as const },
options: { searchFilters: ['section', 'name'], maxResultsLimit: 1 },
expectedNames: ['card']
},
{
description: 'section filter wins and aborts sibling name scans, dynamic maxResultsLimit',
searchQuery: 'layouts',
filters: {},
options: { searchFilters: ['section', 'name'] },
expectedNames: ['card']
}
])('should wire parallel filter passes with shared signal when $description', async ({
Expand Down Expand Up @@ -468,7 +489,7 @@ describe('dynamicFilterPatternFly', () => {
'modal',
{},
mockResources as any,
{ searchFilters: oversizedFilters as (keyof FilterPatternFlyFilters)[] }
{ searchFilters: oversizedFilters as (keyof FilterPatternFlyFilters)[], maxResultsLimit: 1 }
);

expect(result.byEntry.map(entry => entry.name)).toEqual(['modal']);
Expand Down Expand Up @@ -598,6 +619,30 @@ describe('searchPatternFly', () => {
['patternfly://docs/button', new Map([['v6', ['button']], ['v5', ['button']]])],
['patternfly://docs/modal', new Map([['v6', ['modal']]])]
]),
uriIndex: new Map([
['patternfly://docs/button', 'button'],
['patternfly://docs/button?version=v6', 'button'],
['patternfly://docs/button?version=v5', 'button'],
['patternfly://docs/modal', 'modal'],
['patternfly://docs/modal?version=v6', 'modal'],
['patternfly://docs/btn-group', 'button'],
['patternfly://docs/mdl-group', 'modal'],
['patternfly://docs/btn-v6-hash', 'button'],
['patternfly://docs/btn-v5-hash', 'button'],
['patternfly://docs/mdl-v6-hash', 'modal'],
['patternfly://schemas/button', 'button'],
['patternfly://schemas/button?version=v6', 'button'],
['patternfly://schemas/btn-group', 'button'],
['patternfly://schemas/mdl-group', 'modal']
]),
hashIndex: new Map([
['btn-group', 'button'],
['mdl-group', 'modal'],
['btn-v6-hash', 'button'],
['btn-v5-hash', 'button'],
['mdl-v6-hash', 'modal']
]),
pathIndex: new Map(),
latestVersion: 'v6'
};

Expand Down Expand Up @@ -647,6 +692,45 @@ describe('searchPatternFly', () => {
expectedName: 'modal',
expectedType: 'exact'
},
{
description: 'patternfly://docs/{groupId} with filter',
search: 'patternfly://docs/btn-group',
options: { dynamicFilter: true },
expectedLength: 1,
expectedName: 'button',
expectedType: 'exact'
},
{
description: 'patternfly://schemas/{groupId} with filter',
search: 'patternfly://schemas/btn-group',
options: { dynamicFilter: true },
expectedLength: 1,
expectedName: 'button',
expectedType: 'exact'
},
{
description: 'unparameterized doc URI patternfly://docs/button',
search: 'patternfly://docs/button',
options: { dynamicFilter: true },
expectedLength: 1,
expectedName: 'button',
expectedType: 'exact'
},
{
description: 'unparameterized schema URI patternfly://schemas/button',
search: 'patternfly://schemas/button',
options: { dynamicFilter: true },
expectedLength: 1,
expectedName: 'button',
expectedType: 'exact'
},
{
description: 'groupId bare hash without filter',
search: 'btn-group',
expectedLength: 1,
expectedName: 'button',
expectedType: 'exact'
},
{
description: 'hash entry id with filter',
search: 'btn-v6-hash',
Expand All @@ -659,7 +743,7 @@ describe('searchPatternFly', () => {
description: 'hash entry id without filter',
search: 'btn-v6-hash',
options: { dynamicFilter: false },
expectedLength: 2,
expectedLength: 1,
expectedName: 'button',
expectedType: 'exact'
},
Expand Down
100 changes: 100 additions & 0 deletions src/__tests__/server.helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isPlainObject,
isPromise,
isReferenceLike,
isShaHexLike,
isUrl,
isUrlObject,
isPath,
Expand Down Expand Up @@ -708,6 +709,105 @@ describe('isReferenceLike', () => {
});
});

describe('isShaHexLike', () => {
it.each([
{
description: 'lowercase 40-char SHA-1',
value: '96b45dfa08a88ff87015bf35852ce1bce2085de5',
expected: true
},
{
description: 'uppercase 40-char SHA-1',
value: '96B45DFA08A88FF87015BF35852CE1BCE2085DE5',
expected: true
},
{
description: 'mixed-case 40-char SHA-1 with whitespace',
value: ' 96b45dfa08A88ff87015bf35852ce1bce2085de5 ',
expected: true
},
{
description: 'valid 8-char SHA prefix',
value: '96b45dfa',
expected: true
},
{
description: 'valid 12-char SHA prefix',
value: '96b45dfa08a8',
expected: true
},
{
description: 'valid 39-char SHA prefix',
value: '96b45dfa08a88ff87015bf35852ce1bce2085de',
expected: true
},
{
description: 'string below default minLength, 7 chars',
value: '96b45df',
expected: false
},
{
description: 'string above default maxLength, 41 chars',
value: '96b45dfa08a88ff87015bf35852ce1bce2085de5a',
expected: false
},
{
description: 'non-hex characters in string',
value: '96b45dfa08a88ff87015bf35852ce1bce2085xyz',
expected: false
},
{
description: 'string with leading non-hex character',
value: 'g6b45dfa08a88ff87015bf35852ce1bce2085de5',
expected: false
},
{
description: 'empty string',
value: '',
expected: false
},
{
description: 'whitespace string',
value: ' ',
expected: false
},
{
description: 'null value',
value: null,
expected: false
},
{
description: 'undefined value',
value: undefined,
expected: false
},
{
description: 'number value',
value: 12345678,
expected: false
},
{
description: 'object value',
value: { sha: '96b45dfa' },
expected: false
},
{
description: 'custom minLength matching, short prefix',
value: '96b4',
options: { minLength: 4 },
expected: true
},
{
description: 'custom maxLength rejecting a longer value',
value: '96b45dfa08a8',
options: { maxLength: 10 },
expected: false
}
])('should check if value is SHA hex-like, $description', ({ value, options, expected }) => {
expect(isShaHexLike(value, options)).toBe(expected);
});
});

describe('isUrl', () => {
it.each([
{ description: 'http', url: 'http://example.com' },
Expand Down
2 changes: 1 addition & 1 deletion src/docs.filterWords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @note It's tempting to remove category and section names from this list, don't. Instead, the search
* should be leveraging filters which allow for "section" and "category" specifically.
*/
const INDEX_BLOCKLIST_WORDS = ['patternfly', 'component', 'components', 'documentation', 'example', 'examples', 'view'];
const INDEX_BLOCKLIST_WORDS = ['patternfly', 'component', 'components', 'documentation', 'example', 'examples', 'type', 'types', 'view'];

/**
* Technical terms and acronyms that should be exempt from length and noise filtering.
Expand Down
Loading
Loading