plutonium 0.45.3 → 0.47.0

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +150 -0
  3. data/.claude/skills/plutonium-assets/SKILL.md +248 -157
  4. data/.claude/skills/{plutonium-rodauth → plutonium-auth}/SKILL.md +195 -229
  5. data/.claude/skills/plutonium-controller/SKILL.md +9 -2
  6. data/.claude/skills/plutonium-create-resource/SKILL.md +22 -1
  7. data/.claude/skills/plutonium-definition/SKILL.md +521 -7
  8. data/.claude/skills/plutonium-entity-scoping/SKILL.md +317 -0
  9. data/.claude/skills/plutonium-forms/SKILL.md +8 -1
  10. data/.claude/skills/plutonium-installation/SKILL.md +25 -2
  11. data/.claude/skills/plutonium-interaction/SKILL.md +32 -2
  12. data/.claude/skills/plutonium-invites/SKILL.md +11 -7
  13. data/.claude/skills/plutonium-model/SKILL.md +50 -50
  14. data/.claude/skills/plutonium-nested-resources/SKILL.md +18 -1
  15. data/.claude/skills/plutonium-package/SKILL.md +8 -1
  16. data/.claude/skills/plutonium-policy/SKILL.md +69 -78
  17. data/.claude/skills/plutonium-portal/SKILL.md +26 -70
  18. data/.claude/skills/plutonium-testing/SKILL.md +268 -0
  19. data/.claude/skills/plutonium-views/SKILL.md +9 -2
  20. data/.yarnrc.yml +1 -0
  21. data/CHANGELOG.md +38 -0
  22. data/app/assets/plutonium.css +1 -1
  23. data/app/views/rodauth/_login_form.html.erb +0 -3
  24. data/app/views/rodauth/confirm_password.html.erb +0 -4
  25. data/app/views/rodauth/create_account.html.erb +0 -3
  26. data/app/views/rodauth/logout.html.erb +0 -3
  27. data/docs/.vitepress/config.ts +6 -0
  28. data/docs/guides/nested-resources.md +10 -0
  29. data/docs/guides/testing.md +154 -0
  30. data/docs/reference/controller/index.md +9 -4
  31. data/docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md +481 -0
  32. data/docs/superpowers/plans/2026-04-14-plutonium-testing.md +2046 -0
  33. data/docs/superpowers/plans/2026-04-14-plutonium-testing.md.tasks.json +21 -0
  34. data/docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md +236 -0
  35. data/docs/superpowers/specs/2026-04-14-plutonium-testing-design.md +364 -0
  36. data/gemfiles/rails_7.gemfile.lock +1 -1
  37. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  38. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  39. data/lib/generators/pu/core/update/update_generator.rb +8 -0
  40. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +56 -0
  41. data/lib/generators/pu/invites/install_generator.rb +8 -1
  42. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +43 -0
  43. data/lib/generators/pu/profile/concerns/profile_arguments.rb +10 -4
  44. data/lib/generators/pu/profile/conn_generator.rb +9 -12
  45. data/lib/generators/pu/profile/install_generator.rb +5 -2
  46. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
  47. data/lib/generators/pu/saas/portal_generator.rb +4 -9
  48. data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +2 -2
  49. data/lib/generators/pu/test/install/install_generator.rb +34 -0
  50. data/lib/generators/pu/test/install/templates/plutonium_testing.rb.tt +14 -0
  51. data/lib/generators/pu/test/scaffold/scaffold_generator.rb +55 -0
  52. data/lib/generators/pu/test/scaffold/templates/integration_test.rb.tt +65 -0
  53. data/lib/plutonium/core/controller.rb +18 -1
  54. data/lib/plutonium/engine.rb +18 -5
  55. data/lib/plutonium/testing/auth_helpers.rb +62 -0
  56. data/lib/plutonium/testing/dsl.rb +73 -0
  57. data/lib/plutonium/testing/nested_resource.rb +58 -0
  58. data/lib/plutonium/testing/portal_access.rb +49 -0
  59. data/lib/plutonium/testing/resource_crud.rb +104 -0
  60. data/lib/plutonium/testing/resource_definition.rb +61 -0
  61. data/lib/plutonium/testing/resource_interaction.rb +51 -0
  62. data/lib/plutonium/testing/resource_model.rb +53 -0
  63. data/lib/plutonium/testing/resource_policy.rb +72 -0
  64. data/lib/plutonium/testing.rb +16 -0
  65. data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -1
  66. data/lib/plutonium/version.rb +1 -1
  67. data/lib/plutonium.rb +2 -0
  68. data/package.json +1 -1
  69. data/yarn.lock +6037 -3893
  70. metadata +27 -8
  71. data/.claude/skills/plutonium/skill.md +0 -130
  72. data/.claude/skills/plutonium-definition-actions/SKILL.md +0 -424
  73. data/.claude/skills/plutonium-definition-query/SKILL.md +0 -364
  74. data/.claude/skills/plutonium-profile/SKILL.md +0 -276
  75. data/.claude/skills/plutonium-theming/SKILL.md +0 -424
