Manifest and module tests
rivet test compiles a configuration or module with saved facts and chosen variables, then checks the resulting catalog: which resources exist, their properties and content, notifications, ordering, and outputs. It never touches a host, so it runs on macOS or Linux without root, and fits in CI.
Tests check what you declared, not what happens on a real host. To check actual convergence, restarts, and recovery, apply to a disposable Linux machine.
Files and scenarios
Place definitions in tests/*.rivettest.hcl immediately below a configuration or module directory. By default, only the directory passed to rivet test is searched. --recursive also finds suites in child directories, even when the root has no suite. Files are sorted by full path, with tests in declaration order. Discovery skips .git, .rivet, target, node_modules, .cache, and the contents below tests/, so downloaded modules, generated files, and fixture roots are not run. Directory symlinks below the root are not followed, and a symlinked tests directory is rejected. Keep test HCL inside tests/: the normal manifest loader reads every immediate *.hcl file in its source directory.
source is required on each suite and points at the configuration directory under test. All filesystem paths resolve relative to the test file, including paths on individual tests. Normal source-package and asset confinement still applies: tests do not widen a module’s allowed filesystem boundary. To test a module that needs siblings or resource-handle arguments, use a fixture root that invokes it with normal module declarations.
Every test compiles a fresh catalog. Suite variables are defaults for tests; a test replaces individual variables. Map/object variables are replaced as whole values, with no implicit deep merge. Manifest variable defaults apply when neither scope supplies a variable. Inputs and expected values support literal strings, finite numbers, booleans, nulls, lists, and objects; expressions, functions, hooks, and ambient environment inputs are not test values. Selectors are the separate address syntax described below.
facts_file accepts the same JSON target assumptions or structured facts snapshot as rivet compile --facts. A test may replace its suite’s facts_file. Without one, compilation uses Rivet’s fixed Ubuntu baseline and empty inventory; it never collects facts from the machine running the test. A missing fact remains a compile error. Trusted authentication claims are not supplied by a saved inventory snapshot.
Suite names must be unique within a file and test names within a suite. A test without expectations checks compilation success. --filter matches case-sensitive text in suite > test [variant]. An empty discovery or filtered run fails unless --allow-empty is set.
Named fact sets
Use fact_sets to run the same tests against multiple complete saved snapshots:
fact_sets is a nonempty map from names to fixture paths, relative to the test file. Each fixture uses the same assumptions or snapshot format as facts_file. Names must be nonempty and contain no control characters. A suite accepts either facts_file or fact_sets, not both. A test with its own facts_file runs once against that file; other tests run once per named set, in name order. Every expanded case gets a fresh compilation. Fixture names do not change architecture, inventory, or capabilities automatically.
The example runs three cases: the service test on each fact set, then the pinned ARM test once. Text reports append [ubuntu_arm64] or [ubuntu_x86_64]; JSON keeps the original test name and supplies a separate variant field. --filter ubuntu_arm64 selects that variant’s cases. Setup errors remain attached to the failing variant and cannot satisfy expect_error; other selected cases continue.
Resource selectors
Both forms select the same resource:
Exactly one selector is required: an address label or resource attribute. Parentheses around a selector do not change its meaning, including in output assertions. Quoted address text preserves literal template markers in names. Unquoted selectors avoid escaping when names contain paths or expansion keys:
Selectors identify declarations, not physical paths. file["/etc/myapp/config.conf"] selects a resource named /etc/myapp/config.conf; it does not find any file managing that path. Resource names accept identifier labels or normalized absolute paths. The manifest must still declare its path explicitly. Module, variable, and output labels retain identifier rules. Named keys (["0"]) and numeric count slots ([0]) are distinct.
Tests may inspect the compiled resources inside module instances. This does not allow production manifests to read private child declarations. When a module is compiled directly as the test source, its resources have root addresses such as user.account.
present = false checks that the declaration is missing from the catalog. It differs from attributes = { ensure = "absent" }, which checks a declaration that requests removal of a host object. An absence expectation cannot also assert properties.
Assertions
attributeschecks only the named properties of compiled desired state. Other properties are allowed. Values use canonical declaration vocabulary: file modes are four-digit octal strings, service names include.service, and service activity usesensure(thestatealias is also accepted). Unmanaged nullable fields compare as null. Asset acquisition inputs such as filesourceandchecksumare not retained as compiled properties; check content instead.metadatachecks selected effective fields:description,noop,sensitive,tags, andtimeout_ms. Tags are the complete sorted list when asserted.content_equalschecks exact UTF-8 file content, including whitespace and trailing newlines. It works for inline content, templates, local sources, and fixture-backed cloud sources.content_containsis a list of substrings that must all appear in the content.content_not_containsis a list of substrings that must all be absent. Like positive content checks, it fails for unmanaged or non-UTF-8 content. Binary content cannot pass anattributes = { content = null }assertion.notifiesis a list of resource selectors requiring notification relationships. Bothnotifyandsubscribedeclarations satisfy it. An ordering edge alone does not.does_not_notifyforbids notification relationships to the listed resources.notifies_onlychecks the exact set of notification targets, ignoring duplicate entries and duplicate edge reasons.notifies_only = []requires no notification targets. It cannot be combined withnotifiesordoes_not_notifyin the same block.ordered_beforeis a list of resource or module selectors. It checks reachability through the full graph, including module entry and completion barriers. It does not assert an incidental position in a topological sort.not_ordered_beforerequires no ordering path to each listed resource/module. It does not require reverse ordering. Targets of positive and negative relationship assertions must exist; a misspelled target fails the test.expect_output "name"checks root output presence. Optionalequalsaccepts a literal scalar or collection or one resource selector; optionalsensitivechecks its sensitivity flag. Literal collections compare exactly, including list order. Resource outputs compare structured identities, not strings.
All failed expectations are reported. Reports include test locations and resource declaration locations when available. Public mismatches show expected and actual values, truncated when large. Sensitive attributes, outputs, and content redact both expected and actual values in text and JSON.
Except for the exact-set notifies_only matcher, an empty matcher list means no checks. For example, notifies = [] does not assert that the resource has no notification targets.
Resource counts
expect_resources counts matching declarations in the compiled catalog:
Require exactly one selector, type or resource, and a nonnegative integer count. type counts that registered resource type across the entire catalog, including child modules. resource matches a structured address and may use [*] at any module or resource expansion position. A wildcard matches any named key or numeric slot, keeping those key types distinct when a concrete key is supplied. It matches expanded instances only; omit [*] to select an unexpanded declaration. Module depth, declaration labels, and resource types must match exactly. Path labels use the same bracket syntax as singular selectors.
These patterns do not evaluate HCL expressions and do not enable splats in manifests. Singular resource expectations and relationship selectors still require concrete addresses. On a mismatch, the report shows the actual count and up to 50 matching addresses. Counting is the plural block’s only assertion; use expect_resource to check an individual instance’s properties.
Expected compilation failures
expect_error must be the test’s only expectation and requires a nonempty substring. It matches the compiler’s diagnostic message, excluding its location. Compilation success or an unrelated diagnostic fails the test. Broken test definitions, missing fixture or source files, unavailable or unprepared modules, and asset-loading errors are setup failures and cannot satisfy it. This includes files read by file() and templatefile(). The compiler classifies acquisition failures separately from configuration diagnostics; checksum mismatches remain configuration diagnostics that a test may expect.
Offline assets and modules
Run rivet init separately for pinned remote modules. Tests reuse the normal lockfile and cache integrity checks and never fetch missing modules.
Cloud asset acquisition is disabled. Supply local files in their place:
A test can replace individual suite cloud_assets mappings. Fixture bytes still pass the manifest’s required SHA-256 checks. Paths must identify regular files, not symlinks. File validation commands and exec guards are compiled but never executed.
Results and bounds
Exit codes are 0 for success, 1 for failed expectations or unexpected compilation failures, and 2 for invalid definitions, setup errors, or an empty run. JSON output contains passed, failed, errors, and an ordered tests array with per-case failures and source locations. Cases using a saved snapshot include facts_file; named fact-set cases also include variant. Invalid test definitions are reported on stderr before cases run.
One run accepts at most 256 definition files, 16 MiB of combined test HCL, and 1000 selected cases after fact-set expansion and filtering. The case budget is checked before any compilation. Recursive discovery is bounded to 10000 directories and depth 64. HCL nesting is bounded before parsing. Cloud fixture bytes are bounded to 256 MiB per case, and normal compiler/catalog/bundle limits still apply. Tests currently have no nested contexts, shared examples, module mocks, variable-set products, or whole-catalog snapshots.
Approved plugins
Use rivet --plugin-lock /absolute/path/to/plugins.lock.json test ./site to include installed resource definitions, service and package providers, and functions. The policy is host-owned; a test suite cannot authorize an executable. Provide saved facts with the required plugin capabilities rather than probing a runtime.
Plugin resources expose declaration-facing attributes and bundled content through ResourceDefinition::test_view over definition RPC. The same count, relationship, content, output, and sensitivity assertions apply to built-in and external resources. Service assertions include provider plus normalized provider options under the provider’s child-block name.
Plugin definition code executes during testing, but Rivet sends no runtime, action, apply, or recovery requests. Keep definition and projection hooks pure. Transport failures are setup errors and cannot satisfy expect_error; projection failures remain visible in the test report. See subprocess plugins for installation, and resource authoring for how resources expose test attributes.