jellyfin, extracted into plugins/jellyfin — the platform half
Eighth extraction, second resurrection. Same shape as soulseek: hono.ts had the router import and its PROTECTED_MOUNTS entry commented out since 2026-08-13 and the permission reduced to api: [], so assertPermissionTotality was satisfied, the server booted clean, and every panel request 404'd. Also closes the same data hazard — jellyfin_servers was absent from the drizzle barrel, so bun db:push would have dropped it. No frontend call site needed repointing: mountPrefix derives /jellyfin from the appName, which is what the router already hardcoded. Test fixtures fixed properly rather than re-anchored again. Extracting jellyfin broke 18 tests, the FOURTH extraction in a row to break the same files — photos and transmission yesterday, slskd an hour ago, jellyfin now. installer.test.ts now builds literal CatalogueEntry fixtures and asks the catalogue nothing: it pins planSteps and runInstall, which are installer logic, and a service leaving for plugins/ says nothing about whether the installer still plans the right steps. catalogue.test.ts stays the place real entries are checked well-formed. members/catalogue/preflight re-anchored to memos, which has jellyfin's exact shape. The .prettierignore rule for the vendored hls.mjs moved into the plugin rather than being left behind naming a deleted path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,7 @@ describe('permissions it claims to back', () => {
|
||||
|
||||
describe('byId', () => {
|
||||
it('finds a known entry and returns undefined for anything else', () => {
|
||||
expect(byId('jellyfin')?.process).toBe('officer-jellyfin');
|
||||
expect(byId('memos')?.process).toBe('officer-memos');
|
||||
expect(byId('not-a-sidecar')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,21 +169,6 @@ export const CATALOGUE: CatalogueEntry[] = [
|
||||
// bitten twice. `permissionAvailability()` derives from `sidecar_installs`, which a plugin never gets a
|
||||
// row in, so a leftover entry would make the permission permanently `unavailable`, put `/photos` into
|
||||
// `deniedRoutes`, and withhold the dock tile — on a server where the plugin is installed and working.
|
||||
{
|
||||
id: 'jellyfin',
|
||||
ui: { name: 'Video', icon: 'Clapperboard', color: '#a855f7', rootRoute: '/jellyfin', routes: ['/jellyfin'] },
|
||||
process: 'officer-jellyfin',
|
||||
label: 'Jellyfin',
|
||||
summary: 'Films and shows, with a player that handles direct, HLS and progressive',
|
||||
members: 'accounts',
|
||||
modes: ['existing', 'provisioned'],
|
||||
permission: 'jellyfin',
|
||||
composeTemplate: 'jellyfin',
|
||||
existingFields: [
|
||||
{ key: 'url', label: 'Jellyfin URL', type: 'url', required: true, placeholder: 'https://jellyfin.example.com' },
|
||||
{ key: 'secret', label: 'Access token', type: 'secret', required: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'memos',
|
||||
ui: { name: 'Memos', icon: 'NotebookPen', color: '#eab308', rootRoute: '/memos', routes: ['/memos'] },
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { planSteps, runInstall, type InstallEffects, type StepName } from './installer';
|
||||
import { byId } from './catalogue';
|
||||
import { byId, type CatalogueEntry } from './catalogue';
|
||||
|
||||
// The whole install machine, tested without Docker, Postgres, PM2 or an Immich to talk to — which is
|
||||
// the reason the effects are injected. What is being pinned here is the behaviour that only shows up
|
||||
// when something goes wrong: resume, blocking, and not doing work twice.
|
||||
|
||||
const jellyfin = byId('jellyfin')!; // existing|provisioned, members: 'accounts'
|
||||
// A LITERAL, not `byId(...)`. This was `byId('slskd')` until slskd became `plugins/soulseek/`, and
|
||||
// before that the same assertion had to be re-pointed when photos and transmission left. No catalogue
|
||||
// entry today is both `provisioned` and `members: 'none'`, so there is nothing left to borrow — and
|
||||
// borrowing was the mistake anyway: this pins `planSteps`, which is installer logic, and it should not
|
||||
// break because an unrelated service was extracted. Shaped from jellyfin so the rest of the entry stays
|
||||
// realistic.
|
||||
const noMemberConcept = { ...byId('jellyfin')!, id: 'test-single-tenant', members: 'none' as const };
|
||||
// FIXTURES ARE LITERALS, not catalogue lookups.
|
||||
//
|
||||
// These pin `planSteps` and `runInstall` — installer LOGIC. Binding them to catalogue CONTENTS meant
|
||||
// every extraction broke them: photos and transmission on 2026-08-18, slskd on 2026-08-19, and jellyfin
|
||||
// in the same hour. Four times is enough. A service leaving for `plugins/` says nothing about whether
|
||||
// the installer still plans the right steps, so these no longer ask the catalogue anything.
|
||||
//
|
||||
// `catalogue.test.ts` is where real entries are asserted to be well-formed; that is the right place for
|
||||
// it, and it fails loudly if the shape below ever stops resembling one.
|
||||
const fullInstall: CatalogueEntry = {
|
||||
id: 'test-full',
|
||||
ui: { name: 'Test', icon: 'Box', color: '#000', rootRoute: '/test', routes: ['/test'] },
|
||||
process: 'officer-test',
|
||||
label: 'Test',
|
||||
summary: 'A service with accounts and both install modes',
|
||||
members: 'accounts',
|
||||
modes: ['existing', 'provisioned'],
|
||||
permission: 'test',
|
||||
composeTemplate: 'test',
|
||||
existingFields: [{ key: 'url', label: 'URL', type: 'url', required: true }],
|
||||
};
|
||||
const jellyfin = fullInstall; // was byId('jellyfin') until it became `plugins/jellyfin/`
|
||||
const noMemberConcept: CatalogueEntry = { ...fullInstall, id: 'test-single-tenant', members: 'none' };
|
||||
const configOnly = byId('notify')!; // config only
|
||||
|
||||
/** Records what was actually called, so a test can assert on absence as well as presence. */
|
||||
|
||||
@@ -10,7 +10,7 @@ const inviteOnly = { members: 'invite' } as CatalogueEntry;
|
||||
|
||||
describe('what adding a member would do', () => {
|
||||
it('predicts the outcome per service without contacting anything', () => {
|
||||
expect(plannedOutcome(byId('jellyfin')!)).toBe('provisioned');
|
||||
expect(plannedOutcome(byId('memos')!)).toBe('provisioned');
|
||||
expect(plannedOutcome(inviteOnly)).toBe('invited');
|
||||
expect(plannedOutcome(byId('vnc')!)).toBe('not-applicable');
|
||||
});
|
||||
@@ -18,13 +18,13 @@ describe('what adding a member would do', () => {
|
||||
|
||||
describe('which installed services have work to do', () => {
|
||||
it('skips single-tenant daemons entirely', () => {
|
||||
const ids = servicesNeedingMemberProvision(['jellyfin', 'vnc', 'memos']).map((e) => e.id);
|
||||
expect(ids.sort()).toEqual(['jellyfin', 'memos']);
|
||||
const ids = servicesNeedingMemberProvision(['invoiceshelf', 'vnc', 'memos']).map((e) => e.id);
|
||||
expect(ids.sort()).toEqual(['invoiceshelf', 'memos']);
|
||||
});
|
||||
|
||||
it('ignores services that are not installed', () => {
|
||||
// The catalogue is what COULD be installed; only what IS installed has a member to provision.
|
||||
expect(servicesNeedingMemberProvision([])).toEqual([]);
|
||||
expect(servicesNeedingMemberProvision(['jellyfin']).map((e) => e.id)).toEqual(['jellyfin']);
|
||||
expect(servicesNeedingMemberProvision(['memos']).map((e) => e.id)).toEqual(['memos']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,8 +12,8 @@ describe('preflight is per mode, not per entry', () => {
|
||||
it('allows pointing at an existing instance without Docker', async () => {
|
||||
// The whole point: a host with no Docker can still use Photos against an Immich elsewhere.
|
||||
// Refusing the entry outright is the over-strict check that makes people bypass the installer.
|
||||
const jellyfin = byId('jellyfin')!;
|
||||
expect(await preflight(jellyfin, 'existing')).toEqual({ ok: true });
|
||||
const existingCapable = byId('memos')!;
|
||||
expect(await preflight(existingCapable, 'existing')).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('allows a config-only sidecar regardless', async () => {
|
||||
|
||||
Reference in New Issue
Block a user