From 5e649ce892ec159ded1b619f31b0fb974d4b051e Mon Sep 17 00:00:00 2001
From: Dylan Griffith <dyl.griffith@gmail.com>
Date: Fri, 1 Nov 2024 20:31:23 +0000
Subject: [PATCH] Create new WorkflowsInternal API for ai_workflows scope

This MR does not introduce any new API points but instead just moves
them from `Workflows` to `WorkflowsInternal`. The previous `Workflows`
API contains a mix of endpoints that are consumed by the Duo Workflow
Service as well as the VS Code extension in GitLab. The issue is that
the Duo Workflow Service only has an `ai_workflows` scope token which is
meant to be limited to only a narrow set of things it can do. The VS
Code extension has an `api` scoped token.

This MR splits the API up such that the endpoints needed by Duo Workflow
Service are all inside the `WorkflowsInternal` API.

Resolves https://gitlab.com/gitlab-org/gitlab/-/issues/500057

Changelog: other
---
 ee/lib/api/ai/duo_workflows/workflows.rb      | 108 -------
 .../ai/duo_workflows/workflows_internal.rb    | 150 ++++++++++
 ee/lib/ee/api/api.rb                          |   1 +
 .../duo_workflows/workflows_internal_spec.rb  | 280 ++++++++++++++++++
 .../api/ai/duo_workflows/workflows_spec.rb    | 269 +----------------
 5 files changed, 441 insertions(+), 367 deletions(-)
 create mode 100644 ee/lib/api/ai/duo_workflows/workflows_internal.rb
 create mode 100644 ee/spec/requests/api/ai/duo_workflows/workflows_internal_spec.rb

diff --git a/ee/lib/api/ai/duo_workflows/workflows.rb b/ee/lib/api/ai/duo_workflows/workflows.rb
index e2fd52f37eb4c..719c8bda3fa5b 100644
--- a/ee/lib/api/ai/duo_workflows/workflows.rb
+++ b/ee/lib/api/ai/duo_workflows/workflows.rb
@@ -11,8 +11,6 @@ class Workflows < ::API::Base
 
         before { authenticate! }
 
-        allow_access_with_scope :ai_workflows
-
         helpers do
           def find_workflow!(id)
             workflow = ::Ai::DuoWorkflows::Workflow.for_user_with_id!(current_user.id, id)
@@ -21,10 +19,6 @@ def find_workflow!(id)
             forbidden!
           end
 
-          def find_event!(workflow, id)
-            workflow.events.find(id)
-          end
-
           def authorize_run_workflows!(project)
             return if can?(current_user, :duo_workflow, project)
 
@@ -57,15 +51,6 @@ def create_workflow_params
             declared_params(include_missing: false)
           end
 
-          def render_response(response)
-            if response.success?
-              status :ok
-              response.payload
-            else
-              render_api_error!(response.message, response.reason)
-            end
-          end
-
           params :workflow_params do
             requires :project_id, type: String, desc: 'The ID or path of the workflow project',
               documentation: { example: '1' }
@@ -148,99 +133,6 @@ def render_response(response)
 
                 present workflow, with: ::API::Entities::Ai::DuoWorkflows::Workflow
               end
