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
@@ -0,0 +1,21 @@
1
+ {
2
+ "planPath": "docs/superpowers/plans/2026-04-14-plutonium-testing.md",
3
+ "tasks": [
4
+ {"id": 1, "subject": "Create Plutonium::Testing module skeleton and autoload entry", "status": "pending", "planTaskNumber": 1},
5
+ {"id": 2, "subject": "Implement shared DSL and portal resolution", "status": "pending", "blockedBy": [1], "planTaskNumber": 2},
6
+ {"id": 10, "subject": "Implement portal-scoped AuthHelpers", "status": "pending", "blockedBy": [2], "planTaskNumber": 3},
7
+ {"id": 3, "subject": "Implement ResourceCrud concern", "status": "pending", "blockedBy": [2, 10], "planTaskNumber": 4},
8
+ {"id": 4, "subject": "Implement ResourcePolicy concern", "status": "pending", "blockedBy": [2], "planTaskNumber": 5},
9
+ {"id": 5, "subject": "Implement ResourceDefinition concern", "status": "pending", "blockedBy": [2, 3], "planTaskNumber": 6},
10
+ {"id": 6, "subject": "Implement ResourceInteraction concern", "status": "pending", "blockedBy": [1], "planTaskNumber": 7},
11
+ {"id": 7, "subject": "Implement ResourceModel concern", "status": "pending", "blockedBy": [2], "planTaskNumber": 8},
12
+ {"id": 8, "subject": "Implement NestedResource concern", "status": "pending", "blockedBy": [2, 10], "planTaskNumber": 9},
13
+ {"id": 9, "subject": "Implement PortalAccess concern", "status": "pending", "blockedBy": [10], "planTaskNumber": 10},
14
+ {"id": 11, "subject": "Implement pu:test:install generator", "status": "pending", "blockedBy": [1], "planTaskNumber": 11},
15
+ {"id": 12, "subject": "Implement pu:test:scaffold generator", "status": "pending", "blockedBy": [1], "planTaskNumber": 12},
16
+ {"id": 14, "subject": "Write plutonium-testing skill documentation", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "planTaskNumber": 13},
17
+ {"id": 15, "subject": "Add VitePress docs guide for testing", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "planTaskNumber": 14},
18
+ {"id": 13, "subject": "Migrate in-repo shared_tests to use Plutonium::Testing concerns", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "planTaskNumber": 15}
19
+ ],
20
+ "lastUpdated": "2026-04-14T00:00:00Z"
21
+ }
@@ -0,0 +1,236 @@
1
+ # Plutonium Skills Overhaul — Design
2
+
3
+ **Date:** 2026-04-08
4
+ **Status:** Approved (pending user review of this doc)
5
+
6
+ ## Problem
7
+
8
+ The current Plutonium skills (`.claude/skills/plutonium-*`) are content-correct but
9
+ fail to trigger at the right moments. Concrete failure modes observed in real
10
+ sessions:
11
+
12
+ 1. Agents write `relation_scope` overrides without ever loading `plutonium-policy`,
13
+ bypassing `default_relation_scope` and breaking entity scoping.
14
+ 2. Agents hand-write resource files instead of using `pu:res:scaffold`.
15
+ 3. Agents miss cross-cutting concerns (multi-tenancy spans 4 skills; nobody reads 4).
16
+ 4. Greenfield onboarding is one-skill-at-a-time as the agent stumbles into mistakes.
17
+ 5. The definition concept is split across 3 skills — agents read one and miss the others.
18
+
19
+ Root cause: skill `description:` fields list **topics** ("authorization, attribute
20
+ visibility, relation scoping..."). Agents triage descriptions against **moments**
21
+ ("I am about to write `relation_scope`"). Topic-based descriptions don't fire.
22
+
23
+ ## Goals
24
+
25
+ 1. Skills get invoked at the *moment* an agent is about to make a relevant decision.
26
+ 2. The most expensive mistakes live in a fixed 🚨 block at the top of the relevant skill.
27
+ 3. Cross-cutting concerns (entity scoping especially) are consolidated and discoverable.
28
+ 4. **Generators are the default path.** Every skill that touches a generator-backed
29
+ concern leads with "use `pu:...`, do not hand-write."
30
+ 5. **Greenfield onboarding works.** When an agent starts new work, it loads a bundle
31
+ of foundational skills upfront via the index skill.
32
+ 6. The index skill (`plutonium`) acts as a router AND a greenfield bootstrapper.
33
+ 7. Critical workflows are checklists; explanation is prose.
34
+
35
+ ## Non-goals
36
+
37
+ - Not rewriting prose content wholesale — content is mostly correct.
38
+ - No telemetry/measurement infrastructure.
39
+ - No new code examples beyond what's needed for the 🚨 blocks.
40
+ - No skill renames beyond `plutonium-rodauth → plutonium-auth`.
41
+
42
+ ## Final skill set (20 → 17)
43
+
44
+ ### Merges
45
+
46
+ | New skill | Absorbs | Rationale |
47
+ |---|---|---|
48
+ | `plutonium-definition` | + `plutonium-definition-actions`, `plutonium-definition-query` | Agents writing a definition need all three; today's split causes "read one, miss two" |
49
+ | `plutonium-auth` (renamed from `plutonium-rodauth`) | + `plutonium-profile` | Profile is a thin layer over rodauth, almost never edited independently |
50
+ | `plutonium-assets` | + `plutonium-theming` | Both are "configure the frontend toolchain" — Tailwind + Stimulus + tokens are one mental model |
51
+
52
+ ### New skill
53
+
54
+ | Skill | Purpose |
55
+ |---|---|
56
+ | `plutonium-entity-scoping` | Consolidates the entity-scoping/multi-tenancy-specific content currently fragmented across `plutonium-model`, `plutonium-policy`, `plutonium-portal`, and `plutonium-invites`. The single source of truth for: `associated_with` resolution, `default_relation_scope` rules, `relation_scope` override safety, entity strategies (path/custom), and the join-table/grandchild model shapes. |
57
+
58
+ The four source skills retain their general content but defer to
59
+ `plutonium-entity-scoping` for tenancy specifics via cross-reference.
60
+
61
+ ### Stays separate
62
+
63
+ `plutonium`, `plutonium-installation`, `plutonium-create-resource`, `plutonium-model`,
64
+ `plutonium-policy`, `plutonium-controller`, `plutonium-interaction`, `plutonium-portal`,
65
+ `plutonium-package`, `plutonium-nested-resources`, `plutonium-invites`,
66
+ `plutonium-forms`, `plutonium-views`.
67
+
68
+ **Final count: 17 skills** (20 − 3 merges + 1 new − 1 rename effect = 17).
69
+
70
+ ## Skill template
71
+
72
+ Every skill follows this fixed shape so agents always know where to look:
73
+
74
+ ```markdown
75
+ ---
76
+ name: plutonium-<topic>
77
+ description: Use BEFORE <specific verb/construct>. Also when <secondary trigger>. <one-line scope>.
78
+ ---
79
+
80
+ # plutonium-<topic>
81
+
82
+ ## 🚨 Critical (read first)
83
+ - **Use generators, not hand-written files.** `pu:<gen>` — never create <X> manually.
84
+ - **<Top anti-pattern #1>** — one-liner + why.
85
+ - **<Top anti-pattern #2>** — one-liner + why.
86
+ - **Related skills you may also need:** [list with one-line reasons]
87
+
88
+ ## When to use this skill
89
+ Checklist of decision points / code constructs that should trigger loading this skill.
90
+
91
+ ## Quick checklist (bootstrap + high-traffic skills only)
92
+ Numbered checklist for the most common workflow. Agent can TaskCreate from this.
93
+
94
+ ## <Sections — scaled to topic>
95
+ Prose + code examples.
96
+
97
+ ## Gotchas
98
+ The full anti-pattern list with explanations.
99
+
100
+ ## See also
101
+ Cross-references to related skills.
102
+ ```
103
+
104
+ ### Template rules
105
+
106
+ - **Description starts with `Use BEFORE <verb/construct>`** — verbs and code names,
107
+ not topic nouns. This is the single biggest triggering fix.
108
+ - **🚨 Critical block is fixed-position** (always right after the H1) and capped at
109
+ ~5 bullets.
110
+ - **Generator-first** is in the 🚨 block of every skill where a generator exists.
111
+ - **Cross-references in 🚨 are mandatory** — the "Related skills" bullet replaces
112
+ most of the multi-tenancy discoverability problem.
113
+ - **TOC at top of merged skills** so agents can jump to a section without reading
114
+ the whole file.
115
+
116
+ ## The index skill (`plutonium`) as router + bootstrapper
117
+
118
+ ```markdown
119
+ ---
120
+ name: plutonium
121
+ description: Use BEFORE starting any Plutonium work — new app, new feature, or first edit in an unfamiliar area. Routes you to the right skills and bootstraps greenfield work.
122
+ ---
123
+
124
+ # plutonium
125
+
126
+ ## 🚨 Read this first
127
+ - Plutonium is generator-driven. Almost every file you'd hand-write has a `pu:*`
128
+ generator. Use it. Hand-written files drift from conventions.
129
+ - For greenfield (new app or substantial new feature), load the **bootstrap bundle**
130
+ below before writing any code.
131
+ - For targeted edits, use the **router table**.
132
+ - For anything touching tenant scoping, load `plutonium-entity-scoping`.
133
+
134
+ ## Greenfield bootstrap bundle
135
+ Triggers: installing Plutonium, adding the first resource of a new domain, building
136
+ a new portal/package, "set up X from scratch", "build me a Y app".
137
+
138
+ Load ALL of these before writing code:
139
+ 1. `plutonium-installation`
140
+ 2. `plutonium-create-resource`
141
+ 3. `plutonium-model`
142
+ 4. `plutonium-policy`
143
+ 5. `plutonium-entity-scoping` ← new
144
+ 6. `plutonium-portal`
145
+ 7. `plutonium-definition`
146
+
147
+ ## Router (targeted edits)
148
+ | About to... | Load |
149
+ |---|---|
150
+ | Write/edit a model, add associations | `plutonium-model` |
151
+ | Scope a model to a tenant, write `associated_with`, deal with multi-tenancy | `plutonium-entity-scoping` |
152
+ | Write `relation_scope`, `permitted_attributes`, override a policy | `plutonium-policy` (+ `plutonium-entity-scoping` if scoping) |
153
+ | Add fields, search, filters, custom actions to a resource | `plutonium-definition` |
154
+ | Customize a controller action, hook, redirect | `plutonium-controller` |
155
+ | Encapsulate business logic, multi-step ops | `plutonium-interaction` |
156
+ | Build a custom page, panel, table, layout | `plutonium-views` |
157
+ | Customize forms, field builders, inputs | `plutonium-forms` |
158
+ | Configure Tailwind, Stimulus, design tokens | `plutonium-assets` |
159
+ | Set up Rodauth, accounts, profile pages | `plutonium-auth` |
160
+ | Set up user invitations / membership | `plutonium-invites` (+ `plutonium-entity-scoping`) |
161
+ | Configure parent/child resources, nested routes | `plutonium-nested-resources` |
162
+ | Create a portal or feature package | `plutonium-portal` / `plutonium-package` |
163
+
164
+ ## Generator catalog
165
+ [Table of `pu:*` generators with one-line purpose + which skill covers it.]
166
+ ```
167
+
168
+ ## Execution phases
169
+
170
+ ### Phase A — Restructure (mechanical)
171
+ 1. Merge `plutonium-definition-actions` + `plutonium-definition-query` into
172
+ `plutonium-definition` with TOC + sections.
173
+ 2. Rename `plutonium-rodauth` → `plutonium-auth`; merge `plutonium-profile` into it.
174
+ 3. Merge `plutonium-theming` into `plutonium-assets` with TOC + sections.
175
+ 4. Delete merged source skills.
176
+ 5. Grep the codebase for references to deleted/renamed skill names; update.
177
+
178
+ ### Phase B — Create `plutonium-entity-scoping`
179
+ - Extract entity-scoping content from `plutonium-model`, `plutonium-policy`,
180
+ `plutonium-portal`, `plutonium-invites`.
181
+ - Single source of truth for: `associated_with`, `default_relation_scope`,
182
+ `relation_scope` override safety, entity strategies, and the three model shapes
183
+ (direct child, join-table, grandchild) with worked examples.
184
+ - Source skills keep general content but link here for tenancy specifics.
185
+
186
+ ### Phase C — Rewrite descriptions (every skill)
187
+ - Format: `Use BEFORE <verb/construct>. Also when <secondary>. <scope>.`
188
+ - Each calls out specific code constructs.
189
+
190
+ ### Phase D — Add 🚨 Critical block to every skill
191
+ - Fixed position (right after H1).
192
+ - ~5 bullets max.
193
+ - Pull existing anti-patterns from gotchas to the top.
194
+
195
+ ### Phase E — Rewrite `plutonium` index skill
196
+ - Bootstrap bundle.
197
+ - Router table.
198
+ - Generator catalog.
199
+
200
+ ### Phase F — Quick checklists
201
+ Add to bootstrap-bundle skills + `plutonium-definition` + `plutonium-entity-scoping`.
202
+ Skip for low-traffic skills.
203
+
204
+ ### Phase G — Cross-references
205
+ - Every tenancy-touching skill links to `plutonium-entity-scoping`.
206
+ - Definition / policy / model get mutual cross-refs.
207
+ - Verify bidirectionality.
208
+
209
+ ## Risks & mitigations
210
+
211
+ | Risk | Mitigation |
212
+ |---|---|
213
+ | Merged skills are bigger → more tokens per load | TOC at top, section anchors, router tells agent which section to jump to |
214
+ | Renaming `plutonium-rodauth → plutonium-auth` breaks references | Grep + update in Phase A step 5; also update CLAUDE.md if mentioned |
215
+ | Description rewrites are subjective | Same pattern for all (`Use BEFORE <verb>`), reviewed for consistency |
216
+ | `plutonium-entity-scoping` could become a dumping ground | Strict scope: only entity scoping itself. General model/policy stuff stays in source skills. |
217
+ | Source skills' tenancy sections become stubs that drift | Rule: source skill has a 🚨 bullet "for entity scoping, see `plutonium-entity-scoping`" and a one-paragraph teaser, nothing more |
218
+
219
+ ## Out of scope
220
+
221
+ - New code examples beyond 🚨 blocks and the three model-shape examples in
222
+ `plutonium-entity-scoping`.
223
+ - Telemetry / measurement.
224
+ - Skill content rewrites beyond what's needed for the new structure.
225
+ - Consolidating below 17 skills.
226
+
227
+ ## Success criteria
228
+
229
+ 1. An agent about to write `relation_scope` loads `plutonium-policy` AND
230
+ `plutonium-entity-scoping` from description triggering alone.
231
+ 2. An agent doing greenfield work loads the bootstrap bundle from a single read of
232
+ `plutonium`.
233
+ 3. Every skill that has a generator mentions "use the generator" in its 🚨 block.
234
+ 4. The three model shapes (direct child / join table / grandchild) have worked
235
+ examples in `plutonium-entity-scoping`.
236
+ 5. No skill description starts with a topic noun list; all start with `Use BEFORE`.
@@ -0,0 +1,364 @@
1
+ # Plutonium::Testing — Design Spec
2
+
3
+ **Date:** 2026-04-14
4
+ **Status:** Approved (pre-implementation)
5
+ **Audience:** End-app developers using Plutonium
6
+
7
+ ---
8
+
9
+ ## Goal
10
+
11
+ Ship `Plutonium::Testing` — a public, opt-in collection of Minitest concerns that give Plutonium app developers default test coverage for resources, policies, definitions, interactions, models, nested scoping, portal access, and authentication. Pair the module with a `plutonium-testing` skill and a Rails generator (`pu:test:scaffold`) that produces ready-to-run test files per (resource × portal) pairing.
12
+
13
+ ## Non-Goals
14
+
15
+ - RSpec support (Minitest only for first cut).
16
+ - A test data builder / factory layer (out of scope; callers wire their own factories or fixtures).
17
+ - Replacing Plutonium's own internal `test/support/shared_tests/` ahead of the public API landing — the migration is the *last* implementation step and serves as dogfooding.
18
+
19
+ ## Approach
20
+
21
+ - **Loading model:** opt-in `require "plutonium/testing"` in the consumer's `test/test_helper.rb`. No autoload, no production cost. Mirrors `ActiveSupport::Testing::*` conventions.
22
+ - **Granularity:** one concern per category. Callers `include` exactly what they need. No umbrella module.
23
+ - **DSL + stub methods:** declarative config (`resource_tests_for Post, portal: :admin`) for shape; abstract stub methods (raising `NotImplementedError`) for test data the caller must provide.
24
+ - **Portal-centric:** the `portal:` symbol is the single unit of configuration. It resolves to path prefix, default sign-in helper, expected scoping, and allowed action set by introspecting the mounted portal engine. Same resource × different portal = different test file.
25
+
26
+ ---
27
+
28
+ ## Architecture
29
+
30
+ ### File Layout
31
+
32
+ ```
33
+ lib/plutonium/
34
+ testing.rb # top-level require; loads submodules
35
+ testing/
36
+ dsl.rb # shared `resource_tests_for` + portal resolution
37
+ auth_helpers.rb # login_as / sign_out / with_portal (portal-aware)
38
+ resource_crud.rb # integration CRUD matrix
39
+ resource_policy.rb # permit? × action × role; relation_scope filtering
40
+ resource_definition.rb # fields/inputs/displays/columns/scopes smoke
41
+ resource_interaction.rb # interaction outcome assertions
42
+ resource_model.rb # associated_with, SGID, has_cents
43
+ nested_resource.rb # tenant-scoped CRUD + boundary assertions
44
+ portal_access.rb # cross-portal access boundaries
45
+
46
+ lib/generators/pu/test/
47
+ install/install_generator.rb # one-time setup
48
+ install/templates/plutonium_testing.rb.tt
49
+ scaffold/scaffold_generator.rb # per-resource × portal scaffold
50
+ scaffold/templates/integration_test.rb.tt
51
+ scaffold/templates/policy_test.rb.tt
52
+ scaffold/templates/definition_test.rb.tt
53
+
54
+ test/plutonium/testing/ # tests for the testing module itself
55
+ dsl_test.rb
56
+ auth_helpers_test.rb
57
+ resource_crud_test.rb
58
+ resource_policy_test.rb
59
+ resource_definition_test.rb
60
+ resource_interaction_test.rb
61
+ resource_model_test.rb
62
+ nested_resource_test.rb
63
+ portal_access_test.rb
64
+
65
+ test/generators/pu/test/
66
+ install_generator_test.rb
67
+ scaffold_generator_test.rb
68
+
69
+ .claude/skills/plutonium-testing/
70
+ SKILL.md
71
+
72
+ docs/guides/testing.md
73
+ ```
74
+
75
+ ### Loading
76
+
77
+ `lib/plutonium/testing.rb` is the entry point:
78
+
79
+ ```ruby
80
+ require "plutonium/testing/dsl"
81
+ require "plutonium/testing/auth_helpers"
82
+ require "plutonium/testing/resource_crud"
83
+ require "plutonium/testing/resource_policy"
84
+ require "plutonium/testing/resource_definition"
85
+ require "plutonium/testing/resource_interaction"
86
+ require "plutonium/testing/resource_model"
87
+ require "plutonium/testing/nested_resource"
88
+ require "plutonium/testing/portal_access"
89
+ ```
90
+
91
+ Consumers add `require "plutonium/testing"` to `test/test_helper.rb` (the `pu:test:install` generator does this for them).
92
+
93
+ ---
94
+
95
+ ## DSL
96
+
97
+ `Plutonium::Testing::DSL` is included by every concern. Provides one class-level method:
98
+
99
+ ```ruby
100
+ resource_tests_for ResourceClass,
101
+ portal: :admin, # required: portal symbol
102
+ path_prefix: "/admin", # optional: explicit override
103
+ parent: :organization, # optional: nested-resource parent
104
+ actions: %i[index show new create edit update destroy], # optional: opt-in set
105
+ skip: %i[destroy] # optional: opt-out individual tests
106
+ ```
107
+
108
+ The portal symbol drives:
109
+
110
+ | Derived from portal | Example for `:admin` | Example for `:org` |
111
+ |---|---|---|
112
+ | `path_prefix` | `/admin` | `/org/:organization_id` |
113
+ | Default sign-in | admin Rodauth strategy | org-member Rodauth strategy |
114
+ | Expected entity scoping | unscoped | scoped to `@organization` |
115
+ | Allowed action set | from definition | from definition |
116
+
117
+ DSL stores the current portal in a test-class-local attr; `AuthHelpers` reads it as default when no `portal:` kwarg is given.
118
+
119
+ ### Cross-portal tests
120
+
121
+ For tests that touch multiple portals (`PortalAccess` concern), `portal:` is an explicit kwarg on helper calls:
122
+
123
+ ```ruby
124
+ login_as(@admin, portal: :admin)
125
+ login_as(@user, portal: :org)
126
+ with_portal(:org) { get "/org/posts" } # block form
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Concerns
132
+
133
+ ### `Plutonium::Testing::ResourceCrud`
134
+
135
+ Generates index / show / new / create / edit / update / destroy integration tests against the portal-mounted resource.
136
+
137
+ **Stubs (caller must implement):**
138
+ ```ruby
139
+ def create_resource! # -> persisted record
140
+ def valid_create_params # -> Hash for POST
141
+ def valid_update_params # -> Hash for PATCH
142
+ ```
143
+
144
+ Sign-in is automatic from the portal's auth strategy unless the caller overrides `sign_in_for_tests(account, portal:)`.
145
+
146
+ ### `Plutonium::Testing::ResourcePolicy`
147
+
148
+ Asserts the `permit?` matrix across action × role and `relation_scope` filtering.
149
+
150
+ **Stubs:**
151
+ ```ruby
152
+ def policy_roles # -> { admin: -> { @admin }, member: -> { @user } }
153
+ def policy_record # -> record instance under test
154
+ def policy_matrix # -> { index: %i[admin member], destroy: %i[admin] }
155
+ ```
156
+
157
+ ### `Plutonium::Testing::ResourceDefinition`
158
+
159
+ Smoke-tests that all registered fields/inputs/displays/columns/scopes/filters render without error against a persisted record. Introspects the definition class via `Plutonium::Definition::DefineableProps`. **No caller stubs required** for the happy path.
160
+
161
+ ### `Plutonium::Testing::ResourceInteraction`
162
+
163
+ Outcome-assertion helpers for `Plutonium::Resource::Interaction` subclasses.
164
+
165
+ **Helpers:**
166
+ - `assert_interaction_success(interaction_class, **input)`
167
+ - `assert_interaction_failure(interaction_class, **input)`
168
+ - `assert_interaction_redirect(interaction_class, to:, **input)`
169
+ - `assert_interaction_renders(interaction_class, view:, **input)`
170
+
171
+ **Stubs:**
172
+ ```ruby
173
+ def interaction_class
174
+ def valid_interaction_input
175
+ ```
176
+
177
+ ### `Plutonium::Testing::ResourceModel`
178
+
179
+ Covers `associated_with` scope behavior, SGID routing, and `has_cents` money helpers. DSL flags select which features to test:
180
+
181
+ ```ruby
182
+ resource_tests_for Post, portal: :admin,
183
+ associated_with: :organization,
184
+ sgid_routing: true,
185
+ has_cents: %i[price]
186
+ ```
187
+
188
+ Only generates test methods for enabled features.
189
+
190
+ ### `Plutonium::Testing::NestedResource`
191
+
192
+ Same CRUD matrix as `ResourceCrud` but asserts scope boundaries: index excludes records from sibling tenants; show on a sibling-tenant record returns 404.
193
+
194
+ **Stubs:**
195
+ ```ruby
196
+ def parent_record! # -> persisted parent/tenant
197
+ def other_parent_record! # -> a different tenant for boundary tests
198
+ ```
199
+
200
+ ### `Plutonium::Testing::PortalAccess`
201
+
202
+ Cross-portal access boundaries.
203
+
204
+ **DSL:**
205
+ ```ruby
206
+ portal_access_matrix \
207
+ admin: %i[admin_portal],
208
+ member: %i[org_portal storefront_portal]
209
+ ```
210
+
211
+ Asserts unauthorized portals return 403 or redirect to login.
212
+
213
+ ---
214
+
215
+ ## Auth Helpers
216
+
217
+ `Plutonium::Testing::AuthHelpers` is included transitively by every concern.
218
+
219
+ **Public API:**
220
+ ```ruby
221
+ login_as(account) # uses portal from DSL
222
+ login_as(account, portal: :admin) # explicit override
223
+ sign_out # uses portal from DSL
224
+ sign_out(portal: :admin)
225
+ current_account # uses portal from DSL
226
+ current_account(portal: :admin)
227
+ with_portal(:org) { ... } # scoped portal switch
228
+ ```
229
+
230
+ **Implementation:**
231
+ - Looks up the portal's declared auth strategy from the mounted portal engine config.
232
+ - For Rodauth (stock Plutonium): fakes the session cookie directly; no full login round-trip.
233
+ - **Override hook** for non-Rodauth apps: caller defines `sign_in_for_tests(account, portal:)` and `AuthHelpers` defers to it.
234
+
235
+ The `pu:test:install` generator scaffolds a commented-out override in `test/support/plutonium_testing.rb`.
236
+
237
+ ---
238
+
239
+ ## Generators
240
+
241
+ ### `pu:test:install`
242
+
243
+ One-time project setup. Idempotent.
244
+
245
+ - Adds `require "plutonium/testing"` to `test/test_helper.rb` (no-op if present).
246
+ - Creates `test/support/plutonium_testing.rb` with commented-out override stubs for non-Rodauth auth.
247
+
248
+ ### `pu:test:scaffold`
249
+
250
+ Per-resource scaffold. Produces one file per (resource × portal).
251
+
252
+ ```bash
253
+ rails g pu:test:scaffold Blogging::Post --portals=admin,org
254
+ rails g pu:test:scaffold Blogging::Post --portals=admin --concerns=crud,policy,definition
255
+ rails g pu:test:scaffold Blogging::Post --portals=org --parent=organization --dest=blogging
256
+ ```
257
+
258
+ **Flags:**
259
+ - `--portals=admin,org` (required) — emits one file per portal.
260
+ - `--concerns=crud,policy,definition,nested,model,interaction,portal_access` (default: `crud,policy,definition`).
261
+ - `--parent=organization` — wires `NestedResource` parent stub.
262
+ - `--dest=main_app|package_name` — output destination (matches `pu:res:scaffold` convention).
263
+
264
+ **Example output** (`test/integration/admin_portal/blogging_posts_test.rb`):
265
+
266
+ ```ruby
267
+ require "test_helper"
268
+
269
+ class AdminPortal::BloggingPostsTest < ActionDispatch::IntegrationTest
270
+ include Plutonium::Testing::ResourceCrud
271
+ include Plutonium::Testing::ResourcePolicy
272
+ include Plutonium::Testing::ResourceDefinition
273
+
274
+ resource_tests_for Blogging::Post, portal: :admin
275
+
276
+ setup do
277
+ @admin = create_admin! # TODO: replace with your factory
278
+ login_as(@admin)
279
+ end
280
+
281
+ def create_resource!
282
+ Blogging::Post.create!(title: "X", body: "...") # TODO: adjust
283
+ end
284
+
285
+ def valid_create_params
286
+ { title: "New", body: "..." } # TODO: adjust
287
+ end
288
+
289
+ def valid_update_params
290
+ { title: "Updated" } # TODO: adjust
291
+ end
292
+
293
+ def policy_roles
294
+ { admin: -> { @admin } } # TODO: add other roles
295
+ end
296
+
297
+ def policy_record
298
+ create_resource!
299
+ end
300
+
301
+ def policy_matrix
302
+ { index: %i[admin], show: %i[admin], create: %i[admin],
303
+ update: %i[admin], destroy: %i[admin] } # TODO: tighten
304
+ end
305
+ end
306
+ ```
307
+
308
+ Stub method bodies are pre-filled with best-guess values from model introspection (column types, associations).
309
+
310
+ ---
311
+
312
+ ## Skill
313
+
314
+ `.claude/skills/plutonium-testing/SKILL.md` follows the existing skill conventions in `.claude/skills/`.
315
+
316
+ **Frontmatter `description`:**
317
+ > 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.
318
+
319
+ **Sections:**
320
+ 1. When to use
321
+ 2. Quick start (install + scaffold + first run)
322
+ 3. DSL reference (`resource_tests_for` keywords + portal resolution table)
323
+ 4. Concerns catalog (one section per concern with stub contract + example)
324
+ 5. Auth helpers
325
+ 6. Generator reference (`pu:test:install`, `pu:test:scaffold` flags)
326
+ 7. Customization & escape hatches (non-Rodauth auth, skipping defaults, custom assertions)
327
+ 8. Common pitfalls (forgotten stubs, portal mismatch, tenant leakage)
328
+
329
+ The top-level `plutonium` router skill gets one new entry pointing to `plutonium-testing`.
330
+
331
+ ---
332
+
333
+ ## Docs
334
+
335
+ `docs/guides/testing.md` mirrors the skill content for human-facing documentation. Linked from the guides section in `docs/.vitepress/config.ts`.
336
+
337
+ ---
338
+
339
+ ## In-Repo Adoption (Dogfooding)
340
+
341
+ After all concerns and generators land, port the dummy app's tests:
342
+
343
+ - `test/integration/admin_portal/resources_test.rb` and the other portal test files migrate to `Plutonium::Testing::*` concerns.
344
+ - `test/support/shared_tests/blogging_post_tests.rb` and `catalog_product_tests.rb` are deleted or shrunk to anything that doesn't fit the generic concerns.
345
+
346
+ Acceptance: zero coverage loss (compare test method counts before/after) and the entire suite still passes across `rails-7`, `rails-8.0`, `rails-8.1` appraisals.
347
+
348
+ ---
349
+
350
+ ## Implementation Sequence
351
+
352
+ 1. Module skeleton + entry point (`lib/plutonium/testing.rb`).
353
+ 2. Shared DSL + portal resolution.
354
+ 3. Auth helpers.
355
+ 4. Each concern (parallelizable after #2 and #3): ResourceCrud, ResourcePolicy, ResourceDefinition, ResourceInteraction, ResourceModel, NestedResource, PortalAccess.
356
+ 5. Generators: `pu:test:install`, then `pu:test:scaffold`.
357
+ 6. Skill + docs.
358
+ 7. In-repo migration (last — dogfoods the public API).
359
+
360
+ ---
361
+
362
+ ## Open Questions
363
+
364
+ None at design time. Implementation may surface portal-resolution edge cases (engines mounted at non-standard paths, multiple engines per account type) that warrant follow-up.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.45.0)
4
+ plutonium (0.45.3)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.45.0)
4
+ plutonium (0.45.3)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.45.0)
4
+ plutonium (0.46.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -12,12 +12,20 @@ module Pu
12
12
  def start
13
13
  update_gem
14
14
  update_npm_package
15
+ sync_skills_if_present
15
16
  rescue => e
16
17
  exception "#{self.class} failed:", e
17
18
  end
18
19
 
19
20
  private
20
21
 
22
+ def sync_skills_if_present
23
+ return unless File.file?(Rails.root.join(".claude", "skills", "plutonium", "SKILL.md"))
24
+
25
+ say_status :update, "Syncing Plutonium Claude skills...", :green
26
+ Rails::Generators.invoke("pu:skills:sync", [], destination_root: Rails.root)
27
+ end
28
+
21
29
  def update_gem
22
30
  say_status :update, "Updating plutonium gem...", :green
23
31
  run "bundle update plutonium"