Extract Knowledge Base into a plugin (reference non-asset plugin)

First feature extracted from core into a plugin per "plugin is the product",
mirroring the notifications plugin. KB is a NON-asset plugin: it contributes a
model + blueprint + nav item but registers no AssetType.

- plugins/knowledgebase/: manifest.json (api_prefix /api/knowledgebase, no deps),
  models/ (KnowledgeBase, contract-pure imports via shopdb.api), api/ (the
  blueprint, same routes/prefix so the frontend is unchanged), plugin.py
  (get_blueprint + get_models + get_navigation_items).
- De-cored: removed shopdb/core/models/knowledgebase.py + api/knowledgebase.py,
  their __init__ exports, and 'knowledgebase' from CORE_BLUEPRINT_NAMES; dropped
  the hardcoded KB nav item from dashboard.py (now via the plugin nav hook).
- search.py and reports.py lazy-import KnowledgeBase from the plugin and degrade
  gracefully (search skips via _require_enabled when disabled; kb-popularity
  report returns 503 if the plugin is absent).
- Registered in instance/plugins.json (enabled).

The knowledgebase table stays in the core Alembic chain (bundled-plugin schema
folded into core, ADR-004); the model just maps it. KB was never in the
shopdb.api contract surface, so no __contract_version__ bump.

Pinned with characterization tests first (test_knowledgebase.py); they pass
unchanged against the plugin blueprint. 163 tests pass, naming green, app boots
7 bundled plugins, KB endpoint/nav/search verified live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 20:32:37 -04:00
parent 530928d5e7
commit a6f6c3f51f
16 changed files with 396 additions and 244 deletions

View File

@@ -0,0 +1,34 @@
"""Characterization tests for the Knowledge Base API.
Written before extracting Knowledge Base into a plugin (pinning-flask-behavior):
the endpoints must behave identically whether KB is a core blueprint or a plugin
blueprint, since both register at /api/knowledgebase.
"""
def test_create_list_get_article(client, db, auth_headers):
"""Create an article, see it in the list, fetch it by id."""
created = client.post('/api/knowledgebase',
json={'shortdescription': 'How to reset a printer',
'linkurl': 'https://kb.example/printer-reset',
'keywords': 'printer reset'},
headers=auth_headers)
assert created.status_code == 201, created.get_json()
linkid = created.get_json()['data']['linkid']
listing = client.get('/api/knowledgebase', headers=auth_headers)
assert listing.status_code == 200
ids = [a['linkid'] for a in listing.get_json()['data']]
assert linkid in ids
fetched = client.get(f'/api/knowledgebase/{linkid}', headers=auth_headers)
assert fetched.status_code == 200
assert fetched.get_json()['data']['shortdescription'] == 'How to reset a printer'
def test_create_requires_shortdescription(client, db, auth_headers):
"""shortdescription is required."""
resp = client.post('/api/knowledgebase',
json={'linkurl': 'https://kb.example/x'},
headers=auth_headers)
assert resp.status_code == 400

View File

@@ -17,7 +17,7 @@ from shopdb.plugins import plugin_manager
from shopdb.plugins.base import BasePlugin, PluginMeta
BUNDLED_PLUGINS = ('computers', 'equipment', 'network', 'notifications', 'printers', 'usb')
BUNDLED_PLUGINS = ('computers', 'equipment', 'knowledgebase', 'network', 'notifications', 'printers', 'usb')
@pytest.fixture

View File

@@ -89,12 +89,13 @@ def test_paginated_response_shape(client, auth_headers):
def test_plugin_loader_discovers_bundled_plugins(app):
"""Plugin manager finds the six bundled plugins."""
"""Plugin manager finds the bundled plugins."""
from shopdb.plugins import plugin_manager
expected_plugins = {
'computers',
'equipment',
'knowledgebase',
'network',
'notifications',
'printers',