-
-              desc 'Updates the workflow status' do
-                success code: 200
-              end
-              params do
-                requires :id, type: Integer, desc: 'The ID of the workflow', documentation: { example: 1 }
-                requires :status_event, type: String, desc: 'The status event',
-                  documentation: { example: 'finish' }
-              end
-              patch '/:id' do
-                workflow = find_workflow!(params[:id])
-                forbidden! unless current_user.can?(:update_duo_workflow, workflow)
-
-                service = ::Ai::DuoWorkflows::UpdateWorkflowStatusService.new(
-                  workflow: workflow,
-                  status_event: params[:status_event],
-                  current_user: current_user
-                )
-
-                render_response(service.execute)
-              end
-
-              params do
-                requires :id, type: Integer, desc: 'The ID of the workflow'
-                requires :thread_ts, type: String, desc: 'The thread ts'
-                optional :parent_ts, type: String, desc: 'The parent ts'
-                requires :checkpoint, type: Hash, desc: "Checkpoint content"
-                requires :metadata, type: Hash, desc: "Checkpoint metadata"
-              end
-              post '/:id/checkpoints' do
-                workflow = find_workflow!(params[:id])
-                checkpoint_params = declared_params(include_missing: false).except(:id)
-                service = ::Ai::DuoWorkflows::CreateCheckpointService.new(project: workflow.project,
-                  workflow: workflow, params: checkpoint_params)
-                result = service.execute
-
-                bad_request!(result[:message]) if result[:status] == :error
-
-                present result[:checkpoint], with: ::API::Entities::Ai::DuoWorkflows::Checkpoint
-              end
-
-              get '/:id/checkpoints' do
-                workflow = find_workflow!(params[:id])
-                checkpoints = workflow.checkpoints.order(thread_ts: :desc) # rubocop:disable CodeReuse/ActiveRecord -- adding scope for order is no clearer
-                present paginate(checkpoints), with: ::API::Entities::Ai::DuoWorkflows::Checkpoint
-              end
-
-              params do
-                requires :id, type: Integer, desc: 'The ID of the workflow'
-                requires :event_type, type: String, values: %w[response message pause stop resume],
-                  desc: 'The type of event'
-                requires :message, type: String, desc: "Message from the human"
-              end
-              post '/:id/events' do
-                workflow = find_workflow!(params[:id])
-                event_params = declared_params(include_missing: false).except(:id)
-                service = ::Ai::DuoWorkflows::CreateEventService.new(
-                  project: workflow.project,
-                  workflow: workflow,
-                  params: event_params.merge(event_status: :queued)
-                )
-                result = service.execute
-
-                bad_request!(result[:message]) if result[:status] == :error
-
-                present result[:event], with: ::API::Entities::Ai::DuoWorkflows::Event
-              end
-
-              get '/:id/events' do
-                workflow = find_workflow!(params[:id])
-                events = workflow.events.queued
-                present paginate(events), with: ::API::Entities::Ai::DuoWorkflows::Event
-              end
-
-              params do
-                requires :id, type: Integer, desc: 'The ID of the workflow'
-                requires :event_id, type: Integer, desc: 'The ID of the event'
-                requires :event_status, type: String, values: %w[queued delivered], desc: 'The status of the event'
-              end
-              put '/:id/events/:event_id' do
-                workflow = find_workflow!(params[:id])
-                event = find_event!(workflow, params[:event_id])
-                event_params = declared_params(include_missing: false).except(:id, :event_id)
-                service = ::Ai::DuoWorkflows::UpdateEventService.new(
-                  event: event,
-                  params: event_params
-                )
-                result = service.execute
-
-                bad_request!(result[:message]) if result[:status] == :error
-
-                present result[:event], with: ::API::Entities::Ai::DuoWorkflows::Event
-              end
             end
           end
         end
