constable-rails 0.1.0 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a0630c0ca6e000d33e21515a284616186d1939531ba5f5e9c5c61882d6dc9ec
4
- data.tar.gz: 10fcc4a75763bec06db7049bd3815214e1320ecdb4f3ad2780ef0497b8f8af6b
3
+ metadata.gz: c4612ebf00dac193c1557bbd887beccfbd7e7864f157dd88a77386e476897a6e
4
+ data.tar.gz: f208f84658349658dccef504a21f6a62833d27b9b772d1d91ef5f26216b60ae5
5
5
  SHA512:
6
- metadata.gz: 9a823bc22286ffaff8ab4c29f4f357ba40af37e423a9c6d1cab82382edf5d19c0ca0fae9796c0171f256bab6bdef93c9e2f14a65dd9a57e6e49e2cede204e959
7
- data.tar.gz: 0c043327dc97ace117a1dae5919c9b066df22059deafa5a422066e96b3abafc6f67df7e33f911f68a8eb314b3cca6e7bab0150c7d91c3d6b26f92482baee8b0e
6
+ metadata.gz: 296f8609681632e8427d481ecc94a15773b581c90dd7a88fa28c66eb584cd492c1706eec3faebc56aec385709ff0e72320db299c3901cec4659b333d8b94f97a
7
+ data.tar.gz: aeb4696fc4029685e7ffcab4c4b7d3e0287b6a153b86f6c331864353e56546d6b9a9eb0aef640a59dce748ed08e27f6f5e80251b7e521143f1490ed18356d67f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,280 @@ All notable changes to this project are documented here. This project adheres to
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.1.0]
9
+
10
+ Three things that were documented and did not work, plus the command for a docket
11
+ that has gone stale.
12
+
13
+ ### `Constable.configure` actually configures things now
14
+
15
+ The generated `test/case_helper.rb` told you to write `c.parallel_workers = 4`, explained
16
+ when you would want to, and then **nothing in the codebase ever read it**. Four accessors,
17
+ all inert.
18
+
19
+ They work now, and every setting `.constable/config.yml` understands is settable in Ruby
20
+ alongside them — `cold_cases`, `storage`, `warrants`, `warrant_retries`, `auto_relink`,
21
+ `parole_period`, `coverage`, `coverage_threshold`, `coverage_html`, `fail_on_warnings`,
22
+ `parallel_workers`, `output`, `tiers`. A test asserts the two halves stay in step, so a
23
+ setting cannot be added to one and forgotten in the other.
24
+
25
+ Precedence, and the reasoning:
26
+
27
+ ```
28
+ a CLI flag --workers 4, --expanded one run, most specific
29
+ Constable.configure test/case_helper.rb code you deliberately ran
30
+ .constable/config.yml the project's declared default
31
+ Constable's defaults
32
+ ```
33
+
34
+ Which to use? A setting that differs per machine or per branch belongs in the YAML, where
35
+ it is obvious and greppable. A setting that has to be *computed* belongs in Ruby, because
36
+ YAML cannot do this:
37
+
38
+ ```ruby
39
+ Constable.configure do |c|
40
+ c.parallel_workers = ENV.fetch("CI_WORKERS", 4).to_i
41
+ c.coverage = ENV["CI"] == "true"
42
+ end
43
+ ```
44
+
45
+ Values set in Ruby go through the same clamping as values set in the file, so a typo is no
46
+ more dangerous in one than the other.
47
+
48
+ **`storage` is the one exception, and it now says so.** The blotter is opened before
49
+ `case_helper.rb` loads, so that `constable jail`, `warrants`, `watchlist` and `status` can
50
+ read the docket without booting the app — a broken app should not stop you reading the
51
+ docket. Setting it in Ruby would have been silently ignored, which is the exact failure
52
+ this release exists to stop, so it raises and explains why.
53
+
54
+ The docs now lead with Ruby. The generated `case_helper.rb` lists **every** setting at its
55
+ default in one compact block — a test asserts the list stays complete, and that it never
56
+ offers `storage`. The long-form explanation of each stays in `config.yml`, so the two files
57
+ are a reference and a place to write code rather than two competing references.
58
+
59
+ Neither is `config/initializers/`, and the reason is concrete: initializers run on every
60
+ boot including production, where a test-only gem is not in the bundle, so an initializer
61
+ calling `Constable.configure` takes the app down with a NoMethodError. Same reason RSpec,
62
+ SimpleCov, WebMock and Capybara all configure from the test helper.
63
+
64
+ ### `.constable/config.yml` is now genuinely optional
65
+
66
+ You can delete it. `rails generate constable:install --skip-config` never writes it.
67
+
68
+ The one thing keeping it mandatory was `storage`, which cannot be set in Ruby for the
69
+ ordering reason above. It now reads from the environment as well, which is the right shape
70
+ for CI anyway, where the value is a secret and differs per machine:
71
+
72
+ ```
73
+ CONSTABLE_STORAGE_URL=postgres://user:pass@host/constable_metadata
74
+ CONSTABLE_STORAGE_PATH=/var/lib/constable/blotter.sqlite3
75
+ CONSTABLE_STORAGE_ADAPTER=postgres
76
+ ```
77
+
78
+ An adapter is inferred from the URL scheme when it is not given, and an empty variable
79
+ means unset rather than "connect to the empty string". The `.constable/` **directory**
80
+ still exists to hold the blotter — deliberately not `tmp/`, which `rails tmp:clear` and
81
+ most deploys would wipe, taking weeks of flake history with it.
82
+
83
+ One bug found while checking this end to end, in the override layer added above: settings
84
+ were applied *after* the selection had already been asked for its targets, so
85
+ `c.cold_cases` in Ruby was silently ignored and a suite of 192 ran 35. Overrides are now
86
+ applied between requiring the helper and asking the selection anything.
87
+
88
+ ### Settings added after you install are no longer invisible
89
+
90
+ `.constable/config.yml` doubles as the reference — every key at its default, with the
91
+ reasoning above it — which only works if it stays current. A gem upgrade cannot rewrite it
92
+ without clobbering your settings, and Thor's only other answer is to skip the file, so
93
+ **`output` shipped in 1.0.0 and never appeared in any existing config.** The first anyone
94
+ knew was running `bundle update` and finding the setting missing.
95
+
96
+ `rails generate constable:install` now appends only the settings your file does not
97
+ mention, each with its explanation, under a header saying where they came from. Your
98
+ values and your own comments are never touched. Re-run it after any upgrade.
99
+
100
+ ### `constable prune`
101
+
102
+ A test's key is a content hash of its body, so editing a jailed test gives it a new
103
+ identity and leaves the old row behind — pointing at a `file:line` that may now hold
104
+ something else. That is identity working as designed, and it was the one known limitation
105
+ left open in 1.0.0. This is the broom:
106
+
107
+ ```console
108
+ $ constable prune --dry-run # list what would go
109
+ $ constable prune # forget it
110
+ ```
111
+
112
+ It loads the whole suite first, because which tests still exist is only knowable once
113
+ every case file has been read, and it is deliberately conservative in two directions:
114
+
115
+ - **A known identity is never pruned**, even when the file recorded beside it is gone. The
116
+ path is a display label; the identity is the truth. A test that moved file has an
117
+ out-of-date label, not a missing test.
118
+ - **A cold case is never pruned while its file exists.** Cold-case tests cannot be
119
+ enumerated without running their own engine, so their absence says nothing.
120
+
121
+ Flake history is left alone either way — only the docket and outstanding warrants are
122
+ touched.
123
+
124
+
125
+ ## [1.0.0]
126
+
127
+ The first release anybody should install.
128
+
129
+ 0.1.0 shipped the ideas; running it against a real Rails app for the first time found
130
+ that several of them did not survive contact. Four bugs made the documented adoption
131
+ path impossible to follow, three commands raised `NoMethodError` the moment they were
132
+ run, and three separate ways of mistyping a command produced a **green build that ran no
133
+ tests at all**. Those are all fixed, with tests, and the suite has grown from 738 runs to
134
+ 888.
135
+
136
+ The API has not changed. Every 0.1.0 case file, config key and command still works.
137
+
138
+ ### The adoption path now actually works
139
+
140
+ These four were found by installing 0.1.0 into a Rails app with a 188-example RSpec
141
+ suite and following the README from the top.
142
+
143
+ - **`rails generate constable:install` no longer breaks the Gemfile.** It appended a
144
+ `:cold_case` group declaring `rspec-rails` without checking whether the app already
145
+ had it. Any app adopting Constable *from RSpec* — which is the entire target audience
146
+ — was left with a Gemfile Bundler refused to parse: *"You cannot specify the same gem
147
+ twice with different version requirements."* The installer now adds only the engines
148
+ the repo has files for, and only ones the Gemfile does not already declare.
149
+
150
+ - **Cold cases run `before(:suite)` and `after(:suite)` hooks.** `ColdCase::RSpec` drove
151
+ example groups directly and skipped RSpec's `with_suite_hooks`, so the hooks never
152
+ fired. That is where `webmock/rspec` calls `WebMock.enable!`, where VCR and
153
+ DatabaseCleaner install themselves, and where SimpleCov starts. It failed *open*: a
154
+ spec that stubbed HTTP opened a real socket instead of erroring. Each hook now runs
155
+ exactly once per run — after a file has loaded, since a legacy file's own
156
+ `require "rails_helper"` is what registers them.
157
+
158
+ - **Parallel workers get their own database.** Constable forks its own workers and so
159
+ never picked up the per-worker databases Rails builds for `rails test`. Every worker
160
+ opened the same one. On SQLite a suite that passed 188/0 serially collapsed into 130
161
+ `database is locked` failures. On a client/server database it would have been quieter
162
+ and worse. A worker that cannot build its own database now raises rather than falling
163
+ back to the shared one, and an app that cannot shard runs serially with a warning.
164
+
165
+ - **An outage no longer jails healthy tests.** Flake history reads "passed last run,
166
+ failed this run" as evidence about a test, so that one broken parallel run put 29
167
+ healthy tests on the docket marked *"passed, then failed with no code change"*. A run
168
+ where a quarter of the suite fails with the identical error is now recognised as one
169
+ broken run: the failures still stand and the build still goes red, but nothing moves
170
+ through the jail or parole state machine and nothing is written to flake history.
171
+
172
+ - **`constable:install` writes the blotter into `.gitignore`.** It is this machine's
173
+ flake history and docket. Committing it hands CI somebody else's docket and conflicts
174
+ on every run.
175
+
176
+ - **FactoryBot is wired into the tier base classes,** and `test/support/**` loads
177
+ *before* them. `create(:user)` is what a converted spec is full of, and the generated
178
+ `case_helper.rb` both omitted the include and told you to `include Authenticatable` in
179
+ a class defined thirty lines above the file that defines it.
180
+
181
+ ### Commands that had never been run
182
+
183
+ There was no test file for the CLI at all. All three of these are in the published
184
+ command reference and all three raised `NoMethodError` on their first line:
185
+
186
+ - `constable jail parole PATH:LINE`
187
+ - `constable jail release PATH:LINE`
188
+ - `constable warrants release PATH:LINE` — twice over: after fixing the first bug it
189
+ went on to call a second method that does not exist either.
190
+
191
+ Ambiguous targets are also refused rather than guessed at. `constable jail parole
192
+ test/cases/reports_case.rb`, with three tests from that file on the docket, paroled one
193
+ of them — not the first by line, whichever row the database happened to return. It now
194
+ prints the candidates and exits.
195
+
196
+ ### Three ways to get a green build that ran nothing
197
+
198
+ Each of these printed `0 passed, 0 failed` and exited **0**, so a typo in a CI script
199
+ went green having tested nothing. The most expensive kind of bug a test runner can have,
200
+ because it stays invisible for months.
201
+
202
+ - `constable test test/cases/typo_case.rb` — no such file.
203
+ - `constable test users_case.rb:999` — worse than nothing. `PATH:LINE` picks the
204
+ investigation declared nearest above the line so you can point anywhere inside a
205
+ block; unbounded, a line past the end of the file ran the **last** investigation in
206
+ it. Not the test you asked for, not an error, green either way.
207
+ - `constable test --tier nonsense` — and `--tier UNIT`, which matched nothing because
208
+ tiers were case-sensitive.
209
+
210
+ ### Correctness
211
+
212
+ - **Two tests no longer share one identity.** A test's key is a content hash of its
213
+ body, which is what lets history survive a rename — but two tests with byte-identical
214
+ bodies got the same key, in different classes, with different descriptions. The
215
+ blotter treated them as one test: jail either and both went, and their flake histories
216
+ merged. This is routine rather than exotic; the model generator writes an identical
217
+ first investigation into every file it touches.
218
+
219
+ - **A witness can no longer replace the framework.** `witness` defines a real instance
220
+ method, so `witness(:class)` quietly replaced `Object#class` and every later failure
221
+ message reported the wrong thing. `witness(:attest)` was worse: it disabled assertions
222
+ outright, so the tests passed by doing nothing.
223
+
224
+ - **`be(nil)` asserted the opposite of what it said.** `[nil].any?` is false — `Array#any?`
225
+ without a block tests the truthiness of the elements, not presence — so it fell through
226
+ to the truthiness branch.
227
+
228
+ - **`match_array` was aliased to `contain_exactly`,** but RSpec's takes one array where
229
+ `contain_exactly` takes varargs, so `match_array([1, 2])` asserted the collection held
230
+ a single element which was itself `[1, 2]`.
231
+
232
+ ### Matchers
233
+
234
+ `constable modernize` rewrote any matcher name straight into an `attest` call, so a spec
235
+ using one Constable did not implement converted cleanly and then died at runtime.
236
+
237
+ - Added: `contain_exactly`, `match_array`, `be` (identity, truthiness, and the
238
+ `be >= 0` operator form), `be_within(d).of(x)`, `start_with`, `end_with`,
239
+ `be_between`, `satisfy`.
240
+ - `modernize` now **flags any matcher it does not recognize** instead of converting it.
241
+ - `have_http_status` resolves names through `Rack::Utils` and knows
242
+ `:unprocessable_content` — the Rack 3.1 name for 422, and the one Rails 8.1 tells you
243
+ to use while Constable accepted only the deprecated spelling.
244
+ - Helper specs are flagged. They converted reporting *"21 converted, 0 flagged"* and
245
+ then failed every example with `undefined local variable or method 'helper'`.
246
+
247
+ ### Output
248
+
249
+ - **An `expanded` mode.** `output: concise | expanded` in `.constable/config.yml`, or
250
+ `--expanded` / `--concise` for one run. Concise is unchanged and still the default.
251
+ Expanded prints a line per test — glyph, description, duration — so you can see which
252
+ test is hanging while it hangs. Per-test durations are in milliseconds; the run-scale
253
+ format rendered every fast test as `0.0s`.
254
+ - **Sections for the supervision states.** Jailed tests, warrants and tests on parole
255
+ were counted in the headline and then never mentioned again, so "2 jailed" was a
256
+ number with nothing behind it. `WARRANTS`, `JAILED` and `ON PAROLE` now print like
257
+ `FAILURES` does.
258
+ - **Each one ends with what to do next.** "2 jailed" is a fact;
259
+ `constable jail parole PATH:LINE` is an action. Parole shows its progress toward
260
+ release.
261
+ - Parole entries carry their `file:line` — the one thing their own hint asked you to
262
+ pass — and warning text wraps to the 60-column frame instead of spilling out of it.
263
+
264
+ ### Messages instead of stack traces
265
+
266
+ - A YAML typo in `config.yml` raised a raw `Psych::SyntaxError`; a file that parsed but
267
+ was not a mapping raised *"no implicit conversion of Array into Hash"* from inside the
268
+ merge. Both now name the file and the problem.
269
+ - A corrupt blotter raised *"file is not a database: PRAGMA journal_mode = WAL"*. It now
270
+ says what the file holds — flake history, the docket, warrants, never a test — and
271
+ that deleting it is safe.
272
+ - `coverage_threshold` is clamped to a percentage, negative `warrant_retries` means off,
273
+ and `parole_period` clamps in one place. `Jail` already refused a period of zero, but
274
+ the reporter read the raw value and would print *"Day 1 of 0 — 0 clean runs to go"*
275
+ while the docket waited for ten.
276
+
277
+ ### Also
278
+
279
+ - A new logo, with a dark variant, and a `<picture>` element so GitHub picks.
280
+
281
+
8
282
  ## [0.1.0]
