import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { cp, mkdtemp, mkdir, rm, writeFile } from 'fs/promises'; import { join } from 'path'; import { tmpdir } from 'os'; import { TEST_PLUGIN_FIXTURE_DIR, TEST_PLUGIN_ID } from '../e2e/helpers/plugin-api-test-fixture'; const { mockGetPath } = vi.hoisted(() => ({ mockGetPath: vi.fn() })); vi.mock('electron', () => ({ app: { getPath: mockGetPath } })); import { getLocalPluginsPath, listLocalPluginManifests } from './plugin-library'; describe('plugin-library', () => { let userDataPath: string; beforeEach(async () => { userDataPath = await mkdtemp(join(tmpdir(), 'metoyou-plugin-library-')); mockGetPath.mockReturnValue(userDataPath); }); afterEach(async () => { await rm(userDataPath, { recursive: true, force: true }); mockGetPath.mockReset(); }); it('creates and reports the local plugins folder', async () => { const pluginsPath = await getLocalPluginsPath(); const result = await listLocalPluginManifests(); expect(pluginsPath).toBe(join(userDataPath, 'plugins')); expect(result).toEqual({ errors: [], plugins: [], pluginsPath }); }); it('discovers immediate child plugin manifests and safe relative files', async () => { const pluginRoot = join(userDataPath, 'plugins', 'api-test-plugin'); await cp(TEST_PLUGIN_FIXTURE_DIR, pluginRoot, { recursive: true }); const result = await listLocalPluginManifests(); expect(result.errors).toEqual([]); expect(result.plugins).toHaveLength(1); expect(result.plugins[0]).toEqual(expect.objectContaining({ entrypointPath: join(pluginRoot, 'dist', 'main.js'), manifestPath: join(pluginRoot, 'toju-plugin.json'), pluginRoot, readmePath: join(pluginRoot, 'README.md') })); expect(result.plugins[0]?.manifest).toEqual(expect.objectContaining({ id: TEST_PLUGIN_ID })); }); it('reports invalid JSON and keeps scanning other plugins', async () => { const invalidRoot = join(userDataPath, 'plugins', 'invalid-plugin'); const validRoot = join(userDataPath, 'plugins', 'valid-plugin'); await mkdir(invalidRoot, { recursive: true }); await mkdir(validRoot, { recursive: true }); await writeFile(join(invalidRoot, 'plugin.json'), '{', 'utf8'); await writeFile(join(validRoot, 'plugin.json'), JSON.stringify({ apiVersion: '1.0.0', compatibility: { minimumTojuVersion: '1.0.0' }, description: 'Valid plugin', entrypoint: './main.js', id: 'valid.plugin', kind: 'client', schemaVersion: 1, title: 'Valid Plugin', version: '1.0.0' }), 'utf8'); const result = await listLocalPluginManifests(); expect(result.plugins.map((plugin) => plugin.pluginRoot)).toEqual([validRoot]); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toEqual(expect.objectContaining({ manifestPath: join(invalidRoot, 'plugin.json'), pluginRoot: invalidRoot })); }); it('does not resolve entrypoints outside the plugin folder', async () => { const pluginRoot = join(userDataPath, 'plugins', 'unsafe-plugin'); await mkdir(pluginRoot, { recursive: true }); await writeFile(join(userDataPath, 'plugins', 'outside.js'), 'export default {};', 'utf8'); await writeFile(join(pluginRoot, 'plugin.json'), JSON.stringify({ apiVersion: '1.0.0', compatibility: { minimumTojuVersion: '1.0.0' }, description: 'Unsafe plugin', entrypoint: '../outside.js', id: 'unsafe.plugin', kind: 'client', schemaVersion: 1, title: 'Unsafe Plugin', version: '1.0.0' }), 'utf8'); const result = await listLocalPluginManifests(); expect(result.plugins[0]?.entrypointPath).toBeUndefined(); }); });