diff --git a/ee/lib/api/ai/duo_workflows/workflows_internal.rb b/ee/lib/api/ai/duo_workflows/workflows_internal.rb
new file mode 100644
index 0000000000000..94dd13889aa18
--- /dev/null
+++ b/ee/lib/api/ai/duo_workflows/workflows_internal.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+module API
+  module Ai
+    module DuoWorkflows
+      # This API is intended to be consumed by a running Duo Workflow using
+      # the ai_workflows scope token. These are requests coming from Duo Workflow
+      # Service and Duo Workflow Executor. We should not add any more requests to
+      # this API than needed by those 2 components. Otherwise add to
+      # `API::Ai::DuoWorkflows::Workflows`.
+      class WorkflowsInternal < ::API::Base
+        include PaginationParams
+        include APIGuard
+
+        allow_access_with_scope :ai_workflows
+
+        feature_category :duo_workflow
+
+        before { authenticate! }
+
+        helpers do
+          def find_workflow!(id)
+            workflow = ::Ai::DuoWorkflows::Workflow.for_user_with_id!(current_user.id, id)
+            return workflow if current_user.can?(:read_duo_workflow, workflow)
+
+            forbidden!
+          end
+
+          def find_event!(workflow, id)
+            workflow.events.find(id)
+          end
+
+          def render_response(response)
+            if response.success?
+              status :ok
+              response.payload
+            else
+              render_api_error!(response.message, response.reason)
+            end
+          end
+        end
+
+        namespace :ai do
+          namespace :duo_workflows do
+            namespace :workflows do
+              namespace '/:id' do
+                desc 'Updates the workflow status' do
+                  success code: 200
+                end
+                params do
+                  requires :id, type: Integer, desc: 'The ID of the workflow', documentation: { example: 1 }
+                  requires :status_event, type: String, desc: 'The status event',
+                    documentation: { example: 'finish' }
+                end
+                patch do
+                  workflow = find_workflow!(params[:id])
+                  forbidden! unless current_user.can?(:update_duo_workflow, workflow)
+
+                  service = ::Ai::DuoWorkflows::UpdateWorkflowStatusService.new(
+                    workflow: workflow,
+                    status_event: params[:status_event],
+                    current_user: current_user
+                  )
+
+                  render_response(service.execute)
+                end
+
+                namespace :checkpoints do
+                  params do
+                    requires :id, type: Integer, desc: 'The ID of the workflow'
+                    requires :thread_ts, type: String, desc: 'The thread ts'
+                    optional :parent_ts, type: String, desc: 'The parent ts'
+                    requires :checkpoint, type: Hash, desc: "Checkpoint content"
+                    requires :metadata, type: Hash, desc: "Checkpoint metadata"
+                  end
+                  post do
+                    workflow = find_workflow!(params[:id])
+                    checkpoint_params = declared_params(include_missing: false).except(:id)
+                    service = ::Ai::DuoWorkflows::CreateCheckpointService.new(project: workflow.project,
+                      workflow: workflow, params: checkpoint_params)
+                    result = service.execute
+
+                    bad_request!(result[:message]) if result[:status] == :error
+
+                    present result[:checkpoint], with: ::API::Entities::Ai::DuoWorkflows::Checkpoint
+                  end
+
+                  get do
+                    workflow = find_workflow!(params[:id])
+                    checkpoints = workflow.checkpoints.order(thread_ts: :desc) # rubocop:disable CodeReuse/ActiveRecord -- adding scope for order is no clearer
+                    present paginate(checkpoints), with: ::API::Entities::Ai::DuoWorkflows::Checkpoint
+                  end
+                end
+
+                namespace :events do
+                  params do
+                    requires :id, type: Integer, desc: 'The ID of the workflow'
+                    requires :event_type, type: String, values: %w[response message pause stop resume],
+                      desc: 'The type of event'
+                    requires :message, type: String, desc: "Message from the human"
+                  end
+                  post do
+                    workflow = find_workflow!(params[:id])
+                    event_params = declared_params(include_missing: false).except(:id)
+                    service = ::Ai::DuoWorkflows::CreateEventService.new(
+                      project: workflow.project,
+                      workflow: workflow,
+                      params: event_params.merge(event_status: :queued)
+                    )
+                    result = service.execute
+
+                    bad_request!(result[:message]) if result[:status] == :error
+
+                    present result[:event], with: ::API::Entities::Ai::DuoWorkflows::Event
+                  end
+
+                  get do
+                    workflow = find_workflow!(params[:id])
+                    events = workflow.events.queued
+                    present paginate(events), with: ::API::Entities::Ai::DuoWorkflows::Event
+                  end
+
+                  params do
+                    requires :id, type: Integer, desc: 'The ID of the workflow'
+                    requires :event_id, type: Integer, desc: 'The ID of the event'
+                    requires :event_status, type: String, values: %w[queued delivered], desc: 'The status of the event'
+                  end
+                  put '/:event_id' do
+                    workflow = find_workflow!(params[:id])
+                    event = find_event!(workflow, params[:event_id])
+                    event_params = declared_params(include_missing: false).except(:id, :event_id)
+                    service = ::Ai::DuoWorkflows::UpdateEventService.new(
+                      event: event,
+                      params: event_params
+                    )
+                    result = service.execute
+
+                    bad_request!(result[:message]) if result[:status] == :error
+
+                    present result[:event], with: ::API::Entities::Ai::DuoWorkflows::Event
+                  end
+                end
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/ee/api/api.rb b/ee/lib/ee/api/api.rb
index fd071c5d586b3..4bfd8d2cfcb11 100644
--- a/ee/lib/ee/api/api.rb
+++ b/ee/lib/ee/api/api.rb
@@ -71,6 +71,7 @@ module API
         mount ::API::GroupServiceAccounts
         mount ::API::Ai::Llm::GitCommand
         mount ::API::Ai::DuoWorkflows::Workflows
+        mount ::API::Ai::DuoWorkflows::WorkflowsInternal
         mount ::API::CodeSuggestions
         mount ::API::Chat
         mount ::API::DuoCodeReview
