import { describe, expect, test, afterEach } from 'bun:test'; import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { affordanceDirFor, affordanceSourceFor, publishAffordances, unpublishAffordances } from './publish'; import { AFFORDANCES_DIR } from './load'; import type { DiscoveredPlugin } from '../plugins/manifest'; // The failure that matters is not a copy going wrong — it is uninstall taking somebody else's files with // it. Every first-party plugin publishes under one `officerdev/` directory, so removing the wrong level // would strip them all and nobody would notice until an agent stopped knowing something. const root = join(tmpdir(), `aff-pub-${process.pid}`); const plugin = (appName: string, publisher = 'officerdev'): DiscoveredPlugin => ({ appName, dir: join(root, appName), manifest: { publisher } }) as DiscoveredPlugin; const withAffordance = (appName: string, publisher = 'officerdev') => { const p = plugin(appName, publisher); mkdirSync(affordanceSourceFor(p), { recursive: true }); writeFileSync(join(affordanceSourceFor(p), `${appName}.md`), `---\nname: ${appName}\n---\n\nbody\n`); return p; }; afterEach(() => { rmSync(root, { recursive: true, force: true }); for (const pub of ['officerdev', 'alice']) rmSync(join(AFFORDANCES_DIR, pub), { recursive: true, force: true }); }); describe('publishing', () => { test('lands under /, mirroring mountPrefix', () => { const p = withAffordance('test-gitea'); expect(affordanceDirFor(p)).toBe(join(AFFORDANCES_DIR, 'officerdev', 'test-gitea')); }); test('copies what the plugin ships and counts it', async () => { expect(await publishAffordances(withAffordance('test-gitea'))).toBe(1); }); test('a plugin shipping none is not a failure — most ship none', async () => { const bare = plugin('test-bare'); mkdirSync(bare.dir, { recursive: true }); expect(await publishAffordances(bare)).toBe(0); }); test('re-publishing overwrites rather than erroring, so re-install works', async () => { const p = withAffordance('test-gitea'); expect(await publishAffordances(p)).toBe(1); expect(await publishAffordances(p)).toBe(1); }); test('two publishers with the same plugin name do not collide', async () => { await publishAffordances(withAffordance('test-gitea', 'officerdev')); await publishAffordances(withAffordance('test-gitea', 'alice')); expect(existsSync(join(AFFORDANCES_DIR, 'officerdev', 'test-gitea', 'test-gitea.md'))).toBe(true); expect(existsSync(join(AFFORDANCES_DIR, 'alice', 'test-gitea', 'test-gitea.md'))).toBe(true); }); }); describe('unpublishing', () => { test('removes the plugin', async () => { const p = withAffordance('test-gitea'); await publishAffordances(p); await unpublishAffordances(p); expect(existsSync(affordanceDirFor(p))).toBe(false); }); test('leaves the PUBLISHER alone — the thing that would silently strip every sibling', async () => { const a = withAffordance('test-gitea'); const b = withAffordance('test-music'); await publishAffordances(a); await publishAffordances(b); await unpublishAffordances(a); expect(existsSync(affordanceDirFor(b))).toBe(true); }); test('is safe when nothing was ever published', async () => { await unpublishAffordances(plugin('test-never')); }); });