Skip to content

#633 [test] TUI profile 昇格のポート fixture を test transport に移す - #640

Merged
Kewton merged 2 commits into
developfrom
feature/issue-633-test-tui-profile-fixture-test-transport
Oct 9, 2026
Merged

Kewton merged 2 commits into
developfrom
feature/issue-633-test-tui-profile-fixture-test-transport

Conversation

@Kewton

@Kewton Kewton commented Oct 9, 2026

Copy link
Copy Markdown
Owner

概要

TUI の profile 昇格のテスト(tui_slash_promoted_profile_reflected_in_terminal_summary)は、integration test で一度 bind して解放した番号のポートを dev server の子に bind し直させていたため、その番号をほかのプロセスに取られると落ちることがありました(#541 の分割 N)。この PR は、テストを lib のテストに移し、#608 の test transport(子が OS の割り当てたポートに bind し、実ポートを ready ファイルで知らせる)を使います。

変更(対象 4 ファイル、+761/−479)

  • src/tui/profile_transport_tests.rs(新規、全体が #[cfg(test)] mod tests):
    • 移した body(#[ignore] の内側のテスト)。旧 body の assertion 12 個を 1 対 1 でそのまま持つ。加えて、readiness の probe が実の子の HTTP を観測した証跡(browser_probe の status=="ready"・ok・http_status==200・port・requested_port・子の出力)と、子が告知した実ポートが 0 でなく論理ポートと違うことを assert。
    • テストは 127.0.0.1:0 に bind した listener を最後まで握り、その番号を論理ポートにする(番号を解放しない。論理ポートを占有したままで同じ昇格・summary になる)。
    • 外側の必須のテスト(旧と同じ名前、#[ignore] なし)は、自分のテストバイナリを子プロセスとして起動し、内側だけを走らせる。lib の同じプロセスのほかのテストと、pack の環境変数や TUI の共有の状態を取り合わないため。120 秒の上限(超えたら kill・回収して失敗)、exit 0・1 passed・内側の完了の印を確かめ、失敗のときは子の出力を表示する。
    • 偽の dev の子と fake の npm。偽の子は [test] runner dev-server fixture の実ポートを子の ready 通知から解決する #608 の transport のファイルの約束だけを使う(runner の test module の helper は pub(super) で tui から呼べないため。[test] issue475 observer の transport を動的ポート通知へ分離する #622・issue448 と同じ形)。
  • tests/support/tui_profile_fixture.rs(新規):config・FakeClient・FakeUi・tui_command_stop_events を、integration と lib の両方から include! する共通の fixture にした。
  • tests/tui_integration.rs:移した body、共通の fixture、使わなくなった番号解放の helper と #[ignore] の子を削除。
  • src/tui/mod.rs:#[cfg(test)] mod profile_transport_tests; の 3 行だけ(製品側の唯一の差分)。テスト用の環境変数を読むのはこのテストの module の中だけで、通常の library や release binary には入口が無い。
  • 公開 API、製品の既定値、acceptance.rs・runner.rs・cli_pack.rs・presentation.rs は変えていません。#[ignore] でない実のテストは 22 本のまま(integration 23 → 21 は、移した body と使わなくなった #[ignore] の子)。

検証

  • commandmate verify の 15 ゲート:develop ddcb8022 を取り込んだ HEAD で 15/15 pass(075bca1a、scope 4 本、違反 0)。
  • cargo test --test tui_integration:取り込んだ HEAD で exit 0、21 passed・0 failed。cargo test --lib tui:: を 20 回:落ちた回 0/20(新しいテストは 20/20 回 pass)
  • 二点測定(HEAD をコピーし、1 か所ずつ変えた):
    • cfg(test) の probe を有効にする印を書かない(probe が無効のまま)→ FAIL(内側が落ち、外側が子の出力を表示)。
    • transport を外す(子が、テストの握る論理ポートに bind し直す)→ FAIL。
  • 同時実行:受け入れ条件の「2 checkout 同時」は、同じビルドの lib のテストバイナリを 2 プロセス同時に 3 回走らせることで代えました。3 回とも両方 exit 0。
  • 子プロセスで隔離する前は、cargo test --lib tui:: を 20 回で 13 回落ちました(presentation の snapshot のテストと、このテスト)。隔離した後は 0/20 です。

レビュー

テストだけの変更です(製品側は cfg(test) の module の宣言だけ)。merge 前レビューは行いません(利用者承認済み)。

Refs #633
Refs #541

🤖 Generated with Claude Code

Kewton and others added 2 commits October 9, 2026 10:39
Move the profile-promotion body out of tests/tui_integration.rs into the
required library test tui::profile_transport_tests, driving the fake dev
child through the Issue #608 OS-assigned port transport instead of a
freed-and-rebound local port. The logical port, profile promotion, stage,
terminal summary, and evidence assertions are preserved one-to-one.

Extract the shared Config builder, FakeClient, FakeUi, and command-event
helper into tests/support/tui_profile_fixture.rs and include the same file
from both the integration test and the library test.

The fake child is owned by the test module and speaks only the transport's
file contract (the workspace marker, the generation file, the
{"generation","port"} ready file, and the browser probe's mock-port file);
it binds an OS-assigned port and announces it, so no product code or
visibility change is needed. Mirrors the Issue #622/#448 test transports.

Process isolation (PM 案 A): the required #[test] keeps its name but no
longer runs the flow in the lib test process. It re-execs this binary with
`--ignored --exact <inner> --nocapture --test-threads=1` and the entry env
variable COMMANDAGENT_TUI_PROFILE_TRANSPORT_INNER=1, so the moved body (an
#[ignore]d inner test gated by that variable, like the fake dev child) runs
in its own process. The outer waits a bounded 120s, kills and reaps on
timeout, drains stdout/stderr on reader threads, and requires: child exit
status 0, child stdout containing "1 passed" (so --exact matching zero
tests cannot pass), and the inner completion marker file the inner writes
after its final assertion (path passed from outside the tempdir). On failure
it panics with the child's stdout and stderr.

The test itself binds 127.0.0.1:0, keeps the listener alive to the end, and
uses that number as the logical port in the goal, package.json (`-p`), and
the requested_port assertions. The child's announced port (read from
browser-probe-mock-port.txt) is asserted non-zero and different from the
logical port.

assertion 対応 (旧 tests/tui_integration.rs == 新 inner body, 1 対 1):
  1  output.contains("ultra-plan-run complete")
  2  events contains "event":"profile_reinferred"
  3  events contains "profile":"nextjs"
  4  events contains "requested_port":"<port> (goal)"
  5  tui_command_stop.profile == "nextjs"
  6  tui_command_stop.effective_profile == "nextjs"
  7  tui_command_stop.contract_origin == "promoted_union"
  8  tui_command_stop.assurance_level == "full"
  9  tui_command_stop.requested_port == "<port> (goal)"
  10 summary contains "Profile promoted: generic -> nextjs"
  11 summary contains "Profile: nextjs"
  12 summary contains "Requested port: <port> (goal)"
  追加(旧 body に無い証跡 assertion、既存は弱めていない):
  browser_probe event の status=="ready" / ok==true / http_status==200 /
  port==<logical> / requested_port=="<logical> (goal)" / output_excerpt 非空
  子が browser-probe-mock-port.txt に告知した実ポート != 0 かつ != logical
  外側: 子の exit status 0 / 子の stdout に "1 passed" / inner の完了マーカー

読み替え: 「モジュール配線は tui の親へ置く」は新 test file の親モジュール src/tui/mod.rs への mod 宣言と読んだ
判断: dev child は本 test module が所有し、#622/#448 と同じ file 契約だけを使う。test_transport の enable/bind は runner test module の pub(super) で scope 内から呼べないため、製品側を変えず契約経由にした
判断: 移した body は #[ignore] の内側に置き、外側の必須 #[test] が current_exe を再実行して内側だけを走らせる。lib の同一プロセスで共有状態を取り合わないため(案 A、案 B は採らない)
判断: 論理ポートはテスト内で bind(0) した listener を終了まで保持して決め、その番号を goal/package.json/assert に使う。固定番号を使わず、占有したままでも同一の昇格・summary になることを test 自身が確認し、2 プロセス同時でも衝突しない
判断: generality_guardrails の nextjs literal 監査は scope 外のため編集せず、lib test module 全体を #[cfg(test)] mod tests で包み test 行として分類させた
本文に無い指摘: この TUI flow の readiness は minimal_loop::browser_probe で、acceptance.rs の dev_server_lifecycle(start/wait/probe/cleanup) は出ない。よって実子起動と HTTP 観測は browser_probe event の status/ok/http_status/output_excerpt で assert した
本文に無い指摘: browser_probe の dynamic_port には .anvil/evidence/browser-probe-command.json の上書きが要る(無いと論理ポートを poll して 30s timeout)
本文に無い指摘: lib の filter `tui::` 反復で 13/20 落ちた原因は (1) tui::repl が cli_pack の lock 下で COMMANDAGENT_PACK_* を一時設定すること、(2) 新テストの flow が presentation の共有状態を書き換えること。内側の別プロセス化で回避した

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…issue-633-test-tui-profile-fixture-test-transport
@Kewton
Kewton merged commit 66fbb07 into develop Oct 9, 2026
5 checks passed
@Kewton
Kewton deleted the feature/issue-633-test-tui-profile-fixture-test-transport branch October 9, 2026 10:05
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