agent-notes 2.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. agent_notes/VERSION +1 -0
  2. agent_notes/__init__.py +1 -0
  3. agent_notes/__main__.py +4 -0
  4. agent_notes/cli.py +348 -0
  5. agent_notes/commands/__init__.py +27 -0
  6. agent_notes/commands/_install_helpers.py +262 -0
  7. agent_notes/commands/build.py +170 -0
  8. agent_notes/commands/doctor.py +112 -0
  9. agent_notes/commands/info.py +95 -0
  10. agent_notes/commands/install.py +99 -0
  11. agent_notes/commands/list.py +169 -0
  12. agent_notes/commands/memory.py +430 -0
  13. agent_notes/commands/regenerate.py +152 -0
  14. agent_notes/commands/set_role.py +143 -0
  15. agent_notes/commands/uninstall.py +26 -0
  16. agent_notes/commands/update.py +169 -0
  17. agent_notes/commands/validate.py +199 -0
  18. agent_notes/commands/wizard.py +720 -0
  19. agent_notes/config.py +154 -0
  20. agent_notes/data/agents/agents.yaml +352 -0
  21. agent_notes/data/agents/analyst.md +45 -0
  22. agent_notes/data/agents/api-reviewer.md +47 -0
  23. agent_notes/data/agents/architect.md +46 -0
  24. agent_notes/data/agents/coder.md +28 -0
  25. agent_notes/data/agents/database-specialist.md +45 -0
  26. agent_notes/data/agents/debugger.md +47 -0
  27. agent_notes/data/agents/devil.md +47 -0
  28. agent_notes/data/agents/devops.md +38 -0
  29. agent_notes/data/agents/explorer.md +23 -0
  30. agent_notes/data/agents/integrations.md +44 -0
  31. agent_notes/data/agents/lead.md +216 -0
  32. agent_notes/data/agents/performance-profiler.md +44 -0
  33. agent_notes/data/agents/refactorer.md +48 -0
  34. agent_notes/data/agents/reviewer.md +44 -0
  35. agent_notes/data/agents/security-auditor.md +44 -0
  36. agent_notes/data/agents/system-auditor.md +38 -0
  37. agent_notes/data/agents/tech-writer.md +32 -0
  38. agent_notes/data/agents/test-runner.md +36 -0
  39. agent_notes/data/agents/test-writer.md +39 -0
  40. agent_notes/data/cli/claude.yaml +25 -0
  41. agent_notes/data/cli/copilot.yaml +18 -0
  42. agent_notes/data/cli/opencode.yaml +22 -0
  43. agent_notes/data/commands/brainstorm.md +8 -0
  44. agent_notes/data/commands/debug.md +9 -0
  45. agent_notes/data/commands/review.md +10 -0
  46. agent_notes/data/global-claude.md +290 -0
  47. agent_notes/data/global-copilot.md +27 -0
  48. agent_notes/data/global-opencode.md +40 -0
  49. agent_notes/data/hooks/session-context.md.tpl +19 -0
  50. agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
  51. agent_notes/data/models/claude-opus-4-1.yaml +16 -0
  52. agent_notes/data/models/claude-opus-4-5.yaml +16 -0
  53. agent_notes/data/models/claude-opus-4-6.yaml +16 -0
  54. agent_notes/data/models/claude-opus-4-7.yaml +15 -0
  55. agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
  56. agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
  57. agent_notes/data/models/claude-sonnet-4.yaml +16 -0
  58. agent_notes/data/pricing.yaml +33 -0
  59. agent_notes/data/roles/orchestrator.yaml +5 -0
  60. agent_notes/data/roles/reasoner.yaml +5 -0
  61. agent_notes/data/roles/scout.yaml +5 -0
  62. agent_notes/data/roles/worker.yaml +5 -0
  63. agent_notes/data/rules/code-quality.md +9 -0
  64. agent_notes/data/rules/safety.md +10 -0
  65. agent_notes/data/scripts/cost-report +211 -0
  66. agent_notes/data/skills/brainstorming/SKILL.md +57 -0
  67. agent_notes/data/skills/code-review/SKILL.md +64 -0
  68. agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
  69. agent_notes/data/skills/docker-compose/SKILL.md +318 -0
  70. agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
  71. agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
  72. agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
  73. agent_notes/data/skills/git/SKILL.md +87 -0
  74. agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
  75. agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
  76. agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
  77. agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
  78. agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
  79. agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
  80. agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
  81. agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
  82. agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
  83. agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
  84. agent_notes/data/skills/rails-lib/SKILL.md +101 -0
  85. agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
  86. agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
  87. agent_notes/data/skills/rails-models/SKILL.md +459 -0
  88. agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
  89. agent_notes/data/skills/rails-routes/SKILL.md +804 -0
  90. agent_notes/data/skills/rails-style/SKILL.md +538 -0
  91. agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
  92. agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
  93. agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
  94. agent_notes/data/skills/rails-validations/SKILL.md +108 -0
  95. agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
  96. agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
  97. agent_notes/data/skills/rails-views/SKILL.md +413 -0
  98. agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
  99. agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
  100. agent_notes/data/skills/tdd/SKILL.md +57 -0
  101. agent_notes/data/templates/__init__.py +1 -0
  102. agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
  103. agent_notes/data/templates/frontmatter/__init__.py +1 -0
  104. agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
  105. agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
  106. agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
  107. agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
  108. agent_notes/data/templates/frontmatter/claude.py +44 -0
  109. agent_notes/data/templates/frontmatter/opencode.py +104 -0
  110. agent_notes/doctor_checks.py +189 -0
  111. agent_notes/domain/__init__.py +17 -0
  112. agent_notes/domain/agent.py +34 -0
  113. agent_notes/domain/cli_backend.py +40 -0
  114. agent_notes/domain/diagnostics.py +29 -0
  115. agent_notes/domain/diff.py +44 -0
  116. agent_notes/domain/model.py +27 -0
  117. agent_notes/domain/role.py +13 -0
  118. agent_notes/domain/rule.py +13 -0
  119. agent_notes/domain/skill.py +15 -0
  120. agent_notes/domain/state.py +46 -0
  121. agent_notes/install_state.py +11 -0
  122. agent_notes/registries/__init__.py +16 -0
  123. agent_notes/registries/_base.py +46 -0
  124. agent_notes/registries/agent_registry.py +107 -0
  125. agent_notes/registries/cli_registry.py +89 -0
  126. agent_notes/registries/model_registry.py +85 -0
  127. agent_notes/registries/role_registry.py +64 -0
  128. agent_notes/registries/rule_registry.py +80 -0
  129. agent_notes/registries/skill_registry.py +141 -0
  130. agent_notes/services/__init__.py +8 -0
  131. agent_notes/services/diagnostics/__init__.py +47 -0
  132. agent_notes/services/diagnostics/_checks.py +272 -0
  133. agent_notes/services/diagnostics/_display.py +346 -0
  134. agent_notes/services/diagnostics/_fix.py +169 -0
  135. agent_notes/services/diff.py +349 -0
  136. agent_notes/services/fs.py +195 -0
  137. agent_notes/services/install_state_builder.py +210 -0
  138. agent_notes/services/installer.py +293 -0
  139. agent_notes/services/memory_backend.py +155 -0
  140. agent_notes/services/rendering.py +329 -0
  141. agent_notes/services/session_context.py +23 -0
  142. agent_notes/services/settings_writer.py +79 -0
  143. agent_notes/services/state_store.py +249 -0
  144. agent_notes/services/ui.py +419 -0
  145. agent_notes/services/user_config.py +62 -0
  146. agent_notes/services/validation.py +67 -0
  147. agent_notes/state.py +21 -0
  148. agent_notes-2.0.4.dist-info/METADATA +14 -0
  149. agent_notes-2.0.4.dist-info/RECORD +162 -0
  150. agent_notes-2.0.4.dist-info/WHEEL +5 -0
  151. agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
  152. agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
  153. agent_notes-2.0.4.dist-info/top_level.txt +2 -0
  154. tests/conftest.py +20 -0
  155. tests/functional/__init__.py +0 -0
  156. tests/functional/test_build_commands.py +88 -0
  157. tests/functional/test_registries.py +128 -0
  158. tests/integration/__init__.py +0 -0
  159. tests/integration/test_build_output.py +129 -0
  160. tests/plugins/__init__.py +0 -0
  161. tests/plugins/test_agents.py +93 -0
  162. tests/plugins/test_skills.py +77 -0
