diff --git a/apps/docs/CONTRIBUTING.md b/apps/docs/CONTRIBUTING.md index fac90e1aa4706..41cb00108b413 100644 --- a/apps/docs/CONTRIBUTING.md +++ b/apps/docs/CONTRIBUTING.md @@ -191,8 +191,7 @@ Choose the appropriate `type` for your admonition: - `danger`: Warn about actions or conditions that could cause data loss, expose sensitive data, or create another severe and difficult-to-reverse outcome. State the consequence first, and then explain how to avoid it. - `deprecation`: Identify a deprecated feature or behavior. State how the change affects the reader, and then provide the supported alternative or migration path. - `caution`: Warn about behavior that could cause bugs, failed operations, unexpected results, or serious inconvenience but doesn't rise to the severity of `danger`. -- `tip`: Share an optional shortcut, optimization, or best practice that helps the reader complete the task more effectively. The main procedure must still work without it. -- `note`: Highlight an important prerequisite, constraint, or clarification that doesn't represent a risk. If the information is essential to completing a step, include it in the procedure instead. +- `note`: Highlight an important prerequisite, constraint, clarification, or optional shortcut that doesn't represent a risk. If the information is essential to completing a step, include it in the procedure instead. ``` diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx index 12d02b26a597a..a310714f4a360 100644 --- a/apps/docs/app/contributing/content.mdx +++ b/apps/docs/app/contributing/content.mdx @@ -75,12 +75,11 @@ For content that requires progressive disclosure: ### Admonition -For extra information that doesn't fit into the main flow. There are 5 supported types of admonitions: +For extra information that doesn't fit into the main flow, you can use the following types of admonitions: - `danger` to warn the user about any missteps that could cause data loss or data leaks - `deprecation` to notify the user about features that are (or will soon be) deprecated - `caution` to warn about anything that could cause a bug or serious user inconvenience -- `tip` to point out helpful but optional actions - `note` for anything else Leave a blank line between the admonition tag and the contained content. This will prevent Prettier from trying to break the lines within the content. @@ -104,15 +103,9 @@ You should make sure you don't set this up wrong. - - -In certain cases, you may want to do this. - - - -Additional helpful information. +In certain cases, you may want to do this. ``` @@ -135,15 +128,9 @@ You should make sure you don't set this up wrong. - - -In certain cases, you may want to do this. - - - -Additional helpful information. +In certain cases, you may want to do this. diff --git a/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.test.ts b/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.test.ts new file mode 100644 index 0000000000000..52276842beb58 --- /dev/null +++ b/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.test.ts @@ -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') + }) +}) diff --git a/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.ts b/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.ts index b63abea02f323..1b81c0e9f0ad7 100644 --- a/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.ts +++ b/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.ts @@ -9,9 +9,25 @@ interface SkillSummary { installCommand: string } -async function getAiSkillsImpl(): Promise { +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 { 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) diff --git a/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.smoke.test.ts b/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.smoke.test.ts new file mode 100644 index 0000000000000..20be6d927b278 --- /dev/null +++ b/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.smoke.test.ts @@ -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) +}) diff --git a/apps/docs/components/WrapperDashboardIntegration.tsx b/apps/docs/components/WrapperDashboardIntegration.tsx index 4e423f93b2aa8..d48058c51b1c5 100644 --- a/apps/docs/components/WrapperDashboardIntegration.tsx +++ b/apps/docs/components/WrapperDashboardIntegration.tsx @@ -4,7 +4,7 @@ import { Admonition } from 'ui-patterns/Admonition' export function WrapperDashboardIntegration({ title, path }: { title: string; path: string }) { return ( - +

You can enable the {title} wrapper right from the Supabase dashboard.