@@ -1,10 +1,31 @@
1
1
  ---
2
2
  name: plutonium-policy
3
- description: Use when configuring authorization - action permissions, attribute visibility, relation scoping, or per-portal policies
3
+ description: Use BEFORE writing relation_scope, permitted_attributes, permitted_associations, or any policy override. For tenant-scoped relation_scope, also load plutonium-entity-scoping.
4
4
  ---
5
5
 
6
6
  # Plutonium Policies
7
7
 
8
+ ## 🚨 Critical (read first)
9
+ - **Use generators.** `pu:res:scaffold` and `pu:res:conn` create policies — never hand-write policy files.
10
+ - **Never bypass `default_relation_scope`.** Overriding `relation_scope` with a raw `where(organization: …)` or manual joins skips entity scoping and triggers `verify_default_relation_scope_applied!` at runtime. Compose like this: `relation_scope { |r| default_relation_scope(r).where(archived: false) }`. Call `default_relation_scope(r)` **explicitly** — `super` is unreliable inside the DSL block. Full rules in `plutonium-entity-scoping`.
11
+ - **Derived actions inherit.** `update?` falls back to `create?`, `show?` falls back to `read?` — don't duplicate unless the rules genuinely differ. Override `create?` and `read?` explicitly; they default to `false`.
12
+ - **Define `permitted_attributes_for_*` explicitly.** Auto-detection works in development but raises in production.
13
+ - **For `has_cents` fields, list the virtual name (`:price`), not the column (`:price_cents`).** Generators occasionally emit the wrong one — fix it (and verify the model has `has_cents`). See `plutonium-model` › Monetary Handling.
14
+ - **Related skills:** `plutonium-entity-scoping` (tenant-scoped overrides — required for `relation_scope`), `plutonium-model` (`associated_with`), `plutonium-definition` (`permitted_attributes` usage), `plutonium-controller` (how controllers use policies).
15
+
16
+ ## Quick checklist
17
+
18
+ Writing / editing a policy:
19
+
20
+ 1. Confirm the policy was created by `pu:res:scaffold` or `pu:res:conn`.
21
+ 2. Override `create?` and `read?` explicitly — they default to `false`.
22
+ 3. Define `permitted_attributes_for_read` and `permitted_attributes_for_create` (derived methods inherit).
23
+ 4. For custom actions, add `def <action>?` matching the definition's `action :<action>`.
24
+ 5. If you need `relation_scope`, compose with `default_relation_scope(relation).where(...)` — never bypass it.
25
+ 6. For tenant scoping, load `plutonium-entity-scoping` and fix the **model**, not the policy.
26
+ 7. Per-portal overrides go in the portal's policy file (created by `pu:res:conn`).
27
+ 8. Test: log in as a user who should NOT see a record, verify it's filtered out.
28
+
8
29
  **Policies are generated automatically** - never create them manually:
9
30
  - `rails g pu:res:scaffold` creates the base policy
10
31
  - `rails g pu:res:conn` creates portal-specific policies with attribute permissions
@@ -153,119 +174,89 @@ def permitted_attributes_for_read
153
174
  end
154
175
  ```
155
176
 
156
- ### Auto-Detection (Development Only)
157
-
158
- In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly.
177
+ **Key insight:** `permitted_attributes_for_*` controls *which fields appear* on each view (form, show, index). The `column`/`field`/`input`/`display` declarations in the **definition** only control *how those fields render* — they do NOT add or remove fields from the page. If your index page is showing fields you didn't want, override `permitted_attributes_for_index` (it does NOT inherit from `_for_read` automatically when you want a different shape). The same applies to forms: a `field :name` in the definition won't be rendered unless `:name` is in `permitted_attributes_for_create`/`_update`.
159
178
 
160
- ## Association Permissions
161
-
162
- Control which associations can be rendered:
179
+ ### Anti-pattern: nested_attributes hashes in permitted_attributes
163
180
 
164
181
  ```ruby
165
- def permitted_associations
166
- %i[comments tags author]
182
+ # ❌ DO NOT DO THIS
183
+ def permitted_attributes_for_create
184
+ [
185
+ :name,
186
+ {variants_attributes: [:id, :name, :_destroy]},
187
+ {comments_attributes: [:id, :body, :_destroy]}
188
+ ]
167
189
  end
