From 13d0053036785955fde75bc534c5b1f1d7ff6bb7 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 00:54:41 +0900 Subject: [PATCH 1/3] test: build e2e yoi binary provider --- .../artifacts/implementation-report.md | 21 +++ .yoi/tickets/00001KV0TJVN5/item.md | 2 +- .yoi/tickets/00001KV0TJVN5/thread.md | 29 +++ tests/e2e/src/lib.rs | 175 ++++++++++++++++-- tests/e2e/tests/panel.rs | 4 +- 5 files changed, 211 insertions(+), 20 deletions(-) create mode 100644 .yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md diff --git a/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md b/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md new file mode 100644 index 00000000..fc30991e --- /dev/null +++ b/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md @@ -0,0 +1,21 @@ +Implementation report for Ticket 00001KV0TJVN5 + +Files changed: +- `tests/e2e/src/lib.rs` + - Added a cached e2e binary provider using `OnceLock`. + - Preserves `YOI_E2E_BIN=` as the explicit override and skips the default cargo build provider in that path. + - Default path runs `${CARGO:-cargo} build -p yoi --features e2e-test --bin yoi` from the workspace root, then returns the direct `target/{profile}/yoi` binary path for PTY spawning. + - Writes `target/e2e-artifacts/binary-provider.json` and emits diagnostics with provider, build command, and binary path. + - Expanded command-failure diagnostics to include command args. +- `tests/e2e/tests/panel.rs` + - Updated panel tests to use the fallible cached binary provider. + +Validation: +- `cargo fmt --check` — passed. +- `git diff --check` — passed. +- `cargo check -p yoi-e2e --all-targets --features e2e` — passed. +- `unset YOI_E2E_BIN && cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; default provider built the current `yoi` binary and PTY-spawned `target/debug/yoi`. +- `YOI_E2E_BIN=/home/hare/Projects/yoi/.worktree/e2e-binary-provider/target/debug/yoi cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; override provider path used without invoking the default cargo-build provider. + +Remaining gaps: +- None known. diff --git a/.yoi/tickets/00001KV0TJVN5/item.md b/.yoi/tickets/00001KV0TJVN5/item.md index 5a84e797..a2fb4a8b 100644 --- a/.yoi/tickets/00001KV0TJVN5/item.md +++ b/.yoi/tickets/00001KV0TJVN5/item.md @@ -2,7 +2,7 @@ title: 'E2E harness が最新 yoi binary を自動 build して使うようにする' state: 'inprogress' created_at: '2026-06-13T15:46:07Z' -updated_at: '2026-06-13T15:47:00Z' +updated_at: '2026-06-13T15:54:18Z' assignee: null readiness: 'ready' queued_by: 'yoi ticket' diff --git a/.yoi/tickets/00001KV0TJVN5/thread.md b/.yoi/tickets/00001KV0TJVN5/thread.md index c793ea0b..f24a87fe 100644 --- a/.yoi/tickets/00001KV0TJVN5/thread.md +++ b/.yoi/tickets/00001KV0TJVN5/thread.md @@ -83,4 +83,33 @@ Escalate if: Ticket evidence、existing E2E harness code map、Orchestrator worktree clean state を確認した。ユーザーが明示的に修正を依頼しており、blocking relation はない。実装 side effect の前に inprogress acceptance を記録する。 +--- + + + +## Implementation report + +Implementation report for Ticket 00001KV0TJVN5 + +Files changed: +- `tests/e2e/src/lib.rs` + - Added a cached e2e binary provider using `OnceLock`. + - Preserves `YOI_E2E_BIN=` as the explicit override and skips the default cargo build provider in that path. + - Default path runs `${CARGO:-cargo} build -p yoi --features e2e-test --bin yoi` from the workspace root, then returns the direct `target/{profile}/yoi` binary path for PTY spawning. + - Writes `target/e2e-artifacts/binary-provider.json` and emits diagnostics with provider, build command, and binary path. + - Expanded command-failure diagnostics to include command args. +- `tests/e2e/tests/panel.rs` + - Updated panel tests to use the fallible cached binary provider. + +Validation: +- `cargo fmt --check` — passed. +- `git diff --check` — passed. +- `cargo check -p yoi-e2e --all-targets --features e2e` — passed. +- `unset YOI_E2E_BIN && cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; default provider built the current `yoi` binary and PTY-spawned `target/debug/yoi`. +- `YOI_E2E_BIN=/home/hare/Projects/yoi/.worktree/e2e-binary-provider/target/debug/yoi cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; override provider path used without invoking the default cargo-build provider. + +Remaining gaps: +- None known. + + --- diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs index 5c26b6e0..4214d837 100644 --- a/tests/e2e/src/lib.rs +++ b/tests/e2e/src/lib.rs @@ -10,7 +10,7 @@ use std::os::fd::{AsRawFd, FromRawFd}; use std::path::{Path, PathBuf}; use std::process::{Child, Command, ExitStatus, Stdio}; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -23,12 +23,42 @@ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0); pub type Result = std::result::Result; +#[derive(Clone, Debug, Serialize)] +pub struct BinaryProviderInfo { + pub provider: String, + pub binary: PathBuf, + pub workspace_root: PathBuf, + pub cargo: Option, + pub build_args: Vec, + pub build_command: Option, + pub profile: String, +} + +impl BinaryProviderInfo { + fn log(&self) { + match &self.build_command { + Some(command) => eprintln!( + "yoi-e2e binary provider={} command={} binary={}", + self.provider, + command, + self.binary.display() + ), + None => eprintln!( + "yoi-e2e binary provider={} binary={}", + self.provider, + self.binary.display() + ), + } + } +} + #[derive(Debug)] pub enum HarnessError { Io(io::Error), Json(serde_json::Error), CommandFailed { program: PathBuf, + args: Vec, status: ExitStatus, stdout: String, stderr: String, @@ -51,13 +81,14 @@ impl std::fmt::Display for HarnessError { Self::Json(err) => write!(f, "json error: {err}"), Self::CommandFailed { program, + args, status, stdout, stderr, } => write!( f, "{} exited with {status}\nstdout:\n{stdout}\nstderr:\n{stderr}", - program.display() + command_display(program, args) ), Self::Timeout { what, artifacts } => write!( f, @@ -66,7 +97,7 @@ impl std::fmt::Display for HarnessError { ), Self::MissingBinary(path) => write!( f, - "missing yoi binary {}; run `cargo build -p yoi --features e2e-test` or set YOI_E2E_BIN", + "missing yoi binary {}; set YOI_E2E_BIN to an existing binary or inspect target/e2e-artifacts/binary-provider.json", path.display() ), Self::MouseCaptureNotEnabled { artifacts } => write!( @@ -496,13 +527,7 @@ pub struct FixtureWorkspace { impl FixtureWorkspace { pub fn new(binary: &Path) -> Result { - let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(Path::parent) - .ok_or_else(|| { - HarnessError::Protocol("could not resolve workspace root for artifacts".to_owned()) - })? - .to_path_buf(); + let workspace_root = workspace_root()?; let root = workspace_root .join("target") .join("e2e-artifacts") @@ -602,19 +627,134 @@ impl FixtureWorkspace { } } -pub fn yoi_binary() -> PathBuf { - if let Some(path) = std::env::var_os("YOI_E2E_BIN") { - return PathBuf::from(path); +pub fn yoi_binary() -> Result { + Ok(yoi_binary_info()?.binary) +} + +pub fn yoi_binary_info() -> Result { + static BINARY_INFO: OnceLock> = OnceLock::new(); + match BINARY_INFO.get_or_init(|| resolve_yoi_binary().map_err(|err| err.to_string())) { + Ok(info) => Ok(info.clone()), + Err(message) => Err(HarnessError::Protocol(message.clone())), } - let mut path = std::env::current_exe().expect("current executable path"); +} + +fn resolve_yoi_binary() -> Result { + if let Some(path) = std::env::var_os("YOI_E2E_BIN") { + let info = BinaryProviderInfo { + provider: "YOI_E2E_BIN".to_owned(), + binary: PathBuf::from(path), + workspace_root: workspace_root()?, + cargo: None, + build_args: Vec::new(), + build_command: None, + profile: test_profile(), + }; + info.log(); + write_binary_provider_artifact(&info)?; + return Ok(info); + } + + let workspace_root = workspace_root()?; + let cargo = PathBuf::from(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + let mut args = vec![ + "build".to_owned(), + "-p".to_owned(), + "yoi".to_owned(), + "--features".to_owned(), + "e2e-test".to_owned(), + "--bin".to_owned(), + "yoi".to_owned(), + ]; + if test_profile() == "release" { + args.push("--release".to_owned()); + } + + let command = command_display(&cargo, &args); + eprintln!("yoi-e2e binary provider=cargo-build command={command}"); + let output = Command::new(&cargo) + .args(&args) + .current_dir(&workspace_root) + .output()?; + if !output.status.success() { + return Err(HarnessError::CommandFailed { + program: cargo, + args, + status: output.status, + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }); + } + + let binary = current_target_profile_dir()?.join(binary_name()); + let info = BinaryProviderInfo { + provider: "cargo-build".to_owned(), + binary, + workspace_root, + cargo: Some(cargo), + build_args: args, + build_command: Some(command), + profile: test_profile(), + }; + info.log(); + write_binary_provider_artifact(&info)?; + if !info.binary.exists() { + return Err(HarnessError::MissingBinary(info.binary)); + } + Ok(info) +} + +fn workspace_root() -> Result { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .map(Path::to_path_buf) + .ok_or_else(|| HarnessError::Protocol("could not resolve workspace root".to_owned())) +} + +fn current_target_profile_dir() -> Result { + let mut path = std::env::current_exe()?; while let Some(name) = path.file_name().and_then(|name| name.to_str()) { if name == "debug" || name == "release" { - path.push("yoi"); - return path; + return Ok(path); } path.pop(); } - PathBuf::from("target/debug/yoi") + Ok(workspace_root()?.join("target").join(test_profile())) +} + +fn test_profile() -> String { + let Ok(mut path) = std::env::current_exe() else { + return "debug".to_owned(); + }; + while let Some(name) = path.file_name().and_then(|name| name.to_str()) { + if name == "debug" || name == "release" { + return name.to_owned(); + } + path.pop(); + } + "debug".to_owned() +} + +fn binary_name() -> String { + format!("yoi{}", std::env::consts::EXE_SUFFIX) +} + +fn write_binary_provider_artifact(info: &BinaryProviderInfo) -> Result<()> { + let dir = info.workspace_root.join("target").join("e2e-artifacts"); + fs::create_dir_all(&dir)?; + fs::write( + dir.join("binary-provider.json"), + serde_json::to_vec_pretty(info)?, + )?; + Ok(()) +} + +fn command_display(program: &Path, args: &[String]) -> String { + std::iter::once(program.display().to_string()) + .chain(args.iter().cloned()) + .collect::>() + .join(" ") } fn open_pty(size: (u16, u16)) -> Result<(File, File)> { @@ -704,6 +844,7 @@ fn run_yoi_capture( if !output.status.success() { return Err(HarnessError::CommandFailed { program: binary.to_path_buf(), + args: args.iter().map(|arg| (*arg).to_owned()).collect(), status: output.status, stdout: String::from_utf8_lossy(&output.stdout).into_owned(), stderr: String::from_utf8_lossy(&output.stderr).into_owned(), diff --git a/tests/e2e/tests/panel.rs b/tests/e2e/tests/panel.rs index 87a9638c..bf4d492a 100644 --- a/tests/e2e/tests/panel.rs +++ b/tests/e2e/tests/panel.rs @@ -4,7 +4,7 @@ use yoi_e2e::{FixtureWorkspace, KeyPress, PanelHarness, yoi_binary}; #[test] fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result<()> { - let binary = yoi_binary(); + let binary = yoi_binary()?; let fixture = FixtureWorkspace::new(&binary)?; let mut panel = PanelHarness::spawn(fixture.panel_config(binary))?; @@ -39,7 +39,7 @@ fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result #[test] fn panel_ctrl_c_exits_promptly_after_background_barrier() -> yoi_e2e::Result<()> { - let binary = yoi_binary(); + let binary = yoi_binary()?; let fixture = FixtureWorkspace::new(&binary)?; let mut panel = PanelHarness::spawn(fixture.panel_config_holding_background_task(binary, "reload"))?; From 47efeb0143981913ec0f12cefcec69a79d8c6e53 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 01:02:19 +0900 Subject: [PATCH 2/3] test: isolate e2e yoi subprocess env --- .../artifacts/implementation-report.md | 18 +- .yoi/tickets/00001KV0TJVN5/item.md | 2 +- .yoi/tickets/00001KV0TJVN5/thread.md | 41 ++++ tests/e2e/src/lib.rs | 211 +++++++++++++++++- 4 files changed, 263 insertions(+), 9 deletions(-) diff --git a/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md b/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md index fc30991e..3d6f0b26 100644 --- a/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md +++ b/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md @@ -5,17 +5,29 @@ Files changed: - Added a cached e2e binary provider using `OnceLock`. - Preserves `YOI_E2E_BIN=` as the explicit override and skips the default cargo build provider in that path. - Default path runs `${CARGO:-cargo} build -p yoi --features e2e-test --bin yoi` from the workspace root, then returns the direct `target/{profile}/yoi` binary path for PTY spawning. - - Writes `target/e2e-artifacts/binary-provider.json` and emits diagnostics with provider, build command, and binary path. + - Writes `target/e2e-artifacts/binary-provider.json` and emits diagnostics with provider, build command, binary path, and tested-subprocess env policy. - Expanded command-failure diagnostics to include command args. + - Follow-up: isolated tested `yoi` subprocess environments in both `PanelHarness::spawn` and fixture setup `run_yoi_capture` with `env_clear()` plus explicit allowlists only. + - Follow-up: recorded env policy in `run.json`, `binary-provider.json`, and per-fixture `fixture-commands.jsonl` artifacts. + - Follow-up: added a regression assertion that tested-subprocess policies use `env_clear`, do not allow `PATH`, and default-deny provider credentials (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`) and secret-like patterns. + - Follow-up: relative `YOI_E2E_BIN` values are resolved against the workspace root and must exist, so tested subprocess launch does not rely on `PATH` lookup. - `tests/e2e/tests/panel.rs` - Updated panel tests to use the fallible cached binary provider. +Env isolation policy: +- Cargo build provider remains a build-tool command and is not treated as the tested `yoi` subprocess. +- Tested `yoi` fixture setup commands receive only: `HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `YOI_POD_RUNTIME_COMMAND`. +- Tested `yoi panel` commands receive only: fixture `HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `TERM`, `YOI_TUI_TEST_EVENTS`, `YOI_POD_RUNTIME_COMMAND`, and `YOI_TUI_TEST_HOLD_BACKGROUND_TASK` when used. +- `PATH` is intentionally not passed to tested `yoi` subprocesses; the harness launches the already-resolved binary path directly. +- Host provider credentials / token / secret-like environment variables are default-denied. Future provider/LLM E2E should use fixture providers, canned servers, or explicit test env instead of inheriting host credentials. + Validation: - `cargo fmt --check` — passed. - `git diff --check` — passed. - `cargo check -p yoi-e2e --all-targets --features e2e` — passed. -- `unset YOI_E2E_BIN && cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; default provider built the current `yoi` binary and PTY-spawned `target/debug/yoi`. -- `YOI_E2E_BIN=/home/hare/Projects/yoi/.worktree/e2e-binary-provider/target/debug/yoi cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; override provider path used without invoking the default cargo-build provider. +- `cargo test -p yoi-e2e --features e2e tested_yoi_env_policy_is_env_clear_allowlist -- --nocapture` — passed. +- `unset YOI_E2E_BIN && OPENAI_API_KEY=host-secret ANTHROPIC_API_KEY=host-secret GEMINI_API_KEY=host-secret cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; default provider built the current `yoi` binary and tested `yoi` subprocesses used isolated env policy artifacts. Host provider env was present for the harness but is not inherited by tested `yoi` subprocesses because `env_clear()` is applied before the allowlist. +- `YOI_E2E_BIN=/home/hare/Projects/yoi/.worktree/e2e-binary-provider/target/debug/yoi OPENAI_API_KEY=host-secret ANTHROPIC_API_KEY=host-secret GEMINI_API_KEY=host-secret cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; override provider path used without invoking the default cargo-build provider, and tested `yoi` subprocesses still used isolated env policy. Remaining gaps: - None known. diff --git a/.yoi/tickets/00001KV0TJVN5/item.md b/.yoi/tickets/00001KV0TJVN5/item.md index a2fb4a8b..0d40e2af 100644 --- a/.yoi/tickets/00001KV0TJVN5/item.md +++ b/.yoi/tickets/00001KV0TJVN5/item.md @@ -2,7 +2,7 @@ title: 'E2E harness が最新 yoi binary を自動 build して使うようにする' state: 'inprogress' created_at: '2026-06-13T15:46:07Z' -updated_at: '2026-06-13T15:54:18Z' +updated_at: '2026-06-13T16:01:51Z' assignee: null readiness: 'ready' queued_by: 'yoi ticket' diff --git a/.yoi/tickets/00001KV0TJVN5/thread.md b/.yoi/tickets/00001KV0TJVN5/thread.md index f24a87fe..afdc9aeb 100644 --- a/.yoi/tickets/00001KV0TJVN5/thread.md +++ b/.yoi/tickets/00001KV0TJVN5/thread.md @@ -112,4 +112,45 @@ Remaining gaps: - None known. +--- + + + +## Implementation report + +Implementation report for Ticket 00001KV0TJVN5 + +Files changed: +- `tests/e2e/src/lib.rs` + - Added a cached e2e binary provider using `OnceLock`. + - Preserves `YOI_E2E_BIN=` as the explicit override and skips the default cargo build provider in that path. + - Default path runs `${CARGO:-cargo} build -p yoi --features e2e-test --bin yoi` from the workspace root, then returns the direct `target/{profile}/yoi` binary path for PTY spawning. + - Writes `target/e2e-artifacts/binary-provider.json` and emits diagnostics with provider, build command, binary path, and tested-subprocess env policy. + - Expanded command-failure diagnostics to include command args. + - Follow-up: isolated tested `yoi` subprocess environments in both `PanelHarness::spawn` and fixture setup `run_yoi_capture` with `env_clear()` plus explicit allowlists only. + - Follow-up: recorded env policy in `run.json`, `binary-provider.json`, and per-fixture `fixture-commands.jsonl` artifacts. + - Follow-up: added a regression assertion that tested-subprocess policies use `env_clear`, do not allow `PATH`, and default-deny provider credentials (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`) and secret-like patterns. + - Follow-up: relative `YOI_E2E_BIN` values are resolved against the workspace root and must exist, so tested subprocess launch does not rely on `PATH` lookup. +- `tests/e2e/tests/panel.rs` + - Updated panel tests to use the fallible cached binary provider. + +Env isolation policy: +- Cargo build provider remains a build-tool command and is not treated as the tested `yoi` subprocess. +- Tested `yoi` fixture setup commands receive only: `HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `YOI_POD_RUNTIME_COMMAND`. +- Tested `yoi panel` commands receive only: fixture `HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `TERM`, `YOI_TUI_TEST_EVENTS`, `YOI_POD_RUNTIME_COMMAND`, and `YOI_TUI_TEST_HOLD_BACKGROUND_TASK` when used. +- `PATH` is intentionally not passed to tested `yoi` subprocesses; the harness launches the already-resolved binary path directly. +- Host provider credentials / token / secret-like environment variables are default-denied. Future provider/LLM E2E should use fixture providers, canned servers, or explicit test env instead of inheriting host credentials. + +Validation: +- `cargo fmt --check` — passed. +- `git diff --check` — passed. +- `cargo check -p yoi-e2e --all-targets --features e2e` — passed. +- `cargo test -p yoi-e2e --features e2e tested_yoi_env_policy_is_env_clear_allowlist -- --nocapture` — passed. +- `unset YOI_E2E_BIN && OPENAI_API_KEY=host-secret ANTHROPIC_API_KEY=host-secret GEMINI_API_KEY=host-secret cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; default provider built the current `yoi` binary and tested `yoi` subprocesses used isolated env policy artifacts. Host provider env was present for the harness but is not inherited by tested `yoi` subprocesses because `env_clear()` is applied before the allowlist. +- `YOI_E2E_BIN=/home/hare/Projects/yoi/.worktree/e2e-binary-provider/target/debug/yoi OPENAI_API_KEY=host-secret ANTHROPIC_API_KEY=host-secret GEMINI_API_KEY=host-secret cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; override provider path used without invoking the default cargo-build provider, and tested `yoi` subprocesses still used isolated env policy. + +Remaining gaps: +- None known. + + --- diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs index 4214d837..7d866ed9 100644 --- a/tests/e2e/src/lib.rs +++ b/tests/e2e/src/lib.rs @@ -32,6 +32,23 @@ pub struct BinaryProviderInfo { pub build_args: Vec, pub build_command: Option, pub profile: String, + pub tested_yoi_subprocess_env: TestedYoiEnvPolicy, +} + +#[derive(Clone, Debug, Serialize)] +pub struct EnvPolicy { + pub env_clear: bool, + pub allowlist: Vec, + pub path_allowed: bool, + pub provider_credentials_default_deny: Vec, + pub secret_patterns_default_deny: Vec, + pub note: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct TestedYoiEnvPolicy { + pub fixture_setup: EnvPolicy, + pub panel: EnvPolicy, } impl BinaryProviderInfo { @@ -52,6 +69,65 @@ impl BinaryProviderInfo { } } +fn env_policy(allowlist: &[&str], note: &str) -> EnvPolicy { + EnvPolicy { + env_clear: true, + allowlist: allowlist.iter().map(|name| (*name).to_owned()).collect(), + path_allowed: false, + provider_credentials_default_deny: vec![ + "OPENAI_API_KEY".to_owned(), + "ANTHROPIC_API_KEY".to_owned(), + "GEMINI_API_KEY".to_owned(), + ], + secret_patterns_default_deny: vec![ + "*_API_KEY".to_owned(), + "*_TOKEN".to_owned(), + "*_SECRET".to_owned(), + "*_CREDENTIAL*".to_owned(), + ], + note: note.to_owned(), + } +} + +fn fixture_setup_env_policy() -> EnvPolicy { + env_policy( + &[ + "HOME", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + "XDG_CONFIG_HOME", + "YOI_POD_RUNTIME_COMMAND", + ], + "tested yoi fixture setup commands use env_clear and receive only fixture data/config homes plus the explicit runtime binary override", + ) +} + +fn panel_env_policy(include_hold_background_task: bool) -> EnvPolicy { + let mut allowlist = vec![ + "HOME", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + "XDG_CONFIG_HOME", + "TERM", + "YOI_TUI_TEST_EVENTS", + "YOI_POD_RUNTIME_COMMAND", + ]; + if include_hold_background_task { + allowlist.push("YOI_TUI_TEST_HOLD_BACKGROUND_TASK"); + } + env_policy( + &allowlist, + "tested yoi panel subprocess uses env_clear and receives only fixture homes, terminal/test-observer variables, and the explicit runtime binary override", + ) +} + +fn tested_yoi_env_policy_overview() -> TestedYoiEnvPolicy { + TestedYoiEnvPolicy { + fixture_setup: fixture_setup_env_policy(), + panel: panel_env_policy(true), + } +} + #[derive(Debug)] pub enum HarnessError { Io(io::Error), @@ -218,6 +294,7 @@ impl PanelHarness { fs::write(&artifacts.events_jsonl, "")?; fs::write(&artifacts.input_log, "")?; fs::write(&artifacts.output_log, "")?; + let env_policy = panel_env_policy(config.hold_background_task.is_some()); fs::write( &artifacts.run_json, serde_json::to_vec_pretty(&serde_json::json!({ @@ -232,6 +309,7 @@ impl PanelHarness { "rows": config.terminal_size.1, }, "hold_background_task": config.hold_background_task, + "tested_yoi_env_policy": &env_policy, }))?, )?; @@ -244,6 +322,7 @@ impl PanelHarness { .arg("panel") .arg("--workspace") .arg(&config.workspace) + .env_clear() .env("YOI_TUI_TEST_EVENTS", &artifacts.events_jsonl) .env("YOI_POD_RUNTIME_COMMAND", &config.binary) .env("HOME", &config.home) @@ -641,14 +720,25 @@ pub fn yoi_binary_info() -> Result { fn resolve_yoi_binary() -> Result { if let Some(path) = std::env::var_os("YOI_E2E_BIN") { + let workspace_root = workspace_root()?; + let binary = PathBuf::from(path); + let binary = if binary.is_absolute() { + binary + } else { + workspace_root.join(binary) + }; + if !binary.exists() { + return Err(HarnessError::MissingBinary(binary)); + } let info = BinaryProviderInfo { provider: "YOI_E2E_BIN".to_owned(), - binary: PathBuf::from(path), - workspace_root: workspace_root()?, + binary, + workspace_root, cargo: None, build_args: Vec::new(), build_command: None, profile: test_profile(), + tested_yoi_subprocess_env: tested_yoi_env_policy_overview(), }; info.log(); write_binary_provider_artifact(&info)?; @@ -695,6 +785,7 @@ fn resolve_yoi_binary() -> Result { build_args: args, build_command: Some(command), profile: test_profile(), + tested_yoi_subprocess_env: tested_yoi_env_policy_overview(), }; info.log(); write_binary_provider_artifact(&info)?; @@ -832,15 +923,21 @@ fn run_yoi_capture( config: &Path, args: &[&str], ) -> Result { - let output = Command::new(binary) + let env_policy = fixture_setup_env_policy(); + append_fixture_command_artifact(workspace, binary, args, &env_policy)?; + + let mut command = Command::new(binary); + command .args(args) .current_dir(workspace) + .env_clear() .env("HOME", home) .env("XDG_DATA_HOME", data) .env("XDG_STATE_HOME", state) .env("XDG_CONFIG_HOME", config) - .env("YOI_POD_RUNTIME_COMMAND", binary) - .output()?; + .env("YOI_POD_RUNTIME_COMMAND", binary); + + let output = command.output()?; if !output.status.success() { return Err(HarnessError::CommandFailed { program: binary.to_path_buf(), @@ -855,6 +952,37 @@ fn run_yoi_capture( Ok(text) } +fn append_fixture_command_artifact( + workspace: &Path, + binary: &Path, + args: &[&str], + env_policy: &EnvPolicy, +) -> Result<()> { + let fixture_root = workspace.parent().ok_or_else(|| { + HarnessError::Protocol(format!( + "fixture workspace {} has no parent for artifacts", + workspace.display() + )) + })?; + let artifacts_dir = fixture_root.join("artifacts"); + fs::create_dir_all(&artifacts_dir)?; + let mut file = OpenOptions::new() + .append(true) + .create(true) + .open(artifacts_dir.join("fixture-commands.jsonl"))?; + serde_json::to_writer( + &mut file, + &serde_json::json!({ + "ts_ms": now_ms(), + "binary": binary, + "args": args, + "tested_yoi_env_policy": env_policy, + }), + )?; + writeln!(file)?; + Ok(()) +} + fn write_blocking_pod_metadata(data_home: &Path, pod_name: &str) -> Result<()> { let dir = data_home.join("yoi").join("pods").join(pod_name); fs::create_dir_all(&dir)?; @@ -907,3 +1035,76 @@ fn now_ms() -> u128 { .map(|duration| duration.as_millis()) .unwrap_or_default() } + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_host_credentials_default_denied(policy: &EnvPolicy) { + assert!( + policy.env_clear, + "tested yoi subprocesses must use env_clear" + ); + assert!( + !policy.path_allowed, + "tested yoi subprocesses should not inherit or allow PATH" + ); + assert!( + !policy.allowlist.iter().any(|name| name == "PATH"), + "PATH must not be allowlisted for tested yoi subprocesses" + ); + for name in ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"] { + assert!( + !policy.allowlist.iter().any(|allowed| allowed == name), + "{name} must not be allowlisted for tested yoi subprocesses" + ); + assert!( + policy + .provider_credentials_default_deny + .iter() + .any(|denied| denied == name), + "{name} should be recorded as provider credential default-deny" + ); + } + } + + #[test] + fn tested_yoi_env_policy_is_env_clear_allowlist() { + let fixture = fixture_setup_env_policy(); + assert_host_credentials_default_denied(&fixture); + assert_eq!( + fixture.allowlist, + [ + "HOME", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + "XDG_CONFIG_HOME", + "YOI_POD_RUNTIME_COMMAND", + ] + ); + + let panel = panel_env_policy(false); + assert_host_credentials_default_denied(&panel); + assert_eq!( + panel.allowlist, + [ + "HOME", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + "XDG_CONFIG_HOME", + "TERM", + "YOI_TUI_TEST_EVENTS", + "YOI_POD_RUNTIME_COMMAND", + ] + ); + + let panel_with_hold = panel_env_policy(true); + assert_host_credentials_default_denied(&panel_with_hold); + assert!( + panel_with_hold + .allowlist + .iter() + .any(|name| name == "YOI_TUI_TEST_HOLD_BACKGROUND_TASK") + ); + } +} From 7e24a8df0586118cf2e4801d2300f68acf09877c Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 01:07:38 +0900 Subject: [PATCH 3/3] ticket: approve e2e binary provider --- .../review-2026-06-14-e2e-binary-provider.md | 19 +++++++++++++ .yoi/tickets/00001KV0TJVN5/item.md | 2 +- .yoi/tickets/00001KV0TJVN5/thread.md | 27 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 .yoi/tickets/00001KV0TJVN5/artifacts/review-2026-06-14-e2e-binary-provider.md diff --git a/.yoi/tickets/00001KV0TJVN5/artifacts/review-2026-06-14-e2e-binary-provider.md b/.yoi/tickets/00001KV0TJVN5/artifacts/review-2026-06-14-e2e-binary-provider.md new file mode 100644 index 00000000..3c804e85 --- /dev/null +++ b/.yoi/tickets/00001KV0TJVN5/artifacts/review-2026-06-14-e2e-binary-provider.md @@ -0,0 +1,19 @@ +## Review: approve + +Decision: approve for Ticket `00001KV0TJVN5`. + +Evidence reviewed: +- Ticket intent/acceptance criteria require default E2E setup to build `yoi` with `cargo build -p yoi --features e2e-test --bin yoi`, then direct-spawn the produced binary, while preserving `YOI_E2E_BIN` override and existing panel E2E behavior. +- `tests/e2e/src/lib.rs` now resolves `yoi_binary()` through a `OnceLock`-cached `BinaryProviderInfo`. The default path runs `${CARGO:-cargo} build -p yoi --features e2e-test --bin yoi` from the workspace root and returns `target/{debug|release}/yoi`; the override path validates and uses `YOI_E2E_BIN` without invoking the cargo-build provider. +- PTY execution remains `Command::new(&config.binary).arg("panel")`; `cargo run` is not in the process-under-test path. +- `PanelHarness::spawn` and fixture `run_yoi_capture` both call `env_clear()` and then set only explicit fixture/test variables. `PATH` and provider credentials are not allowlisted. `YOI_POD_RUNTIME_COMMAND` is set to the resolved binary path, so tested subprocesses do not need host `PATH`. +- Diagnostics/artifacts include provider/build/env policy in `target/e2e-artifacts/binary-provider.json`, panel `run.json`, and fixture `fixture-commands.jsonl`. +- Existing mouse-capture guard (`expect_mouse_capture_enabled` / SGR 1000+1006 tracking), background-task quit barrier assertions, and `e2e-test` production boundary code were not weakened by this diff. + +Validation: +- Reviewer reran `git diff --check a4df9754..HEAD` — passed. +- Reviewer reran `cargo test -p yoi-e2e --features e2e tested_yoi_env_policy_is_env_clear_allowlist -- --nocapture` — passed. +- Also accepted Orchestrator-reported full validation, including fmt/check, `cargo check -p yoi-e2e --all-targets --features e2e`, default panel E2E with host provider env present, and `YOI_E2E_BIN` override panel E2E with host provider env present — all reported passed. + +Risks / follow-up: +- No blocking issues found. The cargo build provider intentionally still uses build-tool environment; tested `yoi` subprocesses are isolated. \ No newline at end of file diff --git a/.yoi/tickets/00001KV0TJVN5/item.md b/.yoi/tickets/00001KV0TJVN5/item.md index 0d40e2af..720d33a7 100644 --- a/.yoi/tickets/00001KV0TJVN5/item.md +++ b/.yoi/tickets/00001KV0TJVN5/item.md @@ -2,7 +2,7 @@ title: 'E2E harness が最新 yoi binary を自動 build して使うようにする' state: 'inprogress' created_at: '2026-06-13T15:46:07Z' -updated_at: '2026-06-13T16:01:51Z' +updated_at: '2026-06-13T16:06:53Z' assignee: null readiness: 'ready' queued_by: 'yoi ticket' diff --git a/.yoi/tickets/00001KV0TJVN5/thread.md b/.yoi/tickets/00001KV0TJVN5/thread.md index afdc9aeb..7d89d3a5 100644 --- a/.yoi/tickets/00001KV0TJVN5/thread.md +++ b/.yoi/tickets/00001KV0TJVN5/thread.md @@ -153,4 +153,31 @@ Remaining gaps: - None known. +--- + + + +## Review: approve + +## Review: approve + +Decision: approve for Ticket `00001KV0TJVN5`. + +Evidence reviewed: +- Ticket intent/acceptance criteria require default E2E setup to build `yoi` with `cargo build -p yoi --features e2e-test --bin yoi`, then direct-spawn the produced binary, while preserving `YOI_E2E_BIN` override and existing panel E2E behavior. +- `tests/e2e/src/lib.rs` now resolves `yoi_binary()` through a `OnceLock`-cached `BinaryProviderInfo`. The default path runs `${CARGO:-cargo} build -p yoi --features e2e-test --bin yoi` from the workspace root and returns `target/{debug|release}/yoi`; the override path validates and uses `YOI_E2E_BIN` without invoking the cargo-build provider. +- PTY execution remains `Command::new(&config.binary).arg("panel")`; `cargo run` is not in the process-under-test path. +- `PanelHarness::spawn` and fixture `run_yoi_capture` both call `env_clear()` and then set only explicit fixture/test variables. `PATH` and provider credentials are not allowlisted. `YOI_POD_RUNTIME_COMMAND` is set to the resolved binary path, so tested subprocesses do not need host `PATH`. +- Diagnostics/artifacts include provider/build/env policy in `target/e2e-artifacts/binary-provider.json`, panel `run.json`, and fixture `fixture-commands.jsonl`. +- Existing mouse-capture guard (`expect_mouse_capture_enabled` / SGR 1000+1006 tracking), background-task quit barrier assertions, and `e2e-test` production boundary code were not weakened by this diff. + +Validation: +- Reviewer reran `git diff --check a4df9754..HEAD` — passed. +- Reviewer reran `cargo test -p yoi-e2e --features e2e tested_yoi_env_policy_is_env_clear_allowlist -- --nocapture` — passed. +- Also accepted Orchestrator-reported full validation, including fmt/check, `cargo check -p yoi-e2e --all-targets --features e2e`, default panel E2E with host provider env present, and `YOI_E2E_BIN` override panel E2E with host provider env present — all reported passed. + +Risks / follow-up: +- No blocking issues found. The cargo build provider intentionally still uses build-tool environment; tested `yoi` subprocesses are isolated. + + ---