fix: rename bug card anchor to div with _card_link include and add admin role tests

The bug list template wraps each bug in a `<div class="bug-card card-link-host">` instead of a bare `<a>`, pulling in `_card_link.html` for proper link semantics. Two new integration tests (`test_bug_detail_renders_for_member` and `test_bug_detail_renders_for_admin`) verify that the `/bugs/{number}` endpoint returns 200 for both roles, that the "Close ticket" button is absent for members and present for admins, and that the JSON response exposes `viewer_is_admin` as a boolean. Supporting helpers `_role_user` and `_open_ticket` create users with a specific role and open a Gitea-backed ticket.
This commit is contained in:
2026-06-12 20:50:27 +00:00
parent f486778884
commit 79900bfaf6
4 changed files with 60 additions and 2 deletions
+2
View File
@@ -175,6 +175,8 @@ Devii partially configures its OWN system message: every system prompt ends with
- **All dates shown to users are DD/MM/YYYY** (European). Use `format_date()` from `utils.py` (template global), or `time_ago()` which auto-switches to DD/MM/YYYY past 30 days.
- **Slug + UUID lookup:** resources with slugs (posts, gists, news, projects) accept either the slug or the bare UUID - see `resolve_by_slug()` in `database.py`. Slugs are generated via `make_combined_slug(title, uid)` which prefixes the first 8 chars of the UUID.
- **Username:** 3-32 chars, letters/numbers/hyphens/underscores. **Password:** min 6 chars.
- **Roles are stored capitalized:** the `users.role` value is exactly `"Admin"` or `"Member"` (the first registered user becomes `"Admin"`; `auth.py`). Always test admin-ness through the `is_admin(user)` global (`utils.py`, also a Jinja global), which compares `user.get("role") == "Admin"` case-sensitively - never hand-roll a lowercase compare. The **only** lowercase surface is the CLI (`devplace role set <username> <member|admin>` accepts lowercase but writes `role.capitalize()`; `role get` prints `.lower()` for display). A lowercase role in the DB silently fails every admin check.
- **Never pass a `respond()` context key that collides with a Jinja global.** `respond(request, template, ctx, model=XOut)` feeds the **same** `ctx` dict to both the Pydantic model (JSON) and the template render. Jinja context variables shadow `env.globals` across the whole inheritance chain, so a key like `is_admin`/`avatar_url`/`format_date`/`is_self`/`owns`/`guest_disabled` holding a non-callable value turns `base.html`'s `{% if is_admin(user) %}` into `False(user)` -> `TypeError: 'bool' object is not callable`, a 500 that only fires for the branch that calls the global (e.g. logged-in users, not guests). Name viewer/permission flags distinctly (`viewer_is_admin`, not `is_admin`) in both the schema and the context. This was a real bug on `/bugs/{number}`; `tests/test_bugs_gitea.py::test_bug_detail_renders_for_{member,admin}` guard it.
- **Avatars:** Multiavatar SVG generated locally from the username seed in <5ms, in-memory cached (cleared on restart). URL `/avatar/multiavatar/{seed}?size={size}`. Falls back to initial-based SVG on error.
- **Project visibility (`is_private`) and read-only (`read_only`)** are owner-controlled integer flags on the `projects` row (see `AGENTS.md` -> "Project visibility and read-only"). Two invariants: (1) private-read access is gated by the single `content.can_view_project(project, user)` predicate (owner or admin) at EVERY read surface - detail, file routes (`_load_viewable_project`), zip, listing, profile, sitemap - never re-implement the check inline; (2) read-only is enforced as a single data-layer guard `project_files._guard_writable(project_uid)` at the top of every mutation entrypoint in `project_files.py`, so it covers the HTTP routes, Devii (via the routes), and container sync at once - add the guard to any NEW file-mutating function, and never enforce read-only only in a route. Devii may flip read-only or visibility (private/public) only after explicit user confirmation, enforced by `dispatcher.confirmation_error` (the `CONFIRM_REQUIRED` set) before the HTTP call; mirror that pattern for any future irreversible agent action. Both owner toggle buttons (`project_detail.html`) likewise carry `data-confirm` so the human UI gates them through `app.dialog` too. **Deletions ALWAYS require confirmation:** `confirmation_error` gates EVERY content delete tool via the `CONFIRM_REQUIRED` set - `delete_post`, `delete_comment`, `delete_gist`, `delete_project`, `project_delete_file`, `delete_media`, `delete_attachment`, `admin_delete_news` - plus `container_instance_action` with `action="delete"` and any `container_exec` whose command matches `dispatcher.DESTRUCTIVE_COMMAND` (rm/rmdir/unlink/shred/truncate/dd/mkfs/wipefs, `find -delete`, redirect over a file, SQL `drop`/`delete from`). The first call is refused with a message; the agent must show the user the exact target and only then pass `confirm=true` (the `DELETING IS ALWAYS CONFIRMED` system-prompt rule mirrors this). Any new name added to `CONFIRM_REQUIRED` is auto-confirmed by a generic fallback in `confirmation_error`. **Load-bearing: every confirmation-gated tool MUST also declare a `confirm` boolean param in its catalog spec** (use the `confirm()` helper in `catalog.py`, or `arg("confirm", ..., kind="boolean")` for container actions). Tool schemas set `additionalProperties: false`, so a gated tool WITHOUT a declared `confirm` param can never receive `confirm=true` from the model - the gate refuses every call and the agent loops forever (this was a real bug on all the delete tools). The param is harmlessly forwarded to the HTTP endpoint (the routes do not declare it; FastAPI ignores undeclared form fields) and ignored by the LOCAL container/customization controllers. The delete tools stay `requires_auth` (not `requires_admin`) - members delete their own, the endpoint's owner-or-admin guard lets admins delete any.