168
190
  ```
169
191
 
170
- Used for:
171
- - Nested forms
172
- - Related data displays
173
- - Association fields in tables
174
-
175
- ## Collection Scoping
192
+ Plutonium's form pipeline extracts nested params via the **form definition** (`build_form(...).extract_input(...)`), not the policy. Hash entries in `permitted_attributes_for_*` get iterated as field names by the form renderer and end up as literal text inputs with names like `model[{:variants_attributes=>[...]}]`.
176
193
 
177
- Filter which records users can see:
194
+ The correct pattern:
178
195
 
179
196
  ```ruby
180
- relation_scope do |relation|
181
- if user.admin?
182
- relation
183
- else
184
- relation.where(author: user)
185
- end
197
+ # Policy permits just the association name
198
+ def permitted_attributes_for_create
199
+ [:name, :variants, :comments]
186
200
  end
187
201
  ```
188
202
 
189
- ### With Parent Scoping (Nested Resources)
190
-
191
- For nested resources, call `super` to apply automatic parent scoping:
192
-
193
203
  ```ruby
194
- relation_scope do |relation|
195
- relation = super(relation) # Applies parent scoping automatically
196
-
197
- if user.admin?
198
- relation
199
- else
200
- relation.where(published: true)
204
+ # Definition declares the nested input (this drives both rendering AND param extraction)
205
+ class PostDefinition < ResourceDefinition
206
+ nested_input :variants do |n|
207
+ n.input :name
208
+ n.input :is_default, as: :boolean
201
209
  end
202
210
  end
203
211
  ```
204
212
 
205
- **Parent scoping takes precedence over entity scoping.** When a parent is present:
206
- - For `has_many`: scopes via `parent.association_name`
207
- - For `has_one`: scopes via `where(foreign_key: parent.id)`
208
-
209
- ### With Entity Scoping (Multi-tenancy)
210
-
211
- When no parent is present, `super` applies entity scoping:
212
-
213
213
  ```ruby
214
- relation_scope do |relation|
215
- relation = super(relation) # Apply entity scope if no parent
214
+ # Model declares accepts_nested_attributes_for + inverse_of on the back-reference
215
+ class Post < ApplicationRecord
216
+ has_many :variants, inverse_of: :post, dependent: :destroy
217
+ accepts_nested_attributes_for :variants, allow_destroy: true, reject_if: :all_blank
218
+ end
216
219
 
217
- if user.admin?
218
- relation
219
- else
220
- relation.where(published: true)
221
- end
220
+ class Variant < ApplicationRecord
221
+ belongs_to :post, inverse_of: :variants # ← required for nested validation
222
222
  end
223
223
  ```
224
224
 
225
- ### default_relation_scope is Required
226
-
227
- Plutonium verifies that `default_relation_scope` is called in every `relation_scope`. This prevents accidental multi-tenancy leaks when overriding scopes.
225
+ See `plutonium-definition` for the full `nested_input` API.
228
226
 
229
- ```ruby
230
- # ❌ This will raise an error
231
- relation_scope do |relation|
232
- relation.where(published: true) # Missing default_relation_scope!
233
- end
227
+ ### Auto-Detection (Development Only)
234
228
 
235
- # Correct - call default_relation_scope
236
- relation_scope do |relation|
237
- default_relation_scope(relation).where(published: true)
238
- end
229
+ In development, undefined attribute methods auto-detect from the model. This raises errors in production - always define explicitly.
239
230
 
240
- # Also correct - super calls default_relation_scope
241
- relation_scope do |relation|
242
- super(relation).where(published: true)
243
- end
244
- ```
231
+ ## Association Permissions
245
232
 
246
- When overriding an inherited scope:
233
+ Control which associations can be rendered:
247
234
 
248
235
  ```ruby
249
- class AdminPostPolicy < PostPolicy
250
- relation_scope do |relation|
251
- # Replace inherited scope but keep Plutonium's parent/entity scoping
252
- default_relation_scope(relation)
253
- end
236
+ def permitted_associations
237
+ %i[comments tags author]
254
238
  end
255
239
  ```
256
240
 
257
- ### Skipping Default Scoping (Rare)
241
+ Used for:
242
+ - Nested forms
243
+ - Related data displays
244
+ - Association fields in tables
258
245
 
259
- If you intentionally need to skip scoping, call `skip_default_relation_scope!`:
246
+ ## Collection Scoping (relation_scope)
247
+
248
+ Filter which records users can see:
260
249
 
261
250
  ```ruby
262
251
  relation_scope do |relation|
263
- skip_default_relation_scope!
264
- relation # No parent/entity scoping applied
252
+ relation = default_relation_scope(relation)
253
+ user.admin? ? relation : relation.where(author: user)
265
254
  end