@@ -0,0 +1,343 @@
1
+ ---
2
+ name: rails-testing-controllers
3
+ description: "Rails testing: controller tests, system tests, and integration tests"
4
+ group: rails
5
+ ---
6
+
7
+ # Testing Controllers & Integration
8
+
9
+ ## Controller Tests
10
+
11
+ ### Basic CRUD Tests
12
+
13
+ ```ruby
14
+ class BoardsControllerTest < ActionDispatch::IntegrationTest
15
+ setup do
16
+ sign_in_as :kevin
17
+ end
18
+
19
+ test "index shows boards" do
20
+ get boards_path
21
+
22
+ assert_response :success
23
+ assert_select "h1", "Boards"
24
+ end
25
+
26
+ test "show displays board" do
27
+ board = boards(:writebook)
28
+
29
+ get board_path(board)
30
+
31
+ assert_response :success
32
+ assert_select "h1", board.name
33
+ end
34
+
35
+ test "create creates board" do
36
+ assert_difference -> { Board.count }, +1 do
37
+ post boards_path, params: { board: { name: "New Board" } }
38
+ end
39
+
40
+ assert_redirected_to board_path(Board.last)
41
+ assert_equal "New Board", Board.last.name
42
+ end
43
+
44
+ test "update updates board" do
45
+ board = boards(:writebook)
46
+
47
+ patch board_path(board), params: { board: { name: "Updated" } }
48
+
49
+ assert_redirected_to board_path(board)
50
+ assert_equal "Updated", board.reload.name
51
+ end
52
+
53
+ test "destroy removes board" do
54
+ board = boards(:writebook)
55
+
56
+ assert_difference -> { Board.count }, -1 do
57
+ delete board_path(board)
58
+ end
59
+
60
+ assert_redirected_to boards_path
61
+ end
62
+ end
63
+ ```
64
+
65
+ ### Testing Permissions
66
+
67
+ ```ruby
68
+ test "non-admin cannot update board" do
69
+ logout_and_sign_in_as :member
70
+
71
+ board = boards(:writebook)
72
+ original_name = board.name
73
+
74
+ patch board_path(board), params: { board: { name: "Hacked" } }
75
+
76
+ assert_response :forbidden
77
+ assert_equal original_name, board.reload.name
78
+ end
79
+
80
+ test "non-member cannot access board" do
81
+ logout_and_sign_in_as :other_user
82
+
83
+ get board_path(boards(:writebook))
84
+
85
+ assert_response :forbidden
86
+ end
87
+
88
+ test "unauthenticated user redirected to login" do
89
+ logout
90
+
91
+ get boards_path
92
+
93
+ assert_redirected_to new_session_path
94
+ end
95
+
96
+ test "owner can destroy board" do
97
+ sign_in_as boards(:writebook).owner
98
+
99
+ assert_difference -> { Board.count }, -1 do
100
+ delete board_path(boards(:writebook))
101
+ end
102
+
103
+ assert_response :redirect
104
+ end
105
+ ```
106
+
107
+ ### Testing Response Formats
108
+
109
+ ```ruby
110
+ test "responds with HTML" do
111
+ get boards_path
112
+
113
+ assert_response :success
114
+ assert_match "text/html", response.content_type
115
+ end
116
+
117
+ test "responds with JSON" do
118
+ board = boards(:writebook)
119
+
120
+ get board_path(board), as: :json
121
+
122
+ assert_response :success
123
+ assert_match "application/json", response.content_type
124
+
125
+ json = JSON.parse(response.body)
126
+ assert_equal board.name, json["name"]
127
+ end
128
+
129
+ test "responds with Turbo Stream" do
130
+ post boards_path,
131
+ params: { board: { name: "Test" } },
132
+ as: :turbo_stream
133
+
134
+ assert_response :success
135
+ assert_match "text/vnd.turbo-stream.html", response.content_type
136
+ assert_match "turbo-stream", response.body
137
+ end
138
+ ```
139
+
140
+ ### Testing Flash Messages
141
+
142
+ ```ruby
143
+ test "success flash on create" do
144
+ post boards_path, params: { board: { name: "Test" } }
145
+
146
+ assert_equal "Board created", flash[:notice]
147
+ end
148
+
149
+ test "error flash on invalid create" do
150
+ post boards_path, params: { board: { name: "" } }
151
+
152
+ assert_match /error/, flash[:alert].downcase
153
+ end
154
+ ```
155
+
156
+ ### Testing Redirects
157
+
158
+ ```ruby
159
+ test "redirects to board after create" do
160
+ post boards_path, params: { board: { name: "Test" } }
161
+
162
+ assert_redirected_to board_path(Board.last)
163
+ end
164
+
165
+ test "redirects back with fallback" do
166
+ board = boards(:writebook)
167
+
168
+ patch board_path(board),
169
+ params: { board: { name: "Updated" } },
170
+ headers: { "HTTP_REFERER" => boards_path }
171
+
172
+ assert_redirected_to boards_path
173
+ end
174
+ ```
175
+
176
+ ### Testing Parameters
177
+
178
+ ```ruby
179
+ test "permits valid parameters" do
180
+ post boards_path, params: {
181
+ board: {
182
+ name: "Test",
183
+ description: "Test description"
184
+ }
185
+ }
186
+
187
+ board = Board.last
188
+ assert_equal "Test", board.name
189
+ assert_equal "Test description", board.description
190
+ end
191
+
192
+ test "filters unpermitted parameters" do
193
+ post boards_path, params: {
194
+ board: {
195
+ name: "Test",
196
+ admin: true # Unpermitted
197
+ }
198
+ }
199
+
200
+ board = Board.last
201
+ assert_equal "Test", board.name
202
+ assert_not board.respond_to?(:admin)
203
+ end
204
+ ```
205
+
206
+ ---
207
+
208
+ ## System Tests
209
+
210
+ System tests use a real browser (Selenium) to test the full stack.
211
+
212
+ ### Basic System Test
213
+
214
+ ```ruby
215
+ class CardsTest < ApplicationSystemTestCase
216
+ test "creating a card" do
217
+ sign_in_as users(:david)
218
+
219
+ visit board_url(boards(:writebook))
220
+ click_on "Add a card"
221
+
222
+ fill_in "Title", with: "New feature"
223
+ fill_in "Description", with: "Build something awesome"
224
+ click_on "Create card"
225
+
226
+ assert_selector "h3", text: "New feature"
227
+ assert_text "Build something awesome"
228
+ end
229
+
230
+ test "closing a card" do
231
+ sign_in_as users(:david)
232
+ card = cards(:logo)
233
+
234
+ visit card_url(card)
235
+ click_on "Close"
236
+
237
+ assert_selector ".badge", text: "Closed"
238
+ end
239
+
240
+ test "adding a comment" do
241
+ sign_in_as users(:david)
242
+ card = cards(:logo)
243
+
244
+ visit card_url(card)
245
+
246
+ fill_in "Comment", with: "Great work!"
247
+ click_on "Post comment"
248
+
249
+ assert_text "Great work!"
250
+ end
251
+ end
252
+ ```
253
+
254
+ ### Testing JavaScript Interactions
255
+
256
+ ```ruby
257
+ test "opening and closing modal", js: true do
258
+ sign_in_as users(:david)
259
+
260
+ visit boards_path
261
+ click_on "New Board"
262
+
263
+ # Modal should appear
264
+ assert_selector "#new-board-modal", visible: true
265
+
266
+ click_on "Cancel"
267
+
268
+ # Modal should disappear
269
+ assert_no_selector "#new-board-modal", visible: true
270
+ end
271
+
272
+ test "auto-save works", js: true do
273
+ sign_in_as users(:david)
274
+ card = cards(:logo)
275
+
276
+ visit edit_card_path(card)
277
+
278
+ fill_in "Title", with: "Auto-saved title"
279
+
280
+ # Wait for auto-save
281
+ assert_text "Saved", wait: 3
282
+
283
+ visit card_path(card)
284
+ assert_text "Auto-saved title"
285
+ end
286
+ ```
287
+
288
+ ### Testing Turbo Frames
289
+
290
+ ```ruby
291
+ test "navigating within turbo frame" do
292
+ sign_in_as users(:david)
293
+
294
+ visit boards_path
295
+
296
+ within("#boards-frame") do
297
+ click_on "Show archived"
298
+ assert_text "Archived Boards"
299
+ end
300
+
301
+ # Page didn't fully reload
302
+ assert_current_path boards_path
303
+ end
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Integration Tests
309
+
310
+ Test complete user workflows across multiple requests.
311
+
312
+ ```ruby
313
+ class UserFlowTest < ActionDispatch::IntegrationTest
314
+ test "complete card workflow" do
315
+ # Sign in
316
+ sign_in_as :david
317
+
318
+ # Create board
319
+ post boards_path, params: { board: { name: "My Board" } }
320
+ board = Board.last
321
+
322
+ # Create card
323
+ post board_cards_path(board), params: {
324
+ card: { title: "My Card" }
325
+ }
326
+ card = Card.last
327
+
328
+ # Add comment
329
+ post card_comments_path(card), params: {
330
+ comment: { body: "First comment" }
331
+ }
332
+
333
+ # Close card
334
+ post card_closure_path(card)
335
+
336
+ # Verify final state
337
+ assert card.reload.closed?
338
+ assert_equal 1, card.comments.count
339
+ end
340
+ end
341
+ ```
342
+
343
+ ---
@@ -0,0 +1,296 @@
1
+ ---
2
+ name: rails-testing-models
3
+ description: "Rails testing: model tests for associations, validations, scopes, callbacks, and methods"
4
+ group: rails
5
+ ---
6
+
7
+ # Tests Guide
8
+
9
+ Comprehensive guide for Rails testing patterns.
10
+
11
+ ---
12
+
13
+ ## Testing Philosophy
14
+
15
+ 1. **Test behavior, not implementation**
16
+ 2. **Test happy path and edge cases**
17
+ 3. **Test permissions thoroughly**
18
+ 4. **Use fixtures for consistent data**
19
+ 5. **Keep tests independent**
20
+ 6. **Fast tests = tests that get run**
21
+
22
+ ---
23
+
24
+ ## Test Structure
25
+
26
+ ```
27
+ test/
28
+ ├── controllers/
29
+ │ ├── boards_controller_test.rb
30
+ │ └── cards/
31
+ │ └── closures_controller_test.rb
32
+ ├── models/
33
+ │ ├── card_test.rb
34
+ │ └── card/
35
+ │ └── closeable_test.rb
36
+ ├── system/
37
+ │ └── cards_test.rb
38
+ ├── integration/
39
+ │ └── user_flow_test.rb
40
+ ├── helpers/
41
+ │ └── cards_helper_test.rb
42
+ ├── mailers/
43
+ │ └── notification_mailer_test.rb
44
+ ├── jobs/
45
+ │ └── notification_job_test.rb
46
+ └── fixtures/
47
+ ├── cards.yml
48
+ ├── users.yml
49
+ └── boards.yml
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Model Tests
55
+
56
+ ### Test File Structure
57
+
58
+ ```ruby
59
+ class CardTest < ActiveSupport::TestCase
60
+ # Setup runs before each test
61
+ setup do
62
+ @card = cards(:logo)
63
+ end
64
+
65
+ # Teardown runs after each test (optional)
66
+ teardown do
67
+ # Cleanup if needed
68
+ end
69
+
70
+ # Test naming: "test description"
71
+ test "belongs to board" do
72
+ assert_equal boards(:writebook), @card.board
73
+ end
74
+
75
+ test "validates title presence when published" do
76
+ card = Card.new(status: :published)
77
+
78
+ assert_not card.valid?
79
+ assert_includes card.errors[:title], "can't be blank"
80
+ end
81
+ end
82
+ ```
83
+
84
+ ### Testing Associations
85
+
86
+ ```ruby
87
+ test "belongs to board" do
88
+ card = cards(:logo)
89
+ assert_equal boards(:writebook), card.board
90
+ end
91
+
92
+ test "has many comments" do
93
+ card = cards(:logo)
94
+ assert_respond_to card, :comments
95
+ assert_instance_of ActiveRecord::Associations::CollectionProxy, card.comments
96
+ end
97
+
98
+ test "has many tags through taggings" do
99
+ card = cards(:logo)
100
+ tag = tags(:bug)
101
+
102
+ card.taggings.create!(tag: tag)
103
+
104
+ assert_includes card.tags, tag
105
+ end
106
+
107
+ test "destroys dependent comments" do
108
+ card = cards(:logo)
109
+ comment = card.comments.create!(body: "Test", creator: users(:david))
110
+
111
+ assert_difference -> { Comment.count }, -1 do
112
+ card.destroy
113
+ end
114
+ end
115
+ ```
116
+
117
+ ### Testing Validations
118
+
119
+ ```ruby
120
+ test "validates presence of title" do
121
+ card = Card.new
122
+ assert_not card.valid?
123
+ assert_includes card.errors[:title], "can't be blank"
124
+ end
125
+
126
+ test "validates uniqueness of number scoped to account" do
127
+ existing = cards(:logo)
128
+
129
+ card = Card.new(
130
+ account: existing.account,
131
+ board: boards(:writebook),
132
+ number: existing.number
133
+ )
134
+
135
+ assert_not card.valid?
136
+ assert_includes card.errors[:number], "has already been taken"
137
+ end
138
+
139
+ test "validates email format" do
140
+ user = User.new(email: "invalid")
141
+
142
+ assert_not user.valid?
143
+ assert_includes user.errors[:email], "is invalid"
144
+ end
145
+
146
+ test "validates conditional presence" do
147
+ card = Card.new(status: :draft)
148
+ assert card.valid? # title not required for draft
149
+
150
+ card.status = :published
151
+ assert_not card.valid?
152
+ assert_includes card.errors[:title], "can't be blank"
153
+ end
154
+ ```
155
+
156
+ ### Testing Scopes
157
+
158
+ ```ruby
159
+ test "published scope returns published cards" do
160
+ published_card = cards(:logo)
161
+ draft_card = cards(:draft)
162
+
163
+ assert_includes Card.published, published_card
164
+ assert_not_includes Card.published, draft_card
165
+ end
166
+
167
+ test "closed scope returns cards with closure" do
168
+ card = cards(:logo)
169
+ card.close
170
+
171
+ assert_includes Card.closed, card
172
+ assert_not_includes Card.open, card
173
+ end
174
+
175
+ test "parameterized scope filters correctly" do
176
+ user = users(:david)
177
+ card = cards(:logo)
178
+ card.assignments.create!(user: user)
179
+
180
+ assert_includes Card.assigned_to(user), card
181
+ end
182
+ ```
183
+
184
+ ### Testing Callbacks
185
+
186
+ ```ruby
187
+ test "sets default title before save" do
188
+ card = Card.new(board: boards(:writebook), status: :published)
189
+ card.save!
190
+
191
+ assert_equal "Untitled", card.title
192
+ end
193
+
194
+ test "assigns number on create" do
195
+ card = Card.create!(board: boards(:writebook), title: "Test")
196
+
197
+ assert_not_nil card.number
198
+ assert card.number > 0
199
+ end
200
+
201
+ test "touches board after save" do
202
+ card = cards(:logo)
203
+ board = card.board
204
+
205
+ assert_changes -> { board.reload.updated_at } do
206
+ card.update!(title: "New Title")
207
+ end
208
+ end
209
+
210
+ test "callback only runs under condition" do
211
+ card = cards(:draft)
212
+
213
+ assert_no_changes -> { card.board.reload.updated_at } do
214
+ card.update!(title: "New Title") # Draft cards don't touch board
215
+ end
216
+ end
217
+ ```
218
+
219
+ ### Testing Instance Methods
220
+
221
+ ```ruby
222
+ test "closed? returns true when closure exists" do
223
+ card = cards(:logo)
224
+ assert_not card.closed?
225
+
226
+ card.close
227
+ assert card.closed?
228
+ end
229
+
230
+ test "move_to changes board and updates events" do
231
+ card = cards(:logo)
232
+ new_board = boards(:other_board)
233
+
234
+ card.move_to(new_board)
235
+
236
+ assert_equal new_board, card.reload.board
237
+ assert_equal new_board.id, card.events.pluck(:board_id).uniq.first
238
+ end
239
+
240
+ test "archive sets archived_at" do
241
+ card = cards(:logo)
242
+
243
+ freeze_time do
244
+ card.archive
245
+ assert_equal Time.current, card.archived_at
246
+ end
247
+ end
248
+ ```
249
+
250
+ ### Testing Transactions
251
+
252
+ ```ruby
253
+ test "transaction rolls back on error" do
254
+ card = cards(:logo)
255
+
256
+ assert_no_difference "Card::Closure.count" do
257
+ assert_raises(ActiveRecord::RecordInvalid) do
258
+ card.transaction do
259
+ card.create_closure!
260
+ raise ActiveRecord::RecordInvalid # Force rollback
261
+ end
262
+ end
263
+ end
264
+
265
+ assert_not card.closed?
266
+ end
267
+
268
+ test "transaction commits on success" do
269
+ card = cards(:logo)
270
+
271
+ assert_difference -> { card.events.count }, +1 do
272
+ card.close
273
+ end
274
+
275
+ assert card.closed?
276
+ end
277
+ ```
278
+
279
+ ### Testing Class Methods
280
+
281
+ ```ruby
282
+ test "close_all_stale closes inactive cards" do
283
+ old_card = cards(:logo)
284
+ old_card.update!(last_active_at: 2.months.ago)
285
+
286
+ recent_card = cards(:other)
287
+ recent_card.update!(last_active_at: 1.day.ago)
288
+
289
+ Card.close_all_stale
290
+
291
+ assert old_card.reload.closed?
292
+ assert_not recent_card.reload.closed?
293
+ end
294
+ ```
295
+
296
+ ---