Skip to content

Pass environment-derived values into the resolvers their tests steer (#806) - #810

Merged
sehkone merged 4 commits into
mainfrom
sehkone/issue-806
Aug 9, 2026
Merged

Pass environment-derived values into the resolvers their tests steer (#806)#810
sehkone merged 4 commits into
mainfrom
sehkone/issue-806

Conversation

@sehkone

@sehkone sehkone commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Test code in this crate mutated the process environment to steer production functions that read it, serialised behind shared mutexes. In Rust 2024 set_var/remove_var are unsafe, and that lock does not discharge the contract: it serialises only the code that takes the lock, while the tokio runtime, the HTTP client, the env-filtered subscriber and the libc resolver all read the environment from threads that hold nothing.

Every resolver a test steered now takes the value as a parameter, leaving one thin wrapper at the composition root that reads the environment. The variables consulted, their precedence and their fallbacks are unchanged; what moves is where they are read.

  • POSTGRES_*resolve_db_dsn_for_init and resolve_db_dsn receive the six values; init::steps::orchestrator reads them straight after load_dotenv_into_env promotes the compose .env, preserving the precedence the builders rely on.
  • COMPOSE_PROJECT_NAMEcompose_project_name_override takes the override, and both ComposeIdentity::resolve and ComposeIdentity::resolve_for_dir gain an override-taking sibling, so the compose-file resolver test is no longer steered by whatever the invoking shell exported (the E2E harness exports that variable for a whole scenario).
  • OPENBAO_ROOT_TOKENresolve_root_token takes the token.
  • *_HOST_PORThost_port::resolve_host_port, db::resolve_postgres_host_port, the OpenBao URL resolver and rotate::db::resolve_db_admin_dsn take the port. resolve_db_admin_dsn took compose_dir only to derive that port, so the port replaces it; the ca.json fixture that existed to prove no fallthrough went with the directory, since the signature is now what rules it out.
  • load_dotenv_into_env — split into a pure dotenv_pairs_to_apply, which decides which pairs apply (including the COMPOSE_PROJECT_NAME exclusion), and the thin wrapper that keeps the single production set_var with its // SAFETY: comment unchanged.

Deleted as their last users went: compose_project::test_env (ENV_LOCK, env_lock, ScopedEnv, ComposeProjectEnv), openbao_auth's ENV_LOCK/env_lock, init::steps::test_support's ENV_LOCK/env_lock, and the three anonymous OnceLock<Mutex<()>> guards in host_port.rs, openbao_url.rs and db.rs. The rest of init::steps::test_support (default_init_args, test_messages, test_cert_pem) and all of rotate::test_support and verify.rs's ENV_LOCK are untouched — they belong to other issues in this effort.

reinit's scope-check test no longer calls set_current_dir. The working directory is process-global exactly as the environment is, and the . it was reproducing is a property of compose_file_dir, so the test passes the directory in.

Three parameters ended up threaded but steering nothing — status_openbao_url_with_env, init_args_for_reinit's openbao_host_port_env, and every field of HostPortEnv bar postgres could each be replaced with None with the whole suite still green. Each now has an assertion that fails when the value does not reach its resolver.

No new unsafe block, no replacement lock, no --test-threads=1.

Closes #806

Test plan

  • cargo test --bin bootroot passes (980 tests) — the one that actually runs the commands::* tests
  • cargo test --lib passes (400 tests)
  • grep -rn "set_var\|remove_var" over the ten scope-table files returns nothing
  • The same grep over src/commands/dotenv.rs returns only the production call
  • grep -rn "env_lock\|ENV_LOCK\|ScopedEnv\|ComposeProjectEnv\|set_current_dir" over all eleven files returns nothing
  • grep -rn "env_lock" src/commands/init returns only imports and call sites resolving to rotate::test_support
  • rotate::test_support and verify.rs's ENV_LOCK still present and compiling
  • init::steps::test_support still exports default_init_args, test_messages and test_cert_pem to its six importers
  • Converted tests were mutation-checked: passing the wrong value fails the assertion
  • cargo clippy --all-targets -- -D warnings clean
  • cargo fmt -- --check --config group_imports=StdExternalCrate clean
  • Full CI green, including the Docker E2E matrix
  • run-extended from e2e-extended.yml dispatched at the head commit; still running

sehkone added 4 commits August 9, 2026 22:44
In Rust 2024 `set_var`/`remove_var` are unsafe, and the shared mutex
these tests took before each mutation does not discharge that
contract: it serialises only the code that takes the lock, while the
tokio runtime, the HTTP client, the env-filtered subscriber and the
libc resolver all read the environment from threads that hold
nothing.

Every resolver a test steered now takes the value as a parameter, with
one thin wrapper left at the composition root doing the read. The
precedence, the fallbacks and the variables consulted are unchanged;
what moves is where they are read. The `.env` load keeps its single
production `set_var`, with the decision of which pairs to apply split
off into a pure function the tests can drive.

That removes the last user of `compose_project::test_env`, of
`openbao_auth`'s `ENV_LOCK`, of `init::steps::test_support`'s lock and
of the three anonymous `OnceLock<Mutex<()>>` guards, all of which are
deleted — clippy fails on a dead static, so they cannot be left for a
later tidy-up. `reinit`'s scope-check test no longer moves the whole
process into a tempdir either: the working directory is
process-global exactly as the environment is, and the `.` it was
reproducing is a property of `compose_file_dir`, not of where the
process happens to stand.

Closes #806
`resolver_reads_the_dotenv_beside_the_given_compose_file` went through
`ComposeIdentity::resolve`, which is the composition root that reads
`COMPOSE_PROJECT_NAME`. Dropping its `ScopedEnv` left the assertion
steered by whatever the invoking shell exported: with the variable set
the resolver returns the override and the test fails, which is exactly
the ambient dependence the conversion was meant to remove — the E2E
harness exports that variable for a whole scenario.

`resolve` gets the same override-taking sibling `resolve_for_dir`
already has, and the test passes `None` in.

Part of #806
`resolve_db_admin_dsn` no longer takes a compose directory, so the
ca.json the no-source test wrote could not be reached by any code path
the test exercises.  The file proved nothing and its comment claimed
the opposite; what the signature now rules out is stated instead.

Also rewords the `dotenv_pairs_to_apply` summary, which read as a typo.

Part of #806
Mutating the production side showed three of the new parameters were
threaded but never steered anything: `status_openbao_url_with_env`,
`init_args_for_reinit`'s `openbao_host_port_env`, and every field of
`HostPortEnv` bar `postgres`.  Each could be replaced with `None` and
the whole suite still passed, which is the failure mode this work is
supposed to avoid — a resolver that no longer reads the environment
and no test that proves the caller supplies it instead.

Each new assertion now fails when the value does not reach its
resolver.

Part of #806
@sehkone

sehkone commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

[Reviewer Round 1]

No blocking findings. The change follows the issue’s intended shape: the value-steered paths now have parameterized forms (PostgresEnv in src/commands/init/steps/database.rs, *_with_env host-port resolvers, and the OpenBao/compose siblings), while the production wrappers retain the existing precedence reads. The new tests exercise the supplied values rather than ambient state, including the per-service routing assertion in src/commands/infra.rs and the changed-port assertion in src/commands/rotate/db.rs.

I also confirmed the scoped test environment mutations and the associated lock/RAII helpers are gone, reinit no longer changes the process working directory, and the PR links Closes #806 with a test-plan checklist.

@sehkone

sehkone commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

[Review Verdict Round 1: APPROVED]

@sehkone

sehkone commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Suggested squash commit

Title

Pass environment values into the resolvers

Body

Test code steered production functions by mutating the process
environment behind a shared mutex. In Rust 2024 `set_var` and
`remove_var` are `unsafe`, and that lock does not discharge the
contract: it serialises only the code that takes it, while the tokio
runtime, the HTTP client, the env-filtered subscriber and the libc
resolver all read the environment from threads holding nothing.

Every resolver a test steered now takes the value as a parameter,
leaving one thin wrapper at the composition root that reads the
environment. The variables consulted, their precedence and their
fallbacks are unchanged; what moves is where they are read. The
`POSTGRES_*` set reaches `resolve_db_dsn_for_init` and
`resolve_db_dsn` from `init::steps::orchestrator`, which reads it
straight after `load_dotenv_into_env` promotes the compose `.env` so
the builders keep the precedence they rely on. `COMPOSE_PROJECT_NAME`,
`OPENBAO_ROOT_TOKEN` and the `*_HOST_PORT` keys follow the same shape.

`load_dotenv_into_env` is the one function whose job is the mutation,
so it splits instead: a pure `dotenv_pairs_to_apply` decides which
pairs apply, including the `COMPOSE_PROJECT_NAME` exclusion that half
its tests were really about, and the wrapper keeps the single
production `set_var` with its `// SAFETY:` comment unchanged.

`resolve_db_admin_dsn` took `compose_dir` only to derive the host
port, so the port replaces it. That strands the `ca.json` the
no-source test wrote to prove no fallthrough — with no directory to
reach it through, the signature is what rules the fallthrough out.

With their last users gone, `compose_project::test_env`,
`openbao_auth`'s `ENV_LOCK`/`env_lock`, `init::steps::test_support`'s
`ENV_LOCK`/`env_lock` and the three anonymous `OnceLock<Mutex<()>>`
guards in `host_port.rs`, `openbao_url.rs` and `db.rs` are deleted;
clippy would fail on them otherwise. The rest of
`init::steps::test_support`, all of `rotate::test_support` and
`verify.rs`'s `ENV_LOCK` stay for the other issues in this effort.

`reinit`'s scope-check test no longer calls `set_current_dir`. The
working directory is process-global exactly as the environment is,
and the `.` it reproduced is a property of `compose_file_dir`, so the
test passes the directory in rather than moving the process.

Three parameters ended up threaded but steering nothing, each
replaceable with `None` with the suite still green; they now carry an
assertion that fails when the value does not reach its resolver.

No new `unsafe` block, no replacement lock, no `--test-threads=1`.

Closes #806

@sehkone
sehkone merged commit 8c33567 into main Aug 9, 2026
21 checks passed
@sehkone
sehkone deleted the sehkone/issue-806 branch August 9, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pass environment-derived values into the resolvers their tests steer

1 participant