mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
tests: add regression test for skills list (#47819)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Adds regression tests for something we noticed today. AI skills weren't being loaded on https://supabase.com/docs/guides/ai-tools/ai-skills, so while we fixed it, we wanted to make sure we could identify this faster. ## What is the current behavior? Less tests. AI skills loading and not ## What is the new behavior? Two tests, no new workflows — both ride existing CI: - **Unit test** (`AiSkills.utils.test.ts`) mocks GitHub, checks the parsing/shaping logic (dir filtering, frontmatter, install command, sorting, empty→fallback). Runs on every PR. - **Smoke test** (`AiSkillsIndex.smoke.test.ts`) hits the live page and asserts the skills table actually rendered. Runs in the daily docs smoke job, and can be pointed at any environment via `DOCS_SMOKE_URL`. Small supporting change: `getAiSkillsImpl` is now exported so the unit test can call it directly. ## Additional Context - Fixes https://linear.app/supabase/issue/AI-915/skills-docs-page-fails-to-load-available-skills <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Added coverage for AI Skills loading, including directory filtering, metadata parsing, install command generation, fallback descriptions, and error handling. * Added a smoke test confirming the AI Skills documentation page loads successfully and displays install commands. * **Refactor** * Made AI Skills loading functionality accessible for direct testing while preserving existing behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
66658ab339
commit
01958e520f
4 files changed
+107
-2
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getAiSkillsImpl } from './AiSkills.utils'
|
||||
|
||||
const { readFileMock } = vi.hoisted(() => ({
|
||||
readFileMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
readFile: readFileMock,
|
||||
}))
|
||||
|
||||
describe('getAiSkillsImpl', () => {
|
||||
beforeEach(() => {
|
||||
readFileMock.mockReset()
|
||||
})
|
||||
|
||||
it('parses the generated skills JSON', async () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'supabase',
|
||||
description: 'Work with Supabase',
|
||||
installCommand: 'npx skills add supabase/agent-skills --skill supabase',
|
||||
},
|
||||
{
|
||||
name: 'supabase-postgres-best-practices',
|
||||
description: 'Postgres best practices',
|
||||
installCommand:
|
||||
'npx skills add supabase/agent-skills --skill supabase-postgres-best-practices',
|
||||
},
|
||||
]
|
||||
readFileMock.mockResolvedValue(JSON.stringify(skills))
|
||||
|
||||
await expect(getAiSkillsImpl()).resolves.toEqual(skills)
|
||||
})
|
||||
|
||||
it('propagates errors reading the generated file', async () => {
|
||||
readFileMock.mockRejectedValue(new Error('ENOENT'))
|
||||
|
||||
await expect(getAiSkillsImpl()).rejects.toThrow('ENOENT')
|
||||
})
|
||||
|
||||
it('throws when the generated JSON is not an array', async () => {
|
||||
readFileMock.mockResolvedValue(JSON.stringify({ name: 'supabase' }))
|
||||
|
||||
await expect(getAiSkillsImpl()).rejects.toThrow('Malformed ai-skills.json')
|
||||
})
|
||||
|
||||
it('throws when an entry is missing required string fields', async () => {
|
||||
readFileMock.mockResolvedValue(JSON.stringify([{ name: 'supabase', description: 'x' }]))
|
||||
|
||||
await expect(getAiSkillsImpl()).rejects.toThrow('Malformed ai-skills.json')
|
||||
})
|
||||
})
|
||||
@@ -9,9 +9,25 @@ interface SkillSummary {
|
||||
installCommand: string
|
||||
}
|
||||
|
||||
async function getAiSkillsImpl(): Promise<SkillSummary[]> {
|
||||
function isSkillSummary(value: unknown): value is SkillSummary {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
typeof (value as SkillSummary).name === 'string' &&
|
||||
typeof (value as SkillSummary).description === 'string' &&
|
||||
typeof (value as SkillSummary).installCommand === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
export async function getAiSkillsImpl(): Promise<SkillSummary[]> {
|
||||
const raw = await readFile(join(GENERATED_DIRECTORY, 'ai-skills.json'), 'utf-8')
|
||||
return JSON.parse(raw)
|
||||
const parsed: unknown = JSON.parse(raw)
|
||||
|
||||
if (!Array.isArray(parsed) || !parsed.every(isSkillSummary)) {
|
||||
throw new Error('Malformed ai-skills.json: expected an array of SkillSummary objects')
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
export const getAiSkills = cache(getAiSkillsImpl)
|
||||
@@ -0,0 +1,31 @@
|
||||
// Guards against the docs GitHub App losing access to supabase/agent-skills,
|
||||
// which 404s silently and renders an empty table.
|
||||
import { load } from 'cheerio'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
// Override to target a preview deploy or localhost; defaults to production.
|
||||
const DOCS_BASE_URL = process.env.DOCS_SMOKE_URL ?? 'https://supabase.com'
|
||||
const AI_SKILLS_URL = `${DOCS_BASE_URL.replace(/\/$/, '')}/docs/guides/ai-tools/ai-skills`
|
||||
|
||||
describe('prod smoke test: agent skills load on the AI Skills page', () => {
|
||||
it('renders the skills table with at least one skill and no fallback', async () => {
|
||||
const result = await fetch(AI_SKILLS_URL, { signal: AbortSignal.timeout(30_000) })
|
||||
expect(result.status).toBe(200)
|
||||
|
||||
const html = await result.text()
|
||||
expect(html).not.toContain('Unable to load AI skills at the moment.')
|
||||
|
||||
// The install command only appears on real skill rows.
|
||||
const $ = load(html)
|
||||
const installCommands = $('code')
|
||||
.map(function () {
|
||||
return $(this).text()
|
||||
})
|
||||
.get()
|
||||
.filter((text) => text.startsWith('npx skills add supabase/agent-skills --skill '))
|
||||
|
||||
expect(installCommands.length).toBeGreaterThan(0)
|
||||
// Test timeout must outlive the fetch abort so the network error surfaces
|
||||
// instead of a generic vitest timeout.
|
||||
}, 45_000)
|
||||
})
|
||||
@@ -16,6 +16,10 @@
|
||||
"build:markdown": {
|
||||
"outputs": ["public/markdown/**", "public/markdown/manifest.json"],
|
||||
},
|
||||
"test": {
|
||||
// Lets the smoke tests target a preview deploy or localhost instead of production.
|
||||
"env": ["DOCS_SMOKE_URL"],
|
||||
},
|
||||
"build": {
|
||||
"dependsOn": ["^build", "codegen:examples", "codegen:references", "build:markdown"],
|
||||
"env": [
|
||||
|
||||
Reference in new issue
Block a user