diff --git a/datasette/app.py b/datasette/app.py
index bc35669f..5afca2a2 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -2588,7 +2588,7 @@ class Datasette:
JsonDataView.as_view(
self,
"actions.json",
- self._actions,
+ lambda: {"actions": self._actions()},
template="debug_actions.html",
permission="permissions-debug",
),
diff --git a/datasette/templates/debug_actions.html b/datasette/templates/debug_actions.html
index 0ef7b329..c9dccaaa 100644
--- a/datasette/templates/debug_actions.html
+++ b/datasette/templates/debug_actions.html
@@ -9,7 +9,7 @@
{% include "_permissions_debug_tabs.html" %}
- This Datasette instance has registered {{ data|length }} action{{ data|length != 1 and "s" or "" }}.
+ This Datasette instance has registered {{ data.actions|length }} action{{ data.actions|length != 1 and "s" or "" }}.
Actions are used by the permission system to control access to different features.
@@ -26,7 +26,7 @@
- {% for action in data %}
+ {% for action in data.actions %}
| {{ action.name }} |
{% if action.abbr %}{{ action.abbr }}{% endif %} |
diff --git a/docs/introspection.rst b/docs/introspection.rst
index ab47c0a5..0010e8b7 100644
--- a/docs/introspection.rst
+++ b/docs/introspection.rst
@@ -155,6 +155,30 @@ Shows currently attached databases. `Databases example **Status:** recommendation 2 and 3 are implemented — every JSON-object
+> **Status:** recommendations 1-3 are implemented. Every JSON-object
> success response now includes `"ok": true` (`JsonDataView` injects it for
> dict responses; homepage, jump, schema, permission-debug and autocomplete
-> views set it explicitly; covered by `tests/test_success_envelope.py`).
-> Recommendation 1 (wrapping the `/-/plugins`, `/-/databases`, `/-/actions`
-> top-level arrays in objects) is being landed as separate per-endpoint
-> commits. §2a/2b/2c remain open.
+> views set it explicitly), and the three top-level-array endpoints now
+> return objects: `/-/plugins` → `{"ok": true, "plugins": [...]}`,
+> `/-/databases` → `{"ok": true, "databases": [...]}`, `/-/actions` →
+> `{"ok": true, "actions": [...]}`. Covered by
+> `tests/test_success_envelope.py`. The sub-findings §2a (collection
+> representations), §2b (`_extra`/`_shape` coverage) and §2c (count
+> truncation) remain open.
Endpoints disagree about the success envelope:
@@ -372,8 +375,8 @@ Two details make tiering urgent rather than optional:
2. `Forbidden` → JSON 403 for JSON requests (§1a).
3. No `ok: false` with HTTP 200 (§1b: `_shape=object`, write canned-query
SQL errors).
-4. Wrap `/-/plugins`, `/-/databases`, `/-/actions` top-level arrays in
- objects (§2).
+4. ~~Wrap `/-/plugins`, `/-/databases`, `/-/actions` top-level arrays in
+ objects (§2).~~ ✅ Done.
5. Filter `/-/databases.json` by `view-database` or gate it behind
`permissions-debug` (§6).
6. 401 (not silent-anonymous) for invalid/expired bearer tokens (§1c).
diff --git a/tests/test_api.py b/tests/test_api.py
index 3263a88c..b035edb9 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -579,7 +579,7 @@ async def test_actions_json(ds_client):
try:
ds_client.ds.root_enabled = True
response = await ds_client.get("/-/actions.json", actor={"id": "root"})
- data = response.json()
+ data = response.json()["actions"]
finally:
ds_client.ds.root_enabled = original_root_enabled
assert isinstance(data, list)
diff --git a/tests/test_success_envelope.py b/tests/test_success_envelope.py
index 290a51e0..22ce6580 100644
--- a/tests/test_success_envelope.py
+++ b/tests/test_success_envelope.py
@@ -101,3 +101,14 @@ async def test_databases_json_is_object(ds_client):
assert data["ok"] is True
assert isinstance(data["databases"], list)
assert "fixtures" in {db["name"] for db in data["databases"]}
+
+
+@pytest.mark.asyncio
+async def test_actions_json_is_object(ds_envelope):
+ response = await ds_envelope.client.get("/-/actions.json", actor={"id": "root"})
+ assert response.status_code == 200
+ data = response.json()
+ assert set(data.keys()) == {"ok", "actions"}
+ assert data["ok"] is True
+ assert isinstance(data["actions"], list)
+ assert "view-instance" in {action["name"] for action in data["actions"]}