soulseek, extracted into plugins/soulseek — the platform half
Seventh extraction, and the first of something that was already dead. Soulseek had been switched off since 2026-08-13: hono.ts carried the router import and its PROTECTED_MOUNTS entry commented out, so createSidecarProxy never ran and registerProxiedPrefix was never called; officer-slskd was not in the ecosystem at all. Because the permission entry had been reduced to api: [], the totality check was satisfied and the server booted clean — the UI rendered and every request 404'd. Worth naming: the boot check proves every MOUNTED prefix has a permission, and is silent about a feature that mounts nothing. Also closes a live data hazard: schema.ts had the soulseek export commented out, so the three tables were absent from the drizzle barrel and bun db:push would have dropped soulseek_favorites, soulseek_browse_snapshots and soulseek_browse_dirs on any server holding them. rebuild-soulseek-tree.ts documented the workaround rather than the problem; deleted with the rest. The connection stays in core service_connections under service 'slskd' while the mount is now /api/soulseek. Those differ on purpose. Test fixtures: slskd was canonical in three app-store tests, the third extraction running to break them. installer.test.ts now builds a literal fixture instead of reaching into the catalogue — it pins planSteps, which is installer logic, and no catalogue entry is both provisioned and members:'none' any more. catalogue.test.ts and members.test.ts re-anchored to vnc/notify/gitea. Unrelated but mine: check:theme caught 5 palette classes I introduced in CommandHQ earlier tonight against a baseline of 0. Converted to bg-success / bg-warning. check:theme is not part of bun test, which is why the green suite never showed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -107,10 +107,11 @@ describe('member provisioning is declared for every entry', () => {
|
||||
});
|
||||
|
||||
it('never promises accounts for a single-tenant daemon', () => {
|
||||
// slskd has no user concept; claiming otherwise would make the installer try to create accounts
|
||||
// against an API that does not exist. (Transmission was the other example until 2026-08-18, when it
|
||||
// became `plugins/transmission/`.)
|
||||
for (const id of ['slskd']) expect(byId(id)!.members).toBe('none');
|
||||
// A daemon with no user concept; claiming otherwise would make the installer try to create accounts
|
||||
// against an API that does not exist. Transmission was an example until 2026-08-18 and slskd until
|
||||
// 2026-08-19, when they became `plugins/transmission/` and `plugins/soulseek/` — hence the list
|
||||
// rather than a single id: it must stay non-empty, or this assertion passes by iterating nothing.
|
||||
for (const id of ['vnc', 'notify', 'gitea']) expect(byId(id)!.members).toBe('none');
|
||||
});
|
||||
|
||||
// A test asserting the vault is invite-only lived here until 2026-08-18, when it left to become
|
||||
|
||||
@@ -226,27 +226,6 @@ export const CATALOGUE: CatalogueEntry[] = [
|
||||
// Deleting the entry is REQUIRED, not tidying — see the note where photos used to sit, and the trap in
|
||||
// `EXTRACTING-A-PLUGIN.md`. A leftover entry makes the permission permanently `unavailable`, because
|
||||
// `permissionAvailability()` derives from `sidecar_installs` and a plugin never gets a row there.
|
||||
{
|
||||
id: 'slskd',
|
||||
ui: {
|
||||
name: 'Soulseek',
|
||||
image: '/plugins/slskd/icon.png',
|
||||
color: '#ffffff',
|
||||
rootRoute: '/soulseek',
|
||||
routes: ['/soulseek'],
|
||||
},
|
||||
process: 'officer-slskd',
|
||||
label: 'Soulseek',
|
||||
summary: 'slskd — search and download from the Soulseek network',
|
||||
members: 'none',
|
||||
modes: ['existing', 'provisioned'],
|
||||
permission: 'soulseek',
|
||||
composeTemplate: 'slskd',
|
||||
existingFields: [
|
||||
{ key: 'url', label: 'slskd URL', type: 'url', required: true },
|
||||
{ key: 'secret', label: 'API key', type: 'secret', required: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'caldav',
|
||||
ui: {
|
||||
|
||||
@@ -7,7 +7,13 @@ import { byId } from './catalogue';
|
||||
// when something goes wrong: resume, blocking, and not doing work twice.
|
||||
|
||||
const jellyfin = byId('jellyfin')!; // existing|provisioned, members: 'accounts'
|
||||
const slskd = byId('slskd')!; // existing|provisioned, members: 'none'
|
||||
// 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 };
|
||||
const configOnly = byId('notify')!; // config only
|
||||
|
||||
/** Records what was actually called, so a test can assert on absence as well as presence. */
|
||||
@@ -46,7 +52,7 @@ describe('planSteps', () => {
|
||||
it('omits the members step for a service with no user concept', () => {
|
||||
// Otherwise a Transmission install reports a members step that did nothing, which reads as a
|
||||
// silent failure to anyone debugging why a member has no access.
|
||||
expect(planSteps(slskd, 'provisioned')).not.toContain('members');
|
||||
expect(planSteps(noMemberConcept, 'provisioned')).not.toContain('members');
|
||||
expect(planSteps(jellyfin, 'provisioned')).toContain('members');
|
||||
});
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ describe('what adding a member would do', () => {
|
||||
it('predicts the outcome per service without contacting anything', () => {
|
||||
expect(plannedOutcome(byId('jellyfin')!)).toBe('provisioned');
|
||||
expect(plannedOutcome(inviteOnly)).toBe('invited');
|
||||
expect(plannedOutcome(byId('slskd')!)).toBe('not-applicable');
|
||||
expect(plannedOutcome(byId('vnc')!)).toBe('not-applicable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('which installed services have work to do', () => {
|
||||
it('skips single-tenant daemons entirely', () => {
|
||||
const ids = servicesNeedingMemberProvision(['jellyfin', 'slskd', 'memos']).map((e) => e.id);
|
||||
const ids = servicesNeedingMemberProvision(['jellyfin', 'vnc', 'memos']).map((e) => e.id);
|
||||
expect(ids.sort()).toEqual(['jellyfin', 'memos']);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user