2 Commits
Author SHA1 Message Date
Juan Denis d61985e73b test: WP hook inversion matrix (present x absent)
16 tests covering boot registration, idempotency, backup provider
resolve/validate/missing-state, event catalog present/absent, template
provider dispatch, monitor binding gate, health-sweep no-op, analytics
ext-to-ext guards. Existing suites minimally updated for the provider
interfaces (171 WP/monitor + 316 backup/events/templates green).
2026-08-05 01:18:32 -04:00
Juan DenisandClaude Opus 5 e8e94a126f feat(plugins): let extensions contribute backup kinds
An extension with something worth backing up had to hand-roll it — no policy,
no cron mirror, no run history, no retention, no offsite copy, no verification,
no restore, and no place in the Protection panel. A registered kind inherits
every one of those, because everything downstream of the target_type switches
is already generic: the run lifecycle, retention (count, age, incremental-chain
ancestry), the API blueprint, and the panel itself, which threads target_type
through without ever switching on its value.

No migration: target_type is an unconstrained String, and the allowed set lived
purely in a Python tuple. A kind supplies resolve() and execute(), plus an
optional restore().

Four switches had to learn about the registry, and the order matters in three
of them:

- validate_target_type is the keystone — every policy route funnels through it,
  so accepting a registered kind opens the whole blueprint at once.
- _resolve_target and _execute_backup check the registry BEFORE their core
  branches, because both end in an application fallthrough that swallows any
  unrecognised type.
- run_restore_job likewise. This is the sharp one: without it a plugin's
  archive would be unpacked over an app directory by core's else-branch.

A kind that didn't implement restore is refused at request time with a reason,
rather than queueing a job that takes the policy lock and runs a safety backup
before failing.

The registry normalises what plugins hand back — target_type is forced rather
than defaulted, so a kind cannot describe itself as an 'application' and be
restored as one; execute() must return (path, size, meta) with a real path; and
the metadata retention and the panel read is filled in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 13:59:51 -04:00