diff --git a/ee/spec/requests/api/ai/duo_workflows/workflows_internal_spec.rb b/ee/spec/requests/api/ai/duo_workflows/workflows_internal_spec.rb
new file mode 100644
index 0000000000000..84caa07704df7
--- /dev/null
+++ b/ee/spec/requests/api/ai/duo_workflows/workflows_internal_spec.rb
@@ -0,0 +1,280 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ai::DuoWorkflows::WorkflowsInternal, feature_category: :duo_workflow do
+  include HttpBasicAuthHelpers
+
+  let_it_be(:group) { create(:group) }
+  let_it_be(:project) { create(:project, :repository, group: group) }
+  let_it_be(:user) { create(:user, maintainer_of: project) }
+  let_it_be(:workflow) { create(:duo_workflows_workflow, user: user, project: project) }
+  let_it_be(:duo_workflow_service_url) { 'duo-workflow-service.example.com:50052' }
+  let_it_be(:oauth_token) { create(:oauth_access_token, user: user, scopes: [:ai_workflows]) }
+
+  before do
+    allow(::Gitlab::Llm::StageCheck).to receive(:available?).with(project, :duo_workflow).and_return(true)
+  end
+
+  describe 'POST /ai/duo_workflows/workflows/:id/checkpoints' do
+    let(:current_time) { Time.current }
+    let(:thread_ts) { current_time.to_s }
+    let(:later_thread_ts) { (current_time + 1.second).to_s }
+    let(:parent_ts) { (current_time - 1.second).to_s }
+    let(:checkpoint) { { key: 'value' } }
+    let(:metadata) { { key: 'value' } }
+    let(:params) { { thread_ts: thread_ts, checkpoint: checkpoint, parent_ts: parent_ts, metadata: metadata } }
+    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/checkpoints" }
+
+    it 'allows creating multiple checkpoints for a workflow' do
+      expect do
+        post api(path, user), params: params
+        expect(response).to have_gitlab_http_status(:created)
+
+        post api(path, user), params: params.merge(thread_ts: later_thread_ts, parent_ts: thread_ts)
+        expect(response).to have_gitlab_http_status(:created)
+      end.to change { workflow.reload.checkpoints.count }.by(2)
+
+      expect(json_response['id']).to eq(Ai::DuoWorkflows::Checkpoint.last.id)
+    end
+
+    context 'when authenticated with a token that has the ai_workflows scope' do
+      it 'is successful' do
+        post api(path, oauth_access_token: oauth_token),
+          params: params.merge(thread_ts: later_thread_ts, parent_ts: thread_ts)
+
+        expect(response).to have_gitlab_http_status(:created)
+      end
+    end
+
+    it 'fails if the thread_ts is an empty string' do
+      post api(path, user), params: params.merge(thread_ts: '')
+      expect(response).to have_gitlab_http_status(:bad_request)
+      expect(json_response['message']).to include("can't be blank")
+    end
+  end
+
+  describe 'GET /ai/duo_workflows/workflows/:id/checkpoints' do
+    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/checkpoints" }
+
+    it 'returns the checkpoints in descending order of thread_ts' do
+      checkpoint1 = create(:duo_workflows_checkpoint, workflow: workflow)
+      checkpoint2 = create(:duo_workflows_checkpoint, workflow: workflow)
+      workflow.checkpoints << checkpoint1
+      workflow.checkpoints << checkpoint2
+
+      get api(path, user)
+      expect(response).to have_gitlab_http_status(:ok)
+      expect(json_response.pluck('id')).to eq([checkpoint2.id, checkpoint1.id])
+      expect(json_response.pluck('thread_ts')).to eq([checkpoint2.thread_ts, checkpoint1.thread_ts])
+      expect(json_response.pluck('parent_ts')).to eq([checkpoint2.parent_ts, checkpoint1.parent_ts])
+      expect(json_response[0]).to have_key('checkpoint')
+      expect(json_response[0]).to have_key('metadata')
+    end
+  end
+
+  describe 'POST /ai/duo_workflows/workflows/:id/events' do
+    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/events" }
+    let(:params) do
+      {
+        event_type: 'message',
+        message: 'Hello, World!'
+      }
+    end
+
+    context 'when success' do
+      it 'creates a new event' do
+        expect do
+          post api(path, user), params: params
+          expect(response).to have_gitlab_http_status(:created)
+        end.to change { workflow.events.count }.by(1)
+        expect(json_response['id']).to eq(Ai::DuoWorkflows::Event.last.id)
+        expect(json_response['event_type']).to eq('message')
+        expect(json_response['message']).to eq('Hello, World!')
+        expect(json_response['event_status']).to eq('queued')
+      end
+
+      context 'when authenticated with a token that has the ai_workflows scope' do
+        it 'is successful' do
+          expect do
+            post api(path, oauth_access_token: oauth_token), params: params
+            expect(response).to have_gitlab_http_status(:created)
+          end.to change { workflow.events.count }.by(1)
+        end
+      end
+    end
+
+    context 'when required parameters are missing' do
+      it 'returns bad request when event_type is missing' do
+        post api(path, user), params: params.except(:event_type)
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['error']).to include("event_type is missing")
+      end
+
+      it 'returns bad request when message is missing' do
+        post api(path, user), params: params.except(:message)
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['error']).to include("message is missing")
+      end
+    end
+
+    context 'when invalid event_type is provided' do
+      it 'returns bad request' do
+        post api(path, user), params: params.merge(event_type: 'invalid_event_type')
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['error']).to include("event_type does not have a valid value")
+      end
+    end
+
+    context 'with a workflow belonging to a different user' do
+      let(:workflow) { create(:duo_workflows_workflow) }
+
+      it 'returns 404' do
+        post api(path, user), params: params
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+  end
+
+  describe 'GET /ai/duo_workflows/workflows/:id/events' do
+    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/events" }
+
+    it 'returns queued events for the workflow' do
+      event1 = create(:duo_workflows_event, workflow: workflow, event_status: :queued)
+      event2 = create(:duo_workflows_event, workflow: workflow, event_status: :queued)
+
+      get api(path, user)
+      expect(response).to have_gitlab_http_status(:ok)
+      expect(json_response.size).to eq(2)
+      # rubocop:disable Rails/Pluck -- json_response is an array of hashes, we can't use pluck
+      expect(json_response.map { |e| e['id'] }).to contain_exactly(event1.id, event2.id)
+      expect(json_response.map { |e| e['event_status'] }).to all(eq('queued'))
+      # rubocop:enable Rails/Pluck
+    end
+
+    it 'returns empty array if no queued events' do
+      create(:duo_workflows_event, workflow: workflow, event_status: :delivered)
+
+      get api(path, user)
+      expect(response).to have_gitlab_http_status(:ok)
+      expect(json_response).to be_empty
+    end
+
+    context 'with a workflow belonging to a different user' do
+      let(:workflow) { create(:duo_workflows_workflow) }
+
+      it 'returns 404' do
+        get api(path, user)
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+  end
+
+  describe 'PUT /ai/duo_workflows/workflows/:id/events/:event_id' do
+    let(:event) { create(:duo_workflows_event, workflow: workflow, event_status: :queued) }
+    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/events/#{event.id}" }
+    let(:params) { { event_status: 'delivered' } }
+
+    context 'when success' do
+      it 'updates the event status' do
+        put api(path, user), params: params
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(json_response['id']).to eq(event.id)
+        expect(json_response['event_status']).to eq('delivered')
+        expect(event.reload.event_status).to eq('delivered')
+      end
+
+      context 'when authenticated with a token that has the ai_workflows scope' do
+        it 'is successful' do
+          put api(path, oauth_access_token: oauth_token), params: params
+          expect(response).to have_gitlab_http_status(:ok)
+        end
+      end
+    end
+
+    context 'when invalid event_status is provided' do
+      it 'returns bad request' do
+        put api(path, user), params: { event_status: 'InvalidStatus' }
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['error']).to include("event_status does not have a valid value")
+      end
+    end
+
+    context 'when the event does not exist' do
+      let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/events/0" }
+
+      it 'returns 404' do
+        put api(path, user), params: params
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+
+    context 'with a workflow belonging to a different user' do
+      let(:workflow) { create(:duo_workflows_workflow) }
+
+      it 'returns 404' do
+        put api(path, user), params: params
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+
+    context 'with an event belonging to a different workflow' do
+      let(:other_workflow) { create(:duo_workflows_workflow, user: user, project: project) }
+      let(:event) { create(:duo_workflows_event, workflow: other_workflow) }
+
+      it 'returns 404' do
+        put api(path, user), params: params
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+  end
+
+  describe 'PATCH /ai/duo_workflows/workflows/:id' do
+    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}" }
+
+    context 'when update workflow status service returns error' do
+      before do
+        allow_next_instance_of(::Ai::DuoWorkflows::UpdateWorkflowStatusService) do |service|
+          allow(service).to receive(:execute).and_return(ServiceResponse.error(reason: :bad_request,
+            message: 'Cannot update workflow status'))
+        end
+      end
+
+      it 'returns http error status and error message' do
+        patch api(path, user), params: { status_event: "finish" }
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['message']).to eq('Cannot update workflow status')
+      end
+    end
+
+    context 'when update workflow status service returns success' do
+      before do
+        allow_next_instance_of(::Ai::DuoWorkflows::UpdateWorkflowStatusService) do |service|
+          allow(service).to receive(:execute).and_return(ServiceResponse.success(payload: { workflow: workflow },
+            message: 'Workflow status updated'))
+        end
+      end
+
+      it 'returns http status ok' do
+        patch api(path, user), params: { status_event: "finish" }
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(json_response['workflow']['id']).to eq(workflow.id)
+      end
+    end
+
+    context 'when duo_features_enabled settings is turned off' do
+      before do
+        workflow.project.project_setting.update!(duo_features_enabled: false)
+        workflow.project.reload
+      end
+
+      it 'returns forbidden' do
+        patch api(path, user), params: { status_event: "finish" }
+
+        expect(response).to have_gitlab_http_status(:forbidden)
+      end
+    end
+  end
+end
diff --git a/ee/spec/requests/api/ai/duo_workflows/workflows_spec.rb b/ee/spec/requests/api/ai/duo_workflows/workflows_spec.rb
index 26d02a5c9e352..a54bf1da19738 100644
--- a/ee/spec/requests/api/ai/duo_workflows/workflows_spec.rb
+++ b/ee/spec/requests/api/ai/duo_workflows/workflows_spec.rb
@@ -10,7 +10,7 @@
   let_it_be(:user) { create(:user, maintainer_of: project) }
   let_it_be(:workflow) { create(:duo_workflows_workflow, user: user, project: project) }
   let_it_be(:duo_workflow_service_url) { 'duo-workflow-service.example.com:50052' }
