Skip to content

test: pytest のセッション全体で DEVBASE_ROOT を隔離する (#209) - #217

Merged
takemi-ohama merged 2 commits into
release/v3.7.0from
feature/v3.7.0-test-env-isolation
Sep 22, 2026
Merged

takemi-ohama merged 2 commits into
release/v3.7.0from
feature/v3.7.0-test-env-isolation

Conversation

@takemi-ohama

@takemi-ohama takemi-ohama commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request

概要

pytest は実行したシェルの DEVBASE_ROOT を継承する。tmp の root を作るだけで setenv しない
fixture(tests/conftest.py の openbao_root、tests/env/test_backend_config.py の root など)
を使うテストが、自前の monkeypatch.setenv('DEVBASE_ROOT', ...) を忘れると、実環境の
devbase の projects/ と secrets/backend.yml を読む
。ホストの Mac のシェルは
DEVBASE_ROOT を持ち、dev コンテナの中は持たないため、環境で再現したりしなかったりする。

fixture 1 つに setenv を足しても同じ穴は他にも開いたままになる(DEVBASE_ROOT を setenv
しない tmp の root の fixture は openbao_root だけではない)。そこで tests/conftest.py に
autouse fixture を置き、pytest のセッション全体で DEVBASE_ROOT を隔離する。

採ったのは issue #209 の案 B(tmp のディレクトリへ setenv)で、案 A(delenv)ではない。
_require_devbase_root の「未設定」分岐を試すテストが、その場で delenv して書けるように
するためである。案 A だと未設定が既定になり、設定済みの分岐を試す側が毎回 setenv を書くことに
なって現状と変わらない。

このブランチの起点は release/v3.7.0 で、release PR は #212 である。

関連 Issue

変更点

  • tests/conftest.py に autouse fixture _isolate_devbase_root を追加した。
    tmp_path_factory.mktemp('devbase-root') が作るテストごとの空のディレクトリへ
    DEVBASE_ROOT を setenv する
  • 触ったファイルは tests/conftest.py の 1 つだけ(+23 行)。lib/ bin/ containers/ は触っていない

設計上の 3 点

決めたこと 理由
未設定(delenv)ではなく tmp のディレクトリを指す 設定済みを既定にする。未設定を既定にすると、設定済みの分岐を試す側が毎回 setenv を書くことになる(#209 の案 B)
テストごとに別のディレクトリにする setenv を忘れたテストがそこへ書いても、隣のテストへ漏らさない
既存の 32 か所 / 15 ファイルの setenv は 1 行も書き換えない autouse の fixture は同じ scope の明示の fixture より先に立つため、テストの側の setenv が後勝ちでそのまま働く

動作確認

release/v3.7.0 を base にした Pull Request では CI が 1 件も動かない
(.github/workflows/ci.yml の on.pull_request.branches が main だけ。gh pr checks は
no checks reported を返す。起票済み: #216)。検証はすべて手元で実行した。

実行環境: macOS(Darwin 25.6.0) / uv run pytest。

1. テスト全体が 2 つの条件で同じ結果になること(本命)

偽の root は secrets/backend.yml(version: 2 / layout: group / group_aliases: {default: nyle})と
projects/fake-a projects/fake-b .env を持つ、リポジトリの外のディレクトリである。

# 条件 実行 結果 exit
A 修正前 / DEVBASE_ROOT 未設定 env -u DEVBASE_ROOT uv run pytest -q 2863 passed in 122.75s 0
B 修正前 / 偽の root DEVBASE_ROOT=<偽の root> uv run pytest -q 2863 passed in 126.46s 0
C 修正後 / DEVBASE_ROOT 未設定 env -u DEVBASE_ROOT uv run pytest -q 2863 passed in 135.73s 0
D 修正後 / 偽の root DEVBASE_ROOT=<偽の root> uv run pytest -q 2863 passed in 132.91s 0

C と D が同一であり、かつ A・B から変わっていない。

$ env -u DEVBASE_ROOT uv run pytest -q      # C
........................................................................ [ 98%]
.......................................................                  [100%]
2863 passed in 135.73s (0:02:15)
exit=0

$ DEVBASE_ROOT=<偽の root> uv run pytest -q  # D
........................................................................ [ 98%]
.......................................................                  [100%]
2863 passed in 132.91s (0:02:12)
exit=0
  • env -u DEVBASE_ROOT uv run pytest -q(exit=0)
  • DEVBASE_ROOT=<外側の偽の root> uv run pytest -q(exit=0)
  • 上の 2 つの結果が同一である

件数について(issue #209 の完了の目安との違い)

issue #209 の完了の目安は 1 failed, 2861 passed, 1 skipped である。これは dev コンテナ
(Ubuntu。本文の /tmp/pytest-of-ubuntu がその出所)で測った値で、この Mac では 4 条件
すべてが 2863 passed
になる。tests/snapshot/test_restore_incremental.py::test_a_generation_with_swapped_directories_restores_completely
がここでは落ちず、skip される 1 件も skip されないためである。

総数は 2861 + 1 + 1 = 2863 で一致しており、テストは 1 件も増減していない。 目安の主旨で
ある「隔離を入れてもテスト全体の結果が変わらないこと」は、同じ機械で測った A・B と C・D の
比較で満たしている。

2. 隔離が実際に効いていること(1 だけでは示せない)

1 の 4 条件はすべて緑である。issue の言うとおり穴は完全に潜在しているため、
1 は「壊していないこと」しか示さない。効いていることは、openbao_root だけを使って自前の
setenv をしない使い捨てのテストで確かめた(コミットしていない。下記「テストを足さなかった理由」)。

def test_probe_openbao_root_does_not_see_the_outer_root(openbao_root):
    from devbase.commands import container
    root = Path(os.environ['DEVBASE_ROOT'])
    store = container._grouped_store()
    assert store is None
    assert not (root / 'secrets' / 'backend.yml').exists()
    assert not (root / 'projects').exists()

def test_probe_delenv_still_works(monkeypatch):          # 未設定の分岐を試す道が残ること
    monkeypatch.delenv('DEVBASE_ROOT', raising=False)
    assert 'DEVBASE_ROOT' not in os.environ

def test_probe_setenv_wins(tmp_path, monkeypatch):       # テストの側の setenv が後勝ちであること
    monkeypatch.setenv('DEVBASE_ROOT', str(tmp_path))
    assert os.environ['DEVBASE_ROOT'] == str(tmp_path)
# 修正前 + 偽の root
$ DEVBASE_ROOT=<偽の root> uv run pytest -q tests/_probe_issue209.py
E       AssertionError: _grouped_store() が外側の root を見た:
E         root=<偽の root> store.root=<偽の root>
E       assert <devbase.env.secret_store.SecretStore object at 0x108d15940> is None
1 failed, 2 passed in 0.06s
exit=1

# 修正後 + 偽の root
$ DEVBASE_ROOT=<偽の root> uv run pytest -q tests/_probe_issue209.py
...                                                                      [100%]
3 passed in 0.04s
exit=0

issue #209 の「実環境を読むことの確認」の再現(修正前は _grouped_store() が偽の root を
指し、修正後は None を返す)である。delenv と後勝ちの setenv も同時に確かめている。

3. 静的解析

$ uvx ruff check --select=E9,F63,F7,F82 tests/conftest.py lib   # CI の lint ジョブと同じ選択
All checks passed!
exit=0

やらないこと

やらないこと 理由
既存 32 か所 / 15 ファイルの monkeypatch.setenv('DEVBASE_ROOT', ...) の整理 後勝ちでそのまま働く。#209 の範囲外
DEVBASE_ROOT 以外の環境変数の隔離(DEVBASE_ACCOUNT_GROUP DEVBASE_AGE_KEY_FILE COMPOSE_PROJECT_NAME XDG_CONFIG_HOME EDITOR SHELL DEVBASE_OPEN_INDEX DEV_SERVICE_NAME PWD) #209 の完了の目安が DEVBASE_ROOT の隔離だけを対象にしている。#218 として起票した
tests/cli/conftest.py の wrapper 側の仕組みの変更 同じ危険を別の手(wrapper 自身の代入)で既に塞いでいる
pyproject.toml の [tool.pytest.ini_options] に環境変数を書く仕組みの導入 追加の依存(pytest-env)が要る

テストを足さなかった理由

隔離そのものを確かめるテスト(上の探り)を tests/ へ残すと、テストの件数が 1 件以上増える。
issue #209 の完了の目安が件数を含めて変わらないことを求めているため、探りは実行して
証跡だけを残し、コミットしていない。

補足

🤖 Generated with Claude Code

pytest のセッション全体で DEVBASE_ROOT を隔離する作業の口を開ける。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pytest は実行したシェルの環境を継承するため、tmp の root を作るだけで setenv しない
fixture を使うテストが、自前の `monkeypatch.setenv('DEVBASE_ROOT', ...)` を忘れると
実環境の devbase の `projects/` と `secrets/backend.yml` を読む。

fixture 1 つに setenv を足しても同じ穴は他にも残るため、`tests/conftest.py` に autouse
fixture `_isolate_devbase_root` を置き、テストごとの空の tmp の root へ固定する
(#209 の案 B)。autouse は同じ scope の明示の fixture より先に立つので、既存の 32 か所の
setenv は書き換えずに後勝ちで働き、未設定の分岐を試す道 (delenv) も残る。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 1 | kiro | APPROVE

単一ファイル・+23 行の、明確に scope された test 基盤の修正。設計・正確性ともに問題なし。修正提案なし。

確認した点:

  • 漏れの前提が実在する。tests/conftest.py:392 の openbao_root など、tmp の root を作るが DEVBASE_ROOT を setenv しない fixture が実際に存在する。
  • autouse (function scope) + function-scope の monkeypatch の組み合わせは正しい。test 側の monkeypatch.setenv/delenv が後勝ちで働くことを、tests/cli/test_project_dispatch.py:538 の delenv 分岐テスト等で確認。
  • 別の空 tmp dir を指す設計は無害。openbao 系テストは SecretStore(openbao_root) のように path を明示で渡すため、os.environ['DEVBASE_ROOT'] に依存しない。
  • 手元検証: uvx ruff check --select=E9,F63,F7,F82 tests/conftest.py 通過。fake の DEVBASE_ROOT を設定した状態で代表 163 件 (project_dispatch / openbao / secret_injection) を実行し全 pass を再現。
  • 回帰テストを追加しない判断は #209 の「件数不変」要件に沿った妥当なトレードオフとして説明されている。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 1 | agy | APPROVE

指摘事項はありません。テスト環境における DEVBASE_ROOT の隔離実装および後勝ち・未設定分岐の挙動に問題がないことを確認しました。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 2 | kiro | APPROVE

tests/conftest.py の autouse fixture でセッション全体の DEVBASE_ROOT を隔離する変更は目的と範囲が一貫している。4 条件 (修正前後 × 未設定/偽 root) で 2863 件すべて緑という検証で、fixture 順序(autouse が先に立ち、テスト側の setenv/delenv が後勝ち)という load-bearing な前提が実証されている。既存 32 か所の setenv や _require_devbase_root 未設定分岐のテストも、テスト本体で自前の setenv/delenv をするため整合する。修正すべき点は見当たらない。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 2 | codex | APPROVE

修正を求める指摘はありません(関連する既存テスト 268 件成功、全件再実行は未実施)。

@takemi-ohama

Copy link
Copy Markdown
Contributor Author

完了判定(/ndf:quality-gates・モード light)

release/v3.7.0 を base にした Pull Request では CI が 1 件も動かない(#216)ため、手元で実行した。
実行環境: macOS(Darwin 25.6.0) / uv run pytest / uvx ruff。作業ツリー .worktrees/feature/v3.7.0-test-env-isolation(HEAD = 3b35134)。

検証結果

段階 コマンド 対象範囲 実行時刻 結果
全体テスト(受け入れ条件の確認し直し) DEVBASE_ROOT=<偽の root> uv run pytest -q tests/ 全体 2026-09-22 06:41 2863 passed in 130.22s / exit=0
静的解析 uvx ruff check --select=E9,F63,F7,F82 tests/conftest.py lib 変更ファイル + lib 2026-09-22 06:45 All checks passed! / exit=0
静的解析 python3 -m compileall -q lib bin lib bin 2026-09-22 06:45 出力なし / exit=0

light が求める「変更箇所が読み込まれる経路を 1 度実行する」は、tests/conftest.py が
すべての pytest 実行で読み込まれるため、上の全体テストが兼ねている。

受け入れ条件

受け入れ条件: 1/1 満たす。

件数について

issue #209 の完了の目安 1 failed, 2861 passed, 1 skipped は dev コンテナ(Ubuntu)で
測った値で、この Mac では 2863 passed(2861 + 1 + 1)になる。総数は一致しており、
テストは 1 件も増減していない。詳細は本文「件数について」を参照。

実装レビュー(/ndf:cross-review)

2 ラウンドで収束した。母集合の 3 者(codex / agy / kiro)すべてがこの差分を見ている。

round レビュー 指摘
1 agy=APPROVE / kiro=APPROVE 0 件
2 codex=APPROVE / kiro=APPROVE 0 件

最終スイープ後の未解決スレッド: 0 件(GitHub 側で確認済み)。

その他

🤖 Generated with Claude Code

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.

1 participant