Dj celery panel#2 task inspect backend - #3
Conversation
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. Thanks for integrating Codecov - We've got you covered ☂️ |
📝 WalkthroughWalkthroughAdds backend metadata support and exposes a new CeleryTasksInspectBackend for real-time task inspection; propagates backend_info through views, updates templates and styles to render backend metadata and optional task filters, and extends tests and docs to cover DB and Inspect backends. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as User
participant View as Django View
participant Interface as CeleryTasksInterface
participant Backend as Tasks Backend
participant InspectAPI as Celery Inspect API
participant Template as Template Renderer
Client->>View: GET /tasks?filter=active&search=foo
View->>Interface: get_tasks(search_query="foo", filter_type="active")
Interface->>Backend: get_tasks(search_query="foo", filter_type="active")
alt Database backend
Backend->>Backend: DB query (status filter + Q(name|id) search)
Backend->>Backend: paginate & format TaskListPage
else Inspect backend
Backend->>InspectAPI: inspect.active()/inspect.reserved()/inspect.scheduled()
InspectAPI-->>Backend: worker task dicts
Backend->>Backend: in-memory filter/search, paginate, format TaskListPage
end
Backend-->>Interface: TaskListPage
Interface-->>View: TaskListPage
View->>View: get_available_filters(), get_default_filter(), get_backend_info()
View->>Template: render(context with backend_info, task_filters, current_filter)
Template-->>Client: HTML (includes backend info and optional filter UI)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
dj_celery_panel/views.py (1)
101-157: Pass effective filter to template to maintain consistency across pagination and empty state messages.The filter sidebar correctly marks the applied filter as selected using
effective_filter, but pagination links, the search form, and empty state messaging usecurrent_filter(the raw query param). When a backend definesDEFAULT_FILTER(e.g.,CeleryTasksInspectBackendwithDEFAULT_FILTER="active"), pagination links fail to preserve the effective filter across page changes because they only includecurrent_filter.🔧 Suggested fix
- filter_type = request.GET.get("filter", None) + filter_param_present = "filter" in request.GET + filter_type = request.GET.get("filter") if filter_param_present else None - effective_filter = ( - filter_type if filter_type is not None else task_interface.get_default_filter() - ) + if filter_param_present and (filter_type == "" or filter_type is None): + effective_filter = None + elif filter_type is None: + effective_filter = task_interface.get_default_filter() + else: + effective_filter = filter_type ... - "current_filter": filter_type, + "current_filter": effective_filter, + "filter_param_present": filter_param_present,
🤖 Fix all issues with AI agents
In `@dj_celery_panel/celery_utils/tasks.py`:
- Around line 249-337: In get_tasks, validate the incoming filter_type (e.g.,
only accept the supported "active" or fall back to DEFAULT_FILTER) and enforce
pagination bounds: ensure per_page and page are positive integers (clamp
per_page to at least 1 and page to at least 1), compute total_pages and then
clamp page to be no greater than total_pages (or adjust slicing logic
accordingly) so negative/zero page values or per_page do not produce unexpected
slices; update only the get_tasks function to perform these checks before
calling inspect and computing start_idx/end_idx.
In `@dj_celery_panel/templates/admin/dj_celery_panel/tasks.html`:
- Around line 114-121: The pagination and filter link hrefs in the template use
unescaped user input (search_query, current_filter, and filter.value) which can
break URLs; update all hrefs that build query strings (the pagination anchors
referencing page/previous_page/next_page/total_pages and the filter sidebar
links) to URL-encode these variables by applying the template filter (e.g., use
search_query|urlencode, current_filter|urlencode, and filter.value|urlencode) so
every user-provided value is safely encoded in the query string.
- Around line 34-36: Track presence of the filter parameter and treat empty
string as an explicit "no filter": add filter_param_present = "filter" in
request.GET to the view context, update the template snippets that currently use
{% if current_filter %} to {% if filter_param_present %} so the hidden input is
rendered even for an empty filter, change any occurrences of `{% if filter.value
%}` to `{% if filter.value != None %}` to allow empty-string values to pass
through, and in the backend code that checks filter_type replace `if
filter_type:` with `if filter_type is not None:` so an explicit empty filter is
honored instead of falling back to DEFAULT_FILTER (apply the same updates to the
second block around lines 160-166).
In `@docs/configuration.md`:
- Around line 167-170: The example class CustomTasksBackend in the docs does not
inherit the stated base class CeleryAbstractInterface; update the snippet so
CustomTasksBackend subclasses CeleryAbstractInterface (or adjust the prose to no
longer claim it should extend CeleryAbstractInterface). Locate the example
definition for CustomTasksBackend in the doc and either change its declaration
to "class CustomTasksBackend(CeleryAbstractInterface):" or modify the
surrounding text to remove/clarify the inheritance requirement.
| def get_tasks( | ||
| self, search_query=None, page=1, per_page=50, filter_type=None | ||
| ) -> TaskListPage: | ||
| """ | ||
| Get active tasks from Celery inspect API. | ||
|
|
||
| Args: | ||
| search_query: Optional search query to filter by task name or task ID | ||
| page: Page number for pagination | ||
| per_page: Number of tasks per page | ||
| filter_type: Type of tasks to retrieve (only "active" supported). | ||
| Should always be provided by the interface using DEFAULT_FILTER. | ||
|
|
||
| Returns: | ||
| TaskListPage with task information | ||
| """ | ||
| try: | ||
| all_tasks = [] | ||
|
|
||
| # Get active tasks from all workers using CeleryInspector | ||
| try: | ||
| inspect = self.app.control.inspect() | ||
| active_tasks = inspect.active() | ||
| if active_tasks: | ||
| for worker, tasks in active_tasks.items(): | ||
| for task in tasks: | ||
| task["worker"] = worker | ||
| task["state"] = "ACTIVE" | ||
| all_tasks.append(task) | ||
| except Exception as e: | ||
| # Log but don't fail - workers might be temporarily unavailable | ||
| import logging | ||
|
|
||
| logging.warning(f"Failed to get active tasks: {e}") | ||
|
|
||
| # Apply search filter (search by both task name and task ID) | ||
| if search_query: | ||
| search_lower = search_query.lower() | ||
| all_tasks = [ | ||
| task | ||
| for task in all_tasks | ||
| if search_lower in task.get("name", "").lower() | ||
| or search_lower in task.get("id", "").lower() | ||
| ] | ||
|
|
||
| # Calculate pagination | ||
| total_count = len(all_tasks) | ||
| total_pages = (total_count + per_page - 1) // per_page | ||
| start_idx = (page - 1) * per_page | ||
| end_idx = start_idx + per_page | ||
| paginated_tasks = all_tasks[start_idx:end_idx] | ||
|
|
||
| # Format tasks | ||
| formatted_tasks = [] | ||
| for task in paginated_tasks: | ||
| formatted_task = { | ||
| "id": task.get("id", "N/A"), | ||
| "name": task.get("name", "Unknown"), | ||
| "status": task.get("state", "UNKNOWN"), | ||
| "worker": task.get("worker"), | ||
| "args": task.get("args"), | ||
| "kwargs": task.get("kwargs"), | ||
| "date_created": None, # Not available in inspect API | ||
| "date_done": None, | ||
| "date_started": None, | ||
| "result": None, | ||
| } | ||
|
|
||
| # Add ETA for scheduled tasks | ||
| if "eta" in task: | ||
| formatted_task["eta"] = task["eta"] | ||
|
|
||
| # Add time_start for active tasks | ||
| if "time_start" in task: | ||
| formatted_task["time_start"] = task["time_start"] | ||
|
|
||
| formatted_tasks.append(formatted_task) | ||
|
|
||
| return TaskListPage( | ||
| tasks=formatted_tasks, | ||
| total_count=total_count, | ||
| page=page, | ||
| per_page=per_page, | ||
| total_pages=total_pages, | ||
| has_previous=page > 1, | ||
| has_next=page < total_pages, | ||
| previous_page=page - 1 if page > 1 else None, | ||
| next_page=page + 1 if page < total_pages else None, | ||
| ) |
There was a problem hiding this comment.
Validate filter_type and clamp pagination bounds.
filter_type is currently ignored and negative/zero page values can yield unexpected slicing. A small guard makes behavior consistent and avoids confusing UI states.
✅ Suggested guard rails
def get_tasks(
self, search_query=None, page=1, per_page=50, filter_type=None
) -> TaskListPage:
"""
Get active tasks from Celery inspect API.
@@
"""
try:
+ # Normalize pagination
+ if per_page <= 0:
+ per_page = 50
+ if page < 1:
+ page = 1
+
+ # Enforce supported filter(s)
+ if filter_type is not None and filter_type != "active":
+ return TaskListPage(
+ tasks=[],
+ total_count=0,
+ page=page,
+ per_page=per_page,
+ total_pages=0,
+ error="Unsupported filter for inspect backend.",
+ )
all_tasks = []🧰 Tools
🪛 Ruff (0.14.13)
250-250: Unused method argument: filter_type
(ARG002)
278-278: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
In `@dj_celery_panel/celery_utils/tasks.py` around lines 249 - 337, In get_tasks,
validate the incoming filter_type (e.g., only accept the supported "active" or
fall back to DEFAULT_FILTER) and enforce pagination bounds: ensure per_page and
page are positive integers (clamp per_page to at least 1 and page to at least
1), compute total_pages and then clamp page to be no greater than total_pages
(or adjust slicing logic accordingly) so negative/zero page values or per_page
do not produce unexpected slices; update only the get_tasks function to perform
these checks before calling inspect and computing start_idx/end_idx.
| {% if current_filter %} | ||
| <input type="hidden" name="filter" value="{{ current_filter }}"> | ||
| {% endif %} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the template file and examine its structure
find . -name "tasks.html" -type f | head -5Repository: yassi/dj-celery-panel
Length of output: 125
🏁 Script executed:
# Check the overall repository structure
git ls-files | grep -E "(template|view|celery)" | head -20Repository: yassi/dj-celery-panel
Length of output: 1044
🏁 Script executed:
# Read the tasks.html file to examine the filter logic
cat -n ./dj_celery_panel/templates/admin/dj_celery_panel/tasks.html | head -180Repository: yassi/dj-celery-panel
Length of output: 10137
🏁 Script executed:
# Look for view files that might handle the tasks view
find . -name "*.py" -type f | xargs grep -l "tasks.html" | head -10Repository: yassi/dj-celery-panel
Length of output: 91
🏁 Script executed:
# Read the views.py file to understand filter handling
cat -n ./dj_celery_panel/views.pyRepository: yassi/dj-celery-panel
Length of output: 11644
🏁 Script executed:
# Look for filter-related code in celery_utils
grep -r "get_available_filters\|get_default_filter\|filter_type" dj_celery_panel/celery_utils/ -A 5 -B 2Repository: yassi/dj-celery-panel
Length of output: 4638
🏁 Script executed:
# Check the CeleryTasksInterface class specifically
cat -n ./dj_celery_panel/celery_utils/tasks.py | head -150Repository: yassi/dj-celery-panel
Length of output: 5944
🏁 Script executed:
# Continue reading the tasks.py file
tail -n +150 ./dj_celery_panel/celery_utils/tasks.py | head -100Repository: yassi/dj-celery-panel
Length of output: 3466
🏁 Script executed:
# Search for DEFAULT_FILTER definitions in other backends
grep -r "DEFAULT_FILTER" dj_celery_panel/celery_utils/Repository: yassi/dj-celery-panel
Length of output: 561
🏁 Script executed:
# Check if filter_param_present is used anywhere in the codebase
grep -r "filter_param_present" dj_celery_panel/Repository: yassi/dj-celery-panel
Length of output: 47
🏁 Script executed:
# Verify the AVAILABLE_FILTERS structure and "All" filter value
grep -A 10 "AVAILABLE_FILTERS" dj_celery_panel/celery_utils/tasks.pyRepository: yassi/dj-celery-panel
Length of output: 1004
🏁 Script executed:
# Check what happens when filter_type is an empty string in the backend
grep -B 5 -A 10 "if filter_type:" dj_celery_panel/celery_utils/tasks.pyRepository: yassi/dj-celery-panel
Length of output: 584
🏁 Script executed:
# Look at the request.GET handling to understand empty vs missing filter param
grep -B 5 -A 15 "def tasks" dj_celery_panel/views.py | head -40Repository: yassi/dj-celery-panel
Length of output: 599
🏁 Script executed:
# Check if there's any other handling of empty filter strings
grep -r "filter.*=.*\"\"" dj_celery_panel/ --include="*.py"Repository: yassi/dj-celery-panel
Length of output: 47
🏁 Script executed:
# Verify the issue: when user clicks the "All" filter link, what URL is generated?
# Look at line 165 more carefully
sed -n '157,170p' ./dj_celery_panel/templates/admin/dj_celery_panel/tasks.htmlRepository: yassi/dj-celery-panel
Length of output: 701
Implement filter parameter presence tracking to preserve empty filters and allow "All" to override backend defaults.
The current template logic gates on truthiness, which causes two issues:
-
Clicking "All" omits the
filterparameter entirely, allowing the backend'sDEFAULT_FILTERto take precedence even when explicitly selecting "All" (e.g., with the "active" backend, users cannot view all tasks). -
An explicit empty filter (
?filter=) is not persisted because the hidden input is gated on{% if current_filter %}, which is falsy for empty strings. The filter param is then dropped on subsequent searches.
Required changes:
- View must track whether the filter parameter was present: add
filter_param_present = "filter" in request.GETto context - Template must gate on parameter presence rather than truthiness: use
{% if filter_param_present %}instead of{% if current_filter %} - Ensure empty filter passes through: use
{% if filter.value != None %}instead of{% if filter.value %} - Backend must handle explicit empty filter: change
if filter_type:toif filter_type is not None:to treat empty string as "no filter"
Also applies to: lines 160-166
🤖 Prompt for AI Agents
In `@dj_celery_panel/templates/admin/dj_celery_panel/tasks.html` around lines 34 -
36, Track presence of the filter parameter and treat empty string as an explicit
"no filter": add filter_param_present = "filter" in request.GET to the view
context, update the template snippets that currently use {% if current_filter %}
to {% if filter_param_present %} so the hidden input is rendered even for an
empty filter, change any occurrences of `{% if filter.value %}` to `{% if
filter.value != None %}` to allow empty-string values to pass through, and in
the backend code that checks filter_type replace `if filter_type:` with `if
filter_type is not None:` so an explicit empty filter is honored instead of
falling back to DEFAULT_FILTER (apply the same updates to the second block
around lines 160-166).
| <a href="?page=1{% if search_query %}&search={{ search_query }}{% endif %}{% if current_filter %}&filter={{ current_filter }}{% endif %}">{% trans 'First' %}</a> | ||
| <a href="?page={{ previous_page }}{% if search_query %}&search={{ search_query }}{% endif %}{% if current_filter %}&filter={{ current_filter }}{% endif %}" class="prev">{% trans 'Previous' %}</a> | ||
| {% endif %} | ||
| <span class="this-page">{{ page }} {% trans 'of' %} {{ total_pages }}</span> | ||
| {% if has_next %} | ||
| <a href="?page={{ next_page }}{% if search_query %}&search={{ search_query }}{% endif %}" class="next">{% trans 'Next' %}</a> | ||
| <a href="?page={{ total_pages }}{% if search_query %}&search={{ search_query }}{% endif %}">{% trans 'Last' %}</a> | ||
| <a href="?page={{ next_page }}{% if search_query %}&search={{ search_query }}{% endif %}{% if current_filter %}&filter={{ current_filter }}{% endif %}" class="next">{% trans 'Next' %}</a> | ||
| <a href="?page={{ total_pages }}{% if search_query %}&search={{ search_query }}{% endif %}{% if current_filter %}&filter={{ current_filter }}{% endif %}">{% trans 'Last' %}</a> | ||
| {% endif %} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files | grep -E "tasks\.html"Repository: yassi/dj-celery-panel
Length of output: 191
🏁 Script executed:
fd "tasks.html" --type fRepository: yassi/dj-celery-panel
Length of output: 191
🏁 Script executed:
wc -l dj_celery_panel/templates/admin/dj_celery_panel/tasks.htmlRepository: yassi/dj-celery-panel
Length of output: 127
🏁 Script executed:
cat -n dj_celery_panel/templates/admin/dj_celery_panel/tasks.html | sed -n '100,130p'Repository: yassi/dj-celery-panel
Length of output: 2017
🏁 Script executed:
cat -n dj_celery_panel/templates/admin/dj_celery_panel/tasks.htmlRepository: yassi/dj-celery-panel
Length of output: 10137
🏁 Script executed:
rg "search_query|current_filter|filter\.value" dj_celery_panel/templates/admin/dj_celery_panel/tasks.htmlRepository: yassi/dj-celery-panel
Length of output: 1541
🌐 Web query:
Django urlencode filter template
💡 Result:
The Django template filter urlencode percent-encodes a value for use in a URL. By default '/' is treated as safe (not escaped); you can pass an optional string of characters that should NOT be escaped, or pass an empty string "" to escape every character.
Examples:
- Default: {{ value|urlencode }} — leaves '/' unescaped. [1]
- Escape everything (including /): {{ value|urlencode:"" }}. [1]
- You can apply urlencode to a URL produced by {% url %} by saving the url first and then filtering it: {% url 'name' arg as my_url %} {{ my_url|urlencode }} (or use {% filter urlencode %}...{% endfilter %}). [2]
References:
[1] Django docs — Built-in template tags and filters (urlencode).
[2] StackOverflow — encoding URL in Django template (using {% url ... as var %} and filter block).
URL-encode all query parameters in pagination and filter links.
Unescaped search_query, current_filter, and filter.value can break query strings when they contain &, =, or spaces. Apply |urlencode to every user-input variable inserted into href attributes:
- Pagination links (lines 114, 115, 119, 120):
search_queryandcurrent_filter - Filter sidebar links (lines 161, 165):
filter.valueandsearch_query
🔧 Example fix
-<a href="?page=1{% if search_query %}&search={{ search_query }}{% endif %}{% if current_filter %}&filter={{ current_filter }}{% endif %}">
+<a href="?page=1{% if search_query %}&search={{ search_query|urlencode }}{% endif %}{% if current_filter %}&filter={{ current_filter|urlencode }}{% endif %}">Apply the same pattern to all pagination links and filter sidebar links.
🤖 Prompt for AI Agents
In `@dj_celery_panel/templates/admin/dj_celery_panel/tasks.html` around lines 114
- 121, The pagination and filter link hrefs in the template use unescaped user
input (search_query, current_filter, and filter.value) which can break URLs;
update all hrefs that build query strings (the pagination anchors referencing
page/previous_page/next_page/total_pages and the filter sidebar links) to
URL-encode these variables by applying the template filter (e.g., use
search_query|urlencode, current_filter|urlencode, and filter.value|urlencode) so
every user-provided value is safely encoded in the query string.
| from dj_celery_panel.celery_utils import TaskListPage, TaskDetailPage | ||
|
|
||
| class CustomTasksBackend(CeleryAbstractInterface): | ||
| class CustomTasksBackend: | ||
| """ |
There was a problem hiding this comment.
Custom backend example doesn’t match the stated inheritance.
The section says to extend CeleryAbstractInterface, but the example class doesn’t. Either update the text or make the snippet inherit the base class for clarity.
📘 Suggested doc tweak
-from dj_celery_panel.celery_utils import TaskListPage, TaskDetailPage
+from dj_celery_panel.celery_utils import CeleryAbstractInterface, TaskListPage, TaskDetailPage
-class CustomTasksBackend:
+class CustomTasksBackend(CeleryAbstractInterface):📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| from dj_celery_panel.celery_utils import TaskListPage, TaskDetailPage | |
| class CustomTasksBackend(CeleryAbstractInterface): | |
| class CustomTasksBackend: | |
| """ | |
| from dj_celery_panel.celery_utils import CeleryAbstractInterface, TaskListPage, TaskDetailPage | |
| class CustomTasksBackend(CeleryAbstractInterface): | |
| """ |
🤖 Prompt for AI Agents
In `@docs/configuration.md` around lines 167 - 170, The example class
CustomTasksBackend in the docs does not inherit the stated base class
CeleryAbstractInterface; update the snippet so CustomTasksBackend subclasses
CeleryAbstractInterface (or adjust the prose to no longer claim it should extend
CeleryAbstractInterface). Locate the example definition for CustomTasksBackend
in the doc and either change its declaration to "class
CustomTasksBackend(CeleryAbstractInterface):" or modify the surrounding text to
remove/clarify the inheritance requirement.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@tests/test_tasks.py`:
- Around line 128-165: The test test_inspect_backend_displays_active_tasks is
brittle because it assumes order by indexing response.context["tasks"][0];
change assertions to be order-independent by finding the task dict with id
"task-123" (e.g., iterate or use a comprehension/search over
response.context["tasks"]) and assert its fields (name, status, worker) instead
of relying on positional indexing; update similar checks for other tasks to
match by id.
🧹 Nitpick comments (1)
tests/test_tasks.py (1)
249-267: Assert the “silent failure” expectation.The test comments say no warning should be shown, but there’s no assertion. Add one to lock the behavior.
✅ Suggested assertion
self.assertEqual(len(response.context["tasks"]), 0) self.assertEqual(response.context["total_count"], 0) # Backend is designed to fail silently for temporary worker issues # No warning message should be shown to avoid alarming users + messages = list(get_messages(response.wsgi_request)) + self.assertEqual(len(messages), 0)
| @override_settings( | ||
| DJ_CELERY_PANEL_SETTINGS={ | ||
| "tasks_backend": "dj_celery_panel.celery_utils.CeleryTasksInspectBackend" | ||
| } | ||
| ) | ||
| @patch("celery.app.control.Inspect.active") | ||
| def test_inspect_backend_displays_active_tasks(self, mock_active): | ||
| """Test that inspect backend displays active tasks from workers.""" | ||
| mock_active.return_value = { | ||
| "worker1@localhost": [ | ||
| { | ||
| "id": "task-123", | ||
| "name": "app.tasks.process_data", | ||
| "args": [1, 2, 3], | ||
| "kwargs": {"priority": "high"}, | ||
| "time_start": 1234567890.0, | ||
| }, | ||
| { | ||
| "id": "task-456", | ||
| "name": "app.tasks.send_email", | ||
| "args": ["user@example.com"], | ||
| "kwargs": {}, | ||
| "time_start": 1234567891.0, | ||
| }, | ||
| ] | ||
| } | ||
|
|
||
| response = self.client.get(reverse("dj_celery_panel:tasks")) | ||
|
|
||
| self.assertEqual(response.status_code, 200) | ||
| self.assertEqual(len(response.context["tasks"]), 2) | ||
| # Check first task | ||
| task1 = response.context["tasks"][0] | ||
| self.assertEqual(task1["id"], "task-123") | ||
| self.assertEqual(task1["name"], "app.tasks.process_data") | ||
| self.assertEqual(task1["status"], "ACTIVE") | ||
| self.assertEqual(task1["worker"], "worker1@localhost") | ||
|
|
There was a problem hiding this comment.
Avoid order-dependent assertions for active tasks.
If the backend reorders tasks (e.g., by time or worker), indexing [0] can be brittle. Prefer matching by ID.
🔧 Suggested tweak (order-independent)
- # Check first task
- task1 = response.context["tasks"][0]
+ # Check expected task without relying on ordering
+ task1 = next(t for t in response.context["tasks"] if t["id"] == "task-123")
self.assertEqual(task1["id"], "task-123")
self.assertEqual(task1["name"], "app.tasks.process_data")
self.assertEqual(task1["status"], "ACTIVE")
self.assertEqual(task1["worker"], "worker1@localhost")🤖 Prompt for AI Agents
In `@tests/test_tasks.py` around lines 128 - 165, The test
test_inspect_backend_displays_active_tasks is brittle because it assumes order
by indexing response.context["tasks"][0]; change assertions to be
order-independent by finding the task dict with id "task-123" (e.g., iterate or
use a comprehension/search over response.context["tasks"]) and assert its fields
(name, status, worker) instead of relying on positional indexing; update similar
checks for other tasks to match by id.
Summary by CodeRabbit
New Features
Improvements
Documentation
Tests
Chore
✏️ Tip: You can customize this high-level summary in your review settings.