Generated Rails tests
Status: Implemented for the bounded generated application. Broader coverage is an evidence-backed direction.
Every generated application carries its own RSpec suite. The Compiler emits FactoryBot factories, model examples,
request examples, system specs for constructible forms, and query-count checks for selected indexes. Each example
exercises behavior the Compiler actually generated, using records that bounded setup can derive. When setup cannot
supply valid data, the generated README lists the missing test under Test coverage to complete instead of
emitting a placeholder. The application's own bin/ci runs the suite. Application CI owns application behavior,
while First Draft and Core qualify the Dev Container lifecycle and the production-image harness upstream.
Foundation quality (repository-only) states the verification policy these
tests serve, and the Rails target profile routes to each subject's lowering.
On this page
- Test ownership
- Stack and spec paths
- Model examples
- Request specs
- System specs
- Query-count specs
- Test coverage to complete
- Bounded setup and sequences
- Code and checks
Test ownership
Generated application CI owns application behavior, lint, security, setup, and the production-image smoke.
First Draft owns the fixed profile's Dev Container lifecycle qualification upstream. Core tests its exported
application, and the same upstream harness can test an actual committed Compiler output. Its specialized script
and workflow are excluded from the application archive. Applications retain .devcontainer, setup, tests, and CI
without a remote First Draft dependency. After changing dependencies or development-container configuration,
owners rebuild and test that environment or add dedicated coverage; ordinary PR CI does not detect every breakage
there. Compiler identity substitution and composition are qualified here against actual Compiler
output. Core's former Photogram
rename smoke duplicated that substitution with a second implementation; it and the Core layer-inventory test are
retired instead of being carried into each app. The upstream Dev Container lifecycle gate exercises a renamed
checkout, setup, boot, CI, and a warm restart; its separate configuration-only template smoke is redundant.
Application owners can replace the starting home action or add attachment processing without a test requiring the
original template shape. State Machine, Policy, and Scaffold behavior tests remain application-owned.
The application production smoke builds its actual Docker image, runs the normal entrypoint with a disposable
PostgreSQL database, checks applied migrations plus /up and /ready, and requires a non-root runtime user. It
follows the application's declared runtime configuration. Root-page status and a particular non-root UID are not
requirements: an authenticated application may redirect / to sign-in. Application owners test their own jobs and
channels. Core's archive-excluded production harness owns package exclusions, a real production 404, and synthetic
Cache/Cable/Queue concurrency under a named Core configuration. The synthetic job does not ship or reserve an
application model name. Core CI automatically qualifies exported Core; the same harness accepts a clean committed
generated repository. The application smoke does not cover production
page rendering or provider configuration; deployment remains separate.
Applications retain three guards for built-in production Queue worker/dispatcher and Cable polling defaults.
They enforce a conservative one-second floor after clearing environment overrides, explain idle database traffic,
and link the application's editable policy in DEPLOY.md. They measure neither billing nor a universal safe
interval. Default intervals and runtime overrides remain unchanged.
Stack and spec paths
The Rails target emits RSpec examples and FactoryBot factories under ordinary spec/ paths. Core configures the
Rails generators to continue that stack. Factories provide valid model setup; request examples send explicit HTTP
parameters and log in through Rodauth. Factory creation does not stand in for registration or its Sequel write path.
Account request examples separately exercise the default page's guest guard and the emitted Terms/registration
assignments through HTTP. Authored-profile guest coverage is reused. Registration samples must be valid, and the
assignment example needs at least one value distinct from its default; missing setup is disclosed in the README.
The Account owner (repository-only) describes this bounded coverage.
Request and Policy setup uses ordinary FactoryBot records without blanket reloads or preloads. Descriptor reads and
membership assertions retain their intended relationships; application controllers still preload the associations
their requests need. Core's Rails :rescuable test setting renders exceptions with HTTP mappings, such as scoped
404s, and raises unmapped exceptions such as NoMethodError. Resource suites need no global exception override.
Dedicated error-page specs scope production-like presentation and deliberate 500 rendering to their own examples.
Standalone resource forbidden HTML examples check status, format, a nonempty body, and access or nonmutation
assertions. Shared error-page specs own the heading, explanation, and recovery navigation. Non-HTML denials keep
their empty-response checks; hidden scoped records still return 404.
An authored name can coincide with a Core spec basename. In that case its resource suite uses
spec/requests/<collection>/scaffold_spec.rb; an otherwise colliding State Machine suite uses
spec/models/<stem>/state_machine_spec.rb, and a colliding form system suite uses
spec/system/<collection>/forms_spec.rb. RSpec discovers these ordinary subdirectories. Unaffected suites retain
flat paths, and test-file ownership does not reserve additional Entity names or rename application models and routes.
The current Compiler emits ordinary progressing factories and resets only named bounded sequences. It retains Core's
Rails :rescuable test default and ordinary FactoryBot setup. Expected scoped 404s and explicit 403s retain their
checks; unmapped exceptions such as NoMethodError raise.
Model examples
The bounded application layer gives each constructible Entity a factory and model examples for its emitted required
and optional values, normalization, numerical input, References, and supported unconditional validation boundaries.
Ordinary presence, length, and numericality requirements use Shoulda matchers with a valid factory subject where
available. Required numerics use numericality and explicit nil rejection; booleans retain false acceptance.
Optionality uses allow_value(nil).for(:attribute) to check the whole attribute rather than one validator's nil
option. A factory limitation keeps independent attribute examples on described_class.new and discloses examples
needing complete records. Normalization examples use Shoulda's normalize
matcher
with concrete from and to values for the authored pipeline, including blank-to-nil cases. These examples exercise
Rails' public normalizer; future custom setters or persistence interactions need their own application behavior tests.
Assignment and persistence qualification remains in the upstream normalization
smoke (repository-only). An exact length of one combined with
blank-to-nil normalization checks its upper boundary with :wrong_length; the empty lower sample becomes nil, which
the emitted length validator allows. Uniqueness examples create a validated seed and use validate_uniqueness_of on
the authored logical error target, with all physical scope attributes. Case-insensitive columns use
case_insensitive; downcased values use ignoring_case_sensitivity alongside the retained normalization example. A
text seed unchanged by swapcase also uses ignoring_case_sensitivity: duplicate rejection remains checked, but that
seed cannot establish case behavior. When more than one realized uniqueness declaration shares the logical error
target, or that target is a one-to-one Reference, the example instead builds the complete duplicate tuple and checks
:taken on the target. The matcher cannot distinguish overlapping validators with the same error message; changing a
scope can still violate another rule. All model validators remain. Revisit these handwritten fallbacks when the
pinned matcher can isolate overlapping same-message validators, so that matcher examples pass for one Entity that
declares both uniqueness(title, studio) and uniqueness(title). The repeated saved-seed validity check is
omitted; create-time and update-time validation can differ. Existing setup limitations and application-specific
persistence and database checks remain. Date-boundary examples retain explicit Gregorian Date inputs; the comparison
matcher's stringified samples change calendar interpretation near the admitted 1582-10-15 boundary. Realized
Reference deletion rules receive concrete model examples where the existing factories supply their records. They check
cascading removal, retained records with cleared References, or rejected deletion with both rows retained. A cascading
join example also checks that its other required referenced records survive. Restriction follows the realized model
rejection or ordinary foreign-key exception. These examples assert outcomes rather than dependent: options; factory
cycles and unsupported conditional setup retain actionable coverage omissions. State Machine event examples use
ordinary persisted records and event methods; attribute-only checks use new records.
Generated model tests exercise initial state, closed-domain storage, direct-assignment rejection, allowed and rejected events, and transition effects. The Compiler's Oscar runtime qualification checks the AASM method census used by collision admission. Dependency versions belong in the Gemfile and lockfile; generated tests do not freeze a gem's private methods or configuration. Omissions are not duplicated as skipped desired-behavior tests.
Request specs
Policy examples exercise permitted and denied records and actual relation results. Realized Scaffold resources get
complete request examples for their routes where setup is derivable, including persisted writes, invalid required
input, and protected access. Other-Account denial examples name the record owner separately from the signed-in user.
Denied updates retain the expected 404 for scoped lookup or 403 for a Policy gate and compare every persisted
target-record attribute. They intentionally omit a blanket model-row-count invariant; creation count checks and
destroy existence checks remain. Error-kind examples use RSpec's ordinary predicate matcher on record.errors,
keeping the error object and expected attribute/type available in a failure. They do not reduce the requirement to
generic invalidity or translated message copy. Protected descriptors receive their own permitted setup. Examples
assert observable data and access behavior rather than exact scaffold copy, HTML classes, helper inventories, or
default redirect destinations. A collection-level gate denial needs no collection records, so it remains covered even
when a valid collection factory cannot be derived. Member requests and scoped-list exclusions retain the records their
assertions need. Each associated create form receives independent New and create request examples, including
associated-only forms. Each selected standalone POST receives its own coverage independently of New, an authored
return, or associated forms. A partially generated standalone create with no source for its required parent keeps an
application-completion step instead of a false successful example; associated coverage remains independent. Permitted
setup satisfies the parent page, collection gate, and create Policy; collection item filters do not gate New. POST
examples check submitted values and the route-bound parent and current-Account bindings, including competing submitted
IDs. Protected routes receive guest denials and, for a parent record Policy, another Account's parent request. Denied
POSTs preserve the record count. Parent and collection Account gates also get a signed-in denial when a direct Account
Reference rule can independently deny access. Separate parent and collection record Policies and create Policies
needing custom draft setup retain actionable README omissions. Invalid-input examples omit an editable required value,
never the URL-bound parent. No-input POSTs need no model parameter; parent and Account spoof checks remain in the
emitted examples. For CREATE, existing direct or conjunctive Account Reference Policy setup can supply an authorized
editable value. New retains its before-input setup limits. A shared create Policy that needs an editable Account
Reference cannot authorize the empty New record; its omission asks the owner's agent to complete application
form-entry authorization while preserving submitted-record authorization. More fixtures cannot repair that behavior.
Omitting such a Policy input in a standalone or associated POST checks authorization denial rather than 422.
Successful form-write examples require a redirect response with a Location, without fixing the default destination.
Request-only creates check 201 Created for public and otherwise supported protected endpoints; selecting New is not
required. Required-input failures retain 422, while authorization denials retain their own status. A supported,
explicitly authored return gets an ordinary redirect_to expectation for the invoked standalone definition,
associated form, or Account profile. Profile returns take precedence over update returns; dynamic destroy destinations
are captured before deletion. These examples do not follow redirects, so protected return-page setup remains a
separate browser-coverage concern.
System specs
Constructible resource forms also receive owner-local RSpec system specs. They visit the actual new/edit form, address controls by their public HTML names, select native options by value, and check persisted values after submission. These locators preserve repeated labels and authored Unicode whitespace; Core's accessibility checks confirm that controls have labels. Each supported form keeps its valid-first-submission journey. Ordinary rejection and non-persistence remain in model/request examples; required text does not add a generic browser recovery journey. Compiler browser qualification retains representative Turbo error re-render, retained input, correction, and relationship context. Owners add recovery journeys for concrete browser interactions or regressions as needed. This deliberately reduces per-form downstream recovery coverage. Associated creation visits each emitted parent context and checks its persisted inverse Reference. Protected forms use existing factories and bounded Policy setup, then sign in through the browser. Current-Account bindings receive persisted-value assertions. Core supplies Capybara, Selenium, and Turbo synchronization; the Compiler adds no browser framework. These examples do not assert a default redirect, scaffold copy, or layout.
Selection reuses the existing bounded test data. Scalar text, number, checkbox, enum, and time-zone controls are supported; time-zone expectations use the canonical identifier submitted by the select. Date and datetime inputs use Capybara's typed values. Reference selection uses the visible picker and a qualifying related record with a distinguishable descriptor. The native select's public name identifies its enhanced control, and its option value identifies the record label to choose. This does not test calendar navigation or the picker library's primitives.
Conditional validations, unconstructible factories or Policy setup, and inputs without useful bounded values retain
explicit missing browser-coverage disclosures. New forms need authorization before editable values are supplied; setup
cannot replace that application behavior with a permissive draft. Distinct parent and collection record gates need
owner-written setup; a parent's Account environment gate does not qualify its record for a collection gate. A
submit-only form has no generated browser journey. Its continuation suggests a submit-and-result browser example when
the action merits one; it does not ask the owner to invent editable inputs. Request proof remains separate. Associated
form setup includes the parent, create, and collection gates. Collection item filters only select displayed records;
they do not add sign-in or qualifying-record setup for New. The return collection may omit the created record.
Standalone forms account for their explicit return or default show/index page's Policy. Associated forms without an
explicit return use their already-qualified parent collection. An item-scoped index return needs its sign-in setup and
may show an empty collection; an environment gate still needs its Account setup. Direct return setup follows the
selected record: scaffold_record reuses the associated parent's setup, while mutation_record uses the mutation's
own setup. Sharing an Entity and Policy does not qualify a different record. Same-Policy reuse requires authorization
of that record; an Account environment gate qualifies only the Account. A protected return through one unchanged
Reference uses a qualifying related factory. A different Policy on the mutation record, conflicting Policy-owned
References, or longer protected return paths need explicit setup. These records support the submitted form's
navigation without asserting its destination. An edited Reference used by the mutation record's show Policy also needs
qualifying browser input, even when the form and show page use the same Policy. Other request and model coverage,
including denied access, remains independent.
Query-count specs
Selected index requests also compare query growth with n_plus_one_control at two and five records. Their ordinary
FactoryBot setup supplies readable records before measurement; the request must return their text or UUID descriptors.
Selection reuses a complete successful index setup and permits public access, current-Account gates, or a direct
Account Reference rule that can own several records. Account indexes, complex record rules, and any authored
uniqueness in the setup's factory dependencies remain application-owned query-test work. This conservative boundary
avoids exhausting finite sequences or reusing a unique Account relationship; it does not attempt to solve arbitrary
multi-record constraints. Bullet's separate N+1 checks still run across the emitted examples.
Test coverage to complete
When bounded setup cannot supply valid data, the generated README adds a Test coverage to complete section. Each entry names the affected behavior and spec or factory path, the concrete setup limitation, and the owner's next step. Selection and disclosures use the same derivation. Independent runnable examples remain; no empty, skipped, or pending placeholder example substitutes for coverage. Setup omissions describe missing tests. These disclosures do not change the frozen reviewed GapSet or introduce a Foundation Plan testing DSL, a generic constraint solver, or an LLM during Compilation. Straightforward derivable setup belongs in the Compiler.
Bounded setup and sequences
Ordinary unique text uses conventional FactoryBot progression, such as sequence(:title) { |n| "title_#{n}" }.
Reaching the Compiler's 1,000-candidate search budget does not cap an ordinary text sequence without a format
constraint. Dates likewise progress in a direction without an authored limiting comparison when the sampled run
succeeds. These patterns do not promise valid values at every mathematical counter; Rails validation and storage
remain the boundaries if a suite reaches a real limit.
Constraints that end a sampled run early retain finite sequences. Arbitrary formats also retain bounded samples;
1,000 matches do not prove future matches. For a two-character field, the chosen c1 through c9 pattern describes
this setup, not the whole valid domain. Generated RSpec support calls FactoryBot.rewind_sequence(:factory, :field)
only for emitted bounded sequences before each example, because transactions do not reset Ruby sequence state.
Exhaustion within one example remains visible. Ordinary and application-added sequences keep progressing, and an
application without bounded sequences receives no reset support file. The existing missing-setup disclosures remain.
Code and checks
The emitted suite runs through the application's ordinary bin/ci without reading .firstdraft/ or an authoring
context file. Compiler qualification separately checks composition, deterministic paths, dependency and method
censuses, and the exact reviewed GapSet. Its private Minitest helpers belong to this repository and do not require an
emitted Minitest test suite. Broader conditional validation, query, deletion-graph, and application browser coverage
remain follow-up work under #678 (repository-only). The private
AssociatedCollections browser qualification waits for the / destination of its turbo: false Login form before
explicitly auditing that page, so the audit does not scan the old Login document. Its initial Login visit
retains automatic auditing.