diff --git a/app/helpers/submissions_helper.rb b/app/helpers/submissions_helper.rb
index 7e12703c8e..49bcc6e0b8 100644
--- a/app/helpers/submissions_helper.rb
+++ b/app/helpers/submissions_helper.rb
@@ -139,7 +139,8 @@ def submission_link(submission, options)
def field_selection_tag(request_options, field_info, name_format, enforce_required)
select_tag(
name_format % field_info.key,
- options_for_select(field_info.selection.map(&:to_s), request_options[field_info.key]),
+ options_for_select(field_info.selection.map(&:to_s),
+ request_options[field_info.key].presence || field_info.default_value),
class: 'custom-select',
required: enforce_required && field_info.required,
read_only: field_info.selection.size == 1
diff --git a/app/models/bulk_submission.rb b/app/models/bulk_submission.rb
index 170ca24bc5..0f29773072 100644
--- a/app/models/bulk_submission.rb
+++ b/app/models/bulk_submission.rb
@@ -253,7 +253,8 @@ def process # rubocop:todo Metrics/CyclomaticComplexity
'scrna core cells per chip well',
'% phix requested',
'low diversity',
- 'ot recipe'
+ 'ot recipe',
+ 'wafer size'
].freeze
ALIAS_FIELDS = { 'plate barcode' => 'barcode', 'tube barcode' => 'barcode' }.freeze
@@ -360,7 +361,8 @@ def extract_request_options(details)
['scrna core cells per chip well', 'cells_per_chip_well'],
['% phix requested', 'percent_phix_requested'],
['low diversity', 'low_diversity'],
- ['ot recipe', 'ot_recipe']
+ ['ot recipe', 'ot_recipe'],
+ ['wafer size', 'wafer_size']
].each do |source_key, target_key|
assign_value_if_source_present(details, source_key, request_options, target_key)
end
diff --git a/app/models/ultima_sequencing_pipeline.rb b/app/models/ultima_sequencing_pipeline.rb
index 3a28998d3b..639c7ee349 100644
--- a/app/models/ultima_sequencing_pipeline.rb
+++ b/app/models/ultima_sequencing_pipeline.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Specialized sequencing pipeline for Ultima
+# Specialized sequencing pipeline for Ultima UG100
class UltimaSequencingPipeline < SequencingPipeline
def ot_recipe_consistent_for_batch?(batch)
ot_recipe_list = batch.requests.filter_map { |request| request.request_metadata.ot_recipe }
diff --git a/app/models/ultima_ug200_sequencing_pipeline.rb b/app/models/ultima_ug200_sequencing_pipeline.rb
new file mode 100644
index 0000000000..896f4e7696
--- /dev/null
+++ b/app/models/ultima_ug200_sequencing_pipeline.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# Specialized sequencing pipeline for Ultima UG200
+class UltimaUG200SequencingPipeline < UltimaSequencingPipeline
+ def wafer_size_consistent_for_batch?(batch)
+ wafer_size_list = batch.requests.filter_map { |request| request.request_metadata.wafer_size }
+
+ # There are some requests that don't have the wafer_size attribute
+ return false if wafer_size_list.size != batch.requests.size
+
+ (wafer_size_list.uniq.size == 1)
+ end
+end
diff --git a/app/models/ultima_ug200_sequencing_request.rb b/app/models/ultima_ug200_sequencing_request.rb
new file mode 100644
index 0000000000..0bed36c602
--- /dev/null
+++ b/app/models/ultima_ug200_sequencing_request.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+# Request class specific to the Ultima UG200 sequencing platform.
+# Includes wafer size and OT recipe.
+class UltimaUG200SequencingRequest < SequencingRequest
+ include Api::Messages::UseqWaferIo::LaneExtensions
+
+ FREE = 'Free'
+ FLEX = 'Flex'
+ OT_RECIPE_OPTIONS = [FREE, FLEX].freeze
+
+ has_metadata as: Request do
+ # Defining the sequencing request metadata here again, as 'has_metadata'
+ # does not automatically append these custom attributes to the request.
+ #
+ # The has_metadata call dynamically defines an inner Metadata class and
+ # takes the attributes from the block and adds them to the Metadata class.
+ # There is an assumption that the inner Metadata class is available in a
+ # sequencing request class defintion. Calling has_metadata again does not
+ # inherit the attributes given in the block supplied in the superclass.
+ # They need to be supplied again for this class for a proper inner Metadata
+ # class definition. In a future refactoring these attributes can be moved a
+ # class attribute and subclasses can merge its own attibutes to that. A
+ # common method can set up the inner Metadata class in the subclasses.
+ custom_attribute(:fragment_size_required_from, integer: true, minimum: 1)
+ custom_attribute(:fragment_size_required_to, integer: true, minimum: 1)
+
+ custom_attribute(:ot_recipe, default: FREE, in: OT_RECIPE_OPTIONS, required: true)
+ enum :ot_recipe, { Free: 0, Flex: 1 }
+ custom_attribute(:wafer_size, default: '10TB', validator: true, required: true, selection: true)
+ end
+
+ # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests.
+ # This delegation has no real effect outside of the tests.
+ delegate :wafer_size, :ot_recipe, to: :request_metadata
+
+ # Generates unique wafer ID, concatenation of batch_for_opentrons,
+ # id_pool_lims, and request_order.
+ # @return [String] unique wafer ID for LIMS
+ def id_wafer_lims
+ "#{batch.id}_#{source_labware.human_barcode}_#{position}"
+ end
+end
diff --git a/app/validators/ultima_ug200_validator.rb b/app/validators/ultima_ug200_validator.rb
new file mode 100644
index 0000000000..ae66f898ad
--- /dev/null
+++ b/app/validators/ultima_ug200_validator.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+class UltimaUG200Validator < UltimaValidator
+ WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.'
+
+ # Used in _pipeline_limit.html to display custom validation warnings
+ def self.validation_info
+ 'Wafer Size and OT Recipe must be the same for both requests.'
+ end
+
+ # Validates that a batch contains the two requests.
+ def validate(record)
+ validate_exactly_two_requests(record)
+ requests_have_same_ot_recipe(record)
+ requests_have_same_wafer_size(record)
+ end
+
+ private
+
+ def requests_have_same_wafer_size(record)
+ return if record.pipeline.wafer_size_consistent_for_batch?(record)
+
+ record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG)
+ end
+end
diff --git a/app/views/bulk_submissions/_guidance.html.erb b/app/views/bulk_submissions/_guidance.html.erb
index 6532b2d383..4706dfc832 100644
--- a/app/views/bulk_submissions/_guidance.html.erb
+++ b/app/views/bulk_submissions/_guidance.html.erb
@@ -47,4 +47,5 @@ used to split a submission up into orders, and as a result must have the same re
% PhiX requested
Required percentage of PhiX needed.
Low Diversity
Low Diversity value being "Yes" or "No"
OT Recipe
OT (OpenTron liquid handler) Recipe value being "Free" or "Flex"
+
Wafer Size
Wafer size value being "5TB", "10TB" or "20TB"
diff --git a/config/bulk_submission_excel/columns.yml b/config/bulk_submission_excel/columns.yml
index 02de62cb55..8e265391cb 100644
--- a/config/bulk_submission_excel/columns.yml
+++ b/config/bulk_submission_excel/columns.yml
@@ -367,3 +367,22 @@ ot_recipe:
range_name: :ot_recipe
conditional_formattings:
empty_cell:
+wafer_size:
+ heading: "Wafer Size"
+ unlocked: true
+ attribute: :wafer_size
+ validation:
+ options:
+ type: :list
+ formula1: "$A$1:$A$2"
+ allowBlank: false
+ showInputMessage: true
+ showErrorMessage: true
+ errorStyle: :stop
+ errorTitle: "Wafer Size"
+ error: "You must enter a value from the list provided."
+ promptTitle: "Wafer Size"
+ prompt: "Select a value type from the approved list"
+ range_name: :wafer_size
+ conditional_formattings:
+ empty_cell:
diff --git a/config/bulk_submission_excel/ranges.yml b/config/bulk_submission_excel/ranges.yml
index a1c091015a..b211a116cb 100644
--- a/config/bulk_submission_excel/ranges.yml
+++ b/config/bulk_submission_excel/ranges.yml
@@ -27,3 +27,8 @@ ot_recipe:
options:
- "Free"
- "Flex"
+wafer_size:
+ options:
+ - "5TB"
+ - "10TB"
+ - "20TB"
diff --git a/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml b/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml
new file mode 100644
index 0000000000..df4f31fa60
--- /dev/null
+++ b/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml
@@ -0,0 +1,86 @@
+---
+# Section names must be unique inside the record loader folder.
+"OTR carrier Lot # UG200":
+ name: "OTR carrier Lot #"
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Text
+ required: false
+ sorter: 0
+OTR carrier expiry UG200:
+ name: OTR carrier expiry
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Date
+ required: false
+ sorter: 1
+"Reaction Mix 7 Lot # UG200":
+ name: "Reaction Mix 7 Lot #"
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Text
+ required: false
+ sorter: 2
+Reaction Mix 7 expiry UG200:
+ name: Reaction Mix 7 expiry
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Date
+ required: false
+ sorter: 3
+"NFW Lot # UG200":
+ name: "NFW Lot #"
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Text
+ required: false
+ sorter: 4
+NFW expiry UG200:
+ name: NFW expiry
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Date
+ required: false
+ sorter: 5
+"Oil Lot # UG200":
+ name: "Oil Lot #"
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Text
+ required: false
+ sorter: 6
+Oil expiry UG200:
+ name: Oil expiry
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Date
+ required: false
+ sorter: 7
+Pipette carousel UG200:
+ name: Pipette carousel
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Text
+ required: false
+ sorter: 8
+Opentrons Inst. Name UG200:
+ name: Opentrons Inst. Name
+ task: Opentrons
+ workflow: Ultima UG200
+ kind: Text
+ required: true
+ sorter: 9
+Assign Control Bead Tube UG200:
+ name: Assign Control Bead Tube
+ task: Amp
+ workflow: Ultima UG200
+ kind: Text
+ required: false
+ sorter: 0
+UG AMP Inst. Name UG200:
+ name: UG AMP Inst. Name
+ task: Amp
+ workflow: Ultima UG200
+ kind: Text
+ required: true
+ sorter: 1
diff --git a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.wip.yml b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.wip.yml
new file mode 100644
index 0000000000..76a81c2576
--- /dev/null
+++ b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.wip.yml
@@ -0,0 +1,16 @@
+---
+FragmentSizeRequiredFromInformationTypeForUltimaUG200:
+ pipeline_name: Ultima UG200
+ request_information_type_key: fragment_size_required_from
+
+FragmentSizeRequiredToInformationTypeForUltimaUG200:
+ pipeline_name: Ultima UG200
+ request_information_type_key: fragment_size_required_to
+
+WaferSizeInformationTypeForUltimaUG200:
+ pipeline_name: Ultima UG200
+ request_information_type_key: wafer_size
+
+OTRecipeInformationTypeForUltimaUG200:
+ pipeline_name: Ultima UG200
+ request_information_type_key: ot_recipe
diff --git a/config/default_records/pipelines/004_ultima_ug200_pipelines.wip.yml b/config/default_records/pipelines/004_ultima_ug200_pipelines.wip.yml
new file mode 100644
index 0000000000..21b128df05
--- /dev/null
+++ b/config/default_records/pipelines/004_ultima_ug200_pipelines.wip.yml
@@ -0,0 +1,17 @@
+---
+Ultima UG200:
+ name: Ultima UG200 Sequencing
+ sti_type: UltimaUG200SequencingPipeline
+ validator_class_name: UltimaUG200Validator
+ sorter: 10
+ max_size: 2
+ summary: 1
+ externally_managed: 0
+ group_name: Sequencing
+ control_request_type_id: 0
+ min_size: 1
+ request_type_keys:
+ - ultima_ug200_sequencing
+ workflow:
+ name: Ultima UG200
+ item_limit: 2
diff --git a/config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml b/config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml
new file mode 100644
index 0000000000..682ce88e23
--- /dev/null
+++ b/config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml
@@ -0,0 +1,9 @@
+---
+# 96 well input plates
+UPF2 Cherrypicked:
+ type: PlatePurpose::Input
+ stock_plate: true
+ default_state: passed
+ cherrypickable_target: true
+ target_type: Plate
+ size: 96
diff --git a/config/default_records/request_information_types/004_ultima_ug200_request_information_types.wip.yml b/config/default_records/request_information_types/004_ultima_ug200_request_information_types.wip.yml
new file mode 100644
index 0000000000..bde23fdb31
--- /dev/null
+++ b/config/default_records/request_information_types/004_ultima_ug200_request_information_types.wip.yml
@@ -0,0 +1,7 @@
+---
+wafer_size:
+ name: Wafer size
+ key: wafer_size
+ label: Wafer size
+ width: 5
+ hide_in_inbox: 0
diff --git a/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.wip.yml b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.wip.yml
new file mode 100644
index 0000000000..028e593253
--- /dev/null
+++ b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.wip.yml
@@ -0,0 +1,8 @@
+---
+WaferSizeRequestedUltimaUG200Sequencing:
+ request_type_key: ultima_ug200_sequencing
+ request_option: wafer_size
+ valid_options:
+ - 5TB
+ - 10TB
+ - 20TB
diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml
new file mode 100644
index 0000000000..bc5977f439
--- /dev/null
+++ b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml
@@ -0,0 +1,31 @@
+# Request types for Ultima UG200 sequencing platform.
+---
+limber_ultima_ug200_htp_pcr_free:
+ name: Limber Ultima UG200 HTP PCR Free
+ order: 1
+ request_class_name: IlluminaHtp::Requests::StdLibraryRequest
+ asset_type: Well
+ for_multiplexing: false
+ billable: true
+ product_line_name: Ultima # same as UG100
+ acceptable_purposes:
+ - UPF2 Cherrypicked
+ library_types:
+ - Ultima High Throughput PCR Free 96 # same as UG100
+limber_multiplexing_ultima_ug200:
+ name: Limber Multiplexing Ultima UG200
+ asset_type: Well
+ order: 2
+ request_class_name: Request::Multiplexing
+ for_multiplexing: true
+ product_line_name: Ultima # same as UG100
+ target_purpose_name: UPF2 EqVol Norm
+ultima_ug200_sequencing:
+ name: Ultima UG200 sequencing
+ asset_type: LibraryTube
+ order: 2
+ initial_state: pending
+ billable: true
+ product_line_name: Ultima # same as UG100
+ request_class_name: UltimaUG200SequencingRequest
+ request_purpose: standard
diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml
new file mode 100644
index 0000000000..5c534184d0
--- /dev/null
+++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml
@@ -0,0 +1,18 @@
+# This submission template is associated with the Ultima PCR Free Library preparation pipeline
+# and the Ultima UG200 sequencing platform.
+---
+Limber-Htp - Ultima UG200 PCR Free - Ultima UG200 sequencing:
+ submission_class_name: "LinearSubmission"
+ related_records:
+ request_type_keys:
+ ["limber_ultima_ug200_htp_pcr_free", "limber_multiplexing_ultima_ug200", "ultima_ug200_sequencing"]
+ order_role: PCR Free
+ product_line_name: Ultima # same as UG100
+ product_catalogue_name: GenericNoPCR
+Limber-Htp - Ultima UG200 PCR Free - Ultima UG200 sequencing Automated:
+ submission_class_name: "LinearSubmission"
+ related_records:
+ request_type_keys: ["ultima_ug200_sequencing"]
+ order_role: PCR Free
+ product_line_name: Ultima # same as UG100
+ product_catalogue_name: GenericNoPCR
diff --git a/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml b/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml
new file mode 100644
index 0000000000..a72c05029d
--- /dev/null
+++ b/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml
@@ -0,0 +1,14 @@
+---
+# Section names must be unique inside the record loader folder.
+Opentrons UG200:
+ name: Opentrons
+ workflow: Ultima UG200
+ sorted: 0
+ lab_activity: true
+ sti_type: SetDescriptorsTask
+Amp UG200:
+ name: Amp
+ workflow: Ultima UG200
+ sorted: 1
+ lab_activity: true
+ sti_type: SetDescriptorsTask
diff --git a/config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml b/config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml
new file mode 100644
index 0000000000..3cb41b6b30
--- /dev/null
+++ b/config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml
@@ -0,0 +1,6 @@
+---
+UPF2 EqVol Norm:
+ # Even though this is Ultima, not Illumina, the MX tube has the same behaviour
+ type: IlluminaHtp::MxTubePurpose
+ target_type: MultiplexedLibraryTube
+ stock_plate: false
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 1b0cac6328..d3ad112180 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -16,6 +16,7 @@
inflect.acronym 'ENA' # European Nucleotide Archive
inflect.acronym 'HTTP' # HyperText Transfer Protocol
inflect.acronym 'EBI' # European Bioinformatics Institute
+ inflect.acronym 'UG200' # Ultima UG200
end
# These inflection rules are supported but not enabled by default:
diff --git a/config/locales/metadata/en.yml b/config/locales/metadata/en.yml
index 08f1fbd67e..da8fa0683b 100644
--- a/config/locales/metadata/en.yml
+++ b/config/locales/metadata/en.yml
@@ -79,6 +79,9 @@ en:
ot_recipe:
label: OT Recipe
+ wafer_size:
+ label: Wafer Size
+
library_creation_request:
<<: *REQUEST
sequencing_request:
@@ -107,6 +110,9 @@ en:
ultima_sequencing_request:
<<: *REQUEST
+ ultima_ug200_sequencing_request:
+ <<: *REQUEST
+
pulldown:
requests:
wgs_library_request:
diff --git a/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb
new file mode 100644
index 0000000000..f1e811d8f0
--- /dev/null
+++ b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# This migration adds a wafer_size column to the request_metadata table, which is used to store
+# the wafer size for Ultima sequencing requests. This is stored as a string, with possible
+# values of 5TB, 10TB, and 20TB at time of writing.
+class AddWaferSizeToRequestMetadata < ActiveRecord::Migration[7.1]
+ def change
+ add_column :request_metadata, :wafer_size, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 34cce1cefe..4f5e30ff8a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.2].define(version: 2026_03_17_142326) do
+ActiveRecord::Schema[7.2].define(version: 2026_03_24_112536) do
create_table "accession_sample_statuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.integer "sample_id", null: false
t.string "status", null: false
@@ -1191,6 +1191,7 @@
t.boolean "low_diversity"
t.integer "percent_phix_requested"
t.integer "ot_recipe"
+ t.string "wafer_size"
t.index ["request_id"], name: "index_request_metadata_on_request_id"
end
diff --git a/spec/data/bulk_submission_excel/columns.yml b/spec/data/bulk_submission_excel/columns.yml
index 03280783ee..ab5bd8739e 100644
--- a/spec/data/bulk_submission_excel/columns.yml
+++ b/spec/data/bulk_submission_excel/columns.yml
@@ -315,3 +315,22 @@ ot_recipe:
range_name: :ot_recipe
conditional_formattings:
empty_cell:
+wafer_size:
+ heading: "Wafer Size"
+ unlocked: true
+ attribute: :wafer_size
+ validation:
+ options:
+ type: :list
+ formula1: "$A$1:$A$2"
+ allowBlank: false
+ showInputMessage: true
+ showErrorMessage: true
+ errorStyle: :stop
+ errorTitle: "Wafer Size"
+ error: "You must enter a value from the list provided."
+ promptTitle: "Wafer Size"
+ prompt: "Select a value type from the approved list"
+ range_name: :wafer_size
+ conditional_formattings:
+ empty_cell:
diff --git a/spec/data/bulk_submission_excel/ranges.yml b/spec/data/bulk_submission_excel/ranges.yml
index a1c091015a..b211a116cb 100644
--- a/spec/data/bulk_submission_excel/ranges.yml
+++ b/spec/data/bulk_submission_excel/ranges.yml
@@ -27,3 +27,8 @@ ot_recipe:
options:
- "Free"
- "Flex"
+wafer_size:
+ options:
+ - "5TB"
+ - "10TB"
+ - "20TB"
diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb
index 5a012985fc..ad180e5d76 100644
--- a/spec/factories/request_factories.rb
+++ b/spec/factories/request_factories.rb
@@ -124,6 +124,20 @@
end
end
+ factory(:ultima_ug200_sequencing_request) do
+ request_type factory: %i[ultima_ug200_sequencing]
+ request_purpose { :standard }
+ sti_type { 'UltimaUG200SequencingRequest' }
+ request_metadata_attributes do
+ {
+ fragment_size_required_from: 150,
+ fragment_size_required_to: 400,
+ ot_recipe: 'Free',
+ wafer_size: '10TB'
+ }
+ end
+ end
+
factory(:library_creation_request, parent: :request, class: 'LibraryCreationRequest') do
asset factory: %i[sample_tube]
request_type factory: %i[library_creation_request_type]
diff --git a/spec/factories/request_type_factories.rb b/spec/factories/request_type_factories.rb
index 4d18e1eb3d..87b2fecfd4 100644
--- a/spec/factories/request_type_factories.rb
+++ b/spec/factories/request_type_factories.rb
@@ -139,6 +139,11 @@
request_class { UltimaSequencingRequest }
end
+ factory :ultima_ug200_sequencing do
+ asset_type { 'LibraryTube' }
+ request_class { UltimaUG200SequencingRequest }
+ end
+
factory :miseq_sequencing_request_type do
request_class { MiSeqSequencingRequest }
asset_type { 'LibraryTube' }
diff --git a/spec/helpers/submissions_helper_spec.rb b/spec/helpers/submissions_helper_spec.rb
new file mode 100644
index 0000000000..5e5101c42c
--- /dev/null
+++ b/spec/helpers/submissions_helper_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe SubmissionsHelper do
+ describe '#field_input_tag' do
+ let(:field_info) do
+ double(
+ kind: 'Selection',
+ key: :ot_recipe,
+ selection: %w[Free Flex],
+ default_value: 'Free',
+ required: true
+ )
+ end
+
+ let(:wafer_size_field_info) do
+ double(
+ kind: 'Selection',
+ key: :wafer_size,
+ selection: %w[5TB 10TB],
+ default_value: '10TB',
+ required: true
+ )
+ end
+
+ it 'uses the default value for selection fields when no request option is set' do
+ html = helper.field_input_tag(field_info, values: {}, name_format: 'submission[order_params][%s]')
+
+ expect(html).to include('')
+ end
+
+ it 'uses the default value for selection fields when request option is blank' do
+ html = helper.field_input_tag(field_info, values: { ot_recipe: '' }, name_format: 'submission[order_params][%s]')
+
+ expect(html).to include('')
+ end
+
+ it 'uses the default wafer_size when request option is blank' do
+ html = helper.field_input_tag(wafer_size_field_info,
+ values: { wafer_size: '' },
+ name_format: 'submission[order_params][%s]')
+
+ expect(html).to include('')
+ end
+ end
+end
diff --git a/spec/models/ultima_sequencing_request_spec.rb b/spec/models/ultima_sequencing_request_spec.rb
index d152679b4e..3cbaa4864f 100644
--- a/spec/models/ultima_sequencing_request_spec.rb
+++ b/spec/models/ultima_sequencing_request_spec.rb
@@ -33,7 +33,7 @@
end
context 'when ot_recipe value is not assigned' do
- it 'is invalid and displays required percent phix requested error message' do
+ it 'is invalid and displays required OT recipe error message' do
request.request_metadata.ot_recipe = nil
request.validate
expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank")
diff --git a/spec/models/ultima_ug200_sequencing_pipeline_spec.rb b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb
new file mode 100644
index 0000000000..361bce75a5
--- /dev/null
+++ b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+require 'rails_helper'
+
+RSpec.describe UltimaUG200SequencingPipeline do
+ let(:pipeline) do
+ described_class.new(
+ workflow: Workflow.new,
+ request_types: [create(:ultima_ug200_sequencing)]
+ )
+ end
+
+ describe '#ot_recipe_consistent_for_batch?' do
+ it 'returns true when all requests have the same ot_recipe' do
+ batch = pipeline.batches.build
+ r1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' })
+ r2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' })
+ batch.requests << [r1, r2]
+
+ expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be true
+ end
+
+ it 'returns false when requests have different ot_recipes' do
+ batch = pipeline.batches.build
+ req1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' })
+ req2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Flex' })
+ batch.requests << [req1, req2]
+
+ expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be false
+ end
+
+ it 'returns false when some requests are missing ot_recipe' do
+ batch = pipeline.batches.build
+ r1 = create(:sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' })
+ r2 = create(:sequencing_request, request_metadata_attributes: {}) # no ot_recipe
+ batch.requests << [r1, r2]
+
+ expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be false
+ end
+ end
+
+ describe '#wafer_size_consistent_for_batch?' do
+ it 'returns true when all requests have the same wafer_size' do
+ batch = pipeline.batches.build
+ r1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' })
+ r2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' })
+ batch.requests << [r1, r2]
+
+ expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be true
+ end
+
+ it 'returns false when requests have different wafer_sizes' do
+ batch = pipeline.batches.build
+ req1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' })
+ req2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' })
+ batch.requests << [req1, req2]
+
+ expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false
+ end
+
+ # NB. Wafer size is a required field, so cannot be missing. Tested in the request spec.
+ end
+
+ describe '#post_release_batch' do
+ let(:batch) { create(:batch) }
+
+ it 'calls Messenger with UseqWaferIo template and useq_wafer root' do
+ allow(Messenger).to receive(:create!)
+ pipeline.post_release_batch(batch, create(:user))
+
+ expect(Messenger).to have_received(:create!).with(
+ hash_including(target: batch, template: 'UseqWaferIo', root: 'useq_wafer')
+ )
+ end
+ end
+end
diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb
new file mode 100644
index 0000000000..a87bc38fd2
--- /dev/null
+++ b/spec/models/ultima_ug200_sequencing_request_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe UltimaUG200SequencingRequest do
+ let(:request) { create(:ultima_ug200_sequencing_request) }
+
+ describe 'Validations' do
+ context 'when all attributes are valid' do
+ it 'is valid' do
+ expect(request).to be_valid
+ end
+ end
+
+ context 'when fragment_size_required_from is less than 1' do
+ it 'is invalid and displays fragment size from error message' do
+ request.request_metadata.fragment_size_required_from = 0
+ request.validate
+ expect(request.errors[:'request_metadata.fragment_size_required_from']).to include(
+ 'must be greater than or equal to 1'
+ )
+ end
+ end
+
+ context 'when fragment_size_required_to is less than 1' do
+ it 'is invalid and displays fragment size to error message' do
+ request.request_metadata.fragment_size_required_to = 0
+ request.validate
+ expect(request.errors[:'request_metadata.fragment_size_required_to']).to include(
+ 'must be greater than or equal to 1'
+ )
+ end
+ end
+
+ context 'when wafer_size value is not assigned' do
+ it 'is invalid and displays required wafer size error message' do
+ request.request_metadata.wafer_size = nil
+ request.validate
+ expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank")
+ end
+ end
+
+ context 'when ot_recipe value is not assigned' do
+ it 'is invalid and displays required OT recipe error message' do
+ request.request_metadata.ot_recipe = nil
+ request.validate
+ expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank")
+ end
+ end
+ end
+end
diff --git a/spec/validators/ultima_ug200_validator_spec.rb b/spec/validators/ultima_ug200_validator_spec.rb
new file mode 100644
index 0000000000..34082981cb
--- /dev/null
+++ b/spec/validators/ultima_ug200_validator_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+require 'rails_helper'
+
+describe UltimaUG200Validator do
+ describe '#validate' do
+ subject(:validator) { described_class.new }
+
+ let(:pipeline) { UltimaUG200SequencingPipeline.new }
+ let(:batch) { create(:batch, pipeline:) }
+ let(:requests) { [request1, request2] }
+ let(:request1_metadata) { { ot_recipe: 'Free', wafer_size: '10TB' } }
+ let(:request2_metadata) { { ot_recipe: 'Free', wafer_size: '10TB' } }
+ let(:request1) { create(:ultima_sequencing_request, request_metadata_attributes: request1_metadata) }
+ let(:request2) { create(:ultima_sequencing_request, request_metadata_attributes: request2_metadata) }
+
+ before do
+ batch.requests << requests
+ end
+
+ context 'when batch contains two requests with the same OT Recipe' do
+ it 'is valid' do
+ validator.validate(batch)
+ expect(batch.errors[:base]).to be_empty
+ end
+ end
+
+ context 'when batch contains two requests with different ot_recipe' do
+ let(:request2_metadata) { { ot_recipe: 'Flex', wafer_size: '10TB' } }
+
+ it 'is invalid due to ot_recipe mismatch' do
+ validator.validate(batch)
+ expect(batch.errors[:base]).to include(described_class::OT_RECIPE_CONSISTENT_MSG)
+ end
+ end
+
+ context 'when batch contains two requests with the same Wafer Size' do
+ it 'is valid' do
+ validator.validate(batch)
+ expect(batch.errors[:base]).not_to include(described_class::WAFER_SIZE_CONSISTENT_MSG)
+ end
+ end
+
+ context 'when batch contains two requests with different wafer_size' do
+ let(:request1_metadata) { { ot_recipe: 'Free', wafer_size: '5TB' } }
+
+ it 'is invalid due to wafer_size mismatch' do
+ validator.validate(batch)
+ expect(batch.errors[:base]).to include(described_class::WAFER_SIZE_CONSISTENT_MSG)
+ end
+ end
+
+ context 'when batch contains a single request' do
+ let(:requests) { [request1] }
+
+ it 'is invalid' do
+ validator.validate(batch)
+ expect(batch.errors[:base]).to include(described_class::TWO_REQUESTS_MSG)
+ end
+ end
+ end
+end
diff --git a/test/controllers/batches_controller_test.rb b/test/controllers/batches_controller_test.rb
index a109e47d21..a7810bbefd 100644
--- a/test/controllers/batches_controller_test.rb
+++ b/test/controllers/batches_controller_test.rb
@@ -257,6 +257,9 @@ class BatchesControllerTest < ActionController::TestCase
"request_group_#{@plate.id}_#{@submission.id}_size": '1',
commit: 'Submit'
}
+
+ assert_redirected_to batch_path(assigns(:batch))
+ assert_predicate assigns(:batch), :persisted?
end
end
diff --git a/test/lib/label_printer/plate_to_tube_test.rb b/test/lib/label_printer/plate_to_tube_test.rb
index 49bb24fbb4..6a289237cc 100644
--- a/test/lib/label_printer/plate_to_tube_test.rb
+++ b/test/lib/label_printer/plate_to_tube_test.rb
@@ -13,7 +13,12 @@ def setup # rubocop:todo Metrics/AbcSize
@barcode1 = '1111'
@asset_name = 'tube name'
@tube1 = create(:sample_tube, :tube_barcode, barcode: barcode1, prefix: prefix, name: asset_name)
- @sample_tubes = create_list(:sample_tube, 4, :tube_barcode)
+ @sample_tubes = [
+ create(:sample_tube, :tube_barcode, barcode: '2222', prefix: prefix),
+ create(:sample_tube, :tube_barcode, barcode: '3333', prefix: prefix),
+ create(:sample_tube, :tube_barcode, barcode: '4444', prefix: prefix),
+ create(:sample_tube, :tube_barcode, barcode: '5555', prefix: prefix)
+ ]
sample_tubes.unshift(tube1)
options = { sample_tubes: }
@tube_label = LabelPrinter::Label::PlateToTubes.new(options)
diff --git a/test/unit/batch_test.rb b/test/unit/batch_test.rb
index abfc223aef..e9d8ae10a5 100644
--- a/test/unit/batch_test.rb
+++ b/test/unit/batch_test.rb
@@ -647,7 +647,7 @@ class BatchTest < ActiveSupport::TestCase
end
should 'check that with the pipeline that the batch is valid' do
- @batch.complete!(@user)
+ assert_nothing_raised { @batch.complete!(@user) }
end
end
diff --git a/test/unit/event_test.rb b/test/unit/event_test.rb
index 67e6a550e7..68fe62686c 100644
--- a/test/unit/event_test.rb
+++ b/test/unit/event_test.rb
@@ -18,7 +18,7 @@ class EventTest < ActiveSupport::TestCase
setup { @event = Event.create(descriptor_key: '') }
should 'be valid' do
- @event.valid?
+ assert_predicate @event, :valid?
end
end
end
@@ -28,7 +28,7 @@ class EventTest < ActiveSupport::TestCase
setup { @event = Event.create }
should 'be valid' do
- @event.valid?
+ assert_predicate @event, :valid?
end
end
end
diff --git a/test/unit/request_factory_test.rb b/test/unit/request_factory_test.rb
index 5339e13433..538b6eaacc 100644
--- a/test/unit/request_factory_test.rb
+++ b/test/unit/request_factory_test.rb
@@ -42,7 +42,7 @@ class RequestcreateTest < ActiveSupport::TestCase
setup { @project.update!(enforce_quotas: true) }
should 'not fail' do
- RequestFactory.copy_request(@request)
+ assert_nothing_raised { RequestFactory.copy_request(@request) }
end
end
end
diff --git a/test/unit/sequencing_qc_batch_test.rb b/test/unit/sequencing_qc_batch_test.rb
index 89a123a9bb..eda3f8a081 100644
--- a/test/unit/sequencing_qc_batch_test.rb
+++ b/test/unit/sequencing_qc_batch_test.rb
@@ -38,6 +38,7 @@ class SequencingQcBatchTest < ActiveSupport::TestCase
should 'do nothing if the next state is nil' do
@batch.stubs(:qc_next_state).returns(nil)
+ @batch.expects(:update_attribute).never # no state update when there is no next state
@batch.qc_submitted
end
end
@@ -51,6 +52,7 @@ class SequencingQcBatchTest < ActiveSupport::TestCase
should 'do nothing if the next state is nil' do
@batch.stubs(:qc_next_state).returns(nil)
+ @batch.expects(:update_attribute).never # no state update when there is no next state
@batch.qc_criteria_received
end
end
@@ -64,6 +66,7 @@ class SequencingQcBatchTest < ActiveSupport::TestCase
should 'do nothing if the next state is nil' do
@batch.stubs(:qc_next_state).returns(nil)
+ @batch.expects(:update_attribute).never # no state update when there is no next state
@batch.qc_complete
end
end
diff --git a/test/unit/tasks/plate_transfer_task_test.rb b/test/unit/tasks/plate_transfer_task_test.rb
index 9ed5ba55f8..669a0df93d 100644
--- a/test/unit/tasks/plate_transfer_task_test.rb
+++ b/test/unit/tasks/plate_transfer_task_test.rb
@@ -100,6 +100,7 @@ class PlateTransferTaskTest < ActiveSupport::TestCase
end
should 'find the existing plate' do
+ assert_equal 1, Plate.count - @plate_count
end
end
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb
index 126e8414d0..081be0ac56 100644
--- a/test/unit/user_test.rb
+++ b/test/unit/user_test.rb
@@ -123,7 +123,7 @@ class UserTest < ActiveSupport::TestCase
should 'be able to have one assigned' do
code = 'code'
- @user.swipecard_code = code
+ assert_nothing_raised { @user.swipecard_code = code }
end
end