Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/__tests__/__snapshots__/options.defaults.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ exports[`options defaults should return specific properties: defaults 1`] = `
"https://patternfly.org",
"https://github.com/patternfly",
"https://raw.githubusercontent.com/patternfly",
"https://github.com/project-felt",
"https://raw.githubusercontent.com/project-felt",
],
"urlWhitelistProtocols": [
"http",
Expand Down
40 changes: 40 additions & 0 deletions src/__tests__/aiGuidelines.catalog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expandGithubDirectoryInCatalog } from '../catalog.expandGithubDirectory';
import type { PatternFlyMcpDocsCatalogSource } from '../docs.embedded';
import docsJson from '../docs.json';
import { installGithubFetchMock, restoreNativeFetch } from './fixtures/mockGithubFetch';

const FELT_RAW_PREFIX = 'https://raw.githubusercontent.com/project-felt/ai-guidelines/';

const EXPECTED_FILENAMES = [
'ai-design-principles.md',
'animation.md',
'chatbot-avatars.md',
'color.md',
'iconography.md',
'legal-requirements.md',
'transparency-notices.md'
];

describe('AiGuidelines docs.json catalog', () => {
beforeAll(() => installGithubFetchMock());
afterAll(() => restoreNativeFetch());

it('should expand directory stub into seven project-felt entries', async () => {
const expanded = await expandGithubDirectoryInCatalog(docsJson as PatternFlyMcpDocsCatalogSource);
const entries = expanded.docs.AiGuidelines;

expect(entries).toBeDefined();
expect(entries).toHaveLength(EXPECTED_FILENAMES.length);
expect(expanded.meta.totalDocs).toBe(331);

for (const entry of entries ?? []) {
expect(entry.path.startsWith(FELT_RAW_PREFIX)).toBe(true);
expect(entry.path.endsWith('.md')).toBe(true);
expect(entry.version).toBe('v6');
}

const basenames = (entries ?? []).map(docEntry => docEntry.path.split('/').pop());

expect(basenames.sort()).toEqual([...EXPECTED_FILENAMES].sort());
});
});
37 changes: 37 additions & 0 deletions src/__tests__/catalog.expandGithubDirectory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expandGithubDirectoryInCatalog } from '../catalog.expandGithubDirectory';
import type { PatternFlyMcpDocsCatalogSource } from '../docs.embedded';

describe('expandGithubDirectoryInCatalog', () => {
it('should reject expansion for repos outside the allowlist', async () => {
const catalog: PatternFlyMcpDocsCatalogSource = {
version: '1',
generated: new Date().toISOString(),
meta: {
totalEntries: 1,
totalDocs: 1,
source: 'test'
},
docs: {
BadExpand: [
{
displayName: 'Invalid',
description: 'Should fail',
pathSlug: 'invalid',
section: 'guidelines',
category: 'ai',
source: 'github',
version: 'v6',
expandGithubDirectory: {
owner: 'disallowed',
repo: 'vendor',
ref: 'main',
directoryPath: 'docs'
}
}
]
}
};

await expect(expandGithubDirectoryInCatalog(catalog)).rejects.toThrow(/not enabled/);
});
});
26 changes: 17 additions & 9 deletions src/__tests__/docs.json.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { distance } from 'fastest-levenshtein';
import docsJson from '../docs.json';
import type { PatternFlyMcpDocsCatalogDocImport } from '../docs.embedded';

describe('docs.json', () => {
it('should have a valid top-level generated timestamp (ISO date string)', () => {
Expand All @@ -23,21 +24,28 @@ describe('docs.json', () => {
let totalDocs = 0;

Object.entries(docsJson.docs).forEach(([key, entries]) => {
entries.forEach(entry => {
entries.forEach((entry: PatternFlyMcpDocsCatalogDocImport) => {
totalDocs += 1;
allLinks.add(entry.path);
const path = entry.path;
const linkKey = 'expandGithubDirectory' in entry
? `catalog-expand:${entry.expandGithubDirectory.owner}/${entry.expandGithubDirectory.repo}:${entry.expandGithubDirectory.directoryPath}@${entry.expandGithubDirectory.ref}`
: entry.path;

if (!linkMap.has(path)) {
linkMap.set(path, []);
allLinks.add(linkKey);

if (!linkMap.has(linkKey)) {
linkMap.set(linkKey, []);
}

linkMap.get(path)?.push(`${key}: ${entry.displayName} (${entry.category})`);
linkMap.get(linkKey)?.push(`${key}: ${entry.displayName} (${entry.category})`);

if (entry.path.includes('documentation:')) {
if ('expandGithubDirectory' in entry) {
baseHashes.add(entry.expandGithubDirectory.ref);
} else if (entry.path.includes('documentation:')) {
baseHashes.add('documentation:');
} else if (/^https:\/\/raw\.githubusercontent\.com\/patternfly\/[a-zA-Z0-9-]+\//.test(entry.path)) {
baseHashes.add(entry.path.split(/\/patternfly\/[a-zA-Z0-9-]+\//)[1]?.split('/')[0]);
} else if (/^https:\/\/raw\.githubusercontent\.com\/project-felt\/[a-zA-Z0-9-]+\//.test(entry.path)) {
baseHashes.add(entry.path.split(/\/project-felt\/[a-zA-Z0-9-]+\//)[1]?.split('/')[0]);
} else {
baseHashes.add(`new-resource-${entry.path}`);
}
Expand Down Expand Up @@ -67,9 +75,9 @@ describe('docs.json', () => {
* If you are updating `docs.json` with an agent confirm altering this value is acceptable
* when you open your MR/PR. You may be asked to change your git hash to one of the
* existing values and keep this value the same.
* 1 (v6 org) + 1 (v6 react) + 1 (v5 org) + 1 (codemods) + 1 (ai-helpers) + 1 (patternfly-cli)
* 1 (v6 org) + 1 (v6 react) + 1 (v5 org) + 1 (codemods) + 1 (ai-helpers) + 1 (patternfly-cli) + 1 (project-felt/ai-guidelines)
*/
expect(baseHashes.size).toBe(6);
expect(baseHashes.size).toBe(7);

/**
* Confirm total docs count matches metadata
Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/fixtures/githubAiGuidelinesDirectory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** Mirrors GitHub Contents API listing for project-felt/ai-guidelines/content (unit tests + jest.setup). */
export const GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE = [
{ name: 'ai-design-principles.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/ai-design-principles.md' },
{ name: 'animation.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/animation.md' },
{ name: 'chatbot-avatars.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/chatbot-avatars.md' },
{ name: 'color.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/color.md' },
{ name: 'iconography.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/iconography.md' },
{ name: 'legal-requirements.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/legal-requirements.md' },
{ name: 'transparency-notices.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/transparency-notices.md' }
];
36 changes: 36 additions & 0 deletions src/__tests__/fixtures/mockGithubFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE } from './githubAiGuidelinesDirectory';

const nativeFetch = globalThis.fetch;

/**
* Mock fetch to intercept GitHub Contents API calls for ai-guidelines.
* Call in beforeAll/beforeEach of tests that trigger catalog expansion.
*/
export const installGithubFetchMock = () => {

Check warning on line 9 in src/__tests__/fixtures/mockGithubFetch.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Multiple named export declarations; consolidate all named exports into a single export declaration

Check warning on line 9 in src/__tests__/fixtures/mockGithubFetch.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Multiple named export declarations; consolidate all named exports into a single export declaration

Check warning on line 9 in src/__tests__/fixtures/mockGithubFetch.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Multiple named export declarations; consolidate all named exports into a single export declaration
globalThis.fetch = jest.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === 'string'
? input
: input instanceof URL
? input.href
: 'url' in input && typeof input.url === 'string'
? input.url
: String(input);

if (url.includes('api.github.com/repos/project-felt/ai-guidelines/contents/content')) {
return new Response(JSON.stringify(GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}

if (typeof nativeFetch === 'function') {
return nativeFetch(input as RequestInfo, init);
}

throw new Error(`Unhandled fetch in test: ${url}`);
}) as typeof fetch;
};

export const restoreNativeFetch = () => {

Check warning on line 34 in src/__tests__/fixtures/mockGithubFetch.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Multiple named export declarations; consolidate all named exports into a single export declaration

Check warning on line 34 in src/__tests__/fixtures/mockGithubFetch.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Multiple named export declarations; consolidate all named exports into a single export declaration

Check warning on line 34 in src/__tests__/fixtures/mockGithubFetch.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Multiple named export declarations; consolidate all named exports into a single export declaration
globalThis.fetch = nativeFetch;
};
38 changes: 38 additions & 0 deletions src/__tests__/patternFly.search.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { filterPatternFly, searchPatternFly } from '../patternFly.search';
import { installGithubFetchMock, restoreNativeFetch } from './fixtures/mockGithubFetch';

describe('filterPatternFly', () => {
it.each([
Expand Down Expand Up @@ -115,3 +116,40 @@ describe('searchPatternFly', () => {
expect(totalPotentialMatches).toBeGreaterThanOrEqual(totalResults);
});
});

describe('searchPatternFly, AiGuidelines catalog', () => {
beforeAll(() => installGithubFetchMock());
afterAll(() => restoreNativeFetch());
const feltPath = (paths: { path?: string }[]) =>
paths.some(entry => typeof entry.path === 'string' && entry.path.includes('project-felt/ai-guidelines'));

it('should exact-match resource aiguidelines for query AiGuidelines', async () => {
const { exactMatches, searchResults } = await searchPatternFly('AiGuidelines');

const byName = new Map(searchResults.map(result => [result.name, result]));
const aiGuidelinesResource = byName.get('aiguidelines');

expect(aiGuidelinesResource).toBeDefined();
expect(exactMatches.some(match => match.name === 'aiguidelines')).toBe(true);
expect(feltPath(aiGuidelinesResource!.entries)).toBe(true);
expect(aiGuidelinesResource!.entries.length).toBeGreaterThanOrEqual(7);
});

it('should exact-match resource aiguidelines for query aiguidelines', async () => {
const { exactMatches } = await searchPatternFly('aiguidelines');

expect(exactMatches.some(match => match.name === 'aiguidelines')).toBe(true);
});

it.each([
{ description: 'transparency wording', search: 'transparency notices' },
{ description: 'chatbot avatars display name fragment', search: 'chatbot avatars' },
{ description: 'legal requirements description fragment', search: 'legal review' }
])('should surface project-felt AiGuidelines URLs when searching $description', async ({ search }) => {
const { searchResults } = await searchPatternFly(search);

const hitsFelt = searchResults.some(result => feltPath(result.entries));

expect(hitsFelt).toBe(true);
});
});
55 changes: 47 additions & 8 deletions src/__tests__/resource.patternFlyDocsTemplate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ import {
resourceCallback
} from '../resource.patternFlyDocsTemplate';
import { isPlainObject } from '../server.helpers';
import { GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE } from './fixtures/githubAiGuidelinesDirectory';

const getFetchUrl = (input: RequestInfo | URL): string => {
if (typeof input === 'string') {
return input;
}

if (input instanceof URL) {
return input.href;
}

if (typeof input === 'object' && input !== null && 'url' in input && typeof (input as Request).url === 'string') {
return (input as Request).url;
}

return String(input);
};

jest.mock('node:fs/promises', () => ({
...jest.requireActual('node:fs/promises'),
Expand Down Expand Up @@ -74,10 +91,21 @@ describe('resourceCallback', () => {
const mockContent = `Mock content for ${variables.name}`;

mockReadFile.mockResolvedValue(mockContent);
mockFetch.mockResolvedValue({
ok: true,
text: () => mockContent
} as any);
mockFetch.mockImplementation(async (input: RequestInfo | URL, _init?: RequestInit) => {
const urlStr = getFetchUrl(input);

if (urlStr.includes('api.github.com/repos/project-felt/ai-guidelines')) {
return new Response(JSON.stringify(GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}

return {
ok: true,
text: async () => mockContent
} as Response;
});

const result = await resourceCallback(
{ href: `patternfly://docs/${variables.version}/${variables.name}` } as any,
Expand Down Expand Up @@ -149,10 +177,21 @@ describe('resourceCallback', () => {
const mockContent = `Mock content for ${variables.name}`;

mockReadFile.mockResolvedValue(mockContent);
mockFetch.mockResolvedValue({
ok: true,
text: () => mockContent
} as any);
mockFetch.mockImplementation(async (input: RequestInfo | URL, _init?: RequestInit) => {
const urlStr = getFetchUrl(input);

if (urlStr.includes('api.github.com/repos/project-felt/ai-guidelines')) {
return new Response(JSON.stringify(GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}

return {
ok: true,
text: async () => mockContent
} as Response;
});

const uri = new URL('patternfly://docs/test');

Expand Down
Loading
Loading