Skip to content
Open
Show file tree
Hide file tree
Changes from 115 commits
Commits
Show all changes
135 commits
Select commit Hold shift + click to select a range
334b85e
[#73373] PoC: Create semantic WP identifier registry
thykel Mar 26, 2026
c2566fc
add bg job todo
thykel Mar 26, 2026
cf7a41d
remove stale files
thykel Mar 26, 2026
fa179c1
fix broken tests
thykel Mar 26, 2026
490c90e
rubocop
thykel Mar 26, 2026
4a7cedf
minor refactoring & config guards
thykel Mar 26, 2026
471f2f5
address more edge cases
thykel Mar 26, 2026
b9dccaa
rubocop
thykel Mar 26, 2026
b32d601
add missing column
thykel Mar 26, 2026
711dc65
rubocop ignore
thykel Mar 26, 2026
4c2631c
adapt migrations
thykel Mar 29, 2026
42887b4
migration: add semantic_id to wp and remove :current from aliases
thykel Mar 29, 2026
39587d5
work_package_semantic_id => work_package_semantic_aliase
thykel Mar 29, 2026
3b52f25
rename the association on WP
thykel Mar 29, 2026
a0cd7ea
Merge remote-tracking branch 'origin/dev' into poc/73373-semantic-ide…
thykel Mar 29, 2026
e941542
make sure WP table is looked up before the alias table
thykel Mar 29, 2026
595496c
rename the concern
thykel Mar 29, 2026
15a477a
move wp sequence allocation to Project
thykel Mar 29, 2026
de16856
refactor the identifier allocation
thykel Mar 29, 2026
47d6972
remove superfluous migrations
thykel Mar 29, 2026
4460fc4
add down migration
thykel Mar 29, 2026
d5f02b8
address the security warning
thykel Mar 29, 2026
9120f9d
remove the bg job
thykel Mar 29, 2026
f3fc8a7
wip
thykel Mar 29, 2026
306443a
remove the spec as well
thykel Mar 29, 2026
ed304e4
Merge branch 'poc/73373-semantic-identifier-registry' into poc/71645-…
thykel Mar 29, 2026
8830fd8
revert some unrelated linter changes
thykel Mar 29, 2026
1c4aefc
add a unique index on sequence_number
thykel Mar 29, 2026
1145211
anchor the route regex
thykel Mar 29, 2026
8edd09d
transaction isolation fix
thykel Mar 29, 2026
62ea269
fix alignment
thykel Mar 29, 2026
30a752f
rails already anchors routes automatically
thykel Mar 29, 2026
26a890a
address latest PR feedback
thykel Mar 29, 2026
25962e5
squash some rubocop warns
thykel Mar 29, 2026
ba9cfd2
fix a test
thykel Mar 29, 2026
5d143c3
unblock migration
thykel Mar 29, 2026
7f21633
allowlist find_by_semantic_identifier
thykel Mar 29, 2026
6be9cb8
lint
thykel Mar 29, 2026
4752193
revert find back to find_by
thykel Mar 29, 2026
24f3012
align to the new 5-step script and move alias methods to identifier c…
thykel Mar 29, 2026
b4baa10
move the tests to concern spec
thykel Mar 29, 2026
9f65fb7
rename the register methods
thykel Mar 29, 2026
342e744
address rubocop complaints
thykel Mar 29, 2026
c68f43f
rewrite
thykel Mar 30, 2026
6fa6b36
Merge branch 'poc/73373-semantic-identifier-registry' into poc/71645-…
thykel Mar 30, 2026
a3930e3
rewrite to rely more heavily on alias storage
thykel Mar 30, 2026
1772030
fix a class-scoped reference
thykel Mar 30, 2026
a14f008
fix scoping
thykel Mar 30, 2026
3117589
Apply suggestions from code review
thykel Mar 30, 2026
aa8d6b5
Apply suggestion from @Copilot
thykel Mar 30, 2026
aad767e
remove empty line
thykel Mar 30, 2026
87f4c90
Merge remote-tracking branch 'origin/impl/73373-semantic-registry-que…
thykel Mar 30, 2026
7955c21
rename
thykel Mar 30, 2026
d91920b
rename touchups
thykel Mar 30, 2026
ec086c7
Apply suggestions from code review
thykel Mar 30, 2026
6532170
Merge branch 'impl/73373-semantic-registry-query-optimized' into poc/…
thykel Mar 30, 2026
8ab410d
seed alias table
thykel Mar 30, 2026
7f47303
squash the 2 create/move hook methods into a single allocate_and_regi…
thykel Mar 30, 2026
50b56a0
add more comments
thykel Mar 30, 2026
b419054
find_by_identifier => find_by_id_or_identifier
thykel Mar 30, 2026
48b8d16
split semantic ID detection into its own method
thykel Mar 30, 2026
aa4c910
semantic_id => identifier
thykel Mar 30, 2026
17559b3
superfluous to_s line
thykel Mar 30, 2026
9548b0f
cosmetics
thykel Mar 30, 2026
209b0c4
Merge remote-tracking branch 'origin/dev' into impl/73373-semantic-re…
thykel Mar 30, 2026
65790dd
rename migration
thykel Mar 30, 2026
6dc7474
clean up migration
thykel Mar 30, 2026
2ac5a4c
alphanumeric? => semantic?
thykel Mar 30, 2026
fcfbceb
fix the leftover tests still using :numeric?
thykel Mar 30, 2026
a6fc410
cosmetics
thykel Mar 30, 2026
3693926
move the route constraint
thykel Mar 30, 2026
79b9997
Merge branch 'impl/73373-semantic-registry-query-optimized' into poc/…
thykel Mar 30, 2026
7343662
rewrite to use batches and multiple steps
thykel Mar 30, 2026
59f64a8
lint
thykel Mar 30, 2026
fb64770
Merge remote-tracking branch 'origin/dev' into impl/73373-semantic-re…
thykel Mar 31, 2026
e9cf0c1
make sure to destroy the semantic aliases in bulk when wp is destroyed
thykel Mar 31, 2026
1cdef00
simplify find_by_semantic_identifier
thykel Mar 31, 2026
af1e613
reorganize spec files as per suggestion
thykel Mar 31, 2026
20c6d37
optimize the semantic finder
thykel Mar 31, 2026
8216a62
remove the nasty FriendlyId invocations from the operations spec
thykel Mar 31, 2026
bab0b30
rewrite the identifier allocation method as per suggestion
thykel Mar 31, 2026
314bc36
remove the route updates
thykel Mar 31, 2026
ded6728
revert the controller too
thykel Mar 31, 2026
e11d5fe
convert #semantic_identifier_aliases into a project association
thykel Mar 31, 2026
c7f954b
rename SemanticIdentifierOperations to SemanticIdentifier for uniformity
thykel Mar 31, 2026
4acbb83
add method comments
thykel Mar 31, 2026
6307392
patch some Rubocop warns
thykel Mar 31, 2026
1ba50ee
convert alias updates to raw SQL and align batching
thykel Mar 31, 2026
98f1e20
use keywords instead of positional args in the raw query
thykel Mar 31, 2026
df05930
add tests for batching
thykel Mar 31, 2026
8e877b8
revert the migration bulkization
thykel Mar 31, 2026
0e99e46
Merge branch 'impl/73373-semantic-registry-query-optimized' into poc/…
thykel Mar 31, 2026
53c00f9
Merge remote-tracking branch 'origin/dev' into impl/73373-semantic-re…
thykel Apr 1, 2026
4939b5a
Merge branch 'impl/73373-semantic-registry-query-optimized' into poc/…
thykel Apr 1, 2026
5671e11
replace identifier_aliases with simple call to slugs
thykel Apr 1, 2026
de99339
Merge branch 'impl/73373-semantic-registry-query-optimized' into poc/…
thykel Apr 1, 2026
94e79c8
Merge remote-tracking branch 'origin/dev' into poc/71645-convert-to-w…
thykel Apr 1, 2026
3529fa1
Merge remote-tracking branch 'origin/dev' into poc/71645-convert-to-w…
thykel Apr 7, 2026
d92529b
remove private scope for alias builder method
thykel Apr 7, 2026
aa5af20
add retry when data not yet processed at the end
thykel Apr 7, 2026
74dcdd7
move into a better-named top-level module
thykel Apr 7, 2026
99d12fd
add a spec for autofix checker
thykel Apr 7, 2026
90f3c5f
make sure to flip the parameter if data is already in order
thykel Apr 7, 2026
a50e9c0
minor refactoring
thykel Apr 7, 2026
50646af
cleanly assert that a job has been enqueued
thykel Apr 8, 2026
851b551
remove a superfluous spec
thykel Apr 8, 2026
52fce11
add a controller spec
thykel Apr 8, 2026
6931dbc
rework the admin update interface
thykel Apr 8, 2026
4359313
fix the parameter retrieval
thykel Apr 8, 2026
4179141
get rid of the final job
thykel Apr 8, 2026
5422b14
lint
thykel Apr 8, 2026
45ddcfc
Update app/workers/project_identifiers/convert_instance_to_semantic_i…
thykel Apr 8, 2026
b278845
make the controller action more readable & do not enqueue if in progress
thykel Apr 8, 2026
cb1d926
add iteration testing
thykel Apr 8, 2026
b6884b0
fix a test
thykel Apr 8, 2026
7c5508c
restore semantic ID from friendly_id history
thykel Apr 12, 2026
aa3eb23
handle semantic ID mismatch (moved packages)
thykel Apr 12, 2026
5cb7162
move the last slug method to Project concern
thykel Apr 12, 2026
ff68d81
use with_settings in tests
thykel Apr 12, 2026
2116fc7
remove a useless alias upsert
thykel Apr 12, 2026
483bc89
batch the alias insert
thykel Apr 12, 2026
f998398
split the meaty stuff into services
thykel Apr 12, 2026
c52f096
fix the moved package sequence handling
thykel Apr 12, 2026
7710554
Merge remote-tracking branch 'origin/dev' into poc/71645-convert-to-w…
thykel Apr 13, 2026
8eb3662
use total_limit for main converter job to prevent multiple enqueues
thykel Apr 13, 2026
3e3d17f
wrap backfilling in transaction
thykel Apr 13, 2026
7ad067f
mass reset stale identifiers/seqs
thykel Apr 13, 2026
1c0d5d7
discard project backfill on 404
thykel Apr 13, 2026
7520e92
do not skip validation when fixing project identifier
thykel Apr 13, 2026
ba53230
create a better assertion for conversion to classic
thykel Apr 13, 2026
60e63e8
appease rubocop
thykel Apr 13, 2026
a7d6f86
rename the backfill service/job
thykel Apr 13, 2026
85dac65
Revert "do not skip validation when fixing project identifier"
thykel Apr 13, 2026
e684e8f
raise on blank IDs in project converter
thykel Apr 13, 2026
d01c444
Apply suggestions from code review
thykel Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,10 @@ def show
end

def update
return render_400 unless params[:settings]

if autofix_requested?
call = update_service.new(user: current_user).call(settings_params)
call.on_success do
WorkPackages::IdentifierAutofix::ApplyHandlesJob.perform_later
redirect_to action: "show"
end
call.on_failure { failure_callback(call) }
else
super
case params.dig(:settings, :work_packages_identifier)
when Setting::WorkPackageIdentifier::SEMANTIC then switch_to_semantic
when Setting::WorkPackageIdentifier::CLASSIC then switch_to_classic
else render_400
end
end

Expand All @@ -74,12 +67,21 @@ def status

private

def switch_to_semantic
ProjectIdentifiers::ConvertInstanceToSemanticIdsJob.perform_later unless WorkPackages::IdentifierAutofix.job_in_progress?
redirect_to action: "show"
end

def switch_to_classic
call = Settings::UpdateService.new(user: current_user)
.call(work_packages_identifier: Setting::WorkPackageIdentifier::CLASSIC)
call.on_success { redirect_to action: "show" }
call.on_failure { render_400 }
end

def check_feature_flag
render_404 unless OpenProject::FeatureDecisions.semantic_work_package_ids_active?
end

def autofix_requested?
ActiveRecord::Type::Boolean.new.cast(params[:confirm_dangerous_action])
end
end
end
1 change: 1 addition & 0 deletions app/models/setting/work_package_identifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ module WorkPackageIdentifier

def self.semantic? = Setting[:work_packages_identifier] == SEMANTIC
def self.classic? = Setting[:work_packages_identifier] == CLASSIC
def self.enable_semantic! = Setting.work_packages_identifier = SEMANTIC
end
end
2 changes: 0 additions & 2 deletions app/models/work_package/semantic_identifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,6 @@ def allocate_and_register_semantic_id
end
end

private

# Builds alias rows for every identifier this project has ever used at the given sequence (including the current one).
# This also includes "ghost identifiers" -- i.e. those that weren't ever actually generated, but should work
# as a historical alias (e.g. OLDPROJ-42 should work even if WP #42 was created after rename to NEWPROJ)
Expand Down
4 changes: 4 additions & 0 deletions app/models/work_package_semantic_alias.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ class WorkPackageSemanticAlias < ApplicationRecord

validates :identifier, presence: true, uniqueness: true
validates :work_package, presence: true

def self.upsert_rows(rows)
insert_all(rows, unique_by: :identifier) if rows.any?
end
end
5 changes: 4 additions & 1 deletion app/services/work_packages/identifier_autofix.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ module WorkPackages
module IdentifierAutofix
def self.job_in_progress?
GoodJob::Job
.where(job_class: WorkPackages::IdentifierAutofix::ApplyHandlesJob.name)
.where(job_class: [
ProjectIdentifiers::ConvertInstanceToSemanticIdsJob.name,
ProjectIdentifiers::BackfillProjectJob.name
])
.exists?(finished_at: nil)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ def scope

delegate :count, to: :scope

# Returns a symbol classifying why the identifier violates the expected format,
# or nil if the identifier is format-valid. Pure in-memory check — no DB queries.
def format_error_reason(identifier)
FORMAT_RULES.each do |reason, check|
return reason if check.call(identifier, max_identifier_length)
end
nil
end

# Returns a symbol classifying why the identifier is problematic.
# Must handle all identifiers matched by #scope.
def error_reason(identifier)
Expand All @@ -87,13 +96,6 @@ def not_fully_uppercased = Project.where("identifier != UPPER(identifier)")

def max_identifier_length = ProjectIdentifierSuggestionGenerator::IDENTIFIER_LENGTH[:max]

def format_error_reason(identifier)
FORMAT_RULES.each do |reason, check|
return reason if check.call(identifier, max_identifier_length)
end
nil
end

def collision_error_reason(identifier)
if in_use_identifiers.include?(identifier)
:in_use
Expand Down
74 changes: 74 additions & 0 deletions app/workers/project_identifiers/backfill_project_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class ProjectIdentifiers::BackfillProjectJob < ApplicationJob
def perform(project_id)
project = Project.find(project_id)
fix_identifier_if_needed(project)
backfill_work_packages(project)
end

private

def fix_identifier_if_needed(project)
detector = WorkPackages::IdentifierAutofix::ProblematicIdentifiers.new
# Pure format check — no DB queries. nil means the identifier is fine.
return unless detector.format_error_reason(project.identifier)

# Build the exclusion set fresh from the DB at job-execution time.
# Two concurrent jobs may occasionally suggest the same identifier, but the
# unique constraint on projects.identifier will reject the second writer, and
# the job can be retried.
new_identifier = WorkPackages::IdentifierAutofix::ProjectIdentifierSuggestionGenerator
.suggest_identifier(project.name, exclude: detector.exclusion_set)
project.identifier = new_identifier
project.save!(validate: false)
end

def backfill_work_packages(project)
# Assign sequence numbers to any WPs that don't have one yet (oldest first).
WorkPackage.where(project:, sequence_number: nil).order(:id).find_each do |wp|
seq, identifier = project.allocate_wp_semantic_identifier!
wp.update_columns(sequence_number: seq, identifier:)
end

seed_alias_table(project)
end

def seed_alias_table(project)
slug_prefixes = project.slugs.pluck(:slug)
return if slug_prefixes.empty?

alias_rows = WorkPackage.where(project:).pluck(:id, :sequence_number)
.product(slug_prefixes)
.map { |(wp_id, seq), prefix| { identifier: "#{prefix}-#{seq}", work_package_id: wp_id } }
WorkPackageSemanticAlias.upsert_rows(alias_rows)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class ProjectIdentifiers::ConvertInstanceToSemanticIdsJob < ApplicationJob
include GoodJob::ActiveJobExtensions::Concurrency

good_job_control_concurrency_with(perform_limit: 1)

# Maximum number of re-processing passes before giving up and aborting the flip.
# Prevents an infinite loop if work packages are created faster than they can be processed.
MAX_ITERATIONS = 10

# Called directly by the controller (no args) for the initial dispatch,
# or by GoodJob as an on_success batch callback with (batch, params).
def perform(_batch = nil, params = nil)
iteration = params.to_h.with_indifferent_access.fetch(:iteration, 0).to_i
remaining = project_ids_needing_backfill
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I needed a moment to understand what's going on here with the initial dispatch or callback 😅
You coupled these to catch all work packages and projects that were created while the job was running. That's hard to understand for someone not so deep into the topic and I am not sure how well this will work if we really have to iterate over all projects / work packages to catch the moves in the switch back-and-forth scenario.
I would prefer to handle those separately - in case of "the work package / project was created while the conversion migration was already running" we really can check for empty identifier values, in case of "initial dispatch" we can not. An extra "check and cleanup" method after the setting was flipped should be fast enough and easy to undertstand, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough -- I will run a proper batched parallel processing for the first pass, and then just invoke a quick synchronous scanner in a dedicated final batch job.


return switch_instance_to_semantic! if remaining.empty?

convert_identifier_data(remaining, iteration)
end

private

def switch_instance_to_semantic!
Setting::WorkPackageIdentifier.enable_semantic!
end

def convert_identifier_data(remaining, iteration)
if iteration < MAX_ITERATIONS
enqueue_backfill_batch(remaining, next_iteration: iteration + 1)
else
abort_with_error(remaining.size)
end
end

def enqueue_backfill_batch(project_ids, next_iteration:)
GoodJob::Batch.enqueue(on_success: self.class, on_success_params: { iteration: next_iteration }) do
project_ids.each { |project_id| ProjectIdentifiers::BackfillProjectJob.perform_later(project_id) }
end
end

def abort_with_error(remaining_count)
message =
"#{self.class.name}: reached max iterations (#{MAX_ITERATIONS}) with " \
"#{remaining_count} project(s) still unprocessed — aborting flip, manual intervention required"

Rails.logger.error(message)
raise message
end

def project_ids_needing_backfill
problematic_ids = WorkPackages::IdentifierAutofix::ProblematicIdentifiers.new.scope.ids.to_set
needs_backfill = WorkPackage.where(sequence_number: nil).distinct.pluck(:project_id).to_set
needs_backfill | problematic_ids
end
end
40 changes: 0 additions & 40 deletions app/workers/work_packages/identifier_autofix/apply_handles_job.rb

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require "spec_helper"

RSpec.describe Admin::Settings::WorkPackagesIdentifierController,
with_flag: { semantic_work_package_ids: true } do
shared_let(:user) { create(:admin) }

current_user { user }

describe "PATCH #update" do
context "when work_packages_identifier is 'semantic'" do
it "enqueues ProjectIdentifiers::ConvertInstanceToSemanticIdsJob and redirects" do
expect do
patch :update, params: { settings: { work_packages_identifier: "semantic" } }
end.to have_enqueued_job(ProjectIdentifiers::ConvertInstanceToSemanticIdsJob)

expect(response).to redirect_to(action: "show")
end

context "when a migration job is already in progress" do
before do
allow(WorkPackages::IdentifierAutofix).to receive(:job_in_progress?).and_return(true)
end

it "does not enqueue another job but still redirects" do
expect do
patch :update, params: { settings: { work_packages_identifier: "semantic" } }
end.not_to have_enqueued_job(ProjectIdentifiers::ConvertInstanceToSemanticIdsJob)

expect(response).to redirect_to(action: "show")
end
end
end

context "when work_packages_identifier is 'classic'" do
it "does not enqueue ProjectIdentifiers::ConvertInstanceToSemanticIdsJob" do
expect do
patch :update, params: { settings: { work_packages_identifier: "classic" } }
end.not_to have_enqueued_job(ProjectIdentifiers::ConvertInstanceToSemanticIdsJob)
end
end

context "when work_packages_identifier is missing or unknown" do
it "renders 400 without enqueuing a job" do
expect do
patch :update, params: {}
end.not_to have_enqueued_job(ProjectIdentifiers::ConvertInstanceToSemanticIdsJob)

expect(response).to have_http_status(:bad_request)
end
end
end
end
Loading
Loading