← Admin Panel

Test cases

admin panel · workzone
Accepted decisions
User management API — not tested here User CRUD, invite, RBAC, audit log are auth-module endpoints, fully covered in Auth & Security. Admin tests only its own backend (PlatformSettings) and the frontend behavior on top of the delegated APIs.
Frontend → Testing Library + MSW Components are tested with React Testing Library (behavior, not implementation); API responses are mocked via MSW (Mock Service Worker) — a single interception layer for network requests.
Notifications backend — tested here Channels, routes, personal preferences, and the delivery dispatcher are core ORM with CRUD in Admin (like PlatformSettings), so they are covered here. The event sources (sync_runs, brute-force, curation) live and are tested in their own modules — here we verify routing and delivery, not the detector.
System prompt — tested here prompt_settings is core ORM (singleton) with CRUD in Admin, like PlatformSettings. The defaults for both blocks are seed constants in code, and NULL means “use the default.” Here: storage, the token cap, and reset; injecting the prompt into the model request is owned and tested by Query Engine.
Stack and infrastructure
Backend done pytest, pytest-asyncio, pytest-cov, httpx — shared infrastructure
Backend testcontainers, factory_boy — shared from auth-security; we add PlatformSettingsFactory, NotificationChannelFactory, PromptSettingsFactory
Frontend Vitest + React Testing Library + MSW — intercepting fetch and simulating backend responses without a real server
Markers @pytest.mark.unit, @pytest.mark.integration — shared
Unit Pydantic schemas — validation without a DB
test_settings_schema.py
PlatformSettings DTO — fields and constraints
Unit
Cases
valid settingsa full set of valid fields → schema passes
partial updatePATCH schema: only changed fields, the rest Optional
invalid timezonea non-existent IANA tz (Mars/Olympus) → reject
invalid localenot one of ru | en → reject
invalid date_formatnot one of the allowed formats → reject
negative TTL@parametrize access / refresh / session_absolute < 0 → reject
zero TTL@parametrize TTL = 0 → reject (CHECK > 0)
invalid accent_colornot a hex value (red) → reject
invalid themenot one of dark | light → reject
test_notification_schema.py
Channels, routes, payload — validation without a DB
Unit
Cases
webhook requires url+presetkind=webhook without url/preset → reject
builtin no urlkind=in_app|emailpreset/url not allowed
invalid event_typea type outside sync|security|budget|system|discovery → reject
invalid severitynot one of info|warning|critical → reject
slack payloadthe Slack preset formatter → the expected message shape
generic payloadneutral JSON: event·severity·title·source·ts
test_prompt_schema.py
prompt_settings DTO — token cap, fields
Unit
Cases
valid overridesafety_text / org_text within the cap → schema passes
null = defaultboth fields None allowed → the block takes the built-in default
over token captext longer than the limit → reject (the cap protects the window budget)
blank not nullan empty string ''None: either normalized to None (reset) or reject — no “empty” override
known placeholders oktext with {org_name} / {today} (or without them) → schema passes: both tokens are optional
unknown placeholder rejectan unknown token {foo} → reject: the whitelist is closed, and a literal {…} never leaks into the prompt
Integration · P0 PlatformSettings API → HTTP API → Conformance
test_platform_settings.py
CRUD, validation, and singleton integrity
IntegrationP0
Cases
GET → defaultsGET /admin/settings → 200, every field at its migration default
PATCH → partialorg_name + timezone → 200, other fields unchanged
PATCH → TTLsaccess / refresh / session_absolute TTL → values updated in the DB
PATCH → brandingaccent_color → 200, value correct
invalid timezone→ 422 VALIDATION_ERROR
invalid localelocale=fr → 422 VALIDATION_ERROR
negative TTLaccess_token_ttl=-1 → 422 VALIDATION_ERROR
singleton — no POSTPOST /admin/settings → 405 METHOD_NOT_ALLOWED
singleton — no DELETEDELETE /admin/settings → 405 METHOD_NOT_ALLOWED
updated_at triggerafter PATCH, updated_at is strictly greater than before
curation_frequency enumdaily / weekly → 200; anything else (hourly) → 422 (CHECK)
weekday only weeklya linked CHECK: curation_weekday BETWEEN 0 AND 6 is set only when weekly; when daily it is NULL, non-null → 422; weekday=7 → 422
agent_iteration_cap > 0@parametrize ≤ 0 → 422 (CHECK > 0); positive → 200
agent_max_concurrency > 0@parametrize ≤ 0 → 422 (CHECK > 0); positive → 200
mcp_enabled toggleboolean kill switch: PATCH true/false → 200, value in the singleton; non-boolean → 422
mcp_enabled=false — effecta request to /mcp is rejected before the key is checked — the MCP door is closed
test_maintenance.py
Maintenance mode — stub page, allow-list, pause → Maintenance
IntegrationP0
Cases
user gets 503mode on → the middleware answers a regular Member with 503 + Retry-After (the outage is temporary)
owner/admin allowlistOwner and Admin bypass the mode → 200; you cannot lock yourself out — the operator checks the platform before lifting it
sync + agents pausedturning the mode on pauses sync and the background agent lane
toggle owner-onlyonly Owner turns it on (maintenance_mode in the singleton); Admin/Member PATCH → 403 FORBIDDEN
off restoreslifting the mode → regular users get 200 again, sync and agents resume
test_notifications.py
Channels · routes · dispatcher · delivery
IntegrationP0
Cases
seed builtin channelsafter migration, in_app + email exist, is_builtin, non-deletable
webhook CRUDcreate/delete a webhook channel; url_enc / secret_enc are never returned
delete builtin → rejectDELETE in_app/email → 409 CONFLICT
route togglePATCH a type×channel cell → enabled in the DB
route lockedunset the in_app × security/sync route (locked + CHECK enabled OR NOT locked) → 422 · other routes toggle off freely
channel mutations — Owner onlya non-Owner (Member / Admin) POST / DELETE /admin/notification-channels → 403 FORBIDDEN on the backend (not just frontend read-only); anonymous → 401 UNAUTHORIZED
route edits — Owner onlya non-Owner PATCH /admin/notification-routes → 403 FORBIDDEN; anonymous → 401 UNAUTHORIZED
channel listing hides the secretGET /admin/notification-channels → 200; url_enc / secret_enc are returned neither in the list nor in the detail — only a presence flag
dispatch fan-outevent → deliveries to every enabled route (channel not paused)
type silencedall routes of a type off → the event goes to no one; the locked in_app security/sync cannot be zeroed out
channel pausedchannel enabled=false → its deliveries are not created; active channels still deliver
in_app channel stays ondisabling the in_app channel (enabled=false) → reject (CHECK)
recipients by rolepersonal channels → Owner/Admin only; a new admin is enrolled automatically
personal opt-outin_app_enabled / email_enabled independently silence their channel per type
opt-out unrestrictedany personal channel can be turned off for any type, including security/sync — there are no personal locks
dedup seriesa series with one dedup_key within the window → a single row, dedup_count grows, last_seen_at advances; outside the window — a new row
discovery action idempotenta repeated “Add to scope” (container already in the allow-list) → no-op; the event holds no state
in_app read statemarking as read → read_at, the unread counter decreases
email no SMTPSMTP not configured → email delivery failed, in_app delivered
test_prompt_settings.py
Override, reset, cap, and singleton integrity
IntegrationP0
Cases
GET → effective defaultoverride empty → the response returns the built-in text of both blocks (effective), not null
is_default flagoverride empty → is_default=true; after a block PATCH → false; the frontend uses the flag to mark “at default” and dim “Reset”
localized defaultlocale=en → the effective default in English; ru → in Russian; override empty in both
unknown placeholderan override with {foo} → 422 VALIDATION_ERROR; {org_name}/{today} → 200
PATCH org overrideorg_text → 200, saved in the DB; safety_text untouched
reset → nullresetting a block → the column goes NULL, GET returns the built-in default again
over token captext longer than the limit → 422 VALIDATION_ERROR
updated_by recordedafter PATCH, updated_by = the current admin; updated_at strictly greater
singleton — no POST/DELETEPOST / DELETE /admin/ai-prompt → 405 METHOD_NOT_ALLOWED
access — Owner/Admin editOwner and Admin PATCH → 200; Member → 403 FORBIDDEN
test_token_usage.py
AI usage — limits, by user, by model
IntegrationP0
Cases
PATCH limitsagent_weekly_token_budget · chat_weekly_token_budget → 200, values in the singleton
budget alertPATCH ai_monthly_budget + ai_budget_alert_enabled → 200; enabling the flag without an amount → 422 (CHECK); threshold reached → a Budget notification, the request is not blocked
zero/negative limit@parametrize ≤ 0 → 422 (CHECK > 0); null → no limit
list per-userGET /admin/usage → one row per user: agents + chat + total for the week
agents derivedsum = SUM(agent_runs.tokens_used) by agents.user_id over the week window
chat derivedsum = SUM(messages.tokens_used) by conversations.user_id, assistant only
week windowspend outside the current week (before Sun 00:00 org-tz) is not counted
excludes system spendembedding / RAG (model_usage) is not tied to a user — excluded from the sums
chat advisorychat over chat_weekly_token_budget → an “over” indicator, the request is not blocked (v1)
detail breakdownGET /admin/usage/:user → agents by agent, chat by messages.model
by-model spenda breakdown by model from model_usage (model · function, input/output, cost) — org-level, not tied to people; top 5 by cost, the rest behind “Show all”
company totalcompany total = SUM(model_usage) for week / month / year (all functions, tokens + $); derived, with no counter table; @parametrize over the window
total ⊇ per-userthe “total” is broader than the sum over people — it includes impersonal embedding / RAG; the monthly tile is checked against the ai_monthly_budget threshold
sort by totalthe list defaults to descending “Total”; search by name/email
access — Owner/AdminOwner/Admin GET → 200; Member → 403; PATCH of limits — Owner only
Integration · P1 Access control
test_admin_access.py
Access to admin routes by role
IntegrationP1
Cases
Owner GET settings→ 200
Owner PATCH settings→ 200
Admin GET settings→ 200 (read-only access)
Admin PATCH settings→ 403 FORBIDDEN
Member GET settings→ 403 FORBIDDEN
unauthenticated→ 401 UNAUTHORIZED
Frontend React — admin UI
AdminLayout.test.tsx
Layout, navigation, role visibility
Frontend
Cases
Owner → full sidebarall items: Dashboard, Users, Audit Log, Settings
Admin → restrictedDashboard, Users, Settings (read-only); Audit Log hidden (Owner-only)
Member → redirectredirected to the main interface, the sidebar is not rendered
active statethe current route is highlighted, a sub-page highlights its parent
theme togglea click switches dark/light, value in localStorage
language togglea click switches RU/EN, value in localStorage
logout“Log out” → logout, redirect /login
logout all“Log out of all devices” → confirmation → logout-all
PlatformSettings.test.tsx
Settings form — save, validation, states
Frontend
Cases
form pre-fillthe form loads current values from the API
save → PATCHa change + Save → PATCH of changed fields only
validation errorsinvalid values → inline errors, no request sent
success feedbacksuccess → toast “Settings saved”
server error422 → errors from errors[] on the matching fields
read-only for AdminAdmin sees the values, Save is absent / disabled
locale resolution chaindisplay takes the first one set: personal override → organization default → browser locale; an unset override transparently falls through to the organization level → Locale
password policythe “Password policy” section — read-only NIST 800-63B text
slack write-only secretsbot token and signing secret — write-only, not returned in the response/UI
slack test connection“Test connection” → ping Slack (auth.test) → status chip
slack enable toggletoggle → slack_settings.enabled; off → the bot does not respond
slack auto-link togglethe “Auto-link by email” toggle → off → an unlinked user always gets a code, no auto-match
telegram write-only secretsbot token write-only; the webhook secret is generated on setWebhook, not entered manually — not returned in the response/UI
telegram test connection“Test connection” → ping Telegram (getMe) → status chip + bot_username
telegram enable toggletoggle → telegram_settings.enabled; off → the bot does not respond
telegram — no auto-linkno email → no “Auto-link” toggle, always code-linking only
mattermost write-only secretsbot token — write-only, not returned in the response/UI
mattermost test connection“Test connection” → ping the server (users/me) → status chip + bot handle
mattermost enable toggletoggle → mattermost_settings.enabled; enable is atomic — a live probe, refusal → 409 and the switch rolls back; off → the bot does not respond
mattermost — no auto-linkevents carry no email → no “Auto-link” toggle, always code-linking only
loading statebefore the API responds — skeleton/spinner, the form is not rendered
network error500 → a message + “Retry”
UsersPage.test.tsx
User list and invitations
Frontend
Cases
list renderstable: name, email, role, status from the API
tab switchingswitching “List” ↔ “Invitations”
invite form“Invite” → email + role form, email validation
actions by roleOwner — all actions; Admin — restricted (RBAC matrix)
deactivated badgea deactivated user is marked, “Reactivate” available
empty state0 users → “Invite your team”
revoke invite“Revoke” → confirmation → the invite is removed from the list
resend invite“Resend” → POST, success toast
loading statebefore the API responds — table skeleton
network error500 → error message
UserDetailPage.test.tsx
User detail — profile and actions
Frontend
Cases
profile rendersname, email, role, created date, last login
Owner → change rolerole dropdown, save → PATCH, the role updates
Admin → limited actionsactions only on Members, not on Owner/Admin
deactivate“Deactivate” → modal → status updated
deleteOwner: “Delete” → confirmation modal → DELETE
reset password“Reset password” → confirmation → success toast
reactivatedeactivated → “Reactivate” available
self-viewown profile → deactivate and delete are hidden
AuditLogPage.test.tsx
Audit log — table and filters
Frontend
Cases
table rendersrecords: actor, action, outcome, date, IP
filter by actionaction type (login, role_changed…) → the table filters
filter by datedate range → query params, the table refreshes
filter by actoractor user → query param, only their actions
empty state0 records → “No records”
loading statebefore the API responds — table skeleton
network error500 → error message
NotificationsPage.test.tsx
Channels, type×channel matrix, recipients
Frontend
Cases
channels renderin-app + email (with SMTP status) + the webhook list
add webhook“+ Add” → pick a preset (Slack/Generic) → POST, a row in the list
test webhook“Test” → POST, success/error toast by result
channel pause toggleemail/webhook toggle → PATCH enabled; the in-app toggle is locked (disabled)
delete webhook guard“Delete” → confirmation → the row is removed
matrix route toggleclicking a channel pill → PATCH of the route
matrix in-app lockedthe in-app pill for security/sync is locked (a lock, a click does not unset it); other pills toggle
paused channel cella paused channel → its pills in the matrix are dimmed (the route is intact, just not firing)
smtp warningSMTP not configured → the email chip turns red, a warning
recipients notea banner “recipients — Owner / Admin”, links to Users and the profile
read-only for AdminAdmin sees it, only Owner edits
loading statebefore the API responds — skeleton
AiBehaviorPage.test.tsx
Two blocks — edit in place, reset, cap
Frontend
Cases
bubbles rendertwo blocks with current text from the API; “Reset” / “Edit” buttons
edit in place“Edit” → the block text becomes editable in place, buttons → “Cancel / Save”
save → PATCH“Save” → PATCH of this block only, exit editing, success toast
cancel discards“Cancel” → the edit is discarded, no request sent, the block returns to view mode
reset → default“Reset” → PATCH null, the block shows the built-in default
default stateis_default=true → the block is marked “at default”, “Reset” is dimmed; after an edit the flag is false → the button is active
token counter capthe counter grows as you type; over the cap — “Save” disabled + highlight
info tooltipthe “i” icon by the heading → a hint about the block's purpose on hover/focus
independent blocksediting/resetting one block does not affect the other — two independent PATCHes
loading statebefore the API responds — block skeletons
ListControls.test.tsx
Shared mechanics: search, facets, pagination → List Controls
Frontend
Cases
search thresholdthe request fires on both conditions: a typing pause ≈ 300 ms (debounce) and ≥ 2 significant characters; 1 character or before the pause — no request → #threshold
query resets pageany change to the query — new text, facet, or sort — resets to page 1 (otherwise you could land on a page absent from the new set) → #page-params
facet OR / ANDvalues within one facet — OR; different facets — AND (rows at the intersection) → #facet-logic
offset/limit clampa request beyond the range → clamp to the last page; the “X–Y of N” counter stays consistent → #pagination
AgentsPage.test.tsx
All agents list — pause, limits (Admin)
Frontend
Cases
pause agent · guarded“Pause” → confirmation → PATCH; the agent goes paused, runs do not start
resume agent“Resume” on a paused agent → PATCH, status active
save limitsplatform run limits (step cap + concurrent runs platform-wide, shared org-wide) → “Save” → PATCH platform_settings
open profile (read-only)“Open profile” → go to the agent card (model, instructions — read-only)
AiModelsPage.test.tsx
Providers and models — assigning the embedder
Frontend
Cases
change & reembed · guardedchanging the assigned embedder → “Change and re-index” → confirmation (vectors are incompatible) → a full re-embedding run starts
reembed in progresswhile the run is in flight — the row shows progress (%), a repeat change is blocked
disable model · guarded“Disable” / “Remove” a model → confirmation → PATCH; the assigned embedder cannot be removed
ToolsPage.test.tsx
AI tools — toggles and providers
Frontend
Cases
chat / agents togglethe “In chat” / “For agents” toggles → PATCH independently; off → the tool is not offered to that surface
configure provider“Configure” → a credential form (write-only); the secret is not returned in the response/UI
test provider“Test” → POST ping → success/error status chip
access — Owner/Admin/admin/ai-tools · Owner / Admin — edits (toggles, providers) are written as an admin action by both roles
UsagePage.test.tsx
AI usage — views and drill-down to detail
Frontend
Cases
sections renderstacked sections on a single page: “Company total” · “Spend limits” · “Spend by user” · “Spend by model”
total tilesunder “Total” week / month / year — three static tiles at once; no window switcher (the “30 days ▾” selector exists only in the by-model view)
row → detailclicking a user row → go to that user's usage detail
access — Owner/Admin/admin/usage · Owner / Admin; in the limits section a plain “Save”, available to both roles
HarvesterPage.test.tsx
Harvester in Admin — sync run, guarded actions
Frontend
Cases
metrics renderthe source list from the API; run status is shown inline in the source cell (idle · syncing · error)
run sync“Sync” / “Sync all” → POST without confirmation; the source row goes syncing
pause + wipe“Pause” → PATCH without confirmation; “Wipe data” is the only guarded action (type-to-confirm)
access — Owner/Admin/admin/harvester · Owner / Admin — the screen and actions are available to both roles
KnowledgeStorePage.test.tsx
Knowledge Store in Admin — curation, backups
Frontend
Cases
metrics rendergraph/vector counters, the curation schedule, recent runs and snapshots
run curation“Run” → POST without confirmation; the run is running, a repeat start is blocked (button disabled); “Cancel run” — guarded
restore guarded“Restore” a snapshot → type-to-confirm → POST; snapshots are created only on schedule, there is no manual capture
reembed lockre-embedding in progress (assigned via AI models) → starting curation is blocked with an explanation
NotificationFeed.test.tsx
In-app feed — bell, read state, actions
Frontend
Cases
bell unread countthe bell with an unread count (deliveries read_at IS NULL); a click opens the event feed panel
mark all read“Mark all read” → read_at on all of the admin's deliveries, the counter resets to zero
read / unread toggle“Mark as read” dims the card; “Mark as unread” brings it back into the count
add to scope · idempotentactionable discovery: “Add to scope” brings the container into coverage; a repeat (already in scope) — no-op, the button dims for everyone
unread facetthe “Unread” facet (a common default) → unread only; clear it → the full history; grouped Today · Earlier
Structure Test file structure

