Module: Hecks::Bluebook::SmokeTest
- Defined in:
- lib/hecks/bluebook/smoke_test.rb
Overview
BOOTS A REAL BLUEBOOK AND ACTUALLY DISPATCHES AGAINST IT — the
sibling bin/model_check never had: that tool proves a bluebook
is STRUCTURALLY sound (no dead states, no unreachable transitions)
without ever running a single command; this proves it WORKS,
catching exactly the class of bug static analysis structurally
cannot see. Built and proved inside bin/interview's own smoke
command, where it first found two real bugs — a lifecycle field
colliding with an explicit attribute, and a non-creating command
missing Command.ActsOn — that reloaded clean, checked clean,
and only broke once something actually dispatched against them.
SYNTHESIZED ARGS, NEVER RANDOM (Synthesizer) — a String becomes
a fixed marker, a closed set uses its own first admitted member, a
reference reuses whatever this same run already minted for that
target. ONE call per declared command, ONE call per declared
report, per created root; every failure is collected and reported
together rather than stopping at the first.
WHAT THIS CANNOT CATCH: a call that dispatches cleanly but answers WRONG — the read-model join bug this exact tool's own first user hit is the textbook case, a query that silently returned an empty array rather than raising. Smoke-testing proves nothing crashes; it does not prove the answer is the right one.
A FAILURE HERE IS NOT NECESSARILY A DOMAIN BUG, either — it may
just be Synthesizer's own naive values (0, an empty list,
"smoke-test") not satisfying a real invariant this simple
generator was never built to reason about (a positive amount, a
non-empty topping list, an email pattern). Confirmed against
Banking and Pizzas: both real domains, both showing exactly that
class of failure, neither one an actual bug. Building a
synthesizer that respects arbitrary declared invariants is real
constraint-aware generation — a separate, larger tool than this.
Defined Under Namespace
Classes: Failure
Class Method Summary collapse
-
.call(dir) ⇒ Object
diris any real, bootable hecks app directory — a saved domain, or a throwaway one a caller rendered into a temp dir for exactly this purpose. -
.isolate!(dir, scratch) ⇒ Object
ONLY THE
.bluebook, NEVER.worldEITHER — a real world's own settings (persisted_by("Heki") { dir "..." }) are keyed to the REAL adapter it names, not to Memory; copied verbatim, they apply to the wrong binding and refuse with aWiringError("Memory does not declare :dir") — measured, not assumed, the first time this ran against Banking. -
.smoke_domain(dispatcher, domain) ⇒ Object
AGGREGATES WALKED IN DECLARATION ORDER, on purpose — so whatever a LATER aggregate's own shape needs from an EARLIER one (a
TripItemneeding a realPackingItem, say) already exists by the time that aggregate's own turn comes, the same reasonSynthesizer#args_for'screatedmap is threaded through in this same order.
Class Method Details
.call(dir) ⇒ Object
dir is any real, bootable hecks app directory — a saved
domain, or a throwaway one a caller rendered into a temp dir
for exactly this purpose. Plain Hecks.boot, not
Router.boot — proven all night, and it works against a
domain with framework-attached chapters (uses_framework "Governance", say) that Router.boot refuses outright: those
chapters carry no .world of their own, which Router's
per-domain realm resolution requires and a plain boot never
did. Reports are reachable the identical way regardless of
boot path — dispatcher.query("Domain.report_name", **args)
is the SAME Dispatcher#query either boot hands back; Router
was never actually required for this, only its OWN namespace
SUGAR (Domain.report_name(...) as a bare method call) was.
NEVER BOOTS dir'S OWN REAL BINDINGS — measured, not a
precaution taken on spec: pointed at examples/pizzas (a real,
persistent store carrying real accumulated records), a
synthesized CreatePizza collided with an actual pre-existing
record literally named "smoke-test" — a coincidence this time,
data pollution the next. isolate! copies only the .bluebook
(and .world, for its realm) into a throwaway directory and
deliberately leaves the real .hecksagon behind — with none
present, Ports::Persistence::BindingPolicy.default_binding
binds every aggregate to Memory on its own, no settings needed.
Whatever dir is really bound to in production is never
touched.
install_facade: false — this only ever dispatches by FQN string
(below), so it never needs the Widget::Item.Add(...) Ruby
sugar Hecks.boot installs by default. Skipping it matters
here specifically: that sugar lands as a BARE global constant on
Object per domain and per aggregate name, with no scoping and
no cleanup — and this tool boots throwaway domains under
whatever generic names the caller's .bluebook happens to use
("Widget", "Item", "Tag" in this tool's own specs). Installing
the facade would leak those names into the rest of the process —
measured, not hypothetical: it once left a stale Widget constant
that corrupted an unrelated spec's own unrelated use of the same
bare name.
85 86 87 88 89 90 91 92 93 94 |
# File 'lib/hecks/bluebook/smoke_test.rb', line 85 def call(dir) Dir.mktmpdir("hecks-smoke-") do |scratch| isolate!(dir, scratch) dispatcher = Hecks.boot(scratch, install_facade: false) domain = dispatcher.registry.bluebooks.keys.first next [] unless domain smoke_domain(dispatcher, domain) end end |
.isolate!(dir, scratch) ⇒ Object
ONLY THE .bluebook, NEVER .world EITHER — a real world's own
settings (persisted_by("Heki") { dir "..." }) are keyed to the
REAL adapter it names, not to Memory; copied verbatim, they
apply to the wrong binding and refuse with a WiringError
("Memory does not declare :dir") — measured, not assumed, the
first time this ran against Banking. A plain Hecks.boot
needs no realm to address anything (unlike Router.boot), so
leaving the world out entirely is the correct minimal isolation,
not a workaround. translations/*.bluebook also deliberately
excluded — those are earlier, superseded versions of the same
domain; smoke-testing the current declaration is the point, not
its whole history.
108 109 110 111 112 113 |
# File 'lib/hecks/bluebook/smoke_test.rb', line 108 def isolate!(dir, scratch) source = Adapters::Folder.new.bluebook_directory(dir) target = File.join(scratch, "bluebook") FileUtils.mkdir_p(target) Dir.glob(File.join(source, "*.bluebook")).each { |file| FileUtils.cp(file, target) } end |
.smoke_domain(dispatcher, domain) ⇒ Object
AGGREGATES WALKED IN DECLARATION ORDER, on purpose — so whatever
a LATER aggregate's own shape needs from an EARLIER one (a
TripItem needing a real PackingItem, say) already exists by
the time that aggregate's own turn comes, the same reason
Synthesizer#args_for's created map is threaded through in
this same order.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/hecks/bluebook/smoke_test.rb', line 121 def smoke_domain(dispatcher, domain) chapter = dispatcher.registry.bluebook(domain) return [] unless chapter created = {} failures = [] chapter.aggregates.each do |aggregate| creating, noncreating = aggregate.commands.partition(&:creates?) creating.each do |command| args = Synthesizer.args_for(chapter, aggregate, command, created) begin result = dispatcher.dispatch("#{domain}::#{aggregate.name}.#{command.hecks_name}", **args) created[aggregate.name] = result.instance.id rescue StandardError => e failures << Failure.new(domain: domain, aggregate: aggregate.name, command: command.hecks_name, error: "#{e.class}: #{e.}") next end noncreating.each do |nc_command| nc_args = Synthesizer.args_for(chapter, aggregate, nc_command, created).merge(id: created[aggregate.name]) dispatcher.dispatch("#{domain}::#{aggregate.name}.#{nc_command.hecks_name}", **nc_args) rescue StandardError => e failures << Failure.new(domain: domain, aggregate: aggregate.name, command: nc_command.hecks_name, error: "#{e.class}: #{e.}") end end end chapter.read_models.each do |model| root_id = created[model.reference_target] next unless root_id # nothing was created for this root — nothing to smoke-test yet, not a failure dispatcher.query("#{domain}.#{model.query_name}", model.reference_name => root_id) rescue StandardError => e failures << Failure.new(domain: domain, aggregate: model.name, command: "report(#{model.name})", error: "#{e.class}: #{e.}") end failures end |