9
283
 
10
284
  Initial release.
@@ -84,5 +358,7 @@ Initial release.
84
358
  - Diff-based coverage gate — only lines changed in the current diff are held to the
85
359
  threshold. `constable beat` for the full picture, `--html` for a browsable report.
86
360
 
87
- [Unreleased]: https://github.com/Ray-Hughes/constable/compare/v0.1.0...HEAD
361
+ [Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.1.0...HEAD
362
+ [1.1.0]: https://github.com/Ray-Hughes/constable/compare/v1.0.0...v1.1.0
363
+ [1.0.0]: https://github.com/Ray-Hughes/constable/compare/v0.1.0...v1.0.0
88
364
  [0.1.0]: https://github.com/Ray-Hughes/constable/releases/tag/v0.1.0
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  <div align="center">
2
2
 
3
- <img src="https://raw.githubusercontent.com/Ray-Hughes/constable/main/docs/assets/logo.png" alt="Constable" width="200">
4
-
5
- # Constable
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Ray-Hughes/constable/main/docs/assets/logo-dark.png">
5
+ <img src="https://raw.githubusercontent.com/Ray-Hughes/constable/main/docs/assets/logo.png" alt="Constable" width="340">
6
+ </picture>
6
7
 
7
8
  **A strict Rails testing framework where fast and non-flaky are structural, not disciplinary.**
8
9
 
@@ -215,10 +216,16 @@ Constable::Matchers.define(:be_created) { |response| response.status == 201 }
215
216
  Constable::Matchers.define(:exist) { |model_class, attrs| model_class.exists?(attrs) }
216
217
  ```
217
218
 
218
- Built in: `eq`, `eql`, `include`, `match`, `raise_error`, `have_attributes`, `exist`,
219
- `be_created`, `redirect_to`, `have_http_status`, `change`, plus `be_a`, `be_nil`, `be_empty`,
220
- `be_truthy`, `be_falsey` and a `be_*` / `have_*` predicate fallback. Plain `assert_*` and
221
- `refute_*` primitives are always available alongside `attest`.
219
+ Built in: `eq`, `eql`, `be`, `include`, `match`, `raise_error`, `have_attributes`,
220
+ `exist`, `be_created`, `redirect_to`, `have_http_status`, `change`, `contain_exactly`,
221
+ `match_array`, `start_with`, `end_with`, `be_between`, `be_within(d).of(x)`, `satisfy`,
222
+ plus `be_a`, `be_nil`, `be_empty`, `be_truthy`, `be_falsey` and a `be_*` / `have_*`
223
+ predicate fallback. `be` also takes the operator form — `attest(count).to be > 0`.
224
+ Plain `assert_*` and `refute_*` primitives are always available alongside `attest`.
225
+
226
+ The set is deliberately smaller than RSpec's, so `constable modernize` **flags any
227
+ matcher it does not recognize** rather than converting it into a case that only fails
228
+ once you run it.
222
229
 
223
230
  ### Shared behavior is just Ruby
224
231
 
@@ -369,6 +376,14 @@ Class name, description and file are stored alongside purely as a display label.
369
376
  new one appears and suggests `constable history relink OLD NEW`. Set `auto_relink: true` to
370
377
  confirm high-confidence matches automatically.
371
378
 
379
+ Two tests with byte-identical bodies would otherwise share a key — and bodies repeat more
380
+ than the phrase "content hash" suggests, since
381
+ `attest(build(:thing, name: nil)).not_to be_valid` is the same handful of tokens in every
382
+ model case. Constable re-keys colliding tests on their class and description once the
383
+ suite is loaded, so no two tests ever share a docket row. Rename-survival is weaker for
384
+ exactly those tests, which is the right trade: a history belonging to two tests at once is
385
+ worse than one that resets.
386
+
372
387
  ### Command reference
373
388
 
374
389
  | Command | Runs |
@@ -384,25 +399,73 @@ Class name, description and file are stored alongside purely as a display label.
384
399
  | `constable status` | How the suite is doing over time |
385
400
  | `constable beat [--html]` | Coverage: overall %, per-file, the unpatrolled list |
386
401
  | `constable history relink OLD NEW` | Carry history across a real body change |
402
+ | `constable prune [--dry-run]` | Forget docket rows and warrants for tests that no longer exist |
387
403
  | `constable import --from=rspec` | Adopt an existing suite as cold cases |
388
404
  | `constable modernize PATH` | Opt-in AST rewrite into the native DSL |
389
405
 
390
- Flags: `--full --unsafe --jail --warrants --coverage --seed N --workers N --verbose --tier T --no-color`.
406
+ Flags: `--full --unsafe --jail --warrants --coverage --seed N --workers N --verbose --tier T\n--expanded --concise --output MODE --no-color`.
391
407
 
392
408
  Order is randomized every run for native cases, with the seed printed and replayable via
393
409
  `--seed`. Cold cases keep their own engine's order. Workers run in parallel by default,
394
410
  load-balanced by a cached per-test duration index.
395
411
 
412
+ Each worker gets **its own database**, built from schema the way `rails test` does it.
413
+ Sharing one would not be a speed/safety trade but a correctness bug: on SQLite the run
414
+ dissolves into `database is locked`, and on a client/server database tests quietly see
415
+ each other's rows. If your app has ActiveRecord but cannot shard, Constable runs serially
416
+ and says why — slow is a trade-off, wrong is not.
417
+
396
418
  ### Output
397
419
 
398
420
  stdout is reserved for results. `Rails.logger`, SQL and request/response logging go to
399
421
  `log/test.log`; `--verbose` streams it back for active debugging.
400
422
 
423
+ **While it runs**, the live stream has two modes. `concise` is the default: one glyph per
424
+ test, grouped into a run per case, so a thousand-test suite stays inside one screen and a
425
+ wall of green is the point.
426
+
427
+ ```
428
+ ProjectCase ✓✓✓✓✓✓✓✓✓✓✓✓
429
+ BillingCase ✓✓⚖⛓✓
430
+ ```
431
+
432
+ `expanded` trades that for a line per test — glyph, name, duration — so you can see which
433
+ test is hanging while it hangs, rather than after.
434
+
435
+ ```
436
+ ProjectCase
437
+ ✓ validations rejects a colour outside the palette 2ms
438
+ ✓ validations rejects a duplicate name for the same owner 12ms
439
+ ✗ #completion_ratio is the fraction of done tasks 8ms
440
+
441
+ BillingCase
442
+ ⚖ charges a card 310ms
443
+ ⛓ refunds a charge — assertion failed on the amount
444
+ ✓ issues a receipt 1.4s
445
+ ```
446
+
447
+ Set it in `.constable/config.yml` (`output: concise` or `expanded`), or per run with
448
+ `--expanded` / `--concise`. A jailed test never ran its body, so it is given no duration
449
+ rather than a dishonest `0ms`. **The summary below is identical in both modes** — the mode
450
+ only changes what you watch on the way there.
451
+
401
452
  ```
402
453
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
403
- CONSTABLE 482 tests · 3 cases · 12.4s
454
+ CONSTABLE 6 tests · 3 cases · 12.4s
404
455
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
405
- ✓ 478 passed ✗ 2 failed ⛓ 2 jailed (1 parole violation) ◑ 1 on parole ⚖ 1 warrant issued ⚠ 3 warnings ◐ 92% covered
456
+ ✓ 2 passed ✗ 1 failed ⛓ 2 jailed (1 parole violation) ◑ 1 on parole ⚖ 1 warrant issued ⚠ 2 warnings ◐ 92% covered (3 files unpatrolled)
457
+
458
+ PAROLE VIOLATED
459
+ ───────────────
460
+ ⛓ UsersController::CreatesUserCase
461
+ "creates a user with valid params"
462
+ spec/cases/users_controller/creates_user_case.rb:8
463
+ Failed on day 3 of a 10-run parole — back to jail. This is its 2nd time in jail.
464
+
465
+ → Somebody trusted this test again and it let them down,
466
+ so it is back on the docket. Fix it before the next
467
+ constable jail parole — a second violation is the signal
468
+ that the test, not the flake, is the problem.
406
469
 
407
470
  FAILURES
408
471
  ────────
@@ -417,11 +480,64 @@ stdout is reserved for results. `Rails.logger`, SQL and request/response logging
417
480
 
418
481
  Rerun just this test:
419
482
  constable test spec/cases/sessions_case.rb:12 --seed 8841
483
+
484
+ WARRANTS
485
+ ────────
486
+ ⚖ BillingCase
487
+ "charges a card"
488
+ spec/cases/sessions_case.rb:12
489
+ Failed, then passed 4 of 5 retries run in isolation.
490
+
491
+ → A warrant is "not reproducible", not "not a problem" —
492
+ it stops blocking the build and stays visible until
493
+ someone deals with it. Fixed the flake? constable
494
+ warrants release PATH:LINE
495
+
496
+ JAILED
497
+ ──────
498
+ ⛓ BillingCase
499
+ "refunds a charge"
500
+ spec/cases/sessions_case.rb:12
501
+ Assertion failed on the amount.
502
+
503
+ → Jailed means skipped and tracked, not passing. Think one
504
+ is fixed? constable jail parole PATH:LINE runs it for
505
+ real again — 10 clean runs and it releases itself.
506
+
507
+ ON PAROLE
508
+ ─────────
509
+ ◑ SessionsCase
510
+ "signs a user in"
511
+ spec/cases/sessions_case.rb:12
512
+ Day 4 of 10 — 6 clean runs to go.
513
+
514
+ → A paroled test runs for real and is watched: one failure
515
+ sends it straight back to jail. constable watchlist
516
+ shows everything under supervision.
517
+
518
+ WARNINGS
519
+ ────────
520
+ ⚠ spec/legacy/old_users_spec.rb
521
+ running as a cold case (Constable::ColdCase::RSpec) — 12
522
+ tests not yet under native rules
523
+
524
+ ⚠ spec/controllers/sessions_case.rb:44
525
+ unsafe { sleep(0.1) } — "testing an actual timeout path,
526
+ not a code smell"
527
+
528
+ SLOWEST
529
+ ────────
530
+ 3.2s UsersController::CreatesUserCase "creates a user with valid params"
531
+ 1.1s SessionsCase "times out after thirty seconds"
532
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
420
533
  ```
421
534
 
422
- Sections print worst-to-least-urgent: parole violations, failures, warnings, slowest.
423
- Failures carry their own context and point at your `investigate` line, not at framework
424
- internals.
535
+ Sections print worst-to-least-urgent: parole violations, failures, warrants, jailed, on
536
+ parole, warnings, slowest. A section only appears when it has something to say.
537
+
538
+ Each supervision section ends with one line saying what to do next, because "2 jailed" is
539
+ a fact and `constable jail parole PATH:LINE` is an action. Failures don't get a hint —
540
+ they already end with the exact command to rerun them.
425
541
 
426
542
  ### The blotter
427
543
 
@@ -442,6 +558,50 @@ storage:
442
558
 
443
559
  ### Configuration
444
560
 
561
+ Settings can be written in Ruby, in `test/case_helper.rb` — the same place RSpec puts
562
+ `RSpec.configure` — or in `.constable/config.yml`, or on the command line. The most
563
+ specific wins:
564
+
565
+ ```
566
+ a CLI flag --workers 4, --expanded one run
567
+ Constable.configure test/case_helper.rb code you deliberately ran
568
+ .constable/config.yml the project's declared default
569
+ Constable's defaults
570
+ ```
571
+
572
+ Every key below can be set in either place. Put settings that differ per machine or per
573
+ branch in the YAML, where they are obvious and greppable; put settings that have to be
574
+ *computed* in Ruby, because YAML cannot:
575
+
576
+ ```ruby
577
+ # test/case_helper.rb
578
+ Constable.configure do |c|
579
+ c.parallel_workers = ENV.fetch("CI_WORKERS", 4).to_i
580
+ c.coverage = ENV["CI"] == "true"
581
+ c.output = :expanded
582
+ end
583
+ ```
584
+
585
+ Not `config/initializers/`, which is where a runtime gem like Devise goes. Initializers
586
+ run on **every** boot including production, where a test-only gem is not in the bundle, so
587
+ an initializer calling `Constable.configure` takes the app down. It is the same reason
588
+ RSpec, SimpleCov, WebMock and Capybara all configure from the test helper.
589
+
590
+ `config.yml` is optional, and one setting is the reason it exists: **`storage` can only be
591
+ set there.** The blotter is opened before `case_helper.rb` loads, so that `constable jail`,
592
+ `warrants`, `watchlist` and `status` can read the docket without booting the app — a
593
+ broken app should not stop you reading the docket. Setting it in Ruby raises rather than
594
+ being quietly ignored.
595
+
596
+ Beyond that it is a preference. A settings file is greppable and diffable without running
597
+ anything, which suits values that differ per project or per branch; Ruby suits anything
598
+ computed.
599
+
600
+ `config.yml` doubles as the reference, so re-run
601
+ `rails generate constable:install --skip` after an upgrade: it appends any settings your
602
+ file does not mention and leaves your own values and comments alone. (`--skip` so the
603
+ other generated files, which you have probably edited, are left as they are.)
604
+
445
605
  ```yaml
446
606
  # .constable/config.yml
447
607
  cold_cases:
@@ -462,6 +622,7 @@ coverage_threshold: 90 # diff-based — only lines changed in the curr
462
622
  coverage_html: false
463
623
 
464
624
  fail_on_warnings: false
625
+ output: concise # live stream detail: concise | expanded
465
626
  parallel_workers: auto
466
627
 
467
628
  tiers: # fallback inference; base classes are primary
@@ -60,6 +60,7 @@ module Constable
60
60
  raise ArgumentError, "witness(#{name.inspect}) requires a block" unless block
61
61
 
62
62
  name = name.to_sym
63
+ guard_witness_name!(name)
63
64
  own_witnesses[name] = block
64
65
  define_method(name) do
65
66
  constable_witnesses.fetch(name) { constable_witnesses[name] = instance_exec(&block) }
@@ -161,6 +162,38 @@ module Constable
161
162
  constable_lineage.flat_map(&:own_briefings)
162
163
  end
163
164
 
165
+ # Names a witness may not take.
166
+ #
167
+ # `witness` defines a real instance method, so `witness(:class) { ... }` quietly
168
+ # replaces Object#class on the case and every later `attest` failure reports the
169
+ # wrong thing. Worse, `witness(:attest)` disables assertions outright -- the tests
170
+ # then pass by doing nothing, which is the one failure mode a testing framework
171
+ # must never have.
172
+ #
173
+ # Deliberately a list rather than `method_defined?`: a blanket check would reject
174
+ # ordinary names a tier happens to define (`response` on an integration case), and
175
+ # shadowing those is a legitimate, if unusual, thing to want.
176
+ RESERVED_WITNESS_NAMES = %i[
177
+ class send __send__ __id__ object_id method methods freeze frozen? dup clone
178
+ hash inspect to_s instance_variable_get instance_variable_set instance_variables
179
+ attest unsafe witness briefing investigate docket tier setup teardown
180
+ assert refute flunk skip pass freeze_time travel_to travel_back
181
+ ].freeze
182
+
183
+ def guard_witness_name!(name)
184
+ if RESERVED_WITNESS_NAMES.include?(name)
185
+ raise ArgumentError,
186
+ "witness(:#{name}) would replace Constable::Case##{name}, which the " \
187
+ "framework needs. Pick another name."
188
+ end
189
+
190
+ return unless name.to_s.start_with?("constable_")
191
+
192
+ raise ArgumentError,
193
+ "witness(:#{name}) is in Constable's own namespace. Names beginning " \
194
+ "`constable_` belong to the framework."
195
+ end
196
+
164
197
  def witnesses
165
198
  constable_lineage.each_with_object({}) { |klass, out| out.merge!(klass.own_witnesses) }
166
199
  end