-  let_it_be(:oauth_token) { create(:oauth_access_token, user: user, scopes: [:ai_workflows]) }
+  let_it_be(:ai_workflows_oauth_token) { create(:oauth_access_token, user: user, scopes: [:ai_workflows]) }
 
   before do
     allow(::Gitlab::Llm::StageCheck).to receive(:available?).with(project, :duo_workflow).and_return(true)
@@ -30,10 +30,10 @@
       end
 
       context 'when authenticated with a token that has the ai_workflows scope' do
-        it 'is successful' do
-          post api(path, oauth_access_token: oauth_token), params: params
+        it 'is forbidden' do
+          post api(path, oauth_access_token: ai_workflows_oauth_token), params: params
 
-          expect(response).to have_gitlab_http_status(:created)
+          expect(response).to have_gitlab_http_status(:forbidden)
         end
       end
 
@@ -95,10 +95,10 @@
     end
 
     context 'when authenticated with a token that has the ai_workflows scope' do
-      it 'is successful' do
-        get api(path, oauth_access_token: oauth_token)
+      it 'is forbidden' do
+        get api(path, oauth_access_token: ai_workflows_oauth_token)
 
-        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to have_gitlab_http_status(:forbidden)
       end
     end
 
@@ -124,219 +124,6 @@
     end
   end
 