266
255
  ```
267
256
 
268
- Consider using a separate portal instead of skipping scoping.
257
+ **Always compose with `default_relation_scope(relation)` explicitly** — not `super`. Plutonium enforces this via `verify_default_relation_scope_applied!`. Anything else (a raw `where(organization: ...)`, manual joins) bypasses Plutonium's tenancy handling and will raise.
258
+
259
+ > **For the full rules — why `default_relation_scope` is required, how parent vs entity scoping interact, safe override patterns, `skip_default_relation_scope!`, and how `associated_with` resolution works — see the [plutonium-entity-scoping](../plutonium-entity-scoping/SKILL.md) skill. It is the single source of truth for Plutonium tenant scoping.**
269
260
 
270
261
  ## Portal-Specific Policies
271
262
 
@@ -453,7 +444,7 @@ end
453
444
 
454
445
  1. **Always override `create?` and `read?`** - They default to `false`
455
446
  2. **Define attributes explicitly** - Auto-detection only works in development
456
- 3. **Call `super` in `relation_scope`** - Preserves entity scoping
447
+ 3. **Call `default_relation_scope(relation)` in `relation_scope`** - Preserves parent/entity scoping (do not rely on `super` from inside the block)
457
448
  4. **Use derived methods** - Let `update?` inherit from `create?` when appropriate
458
449
  5. **Keep policies focused** - Authorization logic only, no business logic
459
450
  6. **Test edge cases** - Archived records, nil associations, role combinations
@@ -461,5 +452,5 @@ end
461
452
  ## Related Skills
462
453
 
463
454
  - `plutonium` - How policies fit in the resource architecture
464
- - `plutonium-definition-actions` - Actions that need policy methods
455
+ - `plutonium-definition` - Actions that need policy methods
465
456
  - `plutonium-controller` - How controllers use policies
@@ -1,12 +1,32 @@
1
1
  ---
2
2
  name: plutonium-portal
3
- description: Use when creating portals, connecting resources to portals, configuring authentication, entity scoping, or portal routes
3
+ description: Use BEFORE creating a portal, mounting a portal engine, running pu:pkg:portal, configuring entity strategies, or routing portal-specific resources. For tenancy mechanics, also load plutonium-entity-scoping.
4
4
  ---
5
5
 
6
6
  # Plutonium Portals
7
7
 
8
+ ## 🚨 Critical (read first)
9
+ - **Use `pu:pkg:portal`.** Never hand-craft a portal engine — the generator wires up the controller concerns, routes, and layout.
10
+ - **Always use `pu:res:conn` to connect resources to portals** — resources are invisible until connected. Pass resources directly (not via `--src`) to skip prompts.
11
+ - **Entity scoping is portal-level.** `scope_to_entity Entity, strategy: :path` in the portal engine; then every resource in that portal is scoped automatically. For mechanics, see `plutonium-entity-scoping`.
12
+ - **Pass `--auth=<account>` / `--public` / `--byo`** to `pu:pkg:portal` for unattended runs.
13
+ - **Related skills:** `plutonium-entity-scoping` (tenancy mechanics), `plutonium-auth` (Rodauth integration), `plutonium-package` (portal vs feature packages), `plutonium-policy` (portal-specific policies).
14
+
8
15
  Portals are Rails engines that provide web interfaces for specific user types.
9
16
 
17
+ ## Quick checklist
18
+
19
+ Creating a portal and connecting resources:
20
+
21
+ 1. Run `rails g pu:pkg:portal <name> --auth=<account>` (or `--public` / `--byo`). Add `--scope=Entity` for multi-tenancy.
22
+ 2. Mount the engine in `config/routes.rb`: `mount <Name>Portal::Engine, at: "/<name>"`.
23
+ 3. For each resource, run `rails g pu:res:conn ResourceName --dest=<name>_portal`.
24
+ 4. For singular resources (profile, settings), pass `--singular`.
25
+ 5. Customize the portal's `Concerns::Controller` for auth / before_action hooks.
26
+ 6. Override portal-specific policies/definitions as needed.
27
+ 7. Verify: `bin/rails routes | grep <name>_portal`.
28
+ 8. For multi-tenancy specifics, load `plutonium-entity-scoping`.
29
+
10
30
  ## Creating a Portal
11
31
 
12
32
  ```bash
@@ -181,11 +201,7 @@ end
181
201
 
182
202
  ## Entity Scoping (Multi-tenancy)
183
203
 
184
- Automatically scope all data to a parent entity.
185
-
186
- ### Path Strategy
187
-
188
- Entity ID in URL path:
204
+ Portals can scope all data to a parent entity via `scope_to_entity`:
189
205
 
190
206
  ```ruby
191
207
  module AdminPortal
@@ -199,71 +215,11 @@ module AdminPortal
199
215
  end
200
216
  ```
201
217
 
