From 8acc8fe6251cc98696644ddcd4a872677c604d85 Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Mon, 23 Feb 2026 15:22:53 -0600 Subject: [PATCH 1/5] Switch test framework from Minitest to RSpec + FactoryBot - Replace minitest gem with rspec-rails and factory_bot_rails - Convert all 175 tests from test/ to spec/ (models, requests, services, helpers) - Convert YAML fixtures to FactoryBot factories with proper association chains - Factories derive org from parent objects to keep data consistent - Add sign_in_as helper and ActiveSupport::Testing::TimeHelpers to rails_helper - Update AGENTS.md to document new testing conventions - Remove old test/ directory Amp-Thread-ID: https://ampcode.com/threads/T-019c8c51-0ef2-723d-bab8-a95e7ecbf4e0 Co-authored-by: Amp --- .rspec | 1 + AGENTS.md | 15 +- Gemfile | 3 +- Gemfile.lock | 34 ++- spec/factories/api_tokens.rb | 22 ++ spec/factories/automated_plan_reviewers.rb | 12 + spec/factories/comment_threads.rb | 19 ++ spec/factories/comments.rb | 9 + spec/factories/edit_leases.rb | 11 + spec/factories/organizations.rb | 7 + spec/factories/plan_versions.rb | 10 + spec/factories/plans.rb | 25 +++ spec/factories/users.rb | 12 + spec/helpers/markdown_helper_spec.rb | 46 ++++ spec/models/api_token_spec.rb | 66 ++++++ spec/models/automated_plan_reviewer_spec.rb | 145 ++++++++++++ spec/models/comment_spec.rb | 26 +++ spec/models/comment_thread_spec.rb | 188 ++++++++++++++++ spec/models/edit_lease_spec.rb | 147 +++++++++++++ spec/models/organization_spec.rb | 53 +++++ spec/models/plan_spec.rb | 35 +++ spec/models/plan_version_spec.rb | 28 +++ spec/models/user_spec.rb | 63 ++++++ spec/rails_helper.rb | 24 ++ spec/requests/api/v1/comments_spec.rb | 68 ++++++ spec/requests/api/v1/leases_spec.rb | 90 ++++++++ spec/requests/api/v1/operations_spec.rb | 87 ++++++++ spec/requests/api/v1/plans_spec.rb | 88 ++++++++ spec/requests/api_tokens_spec.rb | 38 ++++ spec/requests/comment_threads_spec.rb | 78 +++++++ spec/requests/comments_spec.rb | 22 ++ spec/requests/plans_spec.rb | 79 +++++++ spec/requests/sessions_spec.rb | 47 ++++ spec/services/plans/apply_operations_spec.rb | 150 +++++++++++++ spec/services/plans/create_spec.rb | 27 +++ spec/spec_helper.rb | 94 ++++++++ test/application_system_test_case.rb | 5 - test/controllers/.keep | 0 .../api/v1/comments_controller_test.rb | 63 ------ .../api/v1/leases_controller_test.rb | 82 ------- .../api/v1/operations_controller_test.rb | 82 ------- .../api/v1/plans_controller_test.rb | 73 ------ .../controllers/api_tokens_controller_test.rb | 37 ---- .../comment_threads_controller_test.rb | 76 ------- test/controllers/comments_controller_test.rb | 21 -- test/controllers/plans_controller_test.rb | 76 ------- test/controllers/sessions_controller_test.rb | 44 ---- test/fixtures/api_tokens.yml | 28 --- test/fixtures/automated_plan_reviewers.yml | 61 ------ test/fixtures/comment_threads.yml | 18 -- test/fixtures/comments.yml | 15 -- test/fixtures/edit_leases.yml | 1 - test/fixtures/files/.keep | 0 test/fixtures/organizations.yml | 11 - test/fixtures/plan_versions.yml | 21 -- test/fixtures/plans.yml | 21 -- test/fixtures/users.yml | 20 -- test/helpers/.keep | 0 test/helpers/markdown_helper_test.rb | 42 ---- test/integration/.keep | 0 test/mailers/.keep | 0 test/models/.keep | 0 test/models/api_token_test.rb | 66 ------ test/models/automated_plan_reviewer_test.rb | 164 -------------- test/models/comment_test.rb | 27 --- test/models/comment_thread_test.rb | 207 ------------------ test/models/edit_lease_test.rb | 145 ------------ test/models/organization_test.rb | 52 ----- test/models/plan_test.rb | 35 --- test/models/plan_version_test.rb | 38 ---- test/models/user_test.rb | 56 ----- test/services/plans/apply_operations_test.rb | 152 ------------- test/services/plans/create_test.rb | 27 --- test/system/.keep | 0 test/test_helper.rb | 17 -- 75 files changed, 1861 insertions(+), 1791 deletions(-) create mode 100644 .rspec create mode 100644 spec/factories/api_tokens.rb create mode 100644 spec/factories/automated_plan_reviewers.rb create mode 100644 spec/factories/comment_threads.rb create mode 100644 spec/factories/comments.rb create mode 100644 spec/factories/edit_leases.rb create mode 100644 spec/factories/organizations.rb create mode 100644 spec/factories/plan_versions.rb create mode 100644 spec/factories/plans.rb create mode 100644 spec/factories/users.rb create mode 100644 spec/helpers/markdown_helper_spec.rb create mode 100644 spec/models/api_token_spec.rb create mode 100644 spec/models/automated_plan_reviewer_spec.rb create mode 100644 spec/models/comment_spec.rb create mode 100644 spec/models/comment_thread_spec.rb create mode 100644 spec/models/edit_lease_spec.rb create mode 100644 spec/models/organization_spec.rb create mode 100644 spec/models/plan_spec.rb create mode 100644 spec/models/plan_version_spec.rb create mode 100644 spec/models/user_spec.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/requests/api/v1/comments_spec.rb create mode 100644 spec/requests/api/v1/leases_spec.rb create mode 100644 spec/requests/api/v1/operations_spec.rb create mode 100644 spec/requests/api/v1/plans_spec.rb create mode 100644 spec/requests/api_tokens_spec.rb create mode 100644 spec/requests/comment_threads_spec.rb create mode 100644 spec/requests/comments_spec.rb create mode 100644 spec/requests/plans_spec.rb create mode 100644 spec/requests/sessions_spec.rb create mode 100644 spec/services/plans/apply_operations_spec.rb create mode 100644 spec/services/plans/create_spec.rb create mode 100644 spec/spec_helper.rb delete mode 100644 test/application_system_test_case.rb delete mode 100644 test/controllers/.keep delete mode 100644 test/controllers/api/v1/comments_controller_test.rb delete mode 100644 test/controllers/api/v1/leases_controller_test.rb delete mode 100644 test/controllers/api/v1/operations_controller_test.rb delete mode 100644 test/controllers/api/v1/plans_controller_test.rb delete mode 100644 test/controllers/api_tokens_controller_test.rb delete mode 100644 test/controllers/comment_threads_controller_test.rb delete mode 100644 test/controllers/comments_controller_test.rb delete mode 100644 test/controllers/plans_controller_test.rb delete mode 100644 test/controllers/sessions_controller_test.rb delete mode 100644 test/fixtures/api_tokens.yml delete mode 100644 test/fixtures/automated_plan_reviewers.yml delete mode 100644 test/fixtures/comment_threads.yml delete mode 100644 test/fixtures/comments.yml delete mode 100644 test/fixtures/edit_leases.yml delete mode 100644 test/fixtures/files/.keep delete mode 100644 test/fixtures/organizations.yml delete mode 100644 test/fixtures/plan_versions.yml delete mode 100644 test/fixtures/plans.yml delete mode 100644 test/fixtures/users.yml delete mode 100644 test/helpers/.keep delete mode 100644 test/helpers/markdown_helper_test.rb delete mode 100644 test/integration/.keep delete mode 100644 test/mailers/.keep delete mode 100644 test/models/.keep delete mode 100644 test/models/api_token_test.rb delete mode 100644 test/models/automated_plan_reviewer_test.rb delete mode 100644 test/models/comment_test.rb delete mode 100644 test/models/comment_thread_test.rb delete mode 100644 test/models/edit_lease_test.rb delete mode 100644 test/models/organization_test.rb delete mode 100644 test/models/plan_test.rb delete mode 100644 test/models/plan_version_test.rb delete mode 100644 test/models/user_test.rb delete mode 100644 test/services/plans/apply_operations_test.rb delete mode 100644 test/services/plans/create_test.rb delete mode 100644 test/system/.keep delete mode 100644 test/test_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/AGENTS.md b/AGENTS.md index 67775e4f..c8f91804 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,12 +42,15 @@ A Rails app for managing engineering design doc review, purpose-built for AI-gen ## Testing -- **Minitest** — not RSpec -- Run the full suite: `bin/rails test` -- Test files mirror app structure: `test/models/`, `test/controllers/`, `test/services/` -- Use **fixtures** (YAML in `test/fixtures/`) — not factories -- Every model, service object, and controller action should have tests -- Fixtures use hardcoded UUIDs for stable cross-references +- **RSpec** with `rspec-rails` +- Run the full suite: `bundle exec rspec` +- Spec files mirror app structure: `spec/models/`, `spec/requests/`, `spec/services/`, `spec/helpers/` +- Use **FactoryBot** (`factory_bot_rails`) — factories live in `spec/factories/` +- Every model, service object, and controller action should have specs +- UUID primary keys are auto-assigned by `ApplicationRecord#assign_uuid` — do **not** set `id` in factories +- Factories derive associations from parent objects (e.g., `organization { plan.organization }`) to keep data consistent +- FactoryBot syntax methods (`create`, `build`) are included globally via `config.include FactoryBot::Syntax::Methods` +- `sign_in_as(user)` helper is defined in `spec/rails_helper.rb` for request specs ## Seeds diff --git a/Gemfile b/Gemfile index 59de9842..07127190 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,6 @@ source "https://rubygems.org" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 8.1.1" -gem "minitest", "~> 5.25" # The modern asset pipeline for Rails [https://github.com/rails/propshaft] gem "propshaft" # Use mysql as the database for Active Record @@ -73,6 +72,8 @@ group :development do end group :test do + gem "rspec-rails" + gem "factory_bot_rails" # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] gem "capybara" gem "selenium-webdriver" diff --git a/Gemfile.lock b/Gemfile.lock index f1bea969..745baf14 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -129,6 +129,7 @@ GEM debug (1.11.1) irb (~> 1.10) reline (>= 0.3.8) + diff-lcs (1.6.2) diffy (3.4.4) dotenv (3.2.0) drb (2.2.3) @@ -138,6 +139,11 @@ GEM et-orbi (1.4.0) tzinfo event_stream_parser (1.0.0) + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) faraday (2.14.1) faraday-net_http (>= 2.0, < 3.5) json @@ -345,6 +351,23 @@ GEM actionpack (>= 7.0) railties (>= 7.0) rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.3) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.7) rubocop (1.84.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -469,16 +492,17 @@ DEPENDENCIES commonmarker debug diffy + factory_bot_rails image_processing (~> 1.2) importmap-rails jbuilder kamal - minitest (~> 5.25) mysql2 (~> 0.5) propshaft puma (>= 5.0) rack-attack rails (~> 8.1.1) + rspec-rails rubocop-rails-omakase ruby-openai selenium-webdriver @@ -532,6 +556,7 @@ CHECKSUMS csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 diffy (3.4.4) sha256=79384ab5ca82d0e115b2771f0961e27c164c456074bd2ec46b637ebf7b6e47e3 dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 @@ -540,6 +565,8 @@ CHECKSUMS erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc event_stream_parser (1.0.0) sha256=a2683bab70126286f8184dc88f7968ffc4028f813161fb073ec90d171f7de3c8 + factory_bot (6.5.6) sha256=12beb373214dccc086a7a63763d6718c49769d5606f0501e0a4442676917e077 + factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 faraday (2.14.1) sha256=a43cceedc1e39d188f4d2cdd360a8aaa6a11da0c407052e426ba8d3fb42ef61c faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c @@ -628,6 +655,11 @@ CHECKSUMS reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 responders (3.2.0) sha256=89c2d6ac0ae16f6458a11524cae4a8efdceba1a3baea164d28ee9046bd3df55a rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.7) sha256=0979034e64b1d7a838aaaddf12bf065ea4dc40ef3d4c39f01f93ae2c66c62b1c + rspec-rails (8.0.3) sha256=b0a440e7a10700317d898a014852e26660867298c4076dbc3baa99c768b79dc1 + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 diff --git a/spec/factories/api_tokens.rb b/spec/factories/api_tokens.rb new file mode 100644 index 00000000..f8333e5b --- /dev/null +++ b/spec/factories/api_tokens.rb @@ -0,0 +1,22 @@ +FactoryBot.define do + factory :api_token do + organization + user { association(:user, organization: organization) } + sequence(:name) { |n| "Token #{n}" } + token_digest { Digest::SHA256.hexdigest(SecureRandom.hex(32)) } + + transient do + raw_token { nil } + end + + after(:build) do |token, evaluator| + if evaluator.raw_token + token.token_digest = Digest::SHA256.hexdigest(evaluator.raw_token) + end + end + + trait :revoked do + revoked_at { 1.day.ago } + end + end +end diff --git a/spec/factories/automated_plan_reviewers.rb b/spec/factories/automated_plan_reviewers.rb new file mode 100644 index 00000000..dbe2726b --- /dev/null +++ b/spec/factories/automated_plan_reviewers.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :automated_plan_reviewer do + organization + sequence(:key) { |n| "reviewer-#{n}" } + sequence(:name) { |n| "Reviewer #{n}" } + prompt_text { "You are a reviewer. Review the plan." } + enabled { true } + trigger_statuses { [] } + ai_provider { "openai" } + ai_model { "gpt-4o" } + end +end diff --git a/spec/factories/comment_threads.rb b/spec/factories/comment_threads.rb new file mode 100644 index 00000000..6fb943e6 --- /dev/null +++ b/spec/factories/comment_threads.rb @@ -0,0 +1,19 @@ +FactoryBot.define do + factory :comment_thread do + plan + organization { plan.organization } + plan_version { plan.current_plan_version } + created_by_user { association(:user, organization: plan.organization) } + status { "open" } + out_of_date { false } + + trait :with_anchor do + anchor_text { "some anchor text" } + end + + trait :resolved do + status { "resolved" } + association :resolved_by_user, factory: :user + end + end +end diff --git a/spec/factories/comments.rb b/spec/factories/comments.rb new file mode 100644 index 00000000..95222177 --- /dev/null +++ b/spec/factories/comments.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :comment do + comment_thread + organization { comment_thread.organization } + author_type { "human" } + author_id { association(:user, organization: organization).id } + body_markdown { "A comment body." } + end +end diff --git a/spec/factories/edit_leases.rb b/spec/factories/edit_leases.rb new file mode 100644 index 00000000..e3e4b0e6 --- /dev/null +++ b/spec/factories/edit_leases.rb @@ -0,0 +1,11 @@ +FactoryBot.define do + factory :edit_lease do + plan + organization + holder_type { "local_agent" } + holder_id { SecureRandom.uuid } + lease_token_digest { Digest::SHA256.hexdigest(SecureRandom.hex(32)) } + expires_at { 5.minutes.from_now } + last_heartbeat_at { Time.current } + end +end diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb new file mode 100644 index 00000000..4b215cac --- /dev/null +++ b/spec/factories/organizations.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :organization do + sequence(:name) { |n| "Organization #{n}" } + sequence(:slug) { |n| "org-#{n}" } + allowed_email_domains { ["example.com"] } + end +end diff --git a/spec/factories/plan_versions.rb b/spec/factories/plan_versions.rb new file mode 100644 index 00000000..e7e8be54 --- /dev/null +++ b/spec/factories/plan_versions.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :plan_version do + plan + organization { plan.organization } + sequence(:revision) { |n| n } + content_markdown { "# Plan Content\n\nSome content here." } + actor_type { "human" } + actor_id { nil } + end +end diff --git a/spec/factories/plans.rb b/spec/factories/plans.rb new file mode 100644 index 00000000..cae0463c --- /dev/null +++ b/spec/factories/plans.rb @@ -0,0 +1,25 @@ +FactoryBot.define do + factory :plan do + organization + created_by_user { association(:user, organization: organization) } + sequence(:title) { |n| "Plan #{n}" } + status { "brainstorm" } + tags { [] } + metadata { {} } + + after(:create) do |plan| + unless plan.current_plan_version + version = create(:plan_version, plan: plan, organization: plan.organization, revision: 1, actor_id: plan.created_by_user_id) + plan.update_columns(current_plan_version_id: version.id, current_revision: 1) + end + end + + trait :considering do + status { "considering" } + end + + trait :brainstorm do + status { "brainstorm" } + end + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 00000000..d9a08ab4 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :user do + organization + sequence(:email) { |n| "user#{n}@example.com" } + sequence(:name) { |n| "User #{n}" } + org_role { "member" } + + trait :admin do + org_role { "admin" } + end + end +end diff --git a/spec/helpers/markdown_helper_spec.rb b/spec/helpers/markdown_helper_spec.rb new file mode 100644 index 00000000..dd8940dc --- /dev/null +++ b/spec/helpers/markdown_helper_spec.rb @@ -0,0 +1,46 @@ +require "rails_helper" + +RSpec.describe MarkdownHelper, type: :helper do + describe "#render_markdown" do + it "converts markdown to HTML" do + html = helper.render_markdown("# Hello\n\n**bold** text") + expect(html).to include("

") + expect(html).to include("bold") + expect(html).to include("markdown-rendered") + end + + it "sanitizes dangerous HTML" do + html = helper.render_markdown('') + expect(html).not_to match(/') + expect(html).not_to match(/') - assert_no_match(/') - assert_no_match(/