merge: e2e tmp isolation

# Conflicts:
#	.yoi/tickets/00001KV0YK5S0/item.md
#	.yoi/tickets/00001KV0YK5S0/thread.md
This commit is contained in:
Keisuke Hirata 2026-06-14 02:33:09 +09:00
commit 20184eeb1f
No known key found for this signature in database
3 changed files with 371 additions and 65 deletions

View File

@ -86,14 +86,45 @@ Escalate if:
Ticket evidence、related E2E tickets、existing code map、Orchestrator worktree clean state を確認した。ユーザーが明示的に修正を依頼しており、blocking relation はない。実装 side effect の前に inprogress acceptance を記録する。 Ticket evidence、related E2E tickets、existing code map、Orchestrator worktree clean state を確認した。ユーザーが明示的に修正を依頼しており、blocking relation はない。実装 side effect の前に inprogress acceptance を記録する。
---
<!-- event: implementation_report author: hare at: 2026-06-13T17:06:29Z -->
## Implementation report
Implemented scoped E2E tmp/runtime isolation for Ticket 00001KV0YK5S0.
Files changed:
- `tests/e2e/src/lib.rs`
- `FixtureWorkspace` now owns a `TempDir` under `target/e2e-tmp` and writes persistent scenario artifacts under `target/e2e-artifacts/<run-id>`.
- Each fixture creates isolated workspace, HOME, XDG data/state/config/runtime dirs and passes `XDG_RUNTIME_DIR` explicitly to all tested `yoi` subprocesses under `env_clear`.
- Fixture metadata records fixture root, workspace/home/XDG paths, runtime policy, env policy, and cleanup result.
- Cleanup snapshots the fixture tree into persistent artifacts, closes/removes the temp root, writes `cleanup.json`, and reports cleanup failure.
- `tests/e2e/tests/panel.rs`
- Existing two Panel E2E tests now assert fixture path isolation, absence of host/runtime Pod marker leakage in rendered rows, and explicit fixture cleanup/removal after each scenario.
Isolation/cleanup policy:
- Tested `yoi` never inherits host env wholesale; fixture setup and Panel subprocesses use `env_clear` with an allowlist that includes only fixture HOME/XDG data/state/config/runtime plus test/runtime-specific variables.
- `XDG_RUNTIME_DIR` is always a fixture-local runtime dir under the temporary fixture root.
- Fixture-local Pod metadata markers (`workspace`, `workspace-orchestrator`) are created only inside fixture data home and panel rows assert no host/fixture runtime markers leak.
- Artifacts persist outside the temp root in `target/e2e-artifacts`; cleanup snapshots selected fixture contents there before removing the temp root.
Validation:
- `cargo check -p yoi-e2e --all-targets --features e2e` — passed.
- `cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed (2 tests).
- `XDG_RUNTIME_DIR=/tmp/yoi-e2e-host-runtime-leak-test OPENAI_API_KEY=host-secret cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed (2 tests); artifacts show fixture `xdg_runtime_dir`, `host_xdg_runtime_dir_present: true`, and `cleanup_success: true`.
- `YOI_E2E_BIN=target/debug/yoi cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed (2 tests).
- `cargo fmt --check` — passed.
- `git diff --check` — passed.
Remaining gaps:
- No known gaps within the delegated scope.
--- ---
<!-- event: review author: orchestrator at: 2026-06-13T17:31:57Z status: approve --> <!-- event: review author: orchestrator at: 2026-06-13T17:31:57Z status: approve -->
## Review: approve ## Review: approve
## Review: approve
外部 Reviewer Pod 起動を試みたが、この Orchestrator の現在の delegation scope では child launch validation が要求する root workspace read grant を再委譲できず、Reviewer Pod を起動できなかった。そのため Orchestrator が実装 diff・validation・artifact を直接確認した。 外部 Reviewer Pod 起動を試みたが、この Orchestrator の現在の delegation scope では child launch validation が要求する root workspace read grant を再委譲できず、Reviewer Pod を起動できなかった。そのため Orchestrator が実装 diff・validation・artifact を直接確認した。
確認した実装: 確認した実装:

View File

@ -16,6 +16,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use tempfile::TempDir;
const DEFAULT_WAIT: Duration = Duration::from_secs(5); const DEFAULT_WAIT: Duration = Duration::from_secs(5);
const DEFAULT_EXIT_WAIT: Duration = Duration::from_millis(1500); const DEFAULT_EXIT_WAIT: Duration = Duration::from_millis(1500);
@ -96,9 +97,10 @@ fn fixture_setup_env_policy() -> EnvPolicy {
"XDG_DATA_HOME", "XDG_DATA_HOME",
"XDG_STATE_HOME", "XDG_STATE_HOME",
"XDG_CONFIG_HOME", "XDG_CONFIG_HOME",
"XDG_RUNTIME_DIR",
"YOI_POD_RUNTIME_COMMAND", "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", "tested yoi fixture setup commands use env_clear and receive only fixture HOME, XDG data/state/config/runtime dirs, and the explicit runtime binary override",
) )
} }
@ -108,6 +110,7 @@ fn panel_env_policy(include_hold_background_task: bool) -> EnvPolicy {
"XDG_DATA_HOME", "XDG_DATA_HOME",
"XDG_STATE_HOME", "XDG_STATE_HOME",
"XDG_CONFIG_HOME", "XDG_CONFIG_HOME",
"XDG_RUNTIME_DIR",
"TERM", "TERM",
"YOI_TUI_TEST_EVENTS", "YOI_TUI_TEST_EVENTS",
"YOI_POD_RUNTIME_COMMAND", "YOI_POD_RUNTIME_COMMAND",
@ -117,7 +120,7 @@ fn panel_env_policy(include_hold_background_task: bool) -> EnvPolicy {
} }
env_policy( env_policy(
&allowlist, &allowlist,
"tested yoi panel subprocess uses env_clear and receives only fixture homes, terminal/test-observer variables, and the explicit runtime binary override", "tested yoi panel subprocess uses env_clear and receives only fixture HOME, XDG data/state/config/runtime dirs, terminal/test-observer variables, and the explicit runtime binary override",
) )
} }
@ -208,6 +211,8 @@ pub struct PanelHarnessConfig {
pub xdg_data_home: PathBuf, pub xdg_data_home: PathBuf,
pub xdg_state_home: PathBuf, pub xdg_state_home: PathBuf,
pub xdg_config_home: PathBuf, pub xdg_config_home: PathBuf,
pub xdg_runtime_dir: PathBuf,
pub fixture_root: PathBuf,
pub terminal_size: (u16, u16), pub terminal_size: (u16, u16),
pub hold_background_task: Option<String>, pub hold_background_task: Option<String>,
pub artifacts_dir: PathBuf, pub artifacts_dir: PathBuf,
@ -304,6 +309,13 @@ impl PanelHarness {
"xdg_data_home": config.xdg_data_home, "xdg_data_home": config.xdg_data_home,
"xdg_state_home": config.xdg_state_home, "xdg_state_home": config.xdg_state_home,
"xdg_config_home": config.xdg_config_home, "xdg_config_home": config.xdg_config_home,
"xdg_runtime_dir": config.xdg_runtime_dir,
"fixture_root": config.fixture_root,
"runtime_policy": {
"host_runtime_inherited": false,
"host_xdg_runtime_dir_present": std::env::var_os("XDG_RUNTIME_DIR").is_some(),
"tested_yoi_runtime_source": "fixture XDG_RUNTIME_DIR"
},
"terminal_size": { "terminal_size": {
"columns": config.terminal_size.0, "columns": config.terminal_size.0,
"rows": config.terminal_size.1, "rows": config.terminal_size.1,
@ -329,6 +341,7 @@ impl PanelHarness {
.env("XDG_DATA_HOME", &config.xdg_data_home) .env("XDG_DATA_HOME", &config.xdg_data_home)
.env("XDG_STATE_HOME", &config.xdg_state_home) .env("XDG_STATE_HOME", &config.xdg_state_home)
.env("XDG_CONFIG_HOME", &config.xdg_config_home) .env("XDG_CONFIG_HOME", &config.xdg_config_home)
.env("XDG_RUNTIME_DIR", &config.xdg_runtime_dir)
.env("TERM", "xterm-256color") .env("TERM", "xterm-256color")
.stdin(Stdio::from(slave_for_stdin)) .stdin(Stdio::from(slave_for_stdin))
.stdout(Stdio::from(slave_for_stdout)) .stdout(Stdio::from(slave_for_stdout))
@ -593,92 +606,130 @@ impl Drop for PanelHarness {
} }
} }
#[derive(Debug, Clone, Serialize)]
pub struct FixtureCleanupReport {
pub fixture_root: PathBuf,
pub artifacts_dir: PathBuf,
pub snapshot_dir: PathBuf,
pub cleanup_attempted: bool,
pub cleanup_success: bool,
pub fixture_root_exists_after: bool,
pub cleanup_error: Option<String>,
pub report_path: PathBuf,
}
#[derive(Debug)] #[derive(Debug)]
pub struct FixtureWorkspace { pub struct FixtureWorkspace {
temp_root: Option<TempDir>,
pub root: PathBuf, pub root: PathBuf,
pub workspace: PathBuf, pub workspace: PathBuf,
pub home: PathBuf, pub home: PathBuf,
pub xdg_data_home: PathBuf, pub xdg_data_home: PathBuf,
pub xdg_state_home: PathBuf, pub xdg_state_home: PathBuf,
pub xdg_config_home: PathBuf, pub xdg_config_home: PathBuf,
pub xdg_runtime_dir: PathBuf,
pub artifacts_dir: PathBuf, pub artifacts_dir: PathBuf,
} }
impl FixtureWorkspace { impl FixtureWorkspace {
pub fn new(binary: &Path) -> Result<Self> { pub fn new(binary: &Path) -> Result<Self> {
let workspace_root = workspace_root()?; let workspace_root = workspace_root()?;
let root = workspace_root let target_dir = workspace_root.join("target");
.join("target") let temp_parent = target_dir.join("e2e-tmp");
.join("e2e-artifacts") let artifact_parent = target_dir.join("e2e-artifacts");
.join(format!( fs::create_dir_all(&temp_parent)?;
"{}-{}-{}", fs::create_dir_all(&artifact_parent)?;
std::process::id(),
now_ms(), let fixture_id = format!(
FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed) "{}-{}-{}",
)); std::process::id(),
now_ms(),
FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed)
);
let temp_root = tempfile::Builder::new()
.prefix(&format!("yoi-e2e-{fixture_id}-"))
.tempdir_in(&temp_parent)?;
let root = temp_root.path().to_path_buf();
let artifacts_dir = artifact_parent.join(fixture_id);
let workspace = root.join("workspace"); let workspace = root.join("workspace");
let home = root.join("home"); let home = root.join("home");
let xdg_data_home = root.join("data"); let xdg_data_home = root.join("data");
let xdg_state_home = root.join("state"); let xdg_state_home = root.join("state");
let xdg_config_home = root.join("config"); let xdg_config_home = root.join("config");
let artifacts_dir = root.join("artifacts"); let xdg_runtime_dir = root.join("runtime");
for dir in [ for dir in [
&workspace, &workspace,
&home, &home,
&xdg_data_home, &xdg_data_home,
&xdg_state_home, &xdg_state_home,
&xdg_config_home, &xdg_config_home,
&xdg_runtime_dir,
&artifacts_dir, &artifacts_dir,
] { ] {
fs::create_dir_all(dir)?; fs::create_dir_all(dir)?;
} }
write_blocking_pod_metadata(&xdg_data_home, "workspace")?;
write_blocking_pod_metadata(&xdg_data_home, "workspace-orchestrator")?; let fixture = Self {
run_yoi( temp_root: Some(temp_root),
binary,
&workspace,
&home,
&xdg_data_home,
&xdg_state_home,
&xdg_config_home,
&["ticket", "init"],
)?;
let first = create_ticket(
binary,
&workspace,
&home,
&xdg_data_home,
&xdg_state_home,
&xdg_config_home,
"Ready E2E Ticket",
)?;
run_yoi(
binary,
&workspace,
&home,
&xdg_data_home,
&xdg_state_home,
&xdg_config_home,
&["ticket", "state", &first, "ready"],
)?;
let _second = create_ticket(
binary,
&workspace,
&home,
&xdg_data_home,
&xdg_state_home,
&xdg_config_home,
"Planning E2E Ticket",
)?;
Ok(Self {
root, root,
workspace, workspace,
home, home,
xdg_data_home, xdg_data_home,
xdg_state_home, xdg_state_home,
xdg_config_home, xdg_config_home,
xdg_runtime_dir,
artifacts_dir, artifacts_dir,
}) };
fixture.write_fixture_metadata("created", None)?;
write_blocking_pod_metadata(&fixture.xdg_data_home, "workspace")?;
write_blocking_pod_metadata(&fixture.xdg_data_home, "workspace-orchestrator")?;
run_yoi(
binary,
&fixture.workspace,
&fixture.home,
&fixture.xdg_data_home,
&fixture.xdg_state_home,
&fixture.xdg_config_home,
&fixture.xdg_runtime_dir,
&fixture.artifacts_dir,
&["ticket", "init"],
)?;
let first = create_ticket(
binary,
&fixture.workspace,
&fixture.home,
&fixture.xdg_data_home,
&fixture.xdg_state_home,
&fixture.xdg_config_home,
&fixture.xdg_runtime_dir,
&fixture.artifacts_dir,
"Ready E2E Ticket",
)?;
run_yoi(
binary,
&fixture.workspace,
&fixture.home,
&fixture.xdg_data_home,
&fixture.xdg_state_home,
&fixture.xdg_config_home,
&fixture.xdg_runtime_dir,
&fixture.artifacts_dir,
&["ticket", "state", &first, "ready"],
)?;
let _second = create_ticket(
binary,
&fixture.workspace,
&fixture.home,
&fixture.xdg_data_home,
&fixture.xdg_state_home,
&fixture.xdg_config_home,
&fixture.xdg_runtime_dir,
&fixture.artifacts_dir,
"Planning E2E Ticket",
)?;
fixture.write_fixture_metadata("ready", None)?;
Ok(fixture)
} }
pub fn panel_config(&self, binary: PathBuf) -> PanelHarnessConfig { pub fn panel_config(&self, binary: PathBuf) -> PanelHarnessConfig {
@ -689,6 +740,8 @@ impl FixtureWorkspace {
xdg_data_home: self.xdg_data_home.clone(), xdg_data_home: self.xdg_data_home.clone(),
xdg_state_home: self.xdg_state_home.clone(), xdg_state_home: self.xdg_state_home.clone(),
xdg_config_home: self.xdg_config_home.clone(), xdg_config_home: self.xdg_config_home.clone(),
xdg_runtime_dir: self.xdg_runtime_dir.clone(),
fixture_root: self.root.clone(),
terminal_size: (100, 32), terminal_size: (100, 32),
hold_background_task: None, hold_background_task: None,
artifacts_dir: self.artifacts_dir.clone(), artifacts_dir: self.artifacts_dir.clone(),
@ -704,6 +757,88 @@ impl FixtureWorkspace {
config.hold_background_task = Some(task.into()); config.hold_background_task = Some(task.into());
config config
} }
pub fn cleanup(mut self) -> Result<FixtureCleanupReport> {
self.cleanup_inner(true)
}
fn cleanup_inner(&mut self, strict: bool) -> Result<FixtureCleanupReport> {
let snapshot_dir = self.artifacts_dir.join("fixture-snapshot");
if snapshot_dir.exists() {
fs::remove_dir_all(&snapshot_dir)?;
}
copy_dir_recursive(&self.root, &snapshot_dir)?;
let mut cleanup_error = None;
if let Some(temp_root) = self.temp_root.take() {
if let Err(err) = temp_root.close() {
cleanup_error = Some(err.to_string());
}
}
let fixture_root_exists_after = self.root.exists();
let cleanup_success = cleanup_error.is_none() && !fixture_root_exists_after;
let report = FixtureCleanupReport {
fixture_root: self.root.clone(),
artifacts_dir: self.artifacts_dir.clone(),
snapshot_dir,
cleanup_attempted: true,
cleanup_success,
fixture_root_exists_after,
cleanup_error,
report_path: self.artifacts_dir.join("cleanup.json"),
};
fs::write(&report.report_path, serde_json::to_vec_pretty(&report)?)?;
self.write_fixture_metadata("cleaned", Some(&report))?;
if strict && !report.cleanup_success {
return Err(HarnessError::Protocol(format!(
"fixture cleanup failed for {}; see {}",
report.fixture_root.display(),
report.report_path.display()
)));
}
Ok(report)
}
fn write_fixture_metadata(
&self,
phase: &str,
cleanup: Option<&FixtureCleanupReport>,
) -> Result<()> {
fs::create_dir_all(&self.artifacts_dir)?;
fs::write(
self.artifacts_dir.join("fixture.json"),
serde_json::to_vec_pretty(&serde_json::json!({
"phase": phase,
"fixture_root": &self.root,
"workspace": &self.workspace,
"home": &self.home,
"xdg_data_home": &self.xdg_data_home,
"xdg_state_home": &self.xdg_state_home,
"xdg_config_home": &self.xdg_config_home,
"xdg_runtime_dir": &self.xdg_runtime_dir,
"artifacts_dir": &self.artifacts_dir,
"env_runtime_policy": {
"tested_yoi_uses_env_clear": true,
"host_runtime_inherited": false,
"host_xdg_runtime_dir_present": std::env::var_os("XDG_RUNTIME_DIR").is_some(),
"tested_yoi_runtime_source": "fixture XDG_RUNTIME_DIR",
"tested_yoi_pod_registry": self.xdg_runtime_dir.join("yoi").join("pods.json"),
"fixture_pod_metadata_root": self.xdg_data_home.join("yoi").join("pods")
},
"tested_yoi_env_policy": tested_yoi_env_policy_overview(),
"cleanup": cleanup,
}))?,
)?;
Ok(())
}
}
impl Drop for FixtureWorkspace {
fn drop(&mut self) {
if self.temp_root.is_some() {
let _ = self.cleanup_inner(false);
}
}
} }
pub fn yoi_binary() -> Result<PathBuf> { pub fn yoi_binary() -> Result<PathBuf> {
@ -882,6 +1017,8 @@ fn create_ticket(
data: &Path, data: &Path,
state: &Path, state: &Path,
config: &Path, config: &Path,
runtime: &Path,
artifacts_dir: &Path,
title: &str, title: &str,
) -> Result<String> { ) -> Result<String> {
let output = run_yoi_capture( let output = run_yoi_capture(
@ -891,6 +1028,8 @@ fn create_ticket(
data, data,
state, state,
config, config,
runtime,
artifacts_dir,
&["ticket", "create", "--title", title], &["ticket", "create", "--title", title],
)?; )?;
output output
@ -907,9 +1046,21 @@ fn run_yoi(
data: &Path, data: &Path,
state: &Path, state: &Path,
config: &Path, config: &Path,
runtime: &Path,
artifacts_dir: &Path,
args: &[&str], args: &[&str],
) -> Result<()> { ) -> Result<()> {
let output = run_yoi_capture(binary, workspace, home, data, state, config, args)?; let output = run_yoi_capture(
binary,
workspace,
home,
data,
state,
config,
runtime,
artifacts_dir,
args,
)?;
drop(output); drop(output);
Ok(()) Ok(())
} }
@ -921,10 +1072,12 @@ fn run_yoi_capture(
data: &Path, data: &Path,
state: &Path, state: &Path,
config: &Path, config: &Path,
runtime: &Path,
artifacts_dir: &Path,
args: &[&str], args: &[&str],
) -> Result<String> { ) -> Result<String> {
let env_policy = fixture_setup_env_policy(); let env_policy = fixture_setup_env_policy();
append_fixture_command_artifact(workspace, binary, args, &env_policy)?; append_fixture_command_artifact(artifacts_dir, workspace, binary, args, &env_policy)?;
let mut command = Command::new(binary); let mut command = Command::new(binary);
command command
@ -935,6 +1088,7 @@ fn run_yoi_capture(
.env("XDG_DATA_HOME", data) .env("XDG_DATA_HOME", data)
.env("XDG_STATE_HOME", state) .env("XDG_STATE_HOME", state)
.env("XDG_CONFIG_HOME", config) .env("XDG_CONFIG_HOME", config)
.env("XDG_RUNTIME_DIR", runtime)
.env("YOI_POD_RUNTIME_COMMAND", binary); .env("YOI_POD_RUNTIME_COMMAND", binary);
let output = command.output()?; let output = command.output()?;
@ -953,19 +1107,13 @@ fn run_yoi_capture(
} }
fn append_fixture_command_artifact( fn append_fixture_command_artifact(
artifacts_dir: &Path,
workspace: &Path, workspace: &Path,
binary: &Path, binary: &Path,
args: &[&str], args: &[&str],
env_policy: &EnvPolicy, env_policy: &EnvPolicy,
) -> Result<()> { ) -> Result<()> {
let fixture_root = workspace.parent().ok_or_else(|| { fs::create_dir_all(artifacts_dir)?;
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() let mut file = OpenOptions::new()
.append(true) .append(true)
.create(true) .create(true)
@ -975,6 +1123,7 @@ fn append_fixture_command_artifact(
&serde_json::json!({ &serde_json::json!({
"ts_ms": now_ms(), "ts_ms": now_ms(),
"binary": binary, "binary": binary,
"workspace": workspace,
"args": args, "args": args,
"tested_yoi_env_policy": env_policy, "tested_yoi_env_policy": env_policy,
}), }),
@ -983,6 +1132,31 @@ fn append_fixture_command_artifact(
Ok(()) Ok(())
} }
fn copy_dir_recursive(source: &Path, destination: &Path) -> Result<()> {
if !source.exists() {
return Ok(());
}
fs::create_dir_all(destination)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let file_type = entry.file_type()?;
let target = destination.join(entry.file_name());
if file_type.is_dir() {
copy_dir_recursive(&entry.path(), &target)?;
} else if file_type.is_file() {
fs::copy(entry.path(), target)?;
} else if file_type.is_symlink() {
// Preserve enough diagnostics for E2E artifacts without following links out of
// the fixture temp root.
fs::write(
target,
format!("symlink -> {}\n", fs::read_link(entry.path())?.display()),
)?;
}
}
Ok(())
}
fn write_blocking_pod_metadata(data_home: &Path, pod_name: &str) -> Result<()> { fn write_blocking_pod_metadata(data_home: &Path, pod_name: &str) -> Result<()> {
let dir = data_home.join("yoi").join("pods").join(pod_name); let dir = data_home.join("yoi").join("pods").join(pod_name);
fs::create_dir_all(&dir)?; fs::create_dir_all(&dir)?;

View File

@ -1,15 +1,23 @@
use std::time::Duration; use std::time::Duration;
use yoi_e2e::{FixtureWorkspace, KeyPress, PanelHarness, yoi_binary}; use yoi_e2e::{
FixtureCleanupReport, FixtureWorkspace, KeyPress, PanelHarness, RenderedPanelRow, yoi_binary,
};
#[test] #[test]
fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result<()> { 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 fixture = FixtureWorkspace::new(&binary)?;
assert_fixture_paths_are_isolated(&fixture);
let mut panel = PanelHarness::spawn(fixture.panel_config(binary))?; let mut panel = PanelHarness::spawn(fixture.panel_config(binary))?;
panel.expect_mouse_capture_enabled()?; panel.expect_mouse_capture_enabled()?;
let rows = panel.wait_for_rows(2)?; let rows = panel.wait_for_rows(2)?;
assert_no_runtime_or_host_pod_leak(
&fixture,
&rows.rows,
panel.artifacts().dir.display().to_string().as_str(),
);
let selected = rows.selected.clone(); let selected = rows.selected.clone();
let target = rows let target = rows
.rows .rows
@ -34,6 +42,8 @@ fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result
panel.press(KeyPress::CtrlC)?; panel.press(KeyPress::CtrlC)?;
let status = panel.expect_exit_within(PanelHarness::default_exit_wait())?; let status = panel.expect_exit_within(PanelHarness::default_exit_wait())?;
assert!(status.success(), "panel should exit cleanly with Ctrl+C"); assert!(status.success(), "panel should exit cleanly with Ctrl+C");
drop(panel);
assert_fixture_cleanup(fixture.cleanup()?);
Ok(()) Ok(())
} }
@ -41,6 +51,7 @@ fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result
fn panel_ctrl_c_exits_promptly_after_background_barrier() -> yoi_e2e::Result<()> { 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 fixture = FixtureWorkspace::new(&binary)?;
assert_fixture_paths_are_isolated(&fixture);
let mut panel = let mut panel =
PanelHarness::spawn(fixture.panel_config_holding_background_task(binary, "reload"))?; PanelHarness::spawn(fixture.panel_config_holding_background_task(binary, "reload"))?;
@ -68,5 +79,95 @@ fn panel_ctrl_c_exits_promptly_after_background_barrier() -> yoi_e2e::Result<()>
"quit_requested observability event missing; artifacts at {}", "quit_requested observability event missing; artifacts at {}",
panel.artifacts().dir.display() panel.artifacts().dir.display()
); );
drop(panel);
assert_fixture_cleanup(fixture.cleanup()?);
Ok(()) Ok(())
} }
fn assert_fixture_paths_are_isolated(fixture: &FixtureWorkspace) {
assert!(
fixture.root.exists(),
"fixture temp root should exist during scenario"
);
assert!(fixture.workspace.starts_with(&fixture.root));
assert!(fixture.home.starts_with(&fixture.root));
assert!(fixture.xdg_data_home.starts_with(&fixture.root));
assert!(fixture.xdg_state_home.starts_with(&fixture.root));
assert!(fixture.xdg_config_home.starts_with(&fixture.root));
assert!(fixture.xdg_runtime_dir.starts_with(&fixture.root));
assert!(
!fixture.artifacts_dir.starts_with(&fixture.root),
"persistent artifacts must live outside the temp root so cleanup can remove the fixture"
);
if let Some(host_runtime) = std::env::var_os("XDG_RUNTIME_DIR") {
assert_ne!(
fixture.xdg_runtime_dir,
std::path::PathBuf::from(host_runtime),
"tested yoi must not reuse host XDG_RUNTIME_DIR"
);
}
}
fn assert_no_runtime_or_host_pod_leak(
fixture: &FixtureWorkspace,
rows: &[RenderedPanelRow],
artifacts: &str,
) {
let rendered = rows
.iter()
.map(|row| {
format!(
"{} {} {} {}",
row.key.kind,
row.key.id,
row.title,
row.status.as_deref().unwrap_or_default()
)
})
.collect::<Vec<_>>()
.join("\n");
for marker in [
"workspace-orchestrator",
"yoi-orchestrator-orchestrator",
"host-runtime-leak",
] {
assert!(
!rendered.contains(marker),
"host/fixture runtime Pod marker {marker:?} leaked into panel rows; artifacts at {artifacts}\n{rendered}"
);
}
if let Some(host_runtime) = std::env::var_os("XDG_RUNTIME_DIR") {
let host_runtime = host_runtime.to_string_lossy();
assert!(
!rendered.contains(host_runtime.as_ref()),
"host XDG_RUNTIME_DIR leaked into panel rows; artifacts at {artifacts}\n{rendered}"
);
}
assert!(
rendered.contains("E2E Ticket"),
"panel should be observing fixture-local Ticket data; artifacts at {artifacts}\n{rendered}"
);
assert!(fixture.xdg_runtime_dir.starts_with(&fixture.root));
}
fn assert_fixture_cleanup(report: FixtureCleanupReport) {
assert!(
report.cleanup_success,
"fixture cleanup failed; report at {}: {:?}",
report.report_path.display(),
report.cleanup_error
);
assert!(
!report.fixture_root.exists(),
"fixture temp root should be removed after scenario: {}",
report.fixture_root.display()
);
assert!(
report.report_path.exists(),
"cleanup artifact should persist"
);
assert!(
report.snapshot_dir.exists(),
"fixture snapshot should persist under target/e2e-artifacts before temp cleanup"
);
}