Skip to content

Back off compat to 1.2.29 and re-introduce dropped unique keys - #90

Merged
cigamit merged 8 commits into
developfrom
chore/compat-1.2.29-unique-keys
Oct 1, 2026
Merged

cigamit merged 8 commits into
developfrom
chore/compat-1.2.29-unique-keys

Conversation

@TheWitness

@TheWitness TheWitness commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Three fleet-wide consistency changes for this plugin:

  1. Compat back-off — lowered the INFO minimum Cacti compat from 1.2.32 → 1.2.29.
  2. Unique-key re-introduction — added webseer_reintroduce_unique_keys(), called after the db_update_table() loop in webseer_upgrade_tables().
  3. File manifest + upgrade-time pruning — adds the fleet manifest.json + prune mechanism.

Why the unique-key fix is needed

Cacti 1.2.29–1.2.31 shipped a db_update_table() that rebuilds a table's indexes only from the keys definition (honoring uniqueness via a per-key 'unique' flag). It does not read a separate unique_keys array, so an existing UNIQUE index declared that way is DROP INDEX'd and never re-added — a silent loss of uniqueness. webseer declares plugin_webseer_contacts.user_id_type (user_id, type) via unique_keys, so any upgrade run on those releases drops it. 1.2.32+ preserves it, so the helper is gated to >= 1.2.29 && < 1.2.32 and is a no-op elsewhere. It also checks db_index_exists() first, so it never double-adds.

Show-stopper assessment (unique key)

  • Duplicate rows: if uniqueness was lost on 1.2.29–1.2.31 and duplicate (user_id, type) contact rows were subsequently inserted, ADD UNIQUE KEY will fail on those databases. The helper is best-effort (guarded by db_index_exists), but an operator with pre-existing duplicates will need to de-duplicate first.
  • Sites already upgraded to 1.2.32+ that lost the key on an earlier 1.2.29–1.2.31 run are not repaired by this gate (per the agreed scope).
  • No other schema concerns: the remaining webseer tables define uniqueness through primary/non-unique keys only.

File manifest + upgrade pruning

  • New root manifest.json with tombstones (empty — no relocations), expected (the current top-level tree; directories carry a trailing /), and whitelist (empty — check results are stored in the database; the bundled ca-bundle.crt ships as stock code).
  • webseer_prune_files() (in setup.php, called from the version-change branch of plugin_webseer_upgrade() after the plugin_config update) removes the dev-only tests/ tree on upgrade, refuses any path resolving outside the plugin directory (a tampered manifest.json), warns in the Cacti log on any file/directory it cannot remove, leaves whitelist/.git* alone, and logs — without removing — any unaccounted top-level entry.
  • tests/bin/validate-manifest.php (wired into plugin-ci-workflow.yml) fails on drift between expected and the real top-level tree.
  • Added tests/Unit/PruneFilesTest.php. SetupHelpersGapsTest and the Integration PluginLifecycleTest sandbox base_path (a temp tree with a copy of INFO, an empty includes/database.php, and no manifest) for their upgrade-path cases so the prune is a no-op there while the new call line stays covered.

Docs

  • README.md: added an Upgrading note — poller_webseer.php is a long-lived background process, so operators should restart it after a file upgrade (Cacti's poller relaunches it) to pick up the new code; also notes the upgrade-time prune of bundled dev-only files.

Validation

Full Pest suite green (61 passed, 315 assertions), patch-coverage 100% (79/79 changed lines) against develop, manifest drift-check passes, and the translation template is unchanged.

Revision: hardening & fleet cleanup

Since the initial description, this PR also:

  • Prunes phpunit.xml on upgrade (alongside the dev-only tests/ tree) and leaves .md* lint configs in place — the drift check now ignores tests/, phpunit.xml, .git*, .md*, and whitelisted paths.
  • Hardens the prune against tampered manifests: refuses any tombstone whose normalized path contains a ./.. traversal segment or resolves outside the plugin directory (including via a symlinked directory), and protects a directory when a whitelisted entry lives beneath it — each with added unit coverage.
  • Renames the prune helpers to the documented naming convention: webseer_prune_files() / webseer_rmtree() (the plugin_webseer_ prefix is reserved for lifecycle/hook-registration functions).
  • Gives tests/Unit/PruneFilesTest.php the full standard GPL v2 header and aligns the Project Structure block in .github/copilot-instructions.md.

- INFO: lower the minimum Cacti compat from 1.2.32 to 1.2.29.
- Add webseer_reintroduce_unique_keys(), called after db_update_table() in
  webseer_upgrade_tables(). Cacti 1.2.29-1.2.31 shipped a db_update_table()
  that rebuilds indexes only from 'keys' and silently drops UNIQUE keys
  declared via 'unique_keys' (here plugin_webseer_contacts.user_id_type).
  On those releases only, re-add the key when it is missing; 1.2.32+
  preserves it, so the helper is a no-op there.
- Test the in-range re-add, the out-of-range no-ops, and the already-exists
  guard; make the db_index_exists bootstrap stub mockable and add a
  cacti_version_compare stub.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Index-creation failures are not propagated, and required operator-facing release notes are missing.

Review effort: Balanced
Findings: 1 High severity · 3 Low severity

Open (4)
What changed in this PR

Lowers Cacti compatibility to 1.2.29 and repairs unique indexes dropped by affected Cacti releases.

Changes:

  • Restores the contacts unique index on Cacti 1.2.29–1.2.31.
  • Adds version-boundary and existing-index tests.
  • Extends database test mocks.
File Description
INFO Lowers minimum Cacti version.
includes/​database.php Adds unique-index repair logic.
tests/​bootstrap-unit.php Adds required test stubs.
tests/​Unit/​ReintroduceUniqueKeysTest.php Tests repair behavior and version gates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread includes/database.php Outdated
Comment thread INFO
Comment thread tests/Unit/ReintroduceUniqueKeysTest.php
Comment thread tests/Unit/ReintroduceUniqueKeysTest.php
Copilot added 7 commits September 30, 2026 16:09
phpunit.xml ships in the repo for CI but is a dev-only test artifact, so it
is removed from manifest.json 'expected', pruned from installs on upgrade
(like tests/), and excluded from the manifest drift check.

The retired markdown-lint configs (.mdlrc, .md_style.rb) are deleted, and
.md* is now ignored like .git*: protected from pruning and excluded from
drift if it reappears.
The upgrade-time prune now rejects any tombstone containing '.'/'..' segments
(which could escape the plugin directory or resolve to its root) and treats
ancestors of whitelist entries as protected, so a tombstone on a parent
directory can no longer delete a whitelisted file beneath it.
The trailing '# ...' comments in the Project Structure block drifted further
right down the tree; align them all to a single column.
- Use the full license header (from the plugin's own setup.php) in
  tests/Unit/PruneFilesTest.php instead of the abbreviated copyright banner.
- Document that the manifest drift check and the upgrade-time prune also
  handle phpunit.xml and .md* files, matching the implemented behavior.
The repository naming contract reserves the plugin_<name>_ prefix for plugin
lifecycle / hook-registration functions; all other functions use the plain
<name>_ prefix. Rename the internal upgrade helpers accordingly:

  plugin_<name>_prune_files() -> <name>_prune_files()
  plugin_<name>_rmtree()      -> <name>_rmtree()

The call site, unit tests, and the copilot-instructions.md references are
updated to match. No behavioral change.
@cigamit
cigamit merged commit 6934964 into develop Oct 1, 2026
3 checks passed
@cigamit
cigamit deleted the chore/compat-1.2.29-unique-keys branch October 1, 2026 06:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants