Extract employee directory into a plugin

Third core feature pulled into a plugin (blueprint-only, like slides). The
employee directory is a read-only lookup over a separate HR database.

- plugins/employees/: manifest (api_prefix /api/employees, no deps), api/ (moved
  blueprint, contract-pure: success/error/ErrorCodes + employee_connection all
  from shopdb.api), plugin.py (get_blueprint, get_models -> []).
- employee_connection STAYS core infrastructure in shopdb.api (config-driven
  external DB connector, shared by search + the notifications shopfloor feed). So
  no get_services needed and no contract change - the plugin owns the directory
  FEATURE, core owns the shared connector.
- Fixed a latent bug in the move: error paths used ErrorCodes.DATABASE_ERROR
  which does not exist -> ErrorCodes.INTERNAL_ERROR (so a directory outage now
  returns a clean 500 envelope instead of an AttributeError crash).
- De-cored: deleted shopdb/core/api/employees.py, removed from
  CORE_BLUEPRINT_NAMES + core/api/__init__ import/__all__. Registered in
  instance/plugins.json.

Pinned first: validation (400) + graceful-degrade (500) characterization tests;
the degrade test caught the DATABASE_ERROR bug and goes green with the fix.
184 tests pass, naming green, app boots 9 bundled plugins, endpoint verified live.

Plugin extractions complete: knowledgebase, slides, employees.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 21:13:15 -04:00
parent b6ec4cb577
commit d20682fd06
10 changed files with 128 additions and 13 deletions

View File

@@ -0,0 +1,26 @@
"""Characterization tests for the employee directory endpoints.
Written before extracting employees into a plugin. The wjf_employees DB is not
available under test, so these assert the DB-independent behaviors: validation
(400) and graceful failure (500 envelope, not an unhandled crash) when the
directory is unreachable. Behavior must be identical as a plugin blueprint.
"""
def test_search_requires_min_query(client, db):
"""A too-short query is rejected before touching the directory DB."""
resp = client.get('/api/employees/search?q=a')
assert resp.status_code == 400
def test_lookup_requires_numeric_sso(client, db):
"""Non-numeric SSO is rejected without touching the directory DB."""
resp = client.get('/api/employees/lookup/abc')
assert resp.status_code == 400
def test_search_degrades_when_directory_unreachable(client, db):
"""With the directory DB unavailable, search returns a clean 500 envelope."""
resp = client.get('/api/employees/search?q=smith')
assert resp.status_code == 500
assert resp.get_json()['status'] == 'error'

View File

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

View File

@@ -94,6 +94,7 @@ def test_plugin_loader_discovers_bundled_plugins(app):
expected_plugins = {
'computers',
'employees',
'equipment',
'knowledgebase',
'network',