Reverses the 2026-08-13 decision to make caldav an app-store entry. Three reasons it does not belong there. It is not an integration. Photos-via-Immich or torrents-via-Transmission assume you already run the thing; a calendar assumes nothing, and a personal server without one is missing a baseline rather than an optional app. It needs TWO top-level routes, /calendar and /contacts, and a plugin gets one — derived from its appName, which is what makes it structurally impossible for a plugin to shadow a core route. Nobody thinks "I'll open my dav", so collapsing them under /dav was the wrong trade, and weakening the namespace guarantee to allow two was a worse one. As core routes the question does not arise. And it never actually needed Docker. Radicale is a ~2MB Python package; sidecar/caldav/radicale.ts already spawns it directly on loopback with http_x_remote_user. The container came from the app store making everything a container, not from anything Radicale requires. So: the routers are mounted again (/caldav for the UI's JSON door, /dav for app passwords, the top-level /dav sync door and the two .well-known redirects), officer-caldav joins CORE_PROCESSES, radicale joins the machine-setup package list for all four package managers, and the catalogue entry is gone. members.test.ts re-anchored a fifth time — caldav was one of two `accounts` services and memos is now the last. Noted in the test that the sixth time should be a literal fixture instead. Verified: assertPermissionTotality accepts /caldav and /dav, tsgo clean, 1085 pass. NOT yet verified against a real client — radicale is not installed on this machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37 lines
2.0 KiB
TypeScript
37 lines
2.0 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { plannedOutcome, servicesNeedingMemberProvision } from './members';
|
|
import { byId, type CatalogueEntry } from './catalogue';
|
|
|
|
// `members: 'invite'` has no catalogue entry as of 2026-08-18 — the vault was the only one, and it left
|
|
// to become `plugins/vault/`. The BRANCH is still live code any future entry can reach, so it is tested
|
|
// against a synthetic entry rather than dropped: deleting the assertion with the last real user of a
|
|
// case is how a switch arm stops being covered without anyone deciding that it should.
|
|
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('memos')!)).toBe('provisioned');
|
|
expect(plannedOutcome(inviteOnly)).toBe('invited');
|
|
expect(plannedOutcome(byId('vnc')!)).toBe('not-applicable');
|
|
});
|
|
});
|
|
|
|
describe('which installed services have work to do', () => {
|
|
it('skips single-tenant daemons entirely', () => {
|
|
// memos provisions accounts; vnc has no user concept and must be skipped.
|
|
//
|
|
// Down to ONE `accounts` service in the catalogue, which is why this names a single id rather than
|
|
// two. It has been re-pointed five times as services left — jellyfin, invoiceshelf and slskd to
|
|
// `plugins/`, caldav to CORE — and memos is the last one standing. If memos ever leaves too, this
|
|
// wants a literal fixture like `installer.test.ts` has, not a sixth re-point.
|
|
const ids = servicesNeedingMemberProvision(['memos', 'vnc']).map((e) => e.id);
|
|
expect(ids.sort()).toEqual(['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(['memos']).map((e) => e.id)).toEqual(['memos']);
|
|
});
|
|
});
|