-  describe 'POST /ai/duo_workflows/workflows/:id/checkpoints' do
-    let(:current_time) { Time.current }
-    let(:thread_ts) { current_time.to_s }
-    let(:later_thread_ts) { (current_time + 1.second).to_s }
-    let(:parent_ts) { (current_time - 1.second).to_s }
-    let(:checkpoint) { { key: 'value' } }
-    let(:metadata) { { key: 'value' } }
-    let(:params) { { thread_ts: thread_ts, checkpoint: checkpoint, parent_ts: parent_ts, metadata: metadata } }
-    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/checkpoints" }
-
-    it 'allows creating multiple checkpoints for a workflow' do
-      expect do
-        post api(path, user), params: params
-        expect(response).to have_gitlab_http_status(:created)
-
-        post api(path, user), params: params.merge(thread_ts: later_thread_ts, parent_ts: thread_ts)
-        expect(response).to have_gitlab_http_status(:created)
-      end.to change { workflow.reload.checkpoints.count }.by(2)
-
-      expect(json_response['id']).to eq(Ai::DuoWorkflows::Checkpoint.last.id)
-    end
-
-    context 'when authenticated with a token that has the ai_workflows scope' do
-      it 'is successful' do
-        post api(path, oauth_access_token: oauth_token),
-          params: params.merge(thread_ts: later_thread_ts, parent_ts: thread_ts)
-
-        expect(response).to have_gitlab_http_status(:created)
-      end
-    end
-
-    it 'fails if the thread_ts is an empty string' do
-      post api(path, user), params: params.merge(thread_ts: '')
-      expect(response).to have_gitlab_http_status(:bad_request)
-      expect(json_response['message']).to include("can't be blank")
-    end
-  end
-
-  describe 'GET /ai/duo_workflows/workflows/:id/checkpoints' do
-    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/checkpoints" }
-
-    it 'returns the checkpoints in descending order of thread_ts' do
-      checkpoint1 = create(:duo_workflows_checkpoint, workflow: workflow)
-      checkpoint2 = create(:duo_workflows_checkpoint, workflow: workflow)
-      workflow.checkpoints << checkpoint1
-      workflow.checkpoints << checkpoint2
-
-      get api(path, user)
-      expect(response).to have_gitlab_http_status(:ok)
-      expect(json_response.pluck('id')).to eq([checkpoint2.id, checkpoint1.id])
-      expect(json_response.pluck('thread_ts')).to eq([checkpoint2.thread_ts, checkpoint1.thread_ts])
-      expect(json_response.pluck('parent_ts')).to eq([checkpoint2.parent_ts, checkpoint1.parent_ts])
-      expect(json_response[0]).to have_key('checkpoint')
-      expect(json_response[0]).to have_key('metadata')
-    end
-  end
-
-  describe 'POST /ai/duo_workflows/workflows/:id/events' do
-    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/events" }
-    let(:params) do
-      {
-        event_type: 'message',
-        message: 'Hello, World!'
-      }
-    end
-
-    context 'when success' do
-      it 'creates a new event' do
-        expect do
-          post api(path, user), params: params
-          expect(response).to have_gitlab_http_status(:created)
-        end.to change { workflow.events.count }.by(1)
-        expect(json_response['id']).to eq(Ai::DuoWorkflows::Event.last.id)
-        expect(json_response['event_type']).to eq('message')
-        expect(json_response['message']).to eq('Hello, World!')
-        expect(json_response['event_status']).to eq('queued')
-      end
-
-      context 'when authenticated with a token that has the ai_workflows scope' do
-        it 'is successful' do
-          expect do
-            post api(path, oauth_access_token: oauth_token), params: params
-            expect(response).to have_gitlab_http_status(:created)
-          end.to change { workflow.events.count }.by(1)
-        end
-      end
-    end
-
-    context 'when required parameters are missing' do
-      it 'returns bad request when event_type is missing' do
-        post api(path, user), params: params.except(:event_type)
-        expect(response).to have_gitlab_http_status(:bad_request)
-        expect(json_response['error']).to include("event_type is missing")
-      end
-
-      it 'returns bad request when message is missing' do
-        post api(path, user), params: params.except(:message)
-        expect(response).to have_gitlab_http_status(:bad_request)
-        expect(json_response['error']).to include("message is missing")
-      end
-    end
-
-    context 'when invalid event_type is provided' do
-      it 'returns bad request' do
-        post api(path, user), params: params.merge(event_type: 'invalid_event_type')
-        expect(response).to have_gitlab_http_status(:bad_request)
-        expect(json_response['error']).to include("event_type does not have a valid value")
-      end
-    end
-
-    context 'with a workflow belonging to a different user' do
-      let(:workflow) { create(:duo_workflows_workflow) }
-
-      it 'returns 404' do
-        post api(path, user), params: params
-        expect(response).to have_gitlab_http_status(:not_found)
-      end
-    end
-  end
-
-  describe 'GET /ai/duo_workflows/workflows/:id/events' do
-    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/events" }
-
-    it 'returns queued events for the workflow' do
-      event1 = create(:duo_workflows_event, workflow: workflow, event_status: :queued)
-      event2 = create(:duo_workflows_event, workflow: workflow, event_status: :queued)
-
-      get api(path, user)
-      expect(response).to have_gitlab_http_status(:ok)
-      expect(json_response.size).to eq(2)
-      # rubocop:disable Rails/Pluck -- json_response is an array of hashes, we can't use pluck
-      expect(json_response.map { |e| e['id'] }).to contain_exactly(event1.id, event2.id)
-      expect(json_response.map { |e| e['event_status'] }).to all(eq('queued'))
-      # rubocop:enable Rails/Pluck
-    end
-
-    it 'returns empty array if no queued events' do
-      create(:duo_workflows_event, workflow: workflow, event_status: :delivered)
-
-      get api(path, user)
-      expect(response).to have_gitlab_http_status(:ok)
-      expect(json_response).to be_empty
-    end
-
-    context 'with a workflow belonging to a different user' do
-      let(:workflow) { create(:duo_workflows_workflow) }
-
-      it 'returns 404' do
-        get api(path, user)
-        expect(response).to have_gitlab_http_status(:not_found)
-      end
-    end
-  end
-
-  describe 'PUT /ai/duo_workflows/workflows/:id/events/:event_id' do
-    let(:event) { create(:duo_workflows_event, workflow: workflow, event_status: :queued) }
-    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/events/#{event.id}" }
-    let(:params) { { event_status: 'delivered' } }
-
-    context 'when success' do
-      it 'updates the event status' do
-        put api(path, user), params: params
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response['id']).to eq(event.id)
-        expect(json_response['event_status']).to eq('delivered')
-        expect(event.reload.event_status).to eq('delivered')
-      end
-
-      context 'when authenticated with a token that has the ai_workflows scope' do
-        it 'is successful' do
-          put api(path, oauth_access_token: oauth_token), params: params
-          expect(response).to have_gitlab_http_status(:ok)
-        end
-      end
-    end
-
-    context 'when invalid event_status is provided' do
-      it 'returns bad request' do
-        put api(path, user), params: { event_status: 'InvalidStatus' }
-        expect(response).to have_gitlab_http_status(:bad_request)
-        expect(json_response['error']).to include("event_status does not have a valid value")
-      end
-    end
-
-    context 'when the event does not exist' do
-      let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}/events/0" }
-
-      it 'returns 404' do
-        put api(path, user), params: params
-        expect(response).to have_gitlab_http_status(:not_found)
-      end
-    end
-
-    context 'with a workflow belonging to a different user' do
-      let(:workflow) { create(:duo_workflows_workflow) }
-
-      it 'returns 404' do
-        put api(path, user), params: params
-        expect(response).to have_gitlab_http_status(:not_found)
-      end
-    end
-
-    context 'with an event belonging to a different workflow' do
-      let(:other_workflow) { create(:duo_workflows_workflow, user: user, project: project) }
-      let(:event) { create(:duo_workflows_event, workflow: other_workflow) }
-
-      it 'returns 404' do
-        put api(path, user), params: params
-        expect(response).to have_gitlab_http_status(:not_found)
-      end
-    end
-  end
-
   describe 'POST /ai/duo_workflows/direct_access' do
     let(:path) { '/ai/duo_workflows/direct_access' }
 
