diff --git a/config/authz/permissions/ai_catalog_item/delete.yml b/config/authz/permissions/ai_catalog_item/delete.yml
new file mode 100644
index 0000000000000000000000000000000000000000..062b850ab23073d7f1af4a34ecbc74cc808febdb
--- /dev/null
+++ b/config/authz/permissions/ai_catalog_item/delete.yml
@@ -0,0 +1,4 @@
+---
+name: delete_ai_catalog_item
+description: Delete an AI Catalog item
+feature_category: workflow_catalog
diff --git a/config/authz/permissions/hard_delete_ai_catalog_item/force.yml b/config/authz/permissions/hard_delete_ai_catalog_item/force.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f5e6cb6d999552796047fb81cedd09e1f8a52e5a
--- /dev/null
+++ b/config/authz/permissions/hard_delete_ai_catalog_item/force.yml
@@ -0,0 +1,4 @@
+---
+name: force_hard_delete_ai_catalog_item
+description: Force hard delete an AI Catalog item
+feature_category: workflow_catalog
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 0798b9f78063d3da58a542136043700897a7084f..d081843768c8c58ec4e2805f89958851bf77e487 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -2561,6 +2561,7 @@ Input type: `AiCatalogAgentDeleteInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `forceHardDelete` | [`Boolean`](#boolean) | When true, the flow will always be hard deleted and never soft deleted. Can only be used by instance admins. |
| `id` | [`AiCatalogItemID!`](#aicatalogitemid) | Global ID of the catalog Agent to delete. |
#### Fields
@@ -2674,6 +2675,7 @@ Input type: `AiCatalogFlowDeleteInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `forceHardDelete` | [`Boolean`](#boolean) | When true, the flow will always be hard deleted and never soft deleted. Can only be used by instance admins. |
| `id` | [`AiCatalogItemID!`](#aicatalogitemid) | Global ID of the catalog flow to delete. |
#### Fields
@@ -2858,6 +2860,7 @@ Input type: `AiCatalogThirdPartyFlowDeleteInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `forceHardDelete` | [`Boolean`](#boolean) | When true, the Third Party Flow will always be hard deleted and never soft deleted. Can only be used by instance admins. |
| `id` | [`AiCatalogItemID!`](#aicatalogitemid) | Global ID of the catalog Third Party Flow to delete. |
#### Fields
diff --git a/ee/app/graphql/mutations/ai/catalog/agent/delete.rb b/ee/app/graphql/mutations/ai/catalog/agent/delete.rb
index a3c7bab6813744c458b7e3419197bc298d0499ec..d930731f6d30f5c2b481d64836bd3ab5dc9eb237 100644
--- a/ee/app/graphql/mutations/ai/catalog/agent/delete.rb
+++ b/ee/app/graphql/mutations/ai/catalog/agent/delete.rb
@@ -15,15 +15,28 @@ class Delete < BaseMutation
required: true,
description: 'Global ID of the catalog Agent to delete.'
- authorize :admin_ai_catalog_item
+ argument :force_hard_delete, GraphQL::Types::Boolean,
+ required: false,
+ description: 'When true, the flow will always be hard deleted and never soft deleted. ' \
+ 'Can only be used by instance admins'
+
+ authorize :delete_ai_catalog_item
def resolve(args)
- agent = authorized_find!(id: args[:id])
+ id = args.delete(:id)
+
+ item = authorized_find!(id: id)
+
+ if args[:force_hard_delete] && !Ability.allowed?(current_user, :force_hard_delete_ai_catalog_item, item)
+ raise_resource_not_available_error!('You must be an instance admin to use forceHardDelete')
+ end
+
+ service_args = args.merge(item: item)
result = ::Ai::Catalog::Agents::DestroyService.new(
- project: agent.project,
+ project: service_args[:item].project,
current_user: current_user,
- params: { item: agent }).execute
+ params: service_args).execute
{
success: result.success?,
diff --git a/ee/app/graphql/mutations/ai/catalog/flow/delete.rb b/ee/app/graphql/mutations/ai/catalog/flow/delete.rb
index 6fa4c6193f3aea3b0c1c4e901cfe4fa4222d1123..8c65c32dff635b3a5d2de4462ea2228259f6a660 100644
--- a/ee/app/graphql/mutations/ai/catalog/flow/delete.rb
+++ b/ee/app/graphql/mutations/ai/catalog/flow/delete.rb
@@ -15,15 +15,28 @@ class Delete < BaseMutation
required: true,
description: 'Global ID of the catalog flow to delete.'
- authorize :admin_ai_catalog_item
+ argument :force_hard_delete, GraphQL::Types::Boolean,
+ required: false,
+ description: 'When true, the flow will always be hard deleted and never soft deleted. ' \
+ 'Can only be used by instance admins'
+
+ authorize :delete_ai_catalog_item
def resolve(args)
- flow = authorized_find!(id: args[:id])
+ id = args.delete(:id)
+
+ item = authorized_find!(id: id)
+
+ if args[:force_hard_delete] && !Ability.allowed?(current_user, :force_hard_delete_ai_catalog_item, item)
+ raise_resource_not_available_error!('You must be an instance admin to use forceHardDelete')
+ end
+
+ service_args = args.merge(item: item)
result = ::Ai::Catalog::Flows::DestroyService.new(
- project: flow.project,
+ project: service_args[:item].project,
current_user: current_user,
- params: { item: flow }).execute
+ params: service_args).execute
{
success: result.success?,
diff --git a/ee/app/graphql/mutations/ai/catalog/third_party_flow/delete.rb b/ee/app/graphql/mutations/ai/catalog/third_party_flow/delete.rb
index f9717cda260fb9a7e8335900e0fd0f701f649e62..c97b8f4c67288a1dc1c6389b132b5424e70b98cf 100644
--- a/ee/app/graphql/mutations/ai/catalog/third_party_flow/delete.rb
+++ b/ee/app/graphql/mutations/ai/catalog/third_party_flow/delete.rb
@@ -15,15 +15,28 @@ class Delete < BaseMutation
required: true,
description: 'Global ID of the catalog Third Party Flow to delete.'
- authorize :admin_ai_catalog_item
+ argument :force_hard_delete, GraphQL::Types::Boolean,
+ required: false,
+ description: 'When true, the Third Party Flow will always be hard deleted and never soft deleted. ' \
+ 'Can only be used by instance admins'
+
+ authorize :delete_ai_catalog_item
def resolve(args)
- third_party_flow = authorized_find!(id: args[:id])
+ id = args.delete(:id)
+
+ item = authorized_find!(id: id)
+
+ if args[:force_hard_delete] && !Ability.allowed?(current_user, :force_hard_delete_ai_catalog_item, item)
+ raise_resource_not_available_error!('You must be an instance admin to use forceHardDelete')
+ end
+
+ service_args = args.merge(item: item)
result = ::Ai::Catalog::ThirdPartyFlows::DestroyService.new(
- project: third_party_flow.project,
+ project: service_args[:item].project,
current_user: current_user,
- params: { item: third_party_flow }).execute
+ params: service_args).execute
{
success: result.success?,
diff --git a/ee/app/policies/ai/catalog/item_policy.rb b/ee/app/policies/ai/catalog/item_policy.rb
index 74a193441dd1c115bdab161378bbea6331f3228a..efff7627f2b94809d6ffd9be1b9d6e60244cea18 100644
--- a/ee/app/policies/ai/catalog/item_policy.rb
+++ b/ee/app/policies/ai/catalog/item_policy.rb
@@ -48,34 +48,44 @@ class ItemPolicy < ::BasePolicy
end
rule { maintainer_access }.policy do
- enable :admin_ai_catalog_item
+ enable :admin_ai_catalog_item # (Create and update)
+ enable :delete_ai_catalog_item
end
- rule { ~ai_catalog_enabled }.policy do
+ rule { admin }.policy do
+ enable :force_hard_delete_ai_catalog_item
+ end
+
+ rule { ~ai_catalog_enabled & ~admin }.policy do
prevent :read_ai_catalog_item
prevent :admin_ai_catalog_item
+ prevent :delete_ai_catalog_item
end
- rule { deleted_item }.policy do
+ rule { deleted_item & ~admin }.policy do
prevent :admin_ai_catalog_item
+ prevent :delete_ai_catalog_item
end
- rule { ~public_item & ~project_ai_catalog_available }.policy do
+ rule { ~public_item & ~project_ai_catalog_available & ~admin }.policy do
prevent :read_ai_catalog_item
end
- rule { ~project_ai_catalog_available }.policy do
+ rule { ~project_ai_catalog_available & ~admin }.policy do
prevent :admin_ai_catalog_item
+ prevent :delete_ai_catalog_item
end
- rule { flow & ~flows_enabled }.policy do
+ rule { flow & ~flows_enabled & ~admin }.policy do
prevent :read_ai_catalog_item
prevent :admin_ai_catalog_item
+ prevent :delete_ai_catalog_item
end
- rule { third_party_flow & ~third_party_flows_enabled }.policy do
+ rule { third_party_flow & ~third_party_flows_enabled & ~admin }.policy do
prevent :read_ai_catalog_item
prevent :admin_ai_catalog_item
+ prevent :delete_ai_catalog_item
end
end
end
diff --git a/ee/app/services/ai/catalog/items/base_destroy_service.rb b/ee/app/services/ai/catalog/items/base_destroy_service.rb
index 510d7196e2d958232c235445058c70aee0a55ecd..2ccc61ca37e0830c8c1421e6986eea8eb06abde5 100644
--- a/ee/app/services/ai/catalog/items/base_destroy_service.rb
+++ b/ee/app/services/ai/catalog/items/base_destroy_service.rb
@@ -13,15 +13,11 @@ def execute
return error_no_permissions unless allowed?
return error_no_item unless valid?
- result = delete_item_consumer
- return error(result.errors) if result.error?
+ result = force_hard_delete? ? perform_hard_delete : perform_soft_delete
- if delete_item
- track_ai_item_events('delete_ai_catalog_item', { label: item.item_type })
- return success
- end
+ track_deletion_event if result.success?
- error_response
+ result
end
private
@@ -29,7 +25,13 @@ def execute
attr_reader :item
def allowed?
- super && Ability.allowed?(current_user, :admin_ai_catalog_item, item)
+ allowed = super && Ability.allowed?(current_user, :delete_ai_catalog_item, item)
+
+ if force_hard_delete? # rubocop:disable Style/IfUnlessModifier -- Improves readability
+ allowed &= Ability.allowed?(current_user, :force_hard_delete_ai_catalog_item, item)
+ end
+
+ allowed
end
def valid?
@@ -44,11 +46,25 @@ def error_no_item
error('Item not found')
end
- def error_response
- error(item.errors.full_messages)
+ def perform_hard_delete
+ item.class.transaction do
+ item.consumers.each_batch do |batch|
+ batch.delete_all
+ end
+ item.destroy
+ end
+
+ ServiceResponse.success
+ end
+
+ def perform_soft_delete
+ result = destroy_item_consumer
+ return result if result.error?
+
+ destroy_item_with_strategy
end
- def delete_item_consumer
+ def destroy_item_consumer
consumer = project.configured_ai_catalog_items.for_item(item).first
return ServiceResponse.success unless consumer
@@ -56,10 +72,22 @@ def delete_item_consumer
Ai::Catalog::ItemConsumers::DestroyService.new(consumer, current_user).execute
end
- def delete_item
- return item.soft_delete if item.consumers.any? || item.dependents.any?
+ def destroy_item_with_strategy
+ success = if item.consumers.any? || item.dependents.any?
+ item.soft_delete
+ else
+ item.destroy
+ end
+
+ success ? ServiceResponse.success : error(item.errors.full_messages)
+ end
+
+ def force_hard_delete?
+ params[:force_hard_delete] == true
+ end
- item.destroy
+ def track_deletion_event
+ track_ai_item_events('delete_ai_catalog_item', { label: item.item_type })
end
end
end
diff --git a/ee/spec/graphql/mutations/ai/catalog/agent/delete_spec.rb b/ee/spec/graphql/mutations/ai/catalog/agent/delete_spec.rb
index e7349b88035ab4625a77a2a7b47ebfcb15ea074f..87218fb491c33f493bfc4f34de2feda1d5ddda0e 100644
--- a/ee/spec/graphql/mutations/ai/catalog/agent/delete_spec.rb
+++ b/ee/spec/graphql/mutations/ai/catalog/agent/delete_spec.rb
@@ -9,9 +9,9 @@
it { is_expected.to have_graphql_name('AiCatalogAgentDelete') }
- it { expect(described_class).to require_graphql_authorizations(:admin_ai_catalog_item) }
+ it { expect(described_class).to require_graphql_authorizations(:delete_ai_catalog_item) }
it { is_expected.to have_graphql_fields(:success, :errors, :client_mutation_id) }
- it { is_expected.to have_graphql_arguments(:id, :client_mutation_id) }
+ it { is_expected.to have_graphql_arguments(:id, :force_hard_delete, :client_mutation_id) }
end
diff --git a/ee/spec/graphql/mutations/ai/catalog/flow/delete_spec.rb b/ee/spec/graphql/mutations/ai/catalog/flow/delete_spec.rb
index 197187f594232c9a744c8dfc86d80734018fcfe0..339c83be2f9e33237d4476952284994186125a6f 100644
--- a/ee/spec/graphql/mutations/ai/catalog/flow/delete_spec.rb
+++ b/ee/spec/graphql/mutations/ai/catalog/flow/delete_spec.rb
@@ -9,9 +9,9 @@
it { is_expected.to have_graphql_name('AiCatalogFlowDelete') }
- it { expect(described_class).to require_graphql_authorizations(:admin_ai_catalog_item) }
+ it { expect(described_class).to require_graphql_authorizations(:delete_ai_catalog_item) }
it { is_expected.to have_graphql_fields(:success, :errors, :client_mutation_id) }
- it { is_expected.to have_graphql_arguments(:id, :client_mutation_id) }
+ it { is_expected.to have_graphql_arguments(:id, :force_hard_delete, :client_mutation_id) }
end
diff --git a/ee/spec/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb b/ee/spec/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb
index 1a95108b26edbd3ede12c9ce0fc65c0efea5caf8..6a7c4760b66cfbbf58d93ea9376881ec2c5e82b7 100644
--- a/ee/spec/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb
+++ b/ee/spec/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb
@@ -9,9 +9,9 @@
it { is_expected.to have_graphql_name('AiCatalogThirdPartyFlowDelete') }
- it { expect(described_class).to require_graphql_authorizations(:admin_ai_catalog_item) }
+ it { expect(described_class).to require_graphql_authorizations(:delete_ai_catalog_item) }
it { is_expected.to have_graphql_fields(:success, :errors, :client_mutation_id) }
- it { is_expected.to have_graphql_arguments(:id, :client_mutation_id) }
+ it { is_expected.to have_graphql_arguments(:id, :force_hard_delete, :client_mutation_id) }
end
diff --git a/ee/spec/policies/ai/catalog/item_policy_spec.rb b/ee/spec/policies/ai/catalog/item_policy_spec.rb
index ae1b43c4c0469baf1257fe33e6c3ca6b1bbf1897..5fdb2f02546ec6874054e0513df448f104a7294c 100644
--- a/ee/spec/policies/ai/catalog/item_policy_spec.rb
+++ b/ee/spec/policies/ai/catalog/item_policy_spec.rb
@@ -9,6 +9,7 @@
let_it_be(:maintainer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
let_it_be_with_reload(:private_project) do
create(:project, :private, guests: guest, reporters: reporter, developers: developer, maintainers: maintainer)
end
@@ -36,13 +37,21 @@
shared_examples 'no permissions' do
it { is_expected.to be_disallowed(:admin_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:delete_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) }
it { is_expected.to be_disallowed(:read_ai_catalog_item) }
+
+ include_examples 'all permissions when admin'
end
shared_examples 'read-only permissions' do
it { is_expected.to be_disallowed(:admin_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:delete_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) }
it { is_expected.to be_allowed(:read_ai_catalog_item) }
+ include_examples 'all permissions when admin'
+
it_behaves_like 'no permissions with global_ai_catalog feature flag disabled'
it_behaves_like 'no permissions with project stage check false, unless item is public'
it_behaves_like 'no permissions when project Duo features disabled, unless item is public'
@@ -51,7 +60,11 @@
shared_examples 'read-write permissions' do
it { is_expected.to be_allowed(:admin_ai_catalog_item) }
+ it { is_expected.to be_allowed(:delete_ai_catalog_item) }
it { is_expected.to be_allowed(:read_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) }
+
+ include_examples 'all permissions when admin'
it_behaves_like 'no permissions with global_ai_catalog feature flag disabled'
it_behaves_like 'no permissions with project stage check false, unless item is public'
@@ -59,6 +72,17 @@
it_behaves_like 'read-only permissions with deleted item'
end
+ shared_examples 'all permissions when admin' do
+ context 'when admin', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ it { is_expected.to be_allowed(:admin_ai_catalog_item) }
+ it { is_expected.to be_allowed(:delete_ai_catalog_item) }
+ it { is_expected.to be_allowed(:force_hard_delete_ai_catalog_item) }
+ it { is_expected.to be_allowed(:read_ai_catalog_item) }
+ end
+ end
+
shared_examples 'no permissions with global_ai_catalog feature flag disabled' do
before do
stub_feature_flags(global_ai_catalog: false)
@@ -73,31 +97,43 @@
end
it { is_expected.to be_disallowed(:admin_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:delete_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) }
it { is_expected.to be_allowed(:read_ai_catalog_item) }
+
+ include_examples 'all permissions when admin'
end
shared_examples 'no permissions with project stage check false, unless item is public' do
let(:stage_check) { false }
it { is_expected.to be_disallowed(:admin_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:delete_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) }
it 'is expected not to allow read_ai_catalog_item, unless item is public' do
allowed = item.public?
expect(policy.allowed?(:read_ai_catalog_item)).to eq(allowed)
end
+
+ include_examples 'all permissions when admin'
end
shared_examples 'no permissions when project Duo features disabled, unless item is public' do
let(:duo_features_enabled) { false }
it { is_expected.to be_disallowed(:admin_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:delete_ai_catalog_item) }
+ it { is_expected.to be_disallowed(:force_hard_delete_ai_catalog_item) }
it 'is expected not to allow read_ai_catalog_item, unless item is public' do
allowed = item.public?
expect(policy.allowed?(:read_ai_catalog_item)).to eq(allowed)
end
+
+ include_examples 'all permissions when admin'
end
context 'when maintainer' do
diff --git a/ee/spec/requests/api/graphql/mutations/ai/catalog/agent/delete_spec.rb b/ee/spec/requests/api/graphql/mutations/ai/catalog/agent/delete_spec.rb
index 425095ccfb29de3b4d9579cbf2103b869a351ebd..f811da6a5b5ae528cabfb65bef9b1054592ac3ae 100644
--- a/ee/spec/requests/api/graphql/mutations/ai/catalog/agent/delete_spec.rb
+++ b/ee/spec/requests/api/graphql/mutations/ai/catalog/agent/delete_spec.rb
@@ -82,5 +82,25 @@
it 'destroy the agent versions' do
expect { execute }.to change { Ai::Catalog::ItemVersion.count }.by(-1)
end
+
+ context 'with `forceHardDelete` argument', :enable_admin_mode do
+ let(:params) { super().merge(force_hard_delete: true) }
+
+ it_behaves_like 'a mutation that returns top-level errors', errors:
+ ['You must be an instance admin to use forceHardDelete']
+
+ it 'does not destroy the agent' do
+ expect { execute }.not_to change { Ai::Catalog::Item.count }
+ end
+
+ context 'when user is an admin' do
+ let(:current_user) { create(:admin) }
+
+ it 'destroys the agent and returns a success response' do
+ expect { execute }.to change { Ai::Catalog::Item.count }.by(-1)
+ expect(graphql_data_at(:ai_catalog_agent_delete, :success)).to be(true)
+ end
+ end
+ end
end
end
diff --git a/ee/spec/requests/api/graphql/mutations/ai/catalog/flow/delete_spec.rb b/ee/spec/requests/api/graphql/mutations/ai/catalog/flow/delete_spec.rb
index 2ba25d003d3d2582cb89ff8dda25d4e32adde371..e9be32510162ef5b9b4566fbba6806a6628124ff 100644
--- a/ee/spec/requests/api/graphql/mutations/ai/catalog/flow/delete_spec.rb
+++ b/ee/spec/requests/api/graphql/mutations/ai/catalog/flow/delete_spec.rb
@@ -74,13 +74,33 @@
end
context 'when destroy service succeeds' do
- it 'destroy the agent and returns a success response' do
+ it 'destroy the flow and returns a success response' do
expect { execute }.to change { Ai::Catalog::Item.count }.by(-1)
expect(graphql_data_at(:ai_catalog_flow_delete, :success)).to be(true)
end
- it 'destroy the agent versions' do
+ it 'destroy the flow versions' do
expect { execute }.to change { Ai::Catalog::ItemVersion.count }.by(-1)
end
+
+ context 'with `forceHardDelete` argument', :enable_admin_mode do
+ let(:params) { super().merge(force_hard_delete: true) }
+
+ it_behaves_like 'a mutation that returns top-level errors', errors:
+ ['You must be an instance admin to use forceHardDelete']
+
+ it 'does not destroy the flow' do
+ expect { execute }.not_to change { Ai::Catalog::Item.count }
+ end
+
+ context 'when user is an admin' do
+ let(:current_user) { create(:admin) }
+
+ it 'destroys the flow and returns a success response' do
+ expect { execute }.to change { Ai::Catalog::Item.count }.by(-1)
+ expect(graphql_data_at(:ai_catalog_flow_delete, :success)).to be(true)
+ end
+ end
+ end
end
end
diff --git a/ee/spec/requests/api/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb b/ee/spec/requests/api/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb
index 47b9e0751359655ae7e2405af503f4222bdeb794..5e4b208d99d4b9c44df5ba85aa1ee240133f6e8b 100644
--- a/ee/spec/requests/api/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb
+++ b/ee/spec/requests/api/graphql/mutations/ai/catalog/third_party_flow/delete_spec.rb
@@ -82,5 +82,25 @@
it 'destroy the third_party_flow versions' do
expect { execute }.to change { Ai::Catalog::ItemVersion.count }.by(-1)
end
+
+ context 'with `forceHardDelete` argument', :enable_admin_mode do
+ let(:params) { super().merge(force_hard_delete: true) }
+
+ it_behaves_like 'a mutation that returns top-level errors', errors:
+ ['You must be an instance admin to use forceHardDelete']
+
+ it 'does not delete the third_party_flow' do
+ expect { execute }.not_to change { Ai::Catalog::Item.count }
+ end
+
+ context 'when user is an admin' do
+ let(:current_user) { create(:admin) }
+
+ it 'destroys the third_party_flow and returns a success response' do
+ expect { execute }.to change { Ai::Catalog::Item.count }.by(-1)
+ expect(graphql_data_at(:ai_catalog_third_party_flow_delete, :success)).to be(true)
+ end
+ end
+ end
end
end
diff --git a/ee/spec/support/shared_examples/services/ai/catalog/items/base_destroy_service_shared_examples.rb b/ee/spec/support/shared_examples/services/ai/catalog/items/base_destroy_service_shared_examples.rb
index 756f0a94f6da99fc4b827b52e9306c13865cf0ba..69bb5576e9c048dfd7371d834f87283f86e573ae 100644
--- a/ee/spec/support/shared_examples/services/ai/catalog/items/base_destroy_service_shared_examples.rb
+++ b/ee/spec/support/shared_examples/services/ai/catalog/items/base_destroy_service_shared_examples.rb
@@ -3,6 +3,7 @@
RSpec.shared_examples Ai::Catalog::Items::BaseDestroyService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
let(:params) { { item: item } }
let(:service) { described_class.new(project: project, current_user: user, params: params) }
@@ -62,6 +63,7 @@
expect { execute_service }
.to trigger_internal_events('delete_ai_catalog_item')
.with(user: user, project: project, additional_properties: { label: item.item_type })
+ .and increment_usage_metrics('counts.count_total_delete_ai_catalog_item')
end
it 'returns success response' do
@@ -88,6 +90,22 @@
end
it_behaves_like 'soft deletes the item'
+
+ context 'with `force_hard_delete` param', :enable_admin_mode do
+ let(:params) { super().merge(force_hard_delete: true) }
+
+ it_behaves_like 'returns insufficient permissions error'
+
+ context 'when the user is an admin' do
+ let(:user) { admin }
+
+ it_behaves_like 'hard deletes the item'
+
+ it 'destroys the item consumers' do
+ expect { execute_service }.to change { Ai::Catalog::ItemConsumer.count }.by(-1)
+ end
+ end
+ end
end
end
@@ -149,6 +167,22 @@
end
it_behaves_like 'soft deletes the item'
+
+ context 'with `force_hard_delete` param', :enable_admin_mode do
+ let(:params) { super().merge(force_hard_delete: true) }
+
+ it_behaves_like 'returns insufficient permissions error'
+
+ context 'when the user is an admin' do
+ let(:user) { admin }
+
+ it_behaves_like 'hard deletes the item'
+
+ it 'destroys the version dependencies' do
+ expect { execute_service }.to change { Ai::Catalog::ItemVersionDependency.count }.by(-1)
+ end
+ end
+ end
end
end