feat(matches): add match metadata updates
This commit is contained in:
parent
547e1c3933
commit
0cbcb64066
|
|
@ -7,9 +7,11 @@ class MatchesController < ApplicationController
|
|||
before_action :set_swap_target, only: %i[swap]
|
||||
before_action -> { require_owner! @match.owner }, only: %i[update swap]
|
||||
before_action -> { require_owner! @group.owner }, only: %i[create]
|
||||
before_action :set_tournament, only: %i[index]
|
||||
before_action :set_tournament, only: %i[index metadata]
|
||||
before_action -> { require_owner! @tournament.owner }, only: %i[metadata]
|
||||
before_action -> { require_writable_tournament!(@match.tournament) }, only: %i[update swap]
|
||||
before_action -> { require_writable_tournament!(@group.tournament) }, only: %i[create]
|
||||
before_action -> { require_writable_tournament!(@tournament) }, only: %i[metadata]
|
||||
|
||||
# GET/tournaments/1/matches
|
||||
def index
|
||||
|
|
@ -63,6 +65,28 @@ class MatchesController < ApplicationController
|
|||
TournamentLiveBroadcast.call(synced_tournament.id) unless synced_tournament.nil?
|
||||
end
|
||||
|
||||
# PATCH /tournaments/1/matches/metadata
|
||||
def metadata
|
||||
attrs = match_metadata_params
|
||||
if attrs.empty?
|
||||
render json: { error: 'At least one metadata field is required' }, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
matches = bulk_metadata_matches
|
||||
return if performed?
|
||||
|
||||
Match.transaction do
|
||||
TournamentTransactionLogger.lock_tournament!(@tournament)
|
||||
@tournament.reload
|
||||
matches.each { |match| match.update!(attrs) }
|
||||
end
|
||||
|
||||
render json: matches.map(&:reload), each_serializer: LiveMatchSerializer, include: %w[match_scores.team]
|
||||
push_sync_if_needed!(@tournament)
|
||||
TournamentLiveBroadcast.call(@tournament.id)
|
||||
end
|
||||
|
||||
# PATCH /matches/1/swap
|
||||
def swap
|
||||
unless @match.swappable_with?(@swap_target)
|
||||
|
|
@ -141,7 +165,7 @@ class MatchesController < ApplicationController
|
|||
|
||||
old_state = @match.state
|
||||
if @match.update(match_params)
|
||||
record_successful_match_update(old_state, new_state)
|
||||
record_successful_match_update(old_state, new_state) unless new_state.nil?
|
||||
render json: @match
|
||||
tournament
|
||||
else
|
||||
|
|
@ -151,6 +175,8 @@ class MatchesController < ApplicationController
|
|||
end
|
||||
|
||||
def validate_transition_inside_transaction(new_state)
|
||||
return if new_state.nil?
|
||||
|
||||
transition_error = match_transition_error(@match, new_state)
|
||||
return if transition_error.nil?
|
||||
|
||||
|
|
@ -175,6 +201,8 @@ class MatchesController < ApplicationController
|
|||
end
|
||||
|
||||
def validate_params
|
||||
return unless params.key?(:state)
|
||||
|
||||
transition_error = match_transition_error(@match, match_params['state'])
|
||||
return if transition_error.nil?
|
||||
|
||||
|
|
@ -201,13 +229,52 @@ class MatchesController < ApplicationController
|
|||
end
|
||||
|
||||
def match_params
|
||||
params.slice(:state).permit!
|
||||
params.slice(:state, :location, :start_time, :notes).permit!
|
||||
end
|
||||
|
||||
def match_index_params
|
||||
params.slice(:state, :stage_id, :stage_level, :position, :group_stage_position).permit!
|
||||
end
|
||||
|
||||
def match_metadata_params
|
||||
params.slice(:location, :start_time, :notes).permit!
|
||||
end
|
||||
|
||||
def bulk_metadata_matches
|
||||
return bulk_metadata_matches_by_id if params.key?(:match_ids)
|
||||
|
||||
return @tournament.matches if ActiveModel::Type::Boolean.new.cast(params[:all])
|
||||
return filtered_bulk_metadata_matches if match_index_params.values.any?(&:present?)
|
||||
|
||||
render json: { error: 'Select matches with match_ids, filters, or all=true' }, status: :bad_request
|
||||
[]
|
||||
end
|
||||
|
||||
def bulk_metadata_matches_by_id
|
||||
ids = parse_match_ids
|
||||
return [] if performed?
|
||||
|
||||
matches = @tournament.matches.select { |match| ids.include?(match.id) }
|
||||
missing_ids = ids - matches.map(&:id)
|
||||
if missing_ids.any?
|
||||
render json: { error: "Matches not found in tournament: #{missing_ids.join(', ')}" }, status: :not_found
|
||||
return []
|
||||
end
|
||||
|
||||
ids.map { |id| matches.find { |match| match.id == id } }
|
||||
end
|
||||
|
||||
def filtered_bulk_metadata_matches
|
||||
TournamentMatchesPayload.matches_for(@tournament, match_index_params)
|
||||
end
|
||||
|
||||
def parse_match_ids
|
||||
Array(params[:match_ids]).map { |id| Integer(id.to_s, 10) }.uniq
|
||||
rescue ArgumentError, TypeError
|
||||
render json: { error: 'match_ids must contain only integer ids' }, status: :bad_request
|
||||
[]
|
||||
end
|
||||
|
||||
def create_match_params
|
||||
params.slice(:team_ids).permit(team_ids: [])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LiveMatchSerializer < ApplicationSerializer
|
||||
attributes :position, :state, :teams, :group, :stage
|
||||
attributes :position, :state, :location, :start_time, :notes, :teams, :group, :stage
|
||||
|
||||
has_many :match_scores
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MatchSerializer < ApplicationSerializer
|
||||
attributes :state, :position, :winner, :decider
|
||||
attributes :state, :position, :winner, :decider, :location, :start_time, :notes
|
||||
|
||||
def winner
|
||||
ActiveModelSerializers::SerializableResource.new(object.winner).as_json
|
||||
|
|
|
|||
|
|
@ -139,6 +139,9 @@ class TournamentSnapshotImporter
|
|||
match.state = attrs.fetch(:state)
|
||||
match.position = attrs[:position]
|
||||
match.decider = attrs[:decider]
|
||||
match.location = attrs[:location]
|
||||
match.start_time = attrs[:start_time]
|
||||
match.notes = attrs[:notes]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class TournamentSyncSchema
|
|||
ignored: %w[created_at updated_at sync_source_id].freeze
|
||||
}.freeze,
|
||||
Match => {
|
||||
synced: %w[id state position stage_id group_id decider].freeze,
|
||||
synced: %w[id state position stage_id group_id decider location start_time notes].freeze,
|
||||
ignored: %w[created_at updated_at sync_source_id].freeze
|
||||
}.freeze,
|
||||
MatchScore => {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ Rails.application.routes.draw do
|
|||
resources :team_action_lists, only: %i[create]
|
||||
patch 'team_action_lists/:key/teams/:team_id', to: 'team_action_lists#update_item'
|
||||
resources :statistics, only: %i[index]
|
||||
resources :matches, only: %i[index]
|
||||
resources :matches, only: %i[index] do
|
||||
collection do
|
||||
patch :metadata
|
||||
end
|
||||
end
|
||||
resources :beamers, only: %i[index show create update destroy] do
|
||||
collection do
|
||||
get :by_name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddMatchMetadataToMatches < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :matches, :location, :string
|
||||
add_column :matches, :start_time, :string
|
||||
add_column :matches, :notes, :text
|
||||
end
|
||||
end
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_04_30_121000) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_05_04_100000) do
|
||||
create_table "beamers", force: :cascade do |t|
|
||||
t.json "config", default: {}, null: false
|
||||
t.datetime "created_at", null: false
|
||||
|
|
@ -74,8 +74,11 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_30_121000) do
|
|||
t.datetime "created_at", precision: nil, null: false
|
||||
t.boolean "decider", default: false, null: false
|
||||
t.integer "group_id"
|
||||
t.string "location"
|
||||
t.text "notes"
|
||||
t.integer "position"
|
||||
t.integer "stage_id"
|
||||
t.string "start_time"
|
||||
t.integer "state", default: 0
|
||||
t.integer "sync_source_id"
|
||||
t.datetime "updated_at", precision: nil, null: false
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ RSpec.describe MatchesController, type: :controller do
|
|||
get :index, params: { tournament_id: @tournament.to_param }
|
||||
match = deserialize_response(response).first
|
||||
|
||||
expect(match.keys).to match_array(%i[id position state teams match_scores group stage])
|
||||
expect(match.keys).to match_array(%i[id position state location start_time notes teams match_scores group stage])
|
||||
expect(match[:teams]).to all(include(:id, :name))
|
||||
end
|
||||
|
||||
|
|
@ -196,6 +196,102 @@ RSpec.describe MatchesController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'PATCH #metadata' do
|
||||
let(:group_stage_tournament) { create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match) }
|
||||
|
||||
context 'as owner' do
|
||||
before do
|
||||
apply_authentication_headers_for group_stage_tournament.owner
|
||||
allow(TournamentSyncEnqueue).to receive(:call)
|
||||
allow(TournamentLiveBroadcast).to receive(:call)
|
||||
end
|
||||
|
||||
it 'updates selected matches by explicit ids' do
|
||||
selected_matches = group_stage_tournament.matches.first(2)
|
||||
|
||||
patch :metadata, params: {
|
||||
tournament_id: group_stage_tournament.to_param,
|
||||
match_ids: selected_matches.map(&:id),
|
||||
location: 'Court A',
|
||||
start_time: '17:00',
|
||||
notes: 'Bring player cards'
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(selected_matches.map { |match| match.reload.location }).to all(eq('Court A'))
|
||||
expect(selected_matches.map { |match| match.reload.start_time }).to all(eq('17:00'))
|
||||
expect(selected_matches.map { |match| match.reload.notes }).to all(eq('Bring player cards'))
|
||||
expect(deserialize_response(response).map { |match| match[:id] }).to eq(selected_matches.map(&:id))
|
||||
expect(TournamentSyncEnqueue).to have_received(:call).with(group_stage_tournament)
|
||||
expect(TournamentLiveBroadcast).to have_received(:call).with(group_stage_tournament.id)
|
||||
end
|
||||
|
||||
it 'updates group stage matches by position filter' do
|
||||
patch :metadata, params: {
|
||||
tournament_id: group_stage_tournament.to_param,
|
||||
group_stage_position: 1,
|
||||
start_time: '18:00'
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
group_stage_tournament.matches.each do |match|
|
||||
expected_start_time = match.group_match? && match.position == 1 ? '18:00' : nil
|
||||
expect(match.reload.start_time).to eq(expected_start_time)
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates playoff matches by stage level filter' do
|
||||
playoff_tournament = create(:stage_tournament, stage_count: 3, user: group_stage_tournament.owner)
|
||||
target_stage = playoff_tournament.stages.find_by!(level: 3)
|
||||
|
||||
patch :metadata, params: {
|
||||
tournament_id: playoff_tournament.to_param,
|
||||
stage_level: target_stage.level,
|
||||
start_time: '21:00'
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
playoff_tournament.matches.each do |match|
|
||||
expected_start_time = match.stage == target_stage ? '21:00' : nil
|
||||
expect(match.reload.start_time).to eq(expected_start_time)
|
||||
end
|
||||
end
|
||||
|
||||
it 'requires a selector' do
|
||||
patch :metadata, params: { tournament_id: group_stage_tournament.to_param, start_time: '17:00' }
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(deserialize_response(response)[:error]).to eq('Select matches with match_ids, filters, or all=true')
|
||||
end
|
||||
|
||||
it 'rejects matches outside the tournament' do
|
||||
other_match = create(:match)
|
||||
|
||||
patch :metadata, params: {
|
||||
tournament_id: group_stage_tournament.to_param,
|
||||
match_ids: [other_match.id],
|
||||
start_time: '17:00'
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect(deserialize_response(response)[:error]).to eq("Matches not found in tournament: #{other_match.id}")
|
||||
end
|
||||
|
||||
it 'rejects read only follower tournaments' do
|
||||
group_stage_tournament.update!(read_only_mode: true, sync_auth_token: 'shared-secret')
|
||||
|
||||
patch :metadata, params: {
|
||||
tournament_id: group_stage_tournament.to_param,
|
||||
match_ids: [group_stage_tournament.matches.first.id],
|
||||
start_time: '17:00'
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:locked)
|
||||
expect(deserialize_response(response)[:error]).to eq('Tournament is in read only follower mode')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #update' do
|
||||
context 'on a running playoff match' do
|
||||
let(:valid_update) do
|
||||
|
|
@ -237,6 +333,29 @@ RSpec.describe MatchesController, type: :controller do
|
|||
expect(body[:state]).to eq(valid_update[:state])
|
||||
end
|
||||
|
||||
it 'updates metadata without changing match state' do
|
||||
expect(TournamentLiveBroadcast).to receive(:call).with(@match.tournament.id)
|
||||
|
||||
put :update, params: {
|
||||
id: @match.to_param,
|
||||
location: 'Main Hall',
|
||||
start_time: '17:00',
|
||||
notes: 'Stream setup needed'
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
@match.reload
|
||||
expect(@match.state).to eq('not_started')
|
||||
expect(@match.location).to eq('Main Hall')
|
||||
expect(@match.start_time).to eq('17:00')
|
||||
expect(@match.notes).to eq('Stream setup needed')
|
||||
expect(deserialize_response(response)).to include(
|
||||
location: 'Main Hall',
|
||||
start_time: '17:00',
|
||||
notes: 'Stream setup needed'
|
||||
)
|
||||
end
|
||||
|
||||
it 'writes a transaction log entry for the state change' do
|
||||
expect(TournamentLiveBroadcast).to receive(:call).with(@match.tournament.id)
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,51 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty
|
||||
|
||||
log_phase!('update match metadata and verify follower mirrors it')
|
||||
source_group_stage = source.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
first_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }.first
|
||||
first_group_matches = first_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) }
|
||||
single_metadata = source_owner_client.patch("/matches/#{first_group_matches.fetch(0).fetch(:id)}", body: {
|
||||
location: 'Main Hall',
|
||||
start_time: '17:00',
|
||||
notes: 'Opening match'
|
||||
})
|
||||
explicit_bulk_metadata = source_owner_client.patch("/tournaments/#{source.fetch(:id)}/matches/metadata", body: {
|
||||
match_ids: first_group_matches.values_at(1, 2).map { |match| match.fetch(:id) },
|
||||
location: 'Court B',
|
||||
start_time: '17:30'
|
||||
})
|
||||
group_position_metadata = source_owner_client.patch("/tournaments/#{source.fetch(:id)}/matches/metadata", body: {
|
||||
group_stage_position: 3,
|
||||
start_time: '18:00',
|
||||
notes: 'Group position 3 wave'
|
||||
})
|
||||
expect(single_metadata[:status]).to eq(200)
|
||||
expect(single_metadata.dig(:json, :location)).to eq('Main Hall')
|
||||
expect(explicit_bulk_metadata[:status]).to eq(200)
|
||||
expect(explicit_bulk_metadata[:json].map { |match| match.fetch(:location) }).to all(eq('Court B'))
|
||||
expect(group_position_metadata[:status]).to eq(200)
|
||||
expect(group_position_metadata[:json].map { |match| match.fetch(:start_time) }).to all(eq('18:00'))
|
||||
|
||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
||||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||
|
||||
follower_group_stage = follower.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
follower_group = follower_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }.first
|
||||
follower_first_match = follower_group.fetch(:matches).sort_by { |match| match.fetch(:position) }.first
|
||||
locked_match_metadata = follower_owner_client.patch("/matches/#{follower_first_match.fetch(:id)}", body: {
|
||||
location: 'Follower edit'
|
||||
})
|
||||
locked_bulk_metadata = follower_owner_client.patch("/tournaments/#{follower.fetch(:id)}/matches/metadata", body: {
|
||||
match_ids: [follower_first_match.fetch(:id)],
|
||||
start_time: '19:00'
|
||||
})
|
||||
expect(locked_match_metadata[:status]).to eq(423)
|
||||
expect(locked_match_metadata.dig(:json, :error)).to eq('Tournament is in read only follower mode')
|
||||
expect(locked_bulk_metadata[:status]).to eq(423)
|
||||
expect(locked_bulk_metadata.dig(:json, :error)).to eq('Tournament is in read only follower mode')
|
||||
|
||||
log_phase!('swap matches on leader and verify follower behavior')
|
||||
source_group_stage = source.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
first_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }.first
|
||||
|
|
@ -144,6 +189,20 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
expect(source.fetch(:team_action_lists)).to eq([])
|
||||
expect(follower.fetch(:team_action_lists)).to eq([])
|
||||
|
||||
log_phase!('bulk update playoff match metadata and verify follower mirrors it')
|
||||
source_playoff_stage = source.fetch(:stages).select { |stage| stage.fetch(:level) >= 0 }.max_by { |stage| stage.fetch(:level) }
|
||||
playoff_metadata = source_owner_client.patch("/tournaments/#{source.fetch(:id)}/matches/metadata", body: {
|
||||
stage_level: source_playoff_stage.fetch(:level),
|
||||
location: 'Finals Arena',
|
||||
start_time: '21:00',
|
||||
notes: 'Playoff wave'
|
||||
})
|
||||
expect(playoff_metadata[:status]).to eq(200)
|
||||
expect(playoff_metadata[:json].map { |match| match.fetch(:location) }).to all(eq('Finals Arena'))
|
||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
||||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||
|
||||
log_phase!('create team action list on source and verify follower mirrors it')
|
||||
created_list = create_team_action_list!(source.fetch(:id))
|
||||
expect(created_list.fetch(:key)).to eq(playoff_token_list_key)
|
||||
|
|
@ -548,6 +607,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
state: match.fetch(:state),
|
||||
position: match.fetch(:position),
|
||||
decider: match.fetch(:decider),
|
||||
location: match[:location],
|
||||
start_time: match[:start_time],
|
||||
notes: match[:notes],
|
||||
scores: match.fetch(:match_scores).map do |score|
|
||||
{
|
||||
team_name: score.dig(:team, :name),
|
||||
|
|
|
|||
|
|
@ -15,5 +15,9 @@ RSpec.describe MatchesController, type: :routing do
|
|||
it 'routes to #swap' do
|
||||
expect(patch: '/matches/1/swap').to route_to('matches#swap', id: '1')
|
||||
end
|
||||
|
||||
it 'routes nested tournament match metadata updates' do
|
||||
expect(patch: '/tournaments/1/matches/metadata').to route_to('matches#metadata', tournament_id: '1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,9 +26,14 @@ RSpec.describe 'Tournament live payloads' do
|
|||
payload = described_class.build(tournament)
|
||||
serialized_match = payload.find { |candidate| candidate[:id] == match.id }
|
||||
|
||||
expect(serialized_match.keys).to match_array(%i[id position state teams match_scores group stage])
|
||||
expect(serialized_match.keys).to match_array(
|
||||
%i[id position state location start_time notes teams match_scores group stage]
|
||||
)
|
||||
expect(serialized_match[:state]).to eq(match.state)
|
||||
expect(serialized_match[:position]).to eq(match.position)
|
||||
expect(serialized_match[:location]).to eq(match.location)
|
||||
expect(serialized_match[:start_time]).to eq(match.start_time)
|
||||
expect(serialized_match[:notes]).to eq(match.notes)
|
||||
expect(serialized_match.dig(:group, :id)).to eq(match.group_id)
|
||||
expect(serialized_match.dig(:stage, :id)).to eq(match.group.stage_id)
|
||||
expect(serialized_match[:teams].map { |team| team[:id] }).to match_array(match.teams.map(&:id))
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
timer_reason: 'custom',
|
||||
timer_reason_text: 'Opening ceremony'
|
||||
)
|
||||
source.matches.first.update!(
|
||||
location: 'Main Hall',
|
||||
start_time: '17:00',
|
||||
notes: 'Opening match'
|
||||
)
|
||||
source.beamers.create!(
|
||||
name: 'stream-a',
|
||||
display_state: 'during_game',
|
||||
|
|
@ -37,6 +42,11 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
expect(follower.team_action_lists.first.team_action_items.count).to eq(snapshot.fetch(:team_action_items).count)
|
||||
expect(follower.stages.count).to eq(source.stages.count)
|
||||
expect(follower.matches.count).to eq(source.matches.count)
|
||||
expect(follower.matches.find { |match| match.sync_source_id == source.matches.first.id }).to have_attributes(
|
||||
location: 'Main Hall',
|
||||
start_time: '17:00',
|
||||
notes: 'Opening match'
|
||||
)
|
||||
expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map { |team| team.fetch(:id) })
|
||||
expect(follower.beamers.pluck(:sync_source_id)).to match_array(snapshot.fetch(:beamers).map { |beamer| beamer.fetch(:id) })
|
||||
end
|
||||
|
|
@ -136,6 +146,11 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
|
||||
tournament.reload
|
||||
play_some_playoff_matches!(tournament)
|
||||
tournament.matches.min_by(&:id).update!(
|
||||
location: 'Feature Court',
|
||||
start_time: '18:30',
|
||||
notes: 'Follower sync metadata fixture'
|
||||
)
|
||||
tournament.update!(
|
||||
timestamp: Time.utc(2026, 4, 24, 10, 15, 0),
|
||||
timer_mode: 'countdown',
|
||||
|
|
@ -254,6 +269,9 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
state: match.state,
|
||||
position: match.position,
|
||||
decider: match.decider,
|
||||
location: match.location,
|
||||
start_time: match.start_time,
|
||||
notes: match.notes,
|
||||
scores: match.match_scores.includes(:team).sort_by { |score| score.team.name }.map do |score|
|
||||
{
|
||||
team_name: score.team.name,
|
||||
|
|
|
|||
7
tasks.py
7
tasks.py
|
|
@ -751,9 +751,10 @@ def start_test_local(
|
|||
|
||||
|
||||
@task
|
||||
def test(c):
|
||||
"""Run the normal RSpec suite."""
|
||||
_stream_subprocess(_rspec_command(), env=_env())
|
||||
def test(c, spec=""):
|
||||
"""Run the normal RSpec suite, or one or more spec files via --spec."""
|
||||
spec_paths = shlex.split(spec) if spec else []
|
||||
_stream_subprocess(_rspec_command(*spec_paths), env=_env())
|
||||
|
||||
|
||||
@task(name="test-shard")
|
||||
|
|
|
|||
Loading…
Reference in New Issue