diff --git a/.changeset/pin-reference-snapshot.md b/.changeset/pin-reference-snapshot.md new file mode 100644 index 0000000..89f670b --- /dev/null +++ b/.changeset/pin-reference-snapshot.md @@ -0,0 +1,5 @@ +--- +'datocms': patch +--- + +Pin reference downloads to an immutable revision so documentation remains available when upstream files move. diff --git a/packages/cli/src/commands/agents/reference.ts b/packages/cli/src/commands/agents/reference.ts index 00235af..12c36fe 100644 --- a/packages/cli/src/commands/agents/reference.ts +++ b/packages/cli/src/commands/agents/reference.ts @@ -2,7 +2,7 @@ import { BaseCommand, oclif } from '@datocms/cli-utils'; const NAME_PATTERN = /^[a-z0-9-]+$/; const SKILLS_BASE_URL = - 'https://raw.githubusercontent.com/datocms/agent-skills/refs/heads/master/skills/'; + 'https://raw.githubusercontent.com/datocms/agent-skills/86c533b74c2913e590797eb0b2622d2cdfe5cf68/skills/'; export default class Command extends BaseCommand { static hidden = true; diff --git a/packages/cli/test/commands/agents-reference.test.ts b/packages/cli/test/commands/agents-reference.test.ts new file mode 100644 index 0000000..758861b --- /dev/null +++ b/packages/cli/test/commands/agents-reference.test.ts @@ -0,0 +1,84 @@ +import { runCommand } from '@oclif/test'; +import { expect } from 'chai'; + +const REFERENCE_URL = + 'https://raw.githubusercontent.com/datocms/agent-skills/86c533b74c2913e590797eb0b2622d2cdfe5cf68/skills/datocms-cma/references/editing-records.md'; + +describe('agents:reference', () => { + const originalFetch = globalThis.fetch; + let requestedUrls: string[]; + let respond: () => Promise; + + beforeEach(() => { + requestedUrls = []; + respond = async () => new Response('# Editing records'); + globalThis.fetch = async (input) => { + requestedUrls.push(String(input)); + return respond(); + }; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + const run = () => + runCommand(['agents:reference', 'datocms-cma', 'editing-records'], { + root: process.cwd(), + }); + + it('fetches the pinned document and adds a missing trailing newline', async () => { + const { stdout, error } = await run(); + expect(error).to.equal(undefined); + expect(requestedUrls).to.deep.equal([REFERENCE_URL]); + expect(stdout).to.equal('# Editing records\n'); + }); + + it('preserves a document that already ends in a newline', async () => { + respond = async () => new Response('# Editing records\n'); + const { stdout, error } = await run(); + expect(error).to.equal(undefined); + expect(stdout).to.equal('# Editing records\n'); + }); + + it('rejects path traversal before fetching', async () => { + for (const args of [ + ['../datocms-cma', 'editing-records'], + ['datocms-cma', '../editing-records'], + ]) { + const { error } = await runCommand(['agents:reference', ...args], { + root: process.cwd(), + }); + expect(error?.message).to.match(/Invalid (skill|reference) name/); + } + expect(requestedUrls).to.deep.equal([]); + }); + + it('reports a missing reference with its pinned URL', async () => { + respond = async () => new Response('', { status: 404 }); + const { stdout, error } = await run(); + expect(stdout).to.equal(''); + expect(error?.message).to.equal( + `Reference "editing-records" not found at ${REFERENCE_URL}`, + ); + }); + + it('preserves HTTP error details', async () => { + respond = async () => + new Response('', { status: 503, statusText: 'Service Unavailable' }); + const { error } = await run(); + expect(error?.message).to.equal( + 'Failed to fetch reference "editing-records": HTTP 503 Service Unavailable', + ); + }); + + it('preserves network error details', async () => { + respond = async () => { + throw new Error('Connection failed'); + }; + const { error } = await run(); + expect(error?.message).to.equal( + 'Failed to fetch reference "editing-records": Connection failed', + ); + }); +}); diff --git a/packages/cli/test/commands/help.test.ts b/packages/cli/test/commands/help.test.ts index bbb9a56..68bf9cb 100644 --- a/packages/cli/test/commands/help.test.ts +++ b/packages/cli/test/commands/help.test.ts @@ -1,7 +1,12 @@ import { runCommand } from '@oclif/test'; import { expect } from 'chai'; -describe('datocms', async () => { - const { stdout } = await runCommand('help'); - expect(stdout).to.contain('plugins'); +describe('datocms', () => { + it('shows the available commands', async () => { + const { stdout, error } = await runCommand('help', { + root: process.cwd(), + }); + expect(error).to.equal(undefined); + expect(stdout).to.contain('plugins'); + }); });