@@ -439,48 +226,12 @@
       end
 
       context 'when authenticated with a token that has the ai_workflows scope' do
-        it 'succeeds' do
-          post api(path, oauth_access_token: oauth_token)
+        it 'is forbidden' do
+          post api(path, oauth_access_token: ai_workflows_oauth_token)
 
-          expect(response).to have_gitlab_http_status(:created)
-        end
-      end
-    end
-  end
-
-  describe 'PATCH /ai/duo_workflows/workflows/:id' do
-    let(:path) { "/ai/duo_workflows/workflows/#{workflow.id}" }
-
-    context 'when update workflow status service returns error' do
-      before do
-        allow_next_instance_of(::Ai::DuoWorkflows::UpdateWorkflowStatusService) do |service|
-          allow(service).to receive(:execute).and_return(ServiceResponse.error(reason: :bad_request,
-            message: 'Cannot update workflow status'))
-        end
-      end
-
-      it 'returns http error status and error message' do
-        patch api(path, user), params: { status_event: "finish" }
-
-        expect(response).to have_gitlab_http_status(:bad_request)
-        expect(json_response['message']).to eq('Cannot update workflow status')
-      end
-    end
-
-    context 'when update workflow status service returns success' do
-      before do
-        allow_next_instance_of(::Ai::DuoWorkflows::UpdateWorkflowStatusService) do |service|
-          allow(service).to receive(:execute).and_return(ServiceResponse.success(payload: { workflow: workflow },
-            message: 'Workflow status updated'))
+          expect(response).to have_gitlab_http_status(:forbidden)
         end
       end
-
-      it 'returns http status ok' do
-        patch api(path, user), params: { status_event: "finish" }
-
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response['workflow']['id']).to eq(workflow.id)
-      end
     end
   end
 end
-- 
GitLab