The backend is thin (PlatformSettings CRUD); most of the logic is frontend. The user management and audit log APIs are covered in auth-security — here it's only the UI on top of them. The data/AI screens (Agents, AI models, Tools, Usage, Harvester, Knowledge Store) and the notification feed are covered by FE cards here — only the React component and its guarded actions; the domain backend logic is owned and tested by the modules themselves. Priority (P0–P1) is orthogonal to the directories and is set by markers (pytest -m p0), not by folders.

  • tests/backend — shared scaffolding
    • conftest.pyDB, session, app client (already present)
    • factories/data factories
      • admin.pyPlatformSettingsFactory · NotificationChannelFactory · PromptSettingsFactory
    • admin/module directory
      • conftest.pyadmin fixtures, admin_client
      • unit/no DB
        • test_settings_schema.pyPydantic DTO
        • test_notification_schema.pychannels · routes · payload
        • test_prompt_schema.pyprompt_settings DTO · token cap
      • integration/with DB
        • test_platform_settings.pyCRUD · validation · singleton CHECK
        • test_maintenance.py503 · allow-list · pause sync/agents
        • test_admin_access.pyrole-based access
        • test_notifications.pychannels · routes · dispatch · delivery
        • test_prompt_settings.pyoverride · reset · cap · singleton
  • frontend/…/admin/__tests__/next to the code
    • AdminLayout.test.tsxsidebar · role · toggles · logout
    • PlatformSettings.test.tsxform · save · validation · states
    • UsersPage.test.tsxlist · tabs · invite · actions
    • UserDetailPage.test.tsxprofile · role · actions · RBAC
    • AuditLogPage.test.tsxtable · filters · states
    • NotificationsPage.test.tsxchannels · matrix · recipients
    • AiBehaviorPage.test.tsxbubbles · edit · reset · cap · tooltip
    • ListControls.test.tsxshared: search threshold · reset page · facets · clamp
    • AgentsPage.test.tsxpause · resume · limits (guarded)
    • AiModelsPage.test.tsxchange & reembed · disable (guarded)
    • ToolsPage.test.tsxchat/agents toggles · configure · test
    • UsagePage.test.tsxslices · window · → detail
    • HarvesterPage.test.tsxrun · pause source (guarded)
    • KnowledgeStorePage.test.tsxcuration · backups (guarded)
    • NotificationFeed.test.tsxbell · read/unread · add to scope · unread-facet