202
- Routes become: `/organizations/:organization_id/posts`
203
-
204
- ### Custom Strategy
205
-
206
- Implement your own lookup method:
207
-
208
- ```ruby
209
- module AdminPortal
210
- class Engine < Rails::Engine
211
- include Plutonium::Portal::Engine
218
+ Strategies: `:path` (entity id in URL) or a custom method name on the portal controller concern.
212
219
 
213
- config.after_initialize do
214
- scope_to_entity Organization, strategy: :current_organization
215
- end
216
- end
217
- end
220
+ Access in controllers/views: `current_scoped_entity`, `scoped_to_entity?`. In policies: `entity_scope`.
218
221
 
219
- # In controller concern
220
- module AdminPortal
221
- module Concerns
222
- module Controller
223
- extend ActiveSupport::Concern
224
- include Plutonium::Portal::Controller
225
-
226
- private
227
-
228
- # Method name must match strategy
229
- def current_organization
230
- @current_organization ||= Organization.find_by!(subdomain: request.subdomain)
231
- end
232
- end
233
- end
234
- end
235
- ```
236
-
237
- ### Accessing the Scoped Entity
238
-
239
- ```ruby
240
- current_scoped_entity # The current Organization/Account/etc.
241
- scoped_to_entity? # true if scoping is active
242
- ```
243
-
244
- ### Model Requirements
245
-
246
- Models must have an association path to the scoped entity:
247
-
248
- ```ruby
249
- # Direct association (preferred)
250
- class Post < ResourceRecord
251
- belongs_to :organization
252
- end
253
-
254
- # Through association
255
- class Comment < ResourceRecord
256
- belongs_to :post
257
- has_one :organization, through: :post
258
- end
259
-
260
- # Complex (define custom scope)
261
- class AuditLog < ResourceRecord
262
- scope :associated_with_organization, ->(org) {
263
- joins(:user).where(users: { organization_id: org.id })
264
- }
265
- end
266
- ```
222
+ > **For the full entity scoping picture — the three model shapes, `associated_with` resolution, `default_relation_scope` rules, safe `relation_scope` overrides, and how parent scoping takes precedence — see the [plutonium-entity-scoping](../plutonium-entity-scoping/SKILL.md) skill. It is the single source of truth.**
267
223
 
268
224
  ## Routes
269
225
 
@@ -448,7 +404,7 @@ rails g pu:res:conn Post --dest=admin_portal
448
404
  ## Related Skills
449
405
 
450
406
  - `plutonium-package` - Package overview (features vs portals)
451
- - `plutonium-rodauth` - Authentication setup and configuration
407
+ - `plutonium-auth` - Authentication setup and configuration
452
408
  - `plutonium-policy` - Portal-specific policies
453
409
  - `plutonium-definition` - Portal-specific definitions
454
410
  - `plutonium-controller` - Portal-specific controllers
