From 009970f38d8c63e2e25a3a6f9ae628df174c3ab7 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Sat, 23 May 2026 22:08:47 +0800 Subject: [PATCH 1/4] Close lost Discord gig posts --- .../src/five08/discord_bot/cogs/jobs.py | 24 +++++++-- .../five08/discord_bot/utils/internal_api.py | 37 ++++++++++---- tests/unit/test_internal_api.py | 49 ++++++++++++++++++- tests/unit/test_jobs.py | 33 ++++++++++++- 4 files changed, 127 insertions(+), 16 deletions(-) diff --git a/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py b/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py index 9173eb39..1bcedc1c 100644 --- a/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py +++ b/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py @@ -2373,7 +2373,13 @@ async def _rename_gig_thread_for_status( base_title = stripped_title base_title = base_title.strip() or f"Discord gig {thread.id}" next_name = f"[{status_marker}] {base_title}"[:100] - if thread.name == next_name: + should_close_thread = status is EngagementStatus.LOST + needs_rename = thread.name != next_name + needs_close = should_close_thread and ( + not getattr(thread, "locked", False) + or not getattr(thread, "archived", False) + ) + if not needs_rename and not needs_close: return next_name if thread.guild is None or thread.guild.me is None: @@ -2382,9 +2388,12 @@ async def _rename_gig_thread_for_status( if not permissions.manage_threads: raise PermissionError("missing_manage_threads_permission") - if thread.archived: + if getattr(thread, "archived", False) and (needs_rename or needs_close): await thread.edit(archived=False, reason=reason) - await thread.edit(name=next_name, reason=reason) + if needs_rename: + await thread.edit(name=next_name, reason=reason) + if should_close_thread: + await thread.edit(locked=True, archived=True, reason=reason) return next_name @staticmethod @@ -3747,10 +3756,15 @@ async def update_gig_status( ) return + close_note = ( + " and closed this thread" + if normalized_status is EngagementStatus.LOST + else "" + ) await interaction.followup.send( "✅ Updated status to " - f"**{status_label(normalized_status)}** and renamed this thread to " - f"`{next_title}`.", + f"**{status_label(normalized_status)}**, renamed this thread to " + f"`{next_title}`{close_note}.", ephemeral=True, ) diff --git a/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py b/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py index db80cf9f..acc3ff0c 100644 --- a/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py +++ b/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py @@ -11,7 +11,11 @@ from pydantic import BaseModel, ValidationError from five08.discord_bot.config import settings -from five08.engagements import normalize_engagement_status, strip_status_from_title +from five08.engagements import ( + EngagementStatus, + normalize_engagement_status, + strip_status_from_title, +) logger = logging.getLogger(__name__) @@ -338,33 +342,48 @@ async def _update_gig_thread_status( base_title = strip_status_from_title(channel.name) or channel.name base_title = base_title.strip() or f"Discord gig {thread_id}" next_name = f"[{status_marker}] {base_title}"[:100] - if channel.name == next_name: + should_close_thread = normalized_status is EngagementStatus.LOST + needs_rename = channel.name != next_name + needs_close = should_close_thread and ( + not getattr(channel, "locked", False) + or not getattr(channel, "archived", False) + ) + if not needs_rename and not needs_close: return { "status": "unchanged", "thread_id": str(thread_id), "title": next_name, + "closed": should_close_thread, }, 200 try: - if channel.archived: + if getattr(channel, "archived", False) and (needs_rename or needs_close): await channel.edit( archived=False, reason="Dashboard gig status update", ) - await channel.edit( - name=next_name, - reason="Dashboard gig status update", - ) + if needs_rename: + await channel.edit( + name=next_name, + reason="Dashboard gig status update", + ) + if should_close_thread: + await channel.edit( + locked=True, + archived=True, + reason="Dashboard gig status update", + ) except discord.Forbidden: return {"error": "thread_rename_forbidden"}, 403 except discord.HTTPException as exc: - logger.warning("Failed renaming gig thread %s: %s", thread_id, exc) - return {"error": "thread_rename_failed"}, 502 + logger.warning("Failed updating gig thread %s: %s", thread_id, exc) + return {"error": "thread_update_failed"}, 502 return { "status": "updated", "thread_id": str(thread_id), "title": next_name, + "closed": should_close_thread, }, 200 async def gig_thread_status_handler(self, request: web.Request) -> web.Response: diff --git a/tests/unit/test_internal_api.py b/tests/unit/test_internal_api.py index 9c5808b7..563cd115 100644 --- a/tests/unit/test_internal_api.py +++ b/tests/unit/test_internal_api.py @@ -3,7 +3,7 @@ import asyncio from types import SimpleNamespace import json -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, call import discord import pytest @@ -162,6 +162,53 @@ def permissions_for(self, _member: object) -> SimpleNamespace: reason="Dashboard gig status update", ) + @pytest.mark.asyncio + async def test_update_gig_thread_status_closes_lost_thread( + self, internal_api_routes, monkeypatch: pytest.MonkeyPatch + ): + """Dashboard lost status changes should lock and archive the Discord thread.""" + + class FakeThread: + id = 123 + name = "[RECRUITING] Old gig" + archived = False + locked = False + guild = SimpleNamespace(me=object()) + + def __init__(self) -> None: + self.edit = AsyncMock() + + def permissions_for(self, _member: object) -> SimpleNamespace: + return SimpleNamespace( + manage_threads=True, + view_channel=True, + send_messages_in_threads=True, + ) + + thread = FakeThread() + monkeypatch.setattr( + "five08.discord_bot.utils.internal_api.discord.Thread", + FakeThread, + ) + internal_api_routes.bot.get_channel.return_value = thread + + result, status_code = await internal_api_routes._update_gig_thread_status( + GigThreadStatusRequest(thread_id="123", status="lost") + ) + + assert status_code == 200 + assert result["status"] == "updated" + assert result["title"] == "[LOST] Old gig" + assert result["closed"] is True + assert thread.edit.await_args_list == [ + call(name="[LOST] Old gig", reason="Dashboard gig status update"), + call( + locked=True, + archived=True, + reason="Dashboard gig status update", + ), + ] + @pytest.mark.asyncio async def test_update_gig_thread_status_reports_missing_manage_threads( self, internal_api_routes, monkeypatch: pytest.MonkeyPatch diff --git a/tests/unit/test_jobs.py b/tests/unit/test_jobs.py index 1a1660e4..b4947ea2 100644 --- a/tests/unit/test_jobs.py +++ b/tests/unit/test_jobs.py @@ -4,7 +4,7 @@ import asyncio from datetime import datetime, timedelta, timezone -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, call from types import SimpleNamespace from unittest.mock import patch @@ -176,6 +176,37 @@ def permissions_for(self, _member: object) -> SimpleNamespace: ) +def test_rename_gig_thread_for_lost_status_locks_and_archives() -> None: + class FakeThread: + id = 200 + name = "[RECRUITING] Need help" + archived = False + locked = False + guild = SimpleNamespace(me=object()) + + def __init__(self) -> None: + self.edit = AsyncMock() + + def permissions_for(self, _member: object) -> SimpleNamespace: + return SimpleNamespace(manage_threads=True) + + thread = FakeThread() + + result = asyncio.run( + JobsCog._rename_gig_thread_for_status( + thread, + EngagementStatus.LOST, + reason="test", + ) + ) + + assert result == "[LOST] Need help" + assert thread.edit.await_args_list == [ + call(name="[LOST] Need help", reason="test"), + call(locked=True, archived=True, reason="test"), + ] + + def test_build_match_candidate_lines_handles_non_string_names() -> None: candidate = _make_candidate( is_member=True, From 01276af05d07f518638d7077e44c1721ffe24d58 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Sat, 23 May 2026 22:18:58 +0800 Subject: [PATCH 2/4] Address lost gig thread review comments --- .../src/five08/discord_bot/cogs/jobs.py | 18 ++++--- .../five08/discord_bot/utils/internal_api.py | 24 ++++++---- tests/unit/test_internal_api.py | 47 +++++++++++++++++++ tests/unit/test_jobs.py | 31 ++++++++++++ 4 files changed, 105 insertions(+), 15 deletions(-) diff --git a/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py b/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py index 1bcedc1c..5ad7c911 100644 --- a/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py +++ b/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py @@ -2374,12 +2374,14 @@ async def _rename_gig_thread_for_status( base_title = base_title.strip() or f"Discord gig {thread.id}" next_name = f"[{status_marker}] {base_title}"[:100] should_close_thread = status is EngagementStatus.LOST + is_locked = bool(getattr(thread, "locked", False)) + is_archived = bool(getattr(thread, "archived", False)) needs_rename = thread.name != next_name - needs_close = should_close_thread and ( - not getattr(thread, "locked", False) - or not getattr(thread, "archived", False) - ) - if not needs_rename and not needs_close: + needs_reopen = not should_close_thread and (is_locked or is_archived) + needs_close = should_close_thread and (not is_locked or not is_archived) + needs_unarchive_for_rename = needs_rename and is_archived + needs_restore_closed = should_close_thread and needs_unarchive_for_rename + if not needs_rename and not needs_close and not needs_reopen: return next_name if thread.guild is None or thread.guild.me is None: @@ -2388,11 +2390,13 @@ async def _rename_gig_thread_for_status( if not permissions.manage_threads: raise PermissionError("missing_manage_threads_permission") - if getattr(thread, "archived", False) and (needs_rename or needs_close): + if needs_reopen: + await thread.edit(locked=False, archived=False, reason=reason) + elif needs_unarchive_for_rename: await thread.edit(archived=False, reason=reason) if needs_rename: await thread.edit(name=next_name, reason=reason) - if should_close_thread: + if needs_close or needs_restore_closed: await thread.edit(locked=True, archived=True, reason=reason) return next_name diff --git a/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py b/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py index acc3ff0c..8483ef3f 100644 --- a/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py +++ b/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py @@ -343,12 +343,14 @@ async def _update_gig_thread_status( base_title = base_title.strip() or f"Discord gig {thread_id}" next_name = f"[{status_marker}] {base_title}"[:100] should_close_thread = normalized_status is EngagementStatus.LOST + is_locked = bool(getattr(channel, "locked", False)) + is_archived = bool(getattr(channel, "archived", False)) needs_rename = channel.name != next_name - needs_close = should_close_thread and ( - not getattr(channel, "locked", False) - or not getattr(channel, "archived", False) - ) - if not needs_rename and not needs_close: + needs_reopen = not should_close_thread and (is_locked or is_archived) + needs_close = should_close_thread and (not is_locked or not is_archived) + needs_unarchive_for_rename = needs_rename and is_archived + needs_restore_closed = should_close_thread and needs_unarchive_for_rename + if not needs_rename and not needs_close and not needs_reopen: return { "status": "unchanged", "thread_id": str(thread_id), @@ -357,7 +359,13 @@ async def _update_gig_thread_status( }, 200 try: - if getattr(channel, "archived", False) and (needs_rename or needs_close): + if needs_reopen: + await channel.edit( + locked=False, + archived=False, + reason="Dashboard gig status update", + ) + elif needs_unarchive_for_rename: await channel.edit( archived=False, reason="Dashboard gig status update", @@ -367,14 +375,14 @@ async def _update_gig_thread_status( name=next_name, reason="Dashboard gig status update", ) - if should_close_thread: + if needs_close or needs_restore_closed: await channel.edit( locked=True, archived=True, reason="Dashboard gig status update", ) except discord.Forbidden: - return {"error": "thread_rename_forbidden"}, 403 + return {"error": "thread_update_forbidden"}, 403 except discord.HTTPException as exc: logger.warning("Failed updating gig thread %s: %s", thread_id, exc) return {"error": "thread_update_failed"}, 502 diff --git a/tests/unit/test_internal_api.py b/tests/unit/test_internal_api.py index 563cd115..13c35690 100644 --- a/tests/unit/test_internal_api.py +++ b/tests/unit/test_internal_api.py @@ -209,6 +209,53 @@ def permissions_for(self, _member: object) -> SimpleNamespace: ), ] + @pytest.mark.asyncio + async def test_update_gig_thread_status_reopens_non_lost_thread( + self, internal_api_routes, monkeypatch: pytest.MonkeyPatch + ): + """Moving a closed lost thread away from lost should make it usable again.""" + + class FakeThread: + id = 123 + name = "[LOST] Old gig" + archived = True + locked = True + guild = SimpleNamespace(me=object()) + + def __init__(self) -> None: + self.edit = AsyncMock() + + def permissions_for(self, _member: object) -> SimpleNamespace: + return SimpleNamespace( + manage_threads=True, + view_channel=True, + send_messages_in_threads=True, + ) + + thread = FakeThread() + monkeypatch.setattr( + "five08.discord_bot.utils.internal_api.discord.Thread", + FakeThread, + ) + internal_api_routes.bot.get_channel.return_value = thread + + result, status_code = await internal_api_routes._update_gig_thread_status( + GigThreadStatusRequest(thread_id="123", status="recruiting") + ) + + assert status_code == 200 + assert result["status"] == "updated" + assert result["title"] == "[RECRUITING] Old gig" + assert result["closed"] is False + assert thread.edit.await_args_list == [ + call( + locked=False, + archived=False, + reason="Dashboard gig status update", + ), + call(name="[RECRUITING] Old gig", reason="Dashboard gig status update"), + ] + @pytest.mark.asyncio async def test_update_gig_thread_status_reports_missing_manage_threads( self, internal_api_routes, monkeypatch: pytest.MonkeyPatch diff --git a/tests/unit/test_jobs.py b/tests/unit/test_jobs.py index b4947ea2..1582e4ac 100644 --- a/tests/unit/test_jobs.py +++ b/tests/unit/test_jobs.py @@ -207,6 +207,37 @@ def permissions_for(self, _member: object) -> SimpleNamespace: ] +def test_rename_gig_thread_for_non_lost_status_reopens_thread() -> None: + class FakeThread: + id = 200 + name = "[LOST] Need help" + archived = True + locked = True + guild = SimpleNamespace(me=object()) + + def __init__(self) -> None: + self.edit = AsyncMock() + + def permissions_for(self, _member: object) -> SimpleNamespace: + return SimpleNamespace(manage_threads=True) + + thread = FakeThread() + + result = asyncio.run( + JobsCog._rename_gig_thread_for_status( + thread, + EngagementStatus.RECRUITING, + reason="test", + ) + ) + + assert result == "[RECRUITING] Need help" + assert thread.edit.await_args_list == [ + call(locked=False, archived=False, reason="test"), + call(name="[RECRUITING] Need help", reason="test"), + ] + + def test_build_match_candidate_lines_handles_non_string_names() -> None: candidate = _make_candidate( is_member=True, From ea288c05cfed42238e45acdb5d9ff789cc3d8ceb Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Sun, 24 May 2026 02:47:48 +0800 Subject: [PATCH 3/4] Preserve moderated gig thread locks --- .../src/five08/discord_bot/cogs/jobs.py | 5 ++- .../five08/discord_bot/utils/internal_api.py | 6 ++- tests/unit/test_internal_api.py | 41 +++++++++++++++++++ tests/unit/test_jobs.py | 28 +++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py b/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py index 5ad7c911..ba199ab3 100644 --- a/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py +++ b/apps/discord_bot/src/five08/discord_bot/cogs/jobs.py @@ -2374,10 +2374,13 @@ async def _rename_gig_thread_for_status( base_title = base_title.strip() or f"Discord gig {thread.id}" next_name = f"[{status_marker}] {base_title}"[:100] should_close_thread = status is EngagementStatus.LOST + was_lost_thread = parse_status_from_title(raw_title) is EngagementStatus.LOST is_locked = bool(getattr(thread, "locked", False)) is_archived = bool(getattr(thread, "archived", False)) needs_rename = thread.name != next_name - needs_reopen = not should_close_thread and (is_locked or is_archived) + needs_reopen = ( + not should_close_thread and was_lost_thread and (is_locked or is_archived) + ) needs_close = should_close_thread and (not is_locked or not is_archived) needs_unarchive_for_rename = needs_rename and is_archived needs_restore_closed = should_close_thread and needs_unarchive_for_rename diff --git a/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py b/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py index 8483ef3f..252f3e6a 100644 --- a/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py +++ b/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py @@ -14,6 +14,7 @@ from five08.engagements import ( EngagementStatus, normalize_engagement_status, + parse_status_from_title, strip_status_from_title, ) @@ -343,10 +344,13 @@ async def _update_gig_thread_status( base_title = base_title.strip() or f"Discord gig {thread_id}" next_name = f"[{status_marker}] {base_title}"[:100] should_close_thread = normalized_status is EngagementStatus.LOST + was_lost_thread = parse_status_from_title(channel.name) is EngagementStatus.LOST is_locked = bool(getattr(channel, "locked", False)) is_archived = bool(getattr(channel, "archived", False)) needs_rename = channel.name != next_name - needs_reopen = not should_close_thread and (is_locked or is_archived) + needs_reopen = ( + not should_close_thread and was_lost_thread and (is_locked or is_archived) + ) needs_close = should_close_thread and (not is_locked or not is_archived) needs_unarchive_for_rename = needs_rename and is_archived needs_restore_closed = should_close_thread and needs_unarchive_for_rename diff --git a/tests/unit/test_internal_api.py b/tests/unit/test_internal_api.py index 13c35690..af103c67 100644 --- a/tests/unit/test_internal_api.py +++ b/tests/unit/test_internal_api.py @@ -256,6 +256,47 @@ def permissions_for(self, _member: object) -> SimpleNamespace: call(name="[RECRUITING] Old gig", reason="Dashboard gig status update"), ] + @pytest.mark.asyncio + async def test_update_gig_thread_status_preserves_moderator_lock( + self, internal_api_routes, monkeypatch: pytest.MonkeyPatch + ): + """Non-lost status sync should not clear locks unrelated to lost closure.""" + + class FakeThread: + id = 123 + name = "[RECRUITING] Old gig" + archived = False + locked = True + guild = SimpleNamespace(me=object()) + + def __init__(self) -> None: + self.edit = AsyncMock() + + def permissions_for(self, _member: object) -> SimpleNamespace: + return SimpleNamespace( + manage_threads=True, + view_channel=True, + send_messages_in_threads=True, + ) + + thread = FakeThread() + monkeypatch.setattr( + "five08.discord_bot.utils.internal_api.discord.Thread", + FakeThread, + ) + internal_api_routes.bot.get_channel.return_value = thread + + result, status_code = await internal_api_routes._update_gig_thread_status( + GigThreadStatusRequest(thread_id="123", status="filled") + ) + + assert status_code == 200 + assert result["title"] == "[FILLED] Old gig" + thread.edit.assert_awaited_once_with( + name="[FILLED] Old gig", + reason="Dashboard gig status update", + ) + @pytest.mark.asyncio async def test_update_gig_thread_status_reports_missing_manage_threads( self, internal_api_routes, monkeypatch: pytest.MonkeyPatch diff --git a/tests/unit/test_jobs.py b/tests/unit/test_jobs.py index 1582e4ac..e6253371 100644 --- a/tests/unit/test_jobs.py +++ b/tests/unit/test_jobs.py @@ -238,6 +238,34 @@ def permissions_for(self, _member: object) -> SimpleNamespace: ] +def test_rename_gig_thread_for_non_lost_status_preserves_moderator_lock() -> None: + class FakeThread: + id = 200 + name = "[RECRUITING] Need help" + archived = False + locked = True + guild = SimpleNamespace(me=object()) + + def __init__(self) -> None: + self.edit = AsyncMock() + + def permissions_for(self, _member: object) -> SimpleNamespace: + return SimpleNamespace(manage_threads=True) + + thread = FakeThread() + + result = asyncio.run( + JobsCog._rename_gig_thread_for_status( + thread, + EngagementStatus.FILLED, + reason="test", + ) + ) + + assert result == "[FILLED] Need help" + thread.edit.assert_awaited_once_with(name="[FILLED] Need help", reason="test") + + def test_build_match_candidate_lines_handles_non_string_names() -> None: candidate = _make_candidate( is_member=True, From 28f2982b654c34f11d752312f81c8c9f8a94b5d1 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Sun, 24 May 2026 02:55:48 +0800 Subject: [PATCH 4/4] Align dashboard gig title fallback --- .../five08/discord_bot/utils/internal_api.py | 12 +++++- tests/unit/test_internal_api.py | 41 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py b/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py index 252f3e6a..5db44779 100644 --- a/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py +++ b/apps/discord_bot/src/five08/discord_bot/utils/internal_api.py @@ -340,11 +340,19 @@ async def _update_gig_thread_status( **permission_payload, }, 403 - base_title = strip_status_from_title(channel.name) or channel.name + raw_title = str(channel.name or "").strip() + stripped_title = strip_status_from_title(raw_title) + if ( + parse_status_from_title(raw_title) is not EngagementStatus.UNKNOWN + and stripped_title == raw_title + ): + base_title = "" + else: + base_title = stripped_title base_title = base_title.strip() or f"Discord gig {thread_id}" next_name = f"[{status_marker}] {base_title}"[:100] should_close_thread = normalized_status is EngagementStatus.LOST - was_lost_thread = parse_status_from_title(channel.name) is EngagementStatus.LOST + was_lost_thread = parse_status_from_title(raw_title) is EngagementStatus.LOST is_locked = bool(getattr(channel, "locked", False)) is_archived = bool(getattr(channel, "archived", False)) needs_rename = channel.name != next_name diff --git a/tests/unit/test_internal_api.py b/tests/unit/test_internal_api.py index af103c67..35798cb0 100644 --- a/tests/unit/test_internal_api.py +++ b/tests/unit/test_internal_api.py @@ -162,6 +162,47 @@ def permissions_for(self, _member: object) -> SimpleNamespace: reason="Dashboard gig status update", ) + @pytest.mark.asyncio + async def test_update_gig_thread_status_uses_fallback_for_marker_only_title( + self, internal_api_routes, monkeypatch: pytest.MonkeyPatch + ): + """Dashboard status sync should not stack markers for marker-only titles.""" + + class FakeThread: + id = 123 + name = "[RECRUITING]" + archived = False + locked = False + guild = SimpleNamespace(me=object()) + + def __init__(self) -> None: + self.edit = AsyncMock() + + def permissions_for(self, _member: object) -> SimpleNamespace: + return SimpleNamespace( + manage_threads=True, + view_channel=True, + send_messages_in_threads=True, + ) + + thread = FakeThread() + monkeypatch.setattr( + "five08.discord_bot.utils.internal_api.discord.Thread", + FakeThread, + ) + internal_api_routes.bot.get_channel.return_value = thread + + result, status_code = await internal_api_routes._update_gig_thread_status( + GigThreadStatusRequest(thread_id="123", status="filled") + ) + + assert status_code == 200 + assert result["title"] == "[FILLED] Discord gig 123" + thread.edit.assert_awaited_once_with( + name="[FILLED] Discord gig 123", + reason="Dashboard gig status update", + ) + @pytest.mark.asyncio async def test_update_gig_thread_status_closes_lost_thread( self, internal_api_routes, monkeypatch: pytest.MonkeyPatch