import { describe, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, rmSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { EXTRACTED_PLUGINS, seedExtractedPlugins } from "./seed"; import type { AppConfig } from "../types/config"; /** * Extracting a built-in plugin is the one change here that can quietly take a * working feature away from an existing user, so the seeding rules are worth * pinning: restore it once, never fight a deliberate removal, and never record * a failure as done — being offline at startup is common, and marking it seeded * would drop the plugin permanently. */ function config(overrides: Partial = {}): AppConfig { return { disabledPlugins: [], seededPlugins: [], ...overrides } as AppConfig; } async function withPluginsDir(setup: (dir: string) => Promise | T): Promise { const dir = mkdtempSync(join(tmpdir(), "gloom-seed-")); try { return await setup(dir); } finally { rmSync(dir, { recursive: true, force: true }); } } describe("seedExtractedPlugins", () => { test("installs an extracted plugin the user has not seen", async () => { const installs: string[] = []; const result = await withPluginsDir((dir) => seedExtractedPlugins(config(), async (ref) => { installs.push(ref); }, dir)); expect(installs).toEqual(EXTRACTED_PLUGINS.map((entry) => entry.repo)); expect(result.installed).toEqual(EXTRACTED_PLUGINS.map((entry) => entry.id)); expect(result.seeded).toEqual(EXTRACTED_PLUGINS.map((entry) => entry.id)); }); test("does nothing once a plugin has been seeded", async () => { const installs: string[] = []; const seeded = EXTRACTED_PLUGINS.map((entry) => entry.id); await withPluginsDir((dir) => seedExtractedPlugins(config({ seededPlugins: seeded }), async (ref) => { installs.push(ref); }, dir)); expect(installs).toEqual([]); }); test("respects a plugin the user disabled before the move", async () => { const installs: string[] = []; const result = await withPluginsDir((dir) => seedExtractedPlugins( config({ disabledPlugins: ["substack"] }), async (ref) => { installs.push(ref); }, dir, )); expect(installs).not.toContain("gloom-sh/gloom-substack"); // Recorded, so we stop asking rather than retrying every launch. expect(result.seeded).toContain("substack"); }); test("records a plugin that is already installed without reinstalling it", async () => { const installs: string[] = []; const result = await withPluginsDir((pluginsDir) => { mkdirSync(join(pluginsDir, "gloom-substack"), { recursive: true }); return seedExtractedPlugins(config(), async (ref) => { installs.push(ref); }, pluginsDir); }); expect(installs).not.toContain("gloom-sh/gloom-substack"); expect(result.seeded).toContain("substack"); }); /** * The plugin repositories are renaming to `gloom-`, so anyone who installed * one before the rename has it in a directory under the old name. Seeding by * the new name alone would clone a second copy, and the loader then refuses * both for sharing an id. */ test("finds an install made before the repository was renamed", async () => { const installs: string[] = []; const result = await withPluginsDir((pluginsDir) => { mkdirSync(join(pluginsDir, "gloomberb-substack"), { recursive: true }); return seedExtractedPlugins(config(), async (ref) => { installs.push(ref); }, pluginsDir); }); expect(installs).not.toContain("gloom-sh/gloom-substack"); expect(result.seeded).toContain("substack"); }); /** * Fear & Greed, Market Halts, Market Heatmap and the IPO calendar were * modules inside Market Overview and Macro, so turning that plugin off was * the only way to turn them off. Restoring them as their own plugins would * put panes back that the user had removed. */ test("keeps an extracted module disabled when the plugin that contained it was", async () => { for (const [owner, extracted] of [ ["market-overview", ["fear-greed", "market-halts", "market-heatmap"]], ["macro", ["ipo-calendar"]], ] as const) { const installs: string[] = []; const result = await withPluginsDir((dir) => seedExtractedPlugins( config({ disabledPlugins: [owner] }), async (ref) => { installs.push(ref); }, dir, )); for (const id of extracted) { const entry = EXTRACTED_PLUGINS.find((candidate) => candidate.id === id)!; expect(installs).not.toContain(entry.repo); // Recorded, so it is not offered again on every launch. expect(result.seeded).toContain(id); } } }); test("keeps TV disabled when its former Macro owner was disabled", async () => { for (const owner of ["macro", "macro-tv", "tv"]) { const installs: string[] = []; const result = await withPluginsDir((dir) => seedExtractedPlugins( config({ disabledPlugins: [owner] }), async (ref) => { installs.push(ref); }, dir, )); expect(installs).not.toContain("gloom-sh/gloom-tv"); expect(result.seeded).toContain("tv"); } }); test("retries next launch instead of recording a failed install", async () => { const result = await withPluginsDir((dir) => seedExtractedPlugins(config(), async () => { throw new Error("offline"); }, dir)); expect(result.failed).toContain("substack"); expect(result.seeded).not.toContain("substack"); }); });