@@ -0,0 +1,268 @@
1
+ ---
2
+ name: plutonium-testing
3
+ description: Use BEFORE writing tests for a Plutonium resource, running pu:test:scaffold, or including Plutonium::Testing::* concerns. Covers the full testing toolkit — CRUD, policy, definition, interaction, model, nested, portal access, and auth helpers.
4
+ ---
5
+
6
+ # Plutonium Testing
7
+
8
+ ## 🚨 Critical (read first)
9
+
10
+ - **Use the generators.** `pu:test:install` once per app, then `pu:test:scaffold ResourceClass --portals=...` per resource × portal. Hand-written test files drift from conventions.
11
+ - **Tests are opt-in.** `Plutonium::Testing` is only loaded when `require "plutonium/testing"` runs — it's never autoloaded, never present in production.
12
+ - **One file per (resource × portal).** Same model in admin and org portals = two test files. Each portal has different auth, scoping, and allowed actions.
13
+ - **Stub methods are required.** Concerns ship with `NotImplementedError` stubs — your test class supplies the test data via `create_resource!`, `valid_create_params`, `policy_roles`, etc.
14
+
15
+ ## Quick start
16
+
17
+ ```bash
18
+ # Once per app
19
+ rails g pu:test:install
20
+
21
+ # Per resource × portal pairing
22
+ rails g pu:test:scaffold Blogging::Post --portals=admin,org
23
+
24
+ # Run
25
+ bin/rails test
26
+ ```
27
+
28
+ `pu:test:install` adds `require "plutonium/testing"` to `test/test_helper.rb` and creates `test/support/plutonium_testing.rb` (a stub for non-Rodauth auth overrides).
29
+
30
+ ## DSL reference
31
+
32
+ Every concern uses the same class-level DSL:
33
+
34
+ ```ruby
35
+ resource_tests_for ResourceClass,
36
+ portal: :admin, # required
37
+ path_prefix: "/admin", # optional override
38
+ parent: :organization, # for nested resources
39
+ actions: %i[index show new create edit update destroy],
40
+ skip: %i[destroy],
41
+ associated_with: :organization, # ResourceModel only
42
+ sgid_routing: true, # ResourceModel only
43
+ has_cents: %i[price] # ResourceModel only
44
+ ```
45
+
46
+ The **portal symbol** drives:
47
+
48
+ | Derived | `:admin` example | `:org` example |
49
+ |---|---|---|
50
+ | `path_prefix` | `/admin` | `/org` |
51
+ | Default sign-in helper | admin Rodauth | user Rodauth |
52
+ | Allowed action set | from definition | from definition |
53
+
54
+ `path_prefix` is auto-resolved from the mounted portal engine. For mounts inside `constraints` (typical Plutonium setup), the resolver walks the route tree and finds the engine.
55
+
56
+ ## Concerns catalog
57
+
58
+ Each concern is `include`d separately. Pick the ones you need.
59
+
60
+ ### `Plutonium::Testing::ResourceCrud`
61
+
62
+ Generates index / show / new / create / edit / update / destroy integration tests against the portal-mounted resource.
63
+
64
+ **Stubs:**
65
+ - `create_resource!` → persisted record
66
+ - `valid_create_params` → Hash for POST
67
+ - `valid_update_params` → Hash for PATCH
68
+
69
+ ```ruby
70
+ class AdminPortal::BloggingPostsTest < ActionDispatch::IntegrationTest
71
+ include IntegrationTestHelper
72
+ include Plutonium::Testing::ResourceCrud
73
+
74
+ resource_tests_for Blogging::Post, portal: :admin
75
+
76
+ setup do
77
+ @admin = create_admin!
78
+ @user = create_user!
79
+ @org = create_organization!
80
+ login_as(@admin)
81
+ end
82
+
83
+ def create_resource! = create_post!(user: @user, organization: @org)
84
+ def valid_create_params
85
+ {title: "x", body: "y", status: :draft, user: @user.to_sgid.to_s, organization: @org.to_sgid.to_s}
86
+ end
87
+ def valid_update_params = {title: "Updated"}
88
+ end
89
+ ```
90
+
91
+ ### `Plutonium::Testing::ResourcePolicy`
92
+
93
+ Asserts the `permit?` matrix across action × role and verifies `relation_scope` returns an `ActiveRecord::Relation`.
94
+
95
+ **Stubs:**
96
+ - `policy_roles` → `{role_sym => -> { account }}`
97
+ - `policy_record` → persisted record under test
98
+ - `policy_matrix` → `{action_sym => [allowed_role_syms]}`
99
+ - `policy_context` (optional) → extra kwargs (defaults to `{entity_scope: nil}`)
100
+
101
+ ```ruby
102
+ def policy_roles = {admin: -> { @admin }, member: -> { @user }}
103
+ def policy_record = create_post!(user: @user, organization: @org)
104
+ def policy_matrix = {
105
+ index: %i[admin member], show: %i[admin member],
106
+ create: %i[admin], update: %i[admin], destroy: %i[admin]
107
+ }
108
+ ```
109
+
110
+ ### `Plutonium::Testing::ResourceDefinition`
111
+
112
+ Smoke-tests the resource definition: the class is constantize-able, every defineable prop dictionary (fields/inputs/displays/columns/scopes/filters/sorts/actions) is queryable, and declared fields exist on the model.
113
+
114
+ **No stubs required** for the happy path.
115
+
116
+ ### `Plutonium::Testing::ResourceInteraction`
117
+
118
+ Outcome-assertion helpers for `Plutonium::Interaction::Base` subclasses.
119
+
120
+ **Helpers:**
121
+ - `assert_interaction_success(klass, **input)` → returns the success outcome
122
+ - `assert_interaction_failure(klass, **input)` → returns the failure outcome
123
+ - `interaction_view_context` (overridable) → defaults to a mock view context
124
+
125
+ ```ruby
126
+ test "RebuildSearchInteraction succeeds" do
127
+ outcome = assert_interaction_success(RebuildSearchInteraction, since: 1.day.ago)
128
+ assert_equal 42, outcome.value[:rebuilt_count]
129
+ end
130
+ ```
131
+
132
+ ### `Plutonium::Testing::ResourceModel`
133
+
134
+ Tests `associated_with` scope, SGID routing, and `has_cents` accessors — gated by DSL flags.
135
+
136
+ **Stubs:**
137
+ - `model_test_record` → persisted record
138
+
139
+ ```ruby
140
+ resource_tests_for Catalog::Product, portal: :admin,
141
+ associated_with: :organization,
142
+ sgid_routing: true,
143
+ has_cents: %i[price]
144
+
145
+ def model_test_record = create_product!(user: @user, organization: @org)
146
+ ```
147
+
148
+ Only the flagged features generate tests.
149
+
150
+ ### `Plutonium::Testing::NestedResource`
151
+
152
+ Asserts CRUD under a parent + scope-boundary tests (sibling tenants invisible).
153
+
154
+ **Stubs:**
155
+ - `parent_record!` → current tenant
156
+ - `other_parent_record!` → sibling tenant
157
+ - `create_resource!(parent:)` → persisted record under given parent
158
+
159
+ ### `Plutonium::Testing::PortalAccess`
160
+
161
+ Cross-portal access boundaries. Uses its own DSL — not `resource_tests_for`.
162
+
163
+ ```ruby
164
+ class PortalAccessTest < ActionDispatch::IntegrationTest
165
+ include IntegrationTestHelper
166
+ include Plutonium::Testing::PortalAccess
167
+
168
+ portal_access_for portals: %i[admin org],
169
+ matrix: {admin: %i[admin], member: %i[org]}
170
+
171
+ setup do
172
+ @admin = create_admin!
173
+ @user = create_user!
174
+ @org = create_organization!
175
+ create_membership!(organization: @org, user: @user)
176
+ end
177
+
178
+ def login_as_role(role)
179
+ case role
180
+ when :admin then login_as(@admin, portal: :admin)
181
+ when :member then login_as(@user, portal: :user)
182
+ end
183
+ end
184
+
185
+ def portal_root_path(portal)
186
+ case portal
187
+ when :admin then "/admin"
188
+ when :org then "/org/#{@org.id}"
189
+ end
190
+ end
191
+ end
192
+ ```
193
+
194
+ Generates one test per (role × portal). Allowed = `200|302`; blocked = `302|401|403|404`.
195
+
196
+ ## Auth helpers
197
+
198
+ `Plutonium::Testing::AuthHelpers` is included transitively by every concern.
199
+
200
+ ```ruby
201
+ login_as(account) # uses portal from DSL
202
+ login_as(account, portal: :admin) # explicit override
203
+ sign_out # uses portal from DSL
204
+ sign_out(portal: :admin)
205
+ current_account # uses portal from DSL
206
+ current_account(portal: :admin)
207
+ with_portal(:org) { ... } # scoped portal switch
208
+ ```
209
+
210
+ **Override hook for non-Rodauth apps:** define `sign_in_for_tests(account, portal:)` in your test class (or in `test/support/plutonium_testing.rb` for project-wide use). `AuthHelpers` will defer to it.
211
+
212
+ ```ruby
213
+ def sign_in_for_tests(account, portal:)
214
+ # your custom auth flow here
215
+ end
216
+ ```
217
+
218
+ ## Generator reference
219
+
220
+ ### `pu:test:install`
221
+
222
+ ```bash
223
+ rails g pu:test:install
224
+ ```
225
+
226
+ - Adds `require "plutonium/testing"` to `test/test_helper.rb` (idempotent)
227
+ - Creates `test/support/plutonium_testing.rb` with override stub
228
+
229
+ ### `pu:test:scaffold`
230
+
231
+ ```bash
232
+ rails g pu:test:scaffold Blogging::Post --portals=admin,org
233
+ rails g pu:test:scaffold Blogging::Post --portals=admin --concerns=crud,policy,definition
234
+ rails g pu:test:scaffold Blogging::Post --portals=org --parent=organization --dest=blogging
235
+ ```
236
+
237
+ | Flag | Default | Purpose |
238
+ |---|---|---|
239
+ | `--portals=admin,org` | required | Emit one file per portal |
240
+ | `--concerns=...` | `crud,policy,definition` | Concerns to include (`crud,policy,definition,nested,model,interaction,portal_access`) |
241
+ | `--parent=organization` | none | Wires `NestedResource` parent |
242
+ | `--dest=main_app\|<package>` | `main_app` | Output destination |
243
+
244
+ Output path: `test/integration/<portal>_portal/<resource_underscored>_test.rb`.
245
+
246
+ ## Customization & escape hatches
247
+
248
+ - **Skip individual tests:** `resource_tests_for Klass, portal: :admin, skip: %i[destroy]`
249
+ - **Restrict action set:** `resource_tests_for Klass, portal: :admin, actions: %i[index show]`
250
+ - **Custom assertions:** add regular `test "..."` blocks alongside the generated matrix — they coexist.
251
+ - **Non-Rodauth auth:** override `sign_in_for_tests`. See AuthHelpers section.
252
+ - **Custom path prefix:** `path_prefix: "/v2/admin"` overrides portal resolution.
253
+
254
+ ## Common pitfalls
255
+
256
+ - **Forgotten stubs raise `NotImplementedError`** with the stub name. Look for the missing method in your test class.
257
+ - **Portal mismatch:** `:admin` portal expects `AdminPortal::Engine` constant. If your portal is named differently, pass `path_prefix:` explicitly.
258
+ - **Tenant leakage in stubs:** `create_resource!` for an org portal must return a record bound to the test's `@org`. Otherwise scope filtering tests will pass for the wrong reason.
259
+ - **`policy_record` for tenant-scoped resources** must belong to a tenant the role has access to — otherwise even allowed roles will see `false`.
260
+ - **Nested resources need `parent: :foo`** in the DSL AND a real parent record from `parent_record!`. Without both, path interpolation fails.
261
+ - **`PortalAccess` doesn't use `resource_tests_for`** — use `portal_access_for` instead. Mixing them on the same class is undefined behavior.
262
+
263
+ ## See also
264
+
265
+ - `plutonium-policy` — write the policy this concern verifies
266
+ - `plutonium-definition` — definition props the smoke test introspects
267
+ - `plutonium-portal` — portal mounting and entity strategies that drive auth/scoping
268
+ - `plutonium-auth` — Rodauth setup behind the default login flow
@@ -1,10 +1,17 @@
1
1
  ---
2
2
  name: plutonium-views
3
- description: Use when building custom pages, display panels, tables, or layouts using Phlex components in Plutonium
3
+ description: Use BEFORE building a custom page, panel, table, layout, or Phlex component in Plutonium. Also when overriding IndexPage/ShowPage/Form classes in a definition.
4
4
  ---
5
5
 
6
6
  # Plutonium Views
7
7
 
8
+ ## 🚨 Critical (read first)
9
+ - **Override via nested classes in the definition.** `class ShowPage < ShowPage; end`, `class Form < Form; end` — don't replace the entire view layer.
10
+ - **Use the render hooks.** `render_before_content`, `render_after_content`, `render_before_toolbar`, etc. — they exist so you don't have to override `view_template` and re-implement everything.
11
+ - **All pages inherit `DynaFrameContent`** so turbo-frame requests render only the content. Don't fight it — modals and frame nav "just work".
12
+ - **For custom components, use `Plutonium::UI::Component::Base`** so you inherit the component kit (`PageHeader`, `Panel`, `Block`, etc.) and access to resource helpers.
13
+ - **Related skills:** `plutonium-forms` (form customization), `plutonium-assets` (theming + component classes), `plutonium-definition` (field-level rendering), `plutonium-controller` (presentation hooks like `present_parent?`).
14
+
8
15
  Plutonium uses [Phlex](https://www.phlex.fun/) for all view components. This provides a Ruby-first approach to building HTML with full IDE support and type safety.
9
16
 
10
17
  ## Architecture Overview
@@ -580,6 +587,6 @@ end
580
587
  - `plutonium-forms` - Custom form templates and field builders
581
588
  - `plutonium-assets` - TailwindCSS and component theming
582
589
  - `plutonium-definition` - Field/input/display configuration
583
- - `plutonium-definition-actions` - Action buttons and interactions
590
+ - `plutonium-definition` - Action buttons and interactions
584
591
  - `plutonium-controller` - Presentation hooks (`present_parent?`, etc.)
585
592
  - `plutonium-portal` - Portal-specific customization
data/.yarnrc.yml ADDED
@@ -0,0 +1 @@
1
+ nodeLinker: node-modules
data/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
1
+ ## [0.47.0] - 2026-04-15
2
+
3
+ ### 🚀 Features
4
+
5
+ - *(core)* Add `interaction:` kwarg to resource_url_for
6
+ - *(testing)* Add Plutonium::Testing module, generators, skill, docs, and migrate in-repo tests
7
+
8
+ ### ⚙️ Miscellaneous Tasks
9
+
10
+ - Update yarn
11
+ ## [0.46.0] - 2026-04-11
12
+
13
+ ### 🚀 Features
14
+
15
+ - *(profile)* Default profile model to {UserModel}Profile
16
+ - *(generators)* Sync skills during pu:core:update if plutonium skill is installed
17
+ - *(generators/active_shrine)* Disable Active Storage railtie and include ActiveShrine::Model
18
+
19
+ ### 🐛 Bug Fixes
20
+
21
+ - *(engine)* Resolve scoped entity class lazily to survive autoreload
22
+ - *(rodauth)* Render page title in layout, drop per-view h1s
23
+ - *(generators/invites)* Use derived user association in current_membership
24
+ - *(generators/rodauth)* Redirect to login after verification email sent
25
+
26
+ ### 🚜 Refactor
27
+
28
+ - *(generators)* Add inject_into_concerns_controller to merge included blocks
29
+
30
+ ### 📚 Documentation
31
+
32
+ - *(skills)* Clarify generator gotchas for installation, rodauth, and unattended runs
33
+ - *(skills)* Comprehensive Plutonium skills overhaul
34
+ - *(skills)* Document nested_attributes gotchas in policy and definition
35
+
36
+ ### ⚙️ Miscellaneous Tasks
37
+
38
+ - Update test lockfiles
1
39
  ## [0.45.3] - 2026-04-07
2
40
 
3
41
  ### 🐛 Bug Fixes