import { readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { parseManifest, validateManifest } from './manifest'; const fixturesDir = path.join( path.dirname(fileURLToPath(import.meta.url)), '..', 'fixtures', 'manifests', ); function loadFixture(kind: 'valid' | 'invalid', name: string): unknown { return JSON.parse(readFileSync(path.join(fixturesDir, kind, name), 'utf8')); } function listFixtures(kind: 'valid' | 'invalid'): string[] { return readdirSync(path.join(fixturesDir, kind)) .filter((f) => f.endsWith('.json')) .sort(); } describe('manifest validation — valid fixtures', () => { const valid = listFixtures('valid'); it('ships at least the reference-plugin manifests', () => { expect(valid).toEqual( expect.arrayContaining(['mermaid.json', 'section-styles-basic.json', 'toc.json']), ); }); it.each(valid)('%s validates cleanly', (name) => { const result = validateManifest(loadFixture('valid', name)); expect(result.issues).toEqual([]); expect(result.success).toBe(true); expect(result.manifest?.id).toBeTruthy(); }); it('parseManifest returns a typed manifest', () => { const manifest = parseManifest(loadFixture('valid', 'toc.json')); expect(manifest.kind).toBe('code'); expect(manifest.extensionPoints[0]?.title.de).toBe('Inhaltsverzeichnis'); // permissions default is applied even though every fixture sets it. expect(manifest.permissions).toContain('readCurrentPage'); }); it('defaults permissions to an empty array when omitted', () => { const manifest = parseManifest(loadFixture('valid', 'section-styles-basic.json')); expect(manifest.permissions).toEqual([]); }); }); describe('manifest validation — invalid fixtures', () => { const invalid = listFixtures('invalid'); it('provides at least ten invalid variants', () => { expect(invalid.length).toBeGreaterThanOrEqual(10); }); it.each(invalid)('%s is rejected with issues', (name) => { const result = validateManifest(loadFixture('invalid', name)); expect(result.success).toBe(false); expect(result.manifest).toBeUndefined(); expect(result.issues.length).toBeGreaterThan(0); for (const issue of result.issues) { expect(issue.path).toBeTruthy(); expect(issue.message).toBeTruthy(); } }); // Spot-check that the messages are actionable and point at the right field. const expectations: Record = { '01-missing-id.json': { path: 'id', message: /required/i }, '02-bad-id.json': { path: 'id', message: /lowercase slug/i }, '03-missing-name.json': { path: 'name', message: /required/i }, '04-bad-version.json': { path: 'version', message: /semantic version/i }, '05-bad-api-version.json': { path: 'apiVersion', message: /major version/i }, '06-bad-kind.json': { path: 'kind', message: /.+/ }, '07-empty-extension-points.json': { path: 'extensionPoints', message: /.+/ }, '08-kind-mismatch.json': { path: 'extensionPoints.0.type', message: /allows only .* extension points/i, }, '09-duplicate-extension-point-id.json': { path: 'extensionPoints.1.id', message: /duplicate extension point id/i, }, '10-title-missing-de.json': { path: 'extensionPoints.0.title.de', message: /required/i }, '11-unknown-permission.json': { path: 'permissions.1', message: /.+/ }, '12-bad-fallback.json': { path: 'fallback.type', message: /discriminator/i }, '13-section-style-with-permissions.json': { path: 'permissions', message: /must not declare permissions/i, }, '14-unknown-field.json': { path: /.+/, message: /unrecognized|extraField/i }, }; it.each(Object.entries(expectations))( '%s yields an actionable message', (name, { path: expectedPath, message }) => { const result = validateManifest(loadFixture('invalid', name)); const matching = result.issues.find( (issue) => (expectedPath instanceof RegExp ? expectedPath.test(issue.path) : issue.path === expectedPath) && message.test(issue.message), ); expect( matching, `expected an issue at "${String(expectedPath)}" matching ${message} but got ${JSON.stringify( result.issues, )}`, ).toBeDefined(); }, ); });