import { describe, expect, test, afterAll } from 'bun:test'; import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { PLUGINS_DIR } from './discover'; import { pullPlugin, enablePlugin } from './update'; // The REFUSALS are what this test is for. A pull that works is easy to believe; a pull that quietly // discarded somebody's uncommitted work is the failure that matters, and every plugin directory on a // developer's machine is a live checkout — this repository's own plugins are edited in place. const tmp = (name: string) => join(PLUGINS_DIR, name); const made: string[] = []; const scratch = (name: string) => { const dir = tmp(name); mkdirSync(dir, { recursive: true }); made.push(dir); return dir; }; afterAll(() => made.forEach((d) => rmSync(d, { recursive: true, force: true }))); describe('pullPlugin refuses rather than guesses', () => { test('a plugin that is not on this machine', async () => { const r = await pullPlugin('definitely-not-installed-xyz'); expect(r.ok).toBe(false); expect(r.error).toContain('not on this machine'); }); test('a directory that is not a git checkout — copied in by hand, or bundled', async () => { scratch('test-update-nogit'); const r = await pullPlugin('test-update-nogit'); expect(r.ok).toBe(false); expect(r.error).toContain('not a git checkout'); // Not phrased as a git error: there is nothing wrong here, there is simply nothing to pull. expect(r.updated).toBe(false); }); test('a checkout with uncommitted work — the one that would destroy something', async () => { const dir = scratch('test-update-dirty'); Bun.spawnSync(['git', 'init', '-q'], { cwd: dir }); writeFileSync(join(dir, 'manifest.ts'), 'export const manifest = {};\n'); const r = await pullPlugin('test-update-dirty'); expect(r.ok).toBe(false); expect(r.error).toContain('uncommitted'); expect(r.updated).toBe(false); }); test('a refusal never reports having updated anything', async () => { for (const name of ['definitely-not-installed-xyz', 'test-update-nogit', 'test-update-dirty']) { const r = await pullPlugin(name); expect(r.ok).toBe(false); expect(r.updated).toBe(false); } }); }); // ── Enabling must survive a refusal to pull ── // // `enablePlugin` refreshes the checkout before turning a plugin on, so that a plugin which sat disabled // for a week does not come back running the code it was switched off with. The risk in that is obvious: // the pull is refused for reasons that say nothing about whether the plugin can run — a dirty tree, an // unreachable forge, a directory that was copied in rather than cloned — and enabling is meant to be the // reversible half of a pair with disable. // // So this asserts the pull refusal is REPORTED and then stepped past. The enable itself still fails here, // because a scratch directory has no install row — and that is the point: the failure that comes back is // about being installed, not about git, which is only possible if the refusal did not short-circuit. describe('enablePlugin steps past a pull it cannot do', () => { test('a directory that is not a git checkout still reaches the enable', async () => { // Named without the word "git" in it, so the last assertion below is about the error and not about // the fixture's own name. scratch('test-enable-plain'); const r = await enablePlugin('test-enable-plain'); expect(r.steps.some((s) => s.includes('not refreshed') && s.includes('not a git checkout'))).toBe(true); expect(r.error).toContain('not installed'); expect(r.error).not.toContain('git'); }); });