From 2cd38d22ca6fc323003a9cc74e344cef2ce7b60d Mon Sep 17 00:00:00 2001 From: Ole Magnus Urdahl Date: Mon, 29 Jun 2026 14:09:17 +0200 Subject: [PATCH] Add assistant thread event and API support Introduce support for Slack assistant thread workflows across endpoints and HTTP client layers. This adds new event types/models, handler interfaces, middleware routing, and DI registration for `assistant_thread_started` and `assistant_thread_context_changed` events. It also extends `ISlackClient`/`SlackClient` with `assistant.threads.setStatus`, `assistant.threads.setTitle`, and `assistant.threads.setSuggestedPrompts`, including new request models and tests focused on correct snake_case JSON serialization expected by Slack. --- .../IHandleAssistantThreadContextChanged.cs | 8 ++ .../IHandleAssistantThreadStarted.cs | 8 ++ .../src/Slackbot.Net.Endpoints/EventTypes.cs | 2 + .../Hosting/IAppBuilderExtensions.cs | 2 + .../Hosting/ISlackbotHandlersBuilder.cs | 2 + .../Hosting/SlackBotHandlersBuilder.cs | 14 ++ .../AssistantThreadContextChangedEvents.cs | 41 ++++++ .../AssistantThreadStartedEvents.cs | 41 ++++++ .../Middlewares/HttpItemsManager.cs | 4 + .../AssistantThreadContextChangedEvent.cs | 7 + .../Events/AssistantThreadStartedEvent.cs | 22 +++ .../ISlackClient.cs | 24 ++++ .../AssistantThreadsSetStatusRequest.cs | 8 ++ ...istantThreadsSetSuggestedPromptsRequest.cs | 15 ++ .../AssistantThreadsSetTitleRequest.cs | 8 ++ .../SlackClient.cs | 19 +++ .../AssistantThreadsTests.cs | 134 ++++++++++++++++++ 17 files changed, 359 insertions(+) create mode 100644 source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAssistantThreadContextChanged.cs create mode 100644 source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAssistantThreadStarted.cs create mode 100644 source/src/Slackbot.Net.Endpoints/Middlewares/AssistantThreadContextChangedEvents.cs create mode 100644 source/src/Slackbot.Net.Endpoints/Middlewares/AssistantThreadStartedEvents.cs create mode 100644 source/src/Slackbot.Net.Endpoints/Models/Events/AssistantThreadContextChangedEvent.cs create mode 100644 source/src/Slackbot.Net.Endpoints/Models/Events/AssistantThreadStartedEvent.cs create mode 100644 source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetStatus/AssistantThreadsSetStatusRequest.cs create mode 100644 source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetSuggestedPrompts/AssistantThreadsSetSuggestedPromptsRequest.cs create mode 100644 source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetTitle/AssistantThreadsSetTitleRequest.cs create mode 100644 source/test/Slackbot.Net.SlackClients.Http.Tests/AssistantThreadsTests.cs diff --git a/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAssistantThreadContextChanged.cs b/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAssistantThreadContextChanged.cs new file mode 100644 index 0000000..57ecab1 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAssistantThreadContextChanged.cs @@ -0,0 +1,8 @@ +using Slackbot.Net.Endpoints.Models.Events; + +namespace Slackbot.Net.Endpoints.Abstractions; + +public interface IHandleAssistantThreadContextChanged +{ + Task Handle(EventMetaData eventMetadata, AssistantThreadContextChangedEvent slackEvent); +} diff --git a/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAssistantThreadStarted.cs b/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAssistantThreadStarted.cs new file mode 100644 index 0000000..5474ac4 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAssistantThreadStarted.cs @@ -0,0 +1,8 @@ +using Slackbot.Net.Endpoints.Models.Events; + +namespace Slackbot.Net.Endpoints.Abstractions; + +public interface IHandleAssistantThreadStarted +{ + Task Handle(EventMetaData eventMetadata, AssistantThreadStartedEvent slackEvent); +} diff --git a/source/src/Slackbot.Net.Endpoints/EventTypes.cs b/source/src/Slackbot.Net.Endpoints/EventTypes.cs index 2e594da..a670b3f 100644 --- a/source/src/Slackbot.Net.Endpoints/EventTypes.cs +++ b/source/src/Slackbot.Net.Endpoints/EventTypes.cs @@ -11,4 +11,6 @@ public static class EventTypes public const string EmojiChanged = "emoji_changed"; public const string Message = "message"; public const string ReactionAdded = "reaction_added"; + public const string AssistantThreadStarted = "assistant_thread_started"; + public const string AssistantThreadContextChanged = "assistant_thread_context_changed"; } diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs b/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs index 13618d1..65738b5 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs @@ -26,6 +26,8 @@ public static IApplicationBuilder UseSlackbot( app.MapWhen(EmojiChangedEvents.ShouldRun, b => b.UseMiddleware()); app.MapWhen(MessageEvents.ShouldRun, b => b.UseMiddleware()); app.MapWhen(ReactionAddedEvents.ShouldRun, b => b.UseMiddleware()); + app.MapWhen(AssistantThreadStartedEvents.ShouldRun, b => b.UseMiddleware()); + app.MapWhen(AssistantThreadContextChangedEvents.ShouldRun, b => b.UseMiddleware()); return app; } diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs b/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs index 658eb37..4b545b1 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs @@ -20,4 +20,6 @@ public ISlackbotHandlersBuilder AddInteractiveBlockActionsHandler() public ISlackbotHandlersBuilder AddEmojiChangedHandler() where T : class, IHandleEmojiChanged; public ISlackbotHandlersBuilder AddMessageHandler() where T : class, IHandleMessage; public ISlackbotHandlersBuilder AddReactionAddedHandler() where T : class, IHandleReactionAdded; + public ISlackbotHandlersBuilder AddAssistantThreadStartedHandler() where T : class, IHandleAssistantThreadStarted; + public ISlackbotHandlersBuilder AddAssistantThreadContextChangedHandler() where T : class, IHandleAssistantThreadContextChanged; } diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs b/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs index 4d86790..d96afa3 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs @@ -77,4 +77,18 @@ public ISlackbotHandlersBuilder AddReactionAddedHandler() where T : class, IH services.AddSingleton(); return this; } + + public ISlackbotHandlersBuilder AddAssistantThreadStartedHandler() + where T : class, IHandleAssistantThreadStarted + { + services.AddSingleton(); + return this; + } + + public ISlackbotHandlersBuilder AddAssistantThreadContextChangedHandler() + where T : class, IHandleAssistantThreadContextChanged + { + services.AddSingleton(); + return this; + } } diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/AssistantThreadContextChangedEvents.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/AssistantThreadContextChangedEvents.cs new file mode 100644 index 0000000..7181372 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/AssistantThreadContextChangedEvents.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Slackbot.Net.Endpoints.Abstractions; +using Slackbot.Net.Endpoints.Models.Events; + +namespace Slackbot.Net.Endpoints.Middlewares; + +public class AssistantThreadContextChangedEvents( + RequestDelegate next, + ILogger logger, + IEnumerable responseHandlers +) +{ + public async Task Invoke(HttpContext context) + { + var assistantEvent = (AssistantThreadContextChangedEvent)context.Items[HttpItemKeys.SlackEventKey]; + var metadata = (EventMetaData)context.Items[HttpItemKeys.EventMetadataKey]; + + foreach (var handler in responseHandlers) + { + try + { + logger.LogInformation("Handling using {HandlerType}", handler.GetType()); + var response = await handler.Handle(metadata, assistantEvent); + logger.LogInformation("Handler response: {Response}", response.Response); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + } + } + + context.Response.StatusCode = 200; + } + + public static bool ShouldRun(HttpContext ctx) + { + return ctx.Items.ContainsKey(HttpItemKeys.EventTypeKey) + && ctx.Items[HttpItemKeys.EventTypeKey].ToString() == EventTypes.AssistantThreadContextChanged; + } +} diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/AssistantThreadStartedEvents.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/AssistantThreadStartedEvents.cs new file mode 100644 index 0000000..fc8a492 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/AssistantThreadStartedEvents.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Slackbot.Net.Endpoints.Abstractions; +using Slackbot.Net.Endpoints.Models.Events; + +namespace Slackbot.Net.Endpoints.Middlewares; + +public class AssistantThreadStartedEvents( + RequestDelegate next, + ILogger logger, + IEnumerable responseHandlers +) +{ + public async Task Invoke(HttpContext context) + { + var assistantEvent = (AssistantThreadStartedEvent)context.Items[HttpItemKeys.SlackEventKey]; + var metadata = (EventMetaData)context.Items[HttpItemKeys.EventMetadataKey]; + + foreach (var handler in responseHandlers) + { + try + { + logger.LogInformation("Handling using {HandlerType}", handler.GetType()); + var response = await handler.Handle(metadata, assistantEvent); + logger.LogInformation("Handler response: {Response}", response.Response); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + } + } + + context.Response.StatusCode = 200; + } + + public static bool ShouldRun(HttpContext ctx) + { + return ctx.Items.ContainsKey(HttpItemKeys.EventTypeKey) + && ctx.Items[HttpItemKeys.EventTypeKey].ToString() == EventTypes.AssistantThreadStarted; + } +} diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs index 7554b30..4a8bcbb 100644 --- a/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs @@ -83,6 +83,10 @@ private static SlackEvent ToEventType(JsonElement eventJson, string raw) return JsonSerializer.Deserialize(json, WebOptions); case EventTypes.ReactionAdded: return JsonSerializer.Deserialize(json, WebOptions); + case EventTypes.AssistantThreadStarted: + return JsonSerializer.Deserialize(json, WebOptions); + case EventTypes.AssistantThreadContextChanged: + return JsonSerializer.Deserialize(json, WebOptions); default: var unknownSlackEvent = JsonSerializer.Deserialize(json, WebOptions); unknownSlackEvent.RawJson = raw; diff --git a/source/src/Slackbot.Net.Endpoints/Models/Events/AssistantThreadContextChangedEvent.cs b/source/src/Slackbot.Net.Endpoints/Models/Events/AssistantThreadContextChangedEvent.cs new file mode 100644 index 0000000..066f2c1 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Models/Events/AssistantThreadContextChangedEvent.cs @@ -0,0 +1,7 @@ +namespace Slackbot.Net.Endpoints.Models.Events; + +public class AssistantThreadContextChangedEvent : SlackEvent +{ + public AssistantThread Assistant_Thread { get; set; } + public string Event_Ts { get; set; } +} diff --git a/source/src/Slackbot.Net.Endpoints/Models/Events/AssistantThreadStartedEvent.cs b/source/src/Slackbot.Net.Endpoints/Models/Events/AssistantThreadStartedEvent.cs new file mode 100644 index 0000000..7bad6cc --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Models/Events/AssistantThreadStartedEvent.cs @@ -0,0 +1,22 @@ +namespace Slackbot.Net.Endpoints.Models.Events; + +public class AssistantThreadStartedEvent : SlackEvent +{ + public AssistantThread Assistant_Thread { get; set; } + public string Event_Ts { get; set; } +} + +public class AssistantThread +{ + public string User_Id { get; set; } + public string Channel_Id { get; set; } + public string Thread_Ts { get; set; } + public AssistantThreadContext Context { get; set; } +} + +public class AssistantThreadContext +{ + public string Channel_Id { get; set; } + public string Team_Id { get; set; } + public string Enterprise_Id { get; set; } +} diff --git a/source/src/Slackbot.Net.SlackClients.Http/ISlackClient.cs b/source/src/Slackbot.Net.SlackClients.Http/ISlackClient.cs index 0592905..5f20a44 100644 --- a/source/src/Slackbot.Net.SlackClients.Http/ISlackClient.cs +++ b/source/src/Slackbot.Net.SlackClients.Http/ISlackClient.cs @@ -1,3 +1,6 @@ +using Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetStatus; +using Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetSuggestedPrompts; +using Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetTitle; using Slackbot.Net.SlackClients.Http.Models.Requests.ChatPostEphemeral; using Slackbot.Net.SlackClients.Http.Models.Requests.ChatPostMessage; using Slackbot.Net.SlackClients.Http.Models.Requests.ChatUpdate; @@ -127,4 +130,25 @@ public interface ISlackClient Task FilesUpload(FileUploadRequest fileupload); Task FilesUpload(FileUploadMultiPartRequest req); + + /// + /// Scopes required: `assistant:write` + /// Sets the status of the assistant (e.g. "is thinking...") shown in the assistant pane. + /// + /// https://api.slack.com/methods/assistant.threads.setStatus + Task AssistantThreadsSetStatus(AssistantThreadsSetStatusRequest request); + + /// + /// Scopes required: `assistant:write` + /// Sets the title of the assistant thread, shown in the History/Chat tabs. + /// + /// https://api.slack.com/methods/assistant.threads.setTitle + Task AssistantThreadsSetTitle(AssistantThreadsSetTitleRequest request); + + /// + /// Scopes required: `assistant:write` + /// Sets suggested prompts for the assistant thread. + /// + /// https://api.slack.com/methods/assistant.threads.setSuggestedPrompts + Task AssistantThreadsSetSuggestedPrompts(AssistantThreadsSetSuggestedPromptsRequest request); } \ No newline at end of file diff --git a/source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetStatus/AssistantThreadsSetStatusRequest.cs b/source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetStatus/AssistantThreadsSetStatusRequest.cs new file mode 100644 index 0000000..9511606 --- /dev/null +++ b/source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetStatus/AssistantThreadsSetStatusRequest.cs @@ -0,0 +1,8 @@ +namespace Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetStatus; + +public class AssistantThreadsSetStatusRequest +{ + public string Channel_Id { get; set; } + public string Thread_Ts { get; set; } + public string Status { get; set; } +} diff --git a/source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetSuggestedPrompts/AssistantThreadsSetSuggestedPromptsRequest.cs b/source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetSuggestedPrompts/AssistantThreadsSetSuggestedPromptsRequest.cs new file mode 100644 index 0000000..91a872c --- /dev/null +++ b/source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetSuggestedPrompts/AssistantThreadsSetSuggestedPromptsRequest.cs @@ -0,0 +1,15 @@ +namespace Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetSuggestedPrompts; + +public class AssistantThreadsSetSuggestedPromptsRequest +{ + public string Channel_Id { get; set; } + public string Thread_Ts { get; set; } + public AssistantThreadPrompt[] Prompts { get; set; } + public string Title { get; set; } +} + +public class AssistantThreadPrompt +{ + public string Title { get; set; } + public string Message { get; set; } +} diff --git a/source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetTitle/AssistantThreadsSetTitleRequest.cs b/source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetTitle/AssistantThreadsSetTitleRequest.cs new file mode 100644 index 0000000..2d2872d --- /dev/null +++ b/source/src/Slackbot.Net.SlackClients.Http/Models/Requests/AssistantThreadsSetTitle/AssistantThreadsSetTitleRequest.cs @@ -0,0 +1,8 @@ +namespace Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetTitle; + +public class AssistantThreadsSetTitleRequest +{ + public string Channel_Id { get; set; } + public string Thread_Ts { get; set; } + public string Title { get; set; } +} diff --git a/source/src/Slackbot.Net.SlackClients.Http/SlackClient.cs b/source/src/Slackbot.Net.SlackClients.Http/SlackClient.cs index ea871ba..2f9771e 100644 --- a/source/src/Slackbot.Net.SlackClients.Http/SlackClient.cs +++ b/source/src/Slackbot.Net.SlackClients.Http/SlackClient.cs @@ -1,5 +1,8 @@ using Microsoft.Extensions.Logging; using Slackbot.Net.SlackClients.Http.Extensions; +using Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetStatus; +using Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetSuggestedPrompts; +using Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetTitle; using Slackbot.Net.SlackClients.Http.Models.Requests.ChatPostEphemeral; using Slackbot.Net.SlackClients.Http.Models.Requests.ChatPostMessage; using Slackbot.Net.SlackClients.Http.Models.Requests.ChatUpdate; @@ -228,5 +231,21 @@ public async Task FilesUpload(FileUploadMultiPartRequest req return await _client.PostParametersAsMultiPartFormData(parameters, req.File, "files.upload", s => _logger.LogTrace(s)); } + /// + public async Task AssistantThreadsSetStatus(AssistantThreadsSetStatusRequest request) + { + return await _client.PostJson(request, "assistant.threads.setStatus", s => _logger.LogTrace(s)); + } + + /// + public async Task AssistantThreadsSetTitle(AssistantThreadsSetTitleRequest request) + { + return await _client.PostJson(request, "assistant.threads.setTitle", s => _logger.LogTrace(s)); + } + /// + public async Task AssistantThreadsSetSuggestedPrompts(AssistantThreadsSetSuggestedPromptsRequest request) + { + return await _client.PostJson(request, "assistant.threads.setSuggestedPrompts", s => _logger.LogTrace(s)); + } } diff --git a/source/test/Slackbot.Net.SlackClients.Http.Tests/AssistantThreadsTests.cs b/source/test/Slackbot.Net.SlackClients.Http.Tests/AssistantThreadsTests.cs new file mode 100644 index 0000000..ca40180 --- /dev/null +++ b/source/test/Slackbot.Net.SlackClients.Http.Tests/AssistantThreadsTests.cs @@ -0,0 +1,134 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetStatus; +using Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetSuggestedPrompts; +using Slackbot.Net.SlackClients.Http.Models.Requests.AssistantThreadsSetTitle; +using Slackbot.Net.Tests.Helpers; + +namespace Slackbot.Net.Tests; + +/// +/// Verifies the assistant.threads.* request models serialize to the snake_case wire format Slack expects. +/// Mirrors the serialization configured in HttpClientExtensions (Web defaults + WhenWritingNull + a +/// lowercasing naming policy), so it catches the pitfall where e.g. a `ThreadTs` property would +/// wrongly serialize to `threadts` instead of `thread_ts`. +/// +public class AssistantThreadsSerializationTests +{ + private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web) + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = new LowerCaseNamingPolicy() + }; + + private class LowerCaseNamingPolicy : JsonNamingPolicy + { + public override string ConvertName(string name) => name.ToLower(); + } + + [Fact] + public void SetStatusSerializesToSnakeCase() + { + var json = JsonSerializer.Serialize( + new AssistantThreadsSetStatusRequest { Channel_Id = "D1", Thread_Ts = "1.2", Status = "is thinking..." }, + Options); + + Assert.Contains("\"channel_id\":\"D1\"", json); + Assert.Contains("\"thread_ts\":\"1.2\"", json); + Assert.Contains("\"status\":\"is thinking...\"", json); + } + + [Fact] + public void SetTitleSerializesToSnakeCase() + { + var json = JsonSerializer.Serialize( + new AssistantThreadsSetTitleRequest { Channel_Id = "D1", Thread_Ts = "1.2", Title = "A title" }, + Options); + + Assert.Contains("\"channel_id\":\"D1\"", json); + Assert.Contains("\"thread_ts\":\"1.2\"", json); + Assert.Contains("\"title\":\"A title\"", json); + } + + [Fact] + public void SetSuggestedPromptsSerializesPromptObjects() + { + var json = JsonSerializer.Serialize( + new AssistantThreadsSetSuggestedPromptsRequest + { + Channel_Id = "D1", + Thread_Ts = "1.2", + Prompts = new[] { new AssistantThreadPrompt { Title = "Ideas", Message = "Give me ideas" } } + }, + Options); + + Assert.Contains("\"channel_id\":\"D1\"", json); + Assert.Contains("\"thread_ts\":\"1.2\"", json); + Assert.Contains("\"prompts\":[", json); + Assert.Contains("\"title\":\"Ideas\"", json); + Assert.Contains("\"message\":\"Give me ideas\"", json); + } + + [Fact] + public void SetSuggestedPromptsOmitsNullTitle() + { + var json = JsonSerializer.Serialize( + new AssistantThreadsSetSuggestedPromptsRequest + { + Channel_Id = "D1", + Thread_Ts = "1.2", + Prompts = new[] { new AssistantThreadPrompt { Title = "Ideas", Message = "Give me ideas" } } + }, + Options); + + Assert.DoesNotContain("\"title\":null", json); + } +} + +/// +/// Live integration tests, consistent with the rest of this project. They require the bot token env var +/// (see ) AND a real assistant thread in an assistant-enabled workspace, so the +/// Channel_Id/Thread_Ts below must be replaced with real values before running manually. +/// +public class AssistantThreadsTests(ITestOutputHelper helper) : Setup(helper) +{ + private const string AssistantChannelId = "D000000000"; + private const string AssistantThreadTs = "0000000000.000000"; + + [Fact(Skip = "Requires a real assistant thread; set AssistantChannelId/AssistantThreadTs and run manually.")] + public async Task SetStatusWorks() + { + var response = await SlackClient.AssistantThreadsSetStatus(new AssistantThreadsSetStatusRequest + { + Channel_Id = AssistantChannelId, Thread_Ts = AssistantThreadTs, Status = "is thinking..." + }); + Assert.True(response.Ok); + } + + [Fact(Skip = "Requires a real assistant thread; set AssistantChannelId/AssistantThreadTs and run manually.")] + public async Task SetTitleWorks() + { + var response = await SlackClient.AssistantThreadsSetTitle(new AssistantThreadsSetTitleRequest + { + Channel_Id = AssistantChannelId, Thread_Ts = AssistantThreadTs, Title = "Test title" + }); + Assert.True(response.Ok); + } + + [Fact(Skip = "Requires a real assistant thread; set AssistantChannelId/AssistantThreadTs and run manually.")] + public async Task SetSuggestedPromptsWorks() + { + var response = await SlackClient.AssistantThreadsSetSuggestedPrompts(new AssistantThreadsSetSuggestedPromptsRequest + { + Channel_Id = AssistantChannelId, + Thread_Ts = AssistantThreadTs, + Title = "What can I help with?", + Prompts = new[] + { + new AssistantThreadPrompt { Title = "Generate ideas", Message = "Give me some marketing ideas" }, + new AssistantThreadPrompt { Title = "Explain a concept", Message = "Explain compound interest" } + } + }); + Assert.True(response.Ok); + } +}