From 5cae7979946ec525e768ec3646246f1892a1838f Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 8 Oct 2026 14:26:44 -0400 Subject: [PATCH] feat: support the user_connections manifest feature Add features.user_connections (is_enabled, auth_hosts) to the app manifest types so the field round-trips through run, deploy, and manifest sync instead of being dropped when the manifest is decoded. Both fields are required by the manifest schema, so neither is omitted when empty: a disabled feature keeps its auth hosts. Co-Authored-By: Claude --- internal/shared/types/app_manifest.go | 6 +++ internal/shared/types/app_manifest_test.go | 61 ++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/internal/shared/types/app_manifest.go b/internal/shared/types/app_manifest.go index 4af726cd..f9d84da6 100644 --- a/internal/shared/types/app_manifest.go +++ b/internal/shared/types/app_manifest.go @@ -80,6 +80,7 @@ type AppFeatures struct { Search *Search `json:"search,omitempty" yaml:"search,flow,omitempty"` RichPreviews *RichPreviews `json:"rich_previews,omitempty" yaml:"rich_previews,flow,omitempty"` CodeChannels *CodeChannels `json:"code_channels,omitempty" yaml:"code_channels,flow,omitempty"` + UserConnections *UserConnections `json:"user_connections,omitempty" yaml:"user_connections,flow,omitempty"` } type RichPreviews struct { @@ -300,6 +301,11 @@ type CodeChannels struct { SlashCommandURL string `json:"slash_command_url,omitempty" yaml:"slash_command_url,omitempty"` } +type UserConnections struct { + IsEnabled bool `json:"is_enabled" yaml:"is_enabled"` + AuthHosts []string `json:"auth_hosts" yaml:"auth_hosts,flow"` +} + // Workflow defines the structure of a workflow in the app manifest. type Workflow struct { Title string `json:"title" yaml:"title"` diff --git a/internal/shared/types/app_manifest_test.go b/internal/shared/types/app_manifest_test.go index 75fe80dc..15b5ca13 100644 --- a/internal/shared/types/app_manifest_test.go +++ b/internal/shared/types/app_manifest_test.go @@ -273,6 +273,30 @@ func Test_AppManifest_AppFeatures(t *testing.T) { }, want: `{"app_home":{},"bot_user":{"display_name":"coding_agent"},"code_channels":{"enabled":false}}`, }, + "includes user connections when provided": { + features: AppFeatures{ + BotUser: BotUser{ + DisplayName: "connector", + }, + UserConnections: &UserConnections{ + IsEnabled: true, + AuthHosts: []string{"example.com", "auth.example.com"}, + }, + }, + want: `{"app_home":{},"bot_user":{"display_name":"connector"},"user_connections":{"is_enabled":true,"auth_hosts":["example.com","auth.example.com"]}}`, + }, + "serializes user connections disabled explicitly": { + features: AppFeatures{ + BotUser: BotUser{ + DisplayName: "connector", + }, + UserConnections: &UserConnections{ + IsEnabled: false, + AuthHosts: []string{"example.com"}, + }, + }, + want: `{"app_home":{},"bot_user":{"display_name":"connector"},"user_connections":{"is_enabled":false,"auth_hosts":["example.com"]}}`, + }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { @@ -283,6 +307,43 @@ func Test_AppManifest_AppFeatures(t *testing.T) { } } +func Test_AppManifest_AppFeatures_UserConnections(t *testing.T) { + tests := map[string]struct { + manifestJSON string + expected *UserConnections + }{ + "undefined user connections are omitted": { + manifestJSON: `{"display_information":{"name":"connector"},"features":{"app_home":{},"bot_user":{"display_name":"connector"}}}`, + expected: nil, + }, + "enabled user connections are preserved": { + manifestJSON: `{"display_information":{"name":"connector"},"features":{"app_home":{},"bot_user":{"display_name":"connector"},"user_connections":{"is_enabled":true,"auth_hosts":["example.com","auth.example.com"]}}}`, + expected: &UserConnections{ + IsEnabled: true, + AuthHosts: []string{"example.com", "auth.example.com"}, + }, + }, + "disabled user connections keep their auth hosts": { + manifestJSON: `{"display_information":{"name":"connector"},"features":{"app_home":{},"bot_user":{"display_name":"connector"},"user_connections":{"is_enabled":false,"auth_hosts":["example.com"]}}}`, + expected: &UserConnections{ + IsEnabled: false, + AuthHosts: []string{"example.com"}, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + var manifest AppManifest + err := json.Unmarshal([]byte(tc.manifestJSON), &manifest) + require.NoError(t, err) + assert.Equal(t, tc.expected, manifest.Features.UserConnections) + actualJSON, err := json.Marshal(manifest) + require.NoError(t, err) + assert.JSONEq(t, tc.manifestJSON, string(actualJSON)) + }) + } +} + func Test_AppManifest_AppSettings_SiwsLinks(t *testing.T) { expectedSiws := SiwsLinks{ InitiateURI: "an initiate uri",