Merge origin/feat/goals-v2-architecture; reconcile beta→preview rename

Remote branch added a beta_gated_nav_item helper + 'Gating the main nav'
docs section. Main concurrently renamed the beta-features gate to
preview-features (concern, predicate, JSONB key, locale flash). Rename
the new helper / partial local / pill marker to match preview naming and
port the nav-gating docs into gating-a-preview-feature.md so the
improvement survives the rename.

Resolved conflicts:
- db/schema.rb: take the later schema version (2026_05_19_100000).
- docs/llm-guides/gating-a-beta-feature.md: accept main's deletion;
  port the 'Gating the main nav' section into the preview guide.

Renames carried through to keep the gate wired end-to-end:
- application_helper.rb: beta_gated_nav_item → preview_gated_nav_item;
  beta_features_enabled? → preview_features_enabled?; beta: → preview:.
- _nav_item.html.erb: beta: local → preview: local; shared.beta i18n
  key → shared.preview.
- application.html.erb: caller renamed to preview_gated_nav_item.
- goals/index.html.erb: pill label uses shared.preview.
- shared/en.yml: 'beta: Beta' → 'preview: Preview'.
- goals_controller, goal_pledges_controller: require_beta_features! →
  require_preview_features!.
- goals_controller_test, goal_pledges_controller_test: flip the
  preference key, flash matcher, and test names to 'preview'.
This commit is contained in:
Guillem Arias
2026-05-20 21:47:27 +02:00
37 changed files with 585 additions and 127 deletions

View File

@@ -138,6 +138,27 @@ class GoalPledgeTest < ActiveSupport::TestCase
assert_equal 0, pledge.days_left
end
test "amount cannot be negative" do
@pledge.amount = -5
assert_not @pledge.valid?
assert_includes @pledge.errors[:amount], "must be greater than 0"
end
test "expire! is a no-op on an already-expired pledge" do
@pledge.expire!
expired_at = @pledge.updated_at
travel 1.second do
@pledge.expire!
assert_equal expired_at.to_i, @pledge.updated_at.to_i, "second expire! should not touch the row"
end
assert @pledge.status_expired?
end
test "cancel! raises on non-open pledge" do
pledge = goal_pledges(:matched_transfer)
assert_raises(GoalPledge::NotOpenError) { pledge.cancel! }
end
private
def build_entry(account:, amount:, date:)
OpenStruct.new(account_id: account.id, amount: BigDecimal(amount.to_s), date: date.to_date)

View File

@@ -25,6 +25,24 @@ class GoalTest < ActiveSupport::TestCase
assert_not @goal.valid?
end
test "color must match hex format" do
@goal.color = "red; cursor: pointer"
assert_not @goal.valid?
assert_includes @goal.errors[:color], "is invalid"
end
test "color accepts standard 6-digit hex" do
@goal.color = "#abcdef"
assert @goal.valid?, @goal.errors.full_messages.to_sentence
end
test "display_status follows AASM state after pause! on the same instance" do
@goal.update!(color: "#4da568") if @goal.color.blank?
initial = @goal.display_status
@goal.pause!
assert_equal :paused, @goal.display_status, "stale memo would have returned #{initial.inspect}"
end
test "must have at least one linked account on create" do
new_goal = @family.goals.new(name: "Test", target_amount: 100, currency: "USD")
assert_not new_goal.valid?
@@ -87,11 +105,13 @@ class GoalTest < ActiveSupport::TestCase
test "progress_percent is 0 for empty active goal" do
fresh = goals(:car_paydown)
fresh.target_amount = 10_000
fresh.update!(target_amount: 10_000)
fresh.linked_accounts.update_all(balance: 0)
fresh.instance_variable_set(:@current_balance, nil)
fresh.linked_accounts.reload
assert_equal 0, fresh.progress_percent
# Refetch instead of poking @current_balance directly so the test
# exercises the real memo lifecycle (a request reads progress_percent
# on a freshly-loaded record after the underlying balances changed).
reloaded = Goal.find(fresh.id)
assert_equal 0, reloaded.progress_percent
end
test "remaining_amount is non-negative" do
@@ -221,11 +241,11 @@ class GoalTest < ActiveSupport::TestCase
test "pledge_action_label_key flips on manual-only goals" do
assert_equal "goals.show.pledge_just_transferred", @goal.pledge_action_label_key
@goal.goal_accounts.where(account_id: @connected.id).destroy_all
@goal.reload
@goal.instance_variable_set(:@current_balance, nil)
# After removing the only connected account, the goal is manual-only;
# the copy must flip to "pledge_just_saved" so users aren't told to
# wait for a sync that won't run.
assert_equal "goals.show.pledge_just_saved", @goal.pledge_action_label_key
# wait for a sync that won't run. Refetch to exercise the real
# request lifecycle rather than poking a memo on the same instance.
reloaded = Goal.find(@goal.id)
assert_equal "goals.show.pledge_just_saved", reloaded.pledge_action_label_key
end
end