Three times in one day — photos, soulseek, jellyfin — the same failure on the same machine: install reports success, the dock tile appears, the frontend rebuilds with the route in it, and every request 404s. The plugin never mounted, because setPluginPermissions refused its key as already core. That refusal was correct and useless. It happens at mount time, after the install has reported success, and its only trace is one line on stderr that nobody is reading. The cause is always the same: the key was a CORE permission until the extraction commit, so a server whose platform predates that commit still owns it. The fix is always git pull, and nothing in the product said so. installPlugin now checks the manifest's keys against the core registry before anything is written, and refuses with a message naming the colliding keys and telling the operator to pull the platform and restart. Placed with the other pre-flight checks, so it changes nothing that would need undoing. corePermissionCollisions is exported from the registry rather than reimplemented in install.ts — the core key set has one owner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
31 lines
1.5 KiB
TypeScript
31 lines
1.5 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import { corePermissionCollisions } from './registry';
|
|
|
|
// The failure this exists to prevent, in one sentence: a plugin whose permission key the platform still
|
|
// claims installs "successfully", shows a dock tile, rebuilds the frontend with its route — and 404s on
|
|
// every request, because `setPluginPermissions` refused the key at mount time and said so only on stderr.
|
|
//
|
|
// It caught photos, soulseek and jellyfin on the same machine inside a day. Each had been a CORE
|
|
// permission until its extraction commit, so any server whose platform predates that commit still owns
|
|
// the key. The remedy is always `git pull`, and nothing in the product ever said so.
|
|
|
|
describe('core permission collisions', () => {
|
|
test('names a key the platform still claims', () => {
|
|
// 'chat' is core and not deniable — the clearest case of something a plugin must never redefine.
|
|
expect(corePermissionCollisions(['chat'])).toEqual(['chat']);
|
|
});
|
|
|
|
test('an ordinary plugin key collides with nothing', () => {
|
|
expect(corePermissionCollisions(['soulseek', 'jellyfin', 'photos'])).toEqual([]);
|
|
});
|
|
|
|
test('reports every collision, not just the first — an install should fail once, naming all of them', () => {
|
|
const found = corePermissionCollisions(['chat', 'soulseek', 'terminal']);
|
|
expect(found.sort()).toEqual(['chat', 'terminal']);
|
|
});
|
|
|
|
test('nothing declared collides with nothing', () => {
|
|
expect(corePermissionCollisions([])).toEqual([]);
|
|
});
|
|
});
|