diff --git a/packages/cli/src/adapter-registry.test.ts b/packages/cli/src/adapter-registry.test.ts new file mode 100644 index 00000000..7f2010c6 --- /dev/null +++ b/packages/cli/src/adapter-registry.test.ts @@ -0,0 +1,103 @@ +/** + * Registry regression tests — ensure the CATEGORIES list in adapter-registry.ts + * stays aligned with the packages// filesystem layout. + * + * Closes #219. + * + * Run: pnpm exec vitest run packages/cli/src/adapter-registry.test.ts + */ +import { describe, it, expect } from 'vitest'; +import { readdirSync, statSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { CATEGORIES } from './adapter-registry.js'; + +// Resolve monorepo root relative to this file (packages/cli/src → ../../..) +const MONOREPO_ROOT = resolve(new URL(import.meta.url).pathname, '../../..'); + +/** + * Read the immediate sub-directories of packages// + * Returns an empty array if the directory doesn't exist (graceful for CI + * environments where a category may not have been populated yet). + */ +function listPackageDirs(categoryId: string): string[] { + const dir = join(MONOREPO_ROOT, 'packages', categoryId); + try { + return readdirSync(dir).filter((name) => { + try { + return statSync(join(dir, name)).isDirectory(); + } catch { + return false; + } + }); + } catch { + return []; + } +} + +describe('adapter-registry CATEGORIES', () => { + it('has no duplicate adapter names within any category', () => { + for (const cat of CATEGORIES) { + const seen = new Set(); + const dupes: string[] = []; + for (const a of cat.adapters) { + if (seen.has(a)) dupes.push(a); + seen.add(a); + } + expect(dupes, `category "${cat.id}" has duplicate adapters: ${dupes.join(', ')}`).toHaveLength(0); + } + }); + + it('has no duplicate category ids', () => { + const ids = CATEGORIES.map((c) => c.id); + const unique = new Set(ids); + expect(ids.length).toBe(unique.size); + }); + + it('targets registry contains all directories under packages/targets/', () => { + const targetsCat = CATEGORIES.find((c) => c.id === 'targets'); + expect(targetsCat, 'targets category must exist in CATEGORIES').toBeDefined(); + + const fsDirs = listPackageDirs('targets'); + if (fsDirs.length === 0) { + // Running in a shallow CI checkout — skip FS check but still pass. + return; + } + + const registeredSet = new Set(targetsCat!.adapters); + const missing = fsDirs.filter((d) => !registeredSet.has(d)); + + expect( + missing, + `packages/targets/ has directories not in CATEGORIES.targets.adapters: ${missing.join(', ')}\n` + + 'Update adapter-registry.ts to add them.', + ).toHaveLength(0); + }); + + it('targets registry has no adapters that no longer exist on disk', () => { + const targetsCat = CATEGORIES.find((c) => c.id === 'targets'); + expect(targetsCat).toBeDefined(); + + const fsDirs = new Set(listPackageDirs('targets')); + if (fsDirs.size === 0) return; // shallow clone — skip + + const phantom = targetsCat!.adapters.filter((a) => !fsDirs.has(a)); + expect( + phantom, + `CATEGORIES.targets.adapters references packages that do not exist on disk: ${phantom.join(', ')}\n` + + 'Remove them from adapter-registry.ts or add the missing packages.', + ).toHaveLength(0); + }); + + it('every category has a non-empty pkgPrefix and description', () => { + for (const cat of CATEGORIES) { + expect(cat.pkgPrefix, `category "${cat.id}" must have pkgPrefix`).toBeTruthy(); + expect(cat.description, `category "${cat.id}" must have description`).toBeTruthy(); + } + }); + + it('every category has at least one adapter', () => { + for (const cat of CATEGORIES) { + expect(cat.adapters.length, `category "${cat.id}" must list at least one adapter`).toBeGreaterThan(0); + } + }); +}); diff --git a/packages/cli/src/adapter-registry.ts b/packages/cli/src/adapter-registry.ts index 8fd480a6..5d208f26 100644 --- a/packages/cli/src/adapter-registry.ts +++ b/packages/cli/src/adapter-registry.ts @@ -121,7 +121,43 @@ export const CATEGORIES: readonly AdapterCategory[] = [ id: 'targets', pkgPrefix: '@profullstack/sh1pt-target', description: 'Distribution targets — stores, registries, CDNs, deploy platforms', - adapters: ['browser-chrome', 'chat-discord', 'chat-signal', 'chat-slack', 'chat-telegram', 'chat-whatsapp', 'console-steam', 'deploy-denodeploy', 'deploy-firebase', 'deploy-fly', 'deploy-netlify', 'deploy-railway', 'deploy-render', 'deploy-vercel', 'deploy-workers', 'desktop-linux', 'desktop-mac', 'desktop-steamos', 'desktop-win', 'mobile-expo', 'mobile-ios', 'pkg-aube', 'pkg-cdn', 'pkg-deno', 'pkg-docker', 'pkg-fdroid', 'pkg-ghpackages', 'pkg-homebrew', 'pkg-jsr', 'pkg-npm', 'tv-androidtv', 'tv-firetv', 'tv-roku', 'tv-tvos', 'tv-webos', 'web-static', 'xr-meta-quest', 'xr-pico', 'xr-sidequest', 'xr-steamvr', 'xr-visionos', 'xr-webxr'], + // Synced with packages/targets/* directories. Run `ls packages/targets/` to regenerate. + adapters: [ + // Browsers + 'browser-chrome', 'browser-edge', 'browser-firefox', 'browser-safari', + // Chat / messaging platforms + 'chat-discord', 'chat-signal', 'chat-slack', 'chat-telegram', 'chat-whatsapp', + // Consoles + 'console-steam', + // Deploy / hosting + 'deploy-coolify', 'deploy-denodeploy', 'deploy-firebase', 'deploy-fly', + 'deploy-lambda', 'deploy-netlify', 'deploy-railway', 'deploy-render', + 'deploy-vercel', 'deploy-workers', + // Desktop + 'desktop-linux', 'desktop-mac', 'desktop-steamos', 'desktop-win', + // Specialty runtimes + 'exe-dev', + // Mobile + 'mobile-android', 'mobile-expo', 'mobile-ios', + // Payment targets + 'payment-adyen', 'payment-coinpay', 'payment-paypal', 'payment-square', 'payment-stripe', + // Packages & registries + 'pkg-apt', 'pkg-aube', 'pkg-aur', 'pkg-cdn', 'pkg-deno', 'pkg-docker', + 'pkg-fdroid', 'pkg-flatpak', 'pkg-ghpackages', 'pkg-homebrew', 'pkg-jsr', + 'pkg-nix', 'pkg-npm', 'pkg-perry', 'pkg-scoop', 'pkg-snap', 'pkg-winget', + // Editor / IDE plugins + 'plugin-jetbrains', 'plugin-vscode', + // QA targets + 'qa-geisterhand', + // SDK registries + 'sdk-pypi', + // Smart TV + 'tv-androidtv', 'tv-firetv', 'tv-roku', 'tv-tvos', 'tv-webos', + // Web + 'web-static', + // XR / VR / AR + 'xr-meta-quest', 'xr-pico', 'xr-sidequest', 'xr-steamvr', 'xr-visionos', 'xr-webxr', + ], }, { id: 'vcs',