import { afterAll, describe, expect, it } from 'bun:test'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { discoverPlugins } from './discover'; // Discovery against a real tree, because every assertion here is about the FILESYSTEM being the // declaration. Mocking `existsSync` would test the mock. const root = mkdtempSync(join(tmpdir(), 'officer-plugins-')); afterAll(() => rmSync(root, { recursive: true, force: true })); const MANIFEST = (over = '') => `export const manifest = { publisher: 'officerdev', version: '1.0.0', platform: '>=1.0.0', label: 'X', summary: 'x', icon: 'Network', color: '#fff', permissions: [], ${over} };`; function plant(appName: string, files: Record) { const dir = join(root, appName); for (const [rel, body] of Object.entries(files)) { const full = join(dir, rel); mkdirSync(join(full, '..'), { recursive: true }); writeFileSync(full, body); } return dir; } plant('full', { 'manifest.ts': MANIFEST(), 'api/router.ts': 'export const router = {};', 'db/schema.ts': 'export const t = {};', 'sidecar/index.ts': 'export {};', 'web/panels.ts': 'export const appRegistryMetas = [];', 'web/layout.ts': 'export const defaultLayout = {};', }); plant('bare', { 'manifest.ts': MANIFEST() }); plant('nodeish', { 'manifest.ts': MANIFEST(), 'sidecar/index.mjs': 'export {};' }); plant('broken', { 'manifest.ts': 'export const manifest = { publisher: 1 };' }); // A frontend that is not a workspace. The rule: every plugin route renders a Workspace with at least one // panel, enforced here rather than by review — a `web/` directory without both files is refused. plant('halfweb', { 'manifest.ts': MANIFEST(), 'web/panels.ts': 'export const appRegistryMetas = [];' }); plant('nomanifest', { 'api/router.ts': 'export const router = {};' }); plant('_scratch', { 'manifest.ts': MANIFEST() }); describe('discoverPlugins', () => { it('reads what the tree declares, and nothing more', async () => { const { plugins } = await discoverPlugins(root); const full = plugins.find((p) => p.appName === 'full')!; expect(full.api).toContain('api/router.ts'); expect(full.schema).toContain('db/schema.ts'); expect(full.sidecar).toEqual({ script: join(root, 'full/sidecar/index.ts'), runtime: 'bun' }); expect(full.web?.panels).toContain('web/panels.ts'); expect(full.web?.layout).toContain('web/layout.ts'); }); it('a manifest alone is a valid plugin — every other part is optional', async () => { const { plugins } = await discoverPlugins(root); const bare = plugins.find((p) => p.appName === 'bare')!; expect([bare.api, bare.schema, bare.sidecar, bare.web]).toEqual([null, null, null, null]); }); // The runtime is the extension, not a field, so it cannot contradict the file it describes. it('reads the runtime off the extension', async () => { const { plugins } = await discoverPlugins(root); expect(plugins.find((p) => p.appName === 'nodeish')!.sidecar?.runtime).toBe('node'); }); it('takes the app name from the directory, so it cannot disagree with where the code sits', async () => { const { plugins } = await discoverPlugins(root); expect(plugins.map((p) => p.appName)).toContain('full'); }); // The property that matters most: one bad plugin must not take the platform down, or hide the good // ones beside it. "Broken, and here is why" is renderable; a failed boot is only greppable. it('collects broken plugins instead of throwing', async () => { const { plugins, broken } = await discoverPlugins(root); expect(broken.map((b) => b.appName).sort()).toEqual(['broken', 'halfweb', 'nomanifest']); expect(broken.find((b) => b.appName === 'nomanifest')!.error).toContain('no manifest.ts'); expect(plugins.length).toBeGreaterThan(0); }); // The exclusionary rule, and the reason it is a shape rather than a policy: a plugin cannot ship a // frontend that is not a workspace, because it never renders the screen. it('refuses a web/ directory that is not panels AND a layout', async () => { const { broken } = await discoverPlugins(root); const halfweb = broken.find((b) => b.appName === 'halfweb')!; expect(halfweb.error).toContain('web/layout.ts'); expect(halfweb.error).toContain('contribute panels and a layout, not a screen'); }); it('skips underscore and dot directories, which are scratch space', async () => { const { plugins, broken } = await discoverPlugins(root); expect([...plugins, ...broken].map((p) => p.appName)).not.toContain('_scratch'); }); it('is empty, not an error, when there is no plugins directory at all', async () => { expect(await discoverPlugins(join(root, 'does-not-exist'))).toEqual({ plugins: [], broken: [] }); }); });