From 57decf707f5ab7d584abda28253dd53969eceede Mon Sep 17 00:00:00 2001 From: Lijin Lan Date: Mon, 1 Dec 2025 23:03:31 +0100 Subject: [PATCH] add getBestPractice --- lib/getBestPractices.js | 123 +++++++++++++++++++++++++++++++++ lib/tools.js | 12 ++++ tests/getBestPractices.test.js | 61 ++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 lib/getBestPractices.js create mode 100644 tests/getBestPractices.test.js diff --git a/lib/getBestPractices.js b/lib/getBestPractices.js new file mode 100644 index 0000000..c190981 --- /dev/null +++ b/lib/getBestPractices.js @@ -0,0 +1,123 @@ +import fs from 'fs/promises' +import path from 'path' +import { fileURLToPath } from 'url' +import * as cheerio from 'cheerio' +import TurndownService from 'turndown' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const bestPracticesPath = path.join(__dirname, '..', 'best-practices.md') +const etagPath = path.join(__dirname, '..', 'best-practices.etag') + +async function downloadBestPractices() { + try { + await fs.mkdir(path.dirname(bestPracticesPath), { recursive: true }) + // Ensure the file exists before proceeding, creating it if necessary. + try { + await fs.access(bestPracticesPath) + } catch { + await fs.writeFile(bestPracticesPath, '') + } + + const urls = { + best: 'https://cap.cloud.sap/docs/about/best-practices', + bad: 'https://cap.cloud.sap/docs/about/bad-practices' + } + + let storedEtags = { best: null, bad: null } + try { + const etagContent = await fs.readFile(etagPath, 'utf-8') + const [bestEtag, badEtag] = etagContent.split('\n') + storedEtags = { best: bestEtag, bad: badEtag } + } catch { + // No stored ETag found + } + + const fetchOptions = (etag) => (etag ? { headers: { 'If-None-Match': etag } } : {}) + + const [bestPracticeResponse, badPracticeResponse] = await Promise.all([ + fetch(urls.best, fetchOptions(storedEtags.best)), + fetch(urls.bad, fetchOptions(storedEtags.bad)) + ]) + + if (bestPracticeResponse.status === 304 && badPracticeResponse.status === 304) { + // Content is unchanged for both + return + } + + const turndownService = new TurndownService() + + const processResponse = async (response) => { + if (response.status === 304) return { content: null, etag: response.headers.get('etag') } + if (!response.ok) throw new Error(`Failed to download from ${response.url}: ${response.status}`) + const html = await response.text() + const $ = cheerio.load(html) + + // Remove unwanted sections + $('aside, nav, footer').remove() + + // Only select headers and paragraphs + const filteredElements = $('h1, h2, h3, h4, h5, h6, p') + const filteredHtml = $('
').append(filteredElements).html() + + return { + content: turndownService.turndown(filteredHtml || ''), + etag: response.headers.get('etag') + } + } + + const [bestResult, badResult] = await Promise.all([ + processResponse(bestPracticeResponse), + processResponse(badPracticeResponse) + ]) + + let bestPracticesMd = bestResult.content + if (!bestPracticesMd) { + const cachedContent = await fs.readFile(bestPracticesPath, 'utf-8') + bestPracticesMd = cachedContent.split('# Things to Avoid')[0] + } + + let badPracticesMd = badResult.content + if (!badPracticesMd) { + const cachedContent = await fs.readFile(bestPracticesPath, 'utf-8') + const sections = cachedContent.split('# Things to Avoid') + badPracticesMd = sections.length > 1 ? sections[1] : '' + } + + const combinedMarkdown = `# Best Practices\n\n${bestPracticesMd}\n\n# Things to Avoid\n\n${badPracticesMd}` + await fs.writeFile(bestPracticesPath, combinedMarkdown) + + const newBestEtag = bestResult.etag || storedEtags.best + const newBadEtag = badResult.etag || storedEtags.bad + if (newBestEtag && newBadEtag) { + await fs.writeFile(etagPath, `${newBestEtag}\n${newBadEtag}`) + } + } catch (error) { + console.error('Error downloading best practices:', error) + try { + await fs.access(bestPracticesPath) + } catch (e) { + throw new Error('Failed to fetch best practices and no cached version is available.') + } + } +} + +export default async function getBestPractices({ filePath } = {}) { + // Always download and read the official best practices first. + await downloadBestPractices() + const officialContent = await fs.readFile(bestPracticesPath, 'utf-8') + + // If no custom file path is provided, just return the official content. + if (!filePath) { + return officialContent + } + + // If a custom file path is provided, read it and append its content. + try { + const customContent = await fs.readFile(filePath, 'utf-8') + // Combine the official and custom content with a clear separator. + return `${officialContent}\n\n---\n\n# Custom Guidelines\n\n${customContent}` + } catch (error) { + console.error(`Error reading custom guidelines file: ${error.message}`) + throw new Error(`Could not read the file at ${filePath}. Please ensure the path is correct and the file exists.`) + } +} diff --git a/lib/tools.js b/lib/tools.js index 6057477..cb4a125 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -2,8 +2,20 @@ import { z } from 'zod' import getModel from './getModel.js' import fuzzyTopN from './fuzzyTopN.js' import searchMarkdownDocs from './searchMarkdownDocs.js' +import getBestPractices from './getBestPractices.js' const tools = { + get_best_practices: { + title: 'Get CDS Best Practices to examine your code', + description: + 'Returns the complete official documentation on proven best practices for CDS. Optionally, a file path to a custom markdown file can be provided, and its content will be appended to the official guidelines.', + inputSchema: { + filePath: z.string().optional().describe('Optional path to a custom markdown file for project-specific guidelines.') + }, + handler: async ({ filePath }) => { + return await getBestPractices({ filePath }) + } + }, search_model: { title: 'Search for CDS definitions', description: diff --git a/tests/getBestPractices.test.js b/tests/getBestPractices.test.js new file mode 100644 index 0000000..f60731b --- /dev/null +++ b/tests/getBestPractices.test.js @@ -0,0 +1,61 @@ +// Node.js test runner (test) for lib/getBestPractices.js +import getBestPractices from '../lib/getBestPractices.js' +import assert from 'node:assert' +import { test, mock } from 'node:test' +import fs from 'fs/promises' +import path from 'path' + +test.describe('getBestPractices', () => { + test('should fetch and return best practices', async () => { + // Mock fetch to avoid actual network requests + global.fetch = mock.fn(async () => { + return { + ok: true, + status: 200, + headers: new Map([['etag', 'W/"12345"']]), + text: async () => ` + + +
...
+

Proven Best Practices

+
+

This is a best practice.

+
    +
  • Use this
  • +
  • Not that
  • +
+
+ + + ` + } + }) + + // Mock fs.writeFile to prevent writing to disk + mock.method(fs, 'writeFile', async () => {}) + + const result = await getBestPractices() + + assert(typeof result === 'string', 'Result should be a string') + assert(result.includes('This is a best practice.'), 'Result should contain best practice text') + assert(result.includes('* Use this'), 'Result should contain list items') + }) + + test('should use cached version on fetch failure', async () => { + // Mock fetch to simulate a failure + global.fetch = mock.fn(async () => { + return { + ok: false, + status: 500 + } + }) + + const cachedContent = '## Proven Best Practices\n\nThis is the cached content.' + // Mock fs.readFile to return cached content + mock.method(fs, 'readFile', async () => cachedContent) + + const result = await getBestPractices() + + assert.equal(result, cachedContent, 'Should return cached content on failure') + }) +})