merge: integrate orchestration branch
# Conflicts: # .yoi/tickets/00001KV0TJVN5/item.md # .yoi/tickets/00001KV0TJVN5/thread.md
This commit is contained in:
commit
fcebd4839b
|
|
@ -2,7 +2,7 @@
|
||||||
title: 'E2E harness が最新 yoi binary を自動 build して使うようにする'
|
title: 'E2E harness が最新 yoi binary を自動 build して使うようにする'
|
||||||
state: 'closed'
|
state: 'closed'
|
||||||
created_at: '2026-06-13T15:46:07Z'
|
created_at: '2026-06-13T15:46:07Z'
|
||||||
updated_at: '2026-06-13T16:34:06Z'
|
updated_at: '2026-06-13T16:53:48Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
readiness: 'ready'
|
readiness: 'ready'
|
||||||
queued_by: 'yoi ticket'
|
queued_by: 'yoi ticket'
|
||||||
|
|
|
||||||
|
|
@ -284,5 +284,26 @@ Ticket を closed にしました。
|
||||||
|
|
||||||
Closed after prior done-state completion.
|
Closed after prior done-state completion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: comment author: orchestrator at: 2026-06-13T16:53:48Z -->
|
||||||
|
|
||||||
|
## Comment
|
||||||
|
|
||||||
|
Post-merge concern from user: E2E isolation may still have leaked or left visible Pod/worktree artifacts.
|
||||||
|
|
||||||
|
Observed concern:
|
||||||
|
- User reports Pods named `yoi-orchestrator-orchestrator` and `workspace-orchestrator` appeared.
|
||||||
|
|
||||||
|
Current assessment:
|
||||||
|
- Earlier E2E runs before the env isolation follow-up inherited host environment, including likely `XDG_RUNTIME_DIR`, so `yoi panel` could observe the host/global runtime Pod registry under `/run/user/...` even though `HOME`/`XDG_DATA_HOME` were fixture paths。
|
||||||
|
- The fixture also intentionally writes blocking Pod metadata for `workspace` and `workspace-orchestrator` under fixture `XDG_DATA_HOME` to drive panel rows. That should be fixture-local, but if runtime/data isolation is wrong it can become visible outside the intended fixture。
|
||||||
|
- The later `env_clear()` + allowlist fix prevents host env credential leak and likely prevents inheriting `XDG_RUNTIME_DIR`, causing runtime fallback to fixture HOME; however, no explicit regression assertion currently proves that E2E cannot see/create global runtime Pod state or workspace-orchestrator worktrees。
|
||||||
|
|
||||||
|
Required follow-up direction:
|
||||||
|
- Add explicit runtime isolation to E2E (`XDG_RUNTIME_DIR` or equivalent controlled fixture runtime path, or an assertion that fallback runtime is fixture-local)。
|
||||||
|
- Add regression assertions/artifacts proving tested `yoi panel` sees only fixture Pod metadata/runtime state and does not observe host live Pods。
|
||||||
|
- Ensure E2E cleanup removes any fixture Pod metadata/runtime/worktree artifacts it creates。
|
||||||
|
- Investigate and clean any residual `yoi-orchestrator-orchestrator` / `workspace-orchestrator` artifacts only after confirming whether they are live Pods, fixture artifacts, or prior Panel-created worktrees。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
0
.yoi/tickets/00001KV0YK5S0/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KV0YK5S0/artifacts/.gitkeep
Normal file
21
.yoi/tickets/00001KV0YK5S0/artifacts/relations.json
Normal file
21
.yoi/tickets/00001KV0YK5S0/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"relations": [
|
||||||
|
{
|
||||||
|
"ticket_id": "00001KV0YK5S0",
|
||||||
|
"kind": "related",
|
||||||
|
"target": "00001KSKBP9YG",
|
||||||
|
"note": "E2E harness first slice の runtime/tmp isolation と cleanup follow-up。",
|
||||||
|
"author": "orchestrator",
|
||||||
|
"at": "2026-06-13T16:56:22Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "00001KV0YK5S0",
|
||||||
|
"kind": "related",
|
||||||
|
"target": "00001KV0TJVN5",
|
||||||
|
"note": "E2E binary/env isolation follow-up の残課題(runtime/data/workspace isolation and cleanup)を補う。",
|
||||||
|
"author": "orchestrator",
|
||||||
|
"at": "2026-06-13T16:56:22Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
43
.yoi/tickets/00001KV0YK5S0/item.md
Normal file
43
.yoi/tickets/00001KV0YK5S0/item.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
title: 'E2E harness を完全な tmp runtime/data/workspace 隔離と cleanup に対応させる'
|
||||||
|
state: 'done'
|
||||||
|
created_at: '2026-06-13T16:56:11Z'
|
||||||
|
updated_at: '2026-06-13T17:33:53Z'
|
||||||
|
assignee: null
|
||||||
|
readiness: 'ready'
|
||||||
|
queued_by: 'yoi ticket'
|
||||||
|
queued_at: '2026-06-13T16:56:31Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
E2E harness は `00001KSKBP9YG` / `00001KV0TJVN5` で Panel PTY E2E、最新 `yoi` binary build、tested subprocess の env isolation を導入した。しかし、ユーザーから `yoi-orchestrator-orchestrator` / `workspace-orchestrator` などの Pod/worktree artifact が出現したとの報告があり、host runtime / Pod registry / worktree artifact isolation と cleanup がまだ十分に証明されていない。
|
||||||
|
|
||||||
|
既知の問題:
|
||||||
|
- 初期 E2E は `env_clear()` 前に `XDG_RUNTIME_DIR` など host env を継承し得た。
|
||||||
|
- Fixture は `workspace` / `workspace-orchestrator` の Pod metadata を作るが、これは fixture-local でなければならない。
|
||||||
|
- 現在の env isolation は host env leak を防ぐが、E2E が完全に clean な tmp runtime/data/workspace で動き、実行後に cleanup することを明示的に保証・検証していない。
|
||||||
|
|
||||||
|
## 要件
|
||||||
|
|
||||||
|
- E2E は毎回完全に clean な temporary environment を作って実行する。
|
||||||
|
- Workspace / HOME / XDG_DATA_HOME / XDG_STATE_HOME / XDG_CONFIG_HOME / runtime dir / artifacts root を fixture ごとに分離する。
|
||||||
|
- Tested `yoi` subprocess は host runtime / Pod registry / session / worktree / data dir を見ない。
|
||||||
|
- Fixture で作る Pod metadata(例: `workspace`, `workspace-orchestrator`)は fixture-local であり、host/global registry に出ない。
|
||||||
|
- 実行後、fixture runtime/data/workspace/temp dirs は成功・失敗に関係なく cleanup される。失敗時に必要な artifact は `target/e2e-artifacts/...` にコピーしてから cleanup する。
|
||||||
|
- Cleanup policy / fixture root / runtime dir / data dir / removed paths を artifact に記録する。
|
||||||
|
- 既存の binary provider、env credential isolation、mouse capture tracking、quit pending barrier を壊さない。
|
||||||
|
|
||||||
|
## 受け入れ条件
|
||||||
|
|
||||||
|
- `cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` が clean tmp env を使い、終了後に fixture temp root を残さない。
|
||||||
|
- E2E artifact から fixture workspace/data/runtime paths と cleanup result が確認できる。
|
||||||
|
- Test または assertion により、Panel が host live Pods / host runtime registry を見ていないことを確認する。
|
||||||
|
- Fixture-created `workspace-orchestrator` 等が fixture-local であり、cleanup 後に temp root ごと消えることを確認する。
|
||||||
|
- Host `XDG_RUNTIME_DIR` などを設定した状態でも tested `yoi` は fixture runtime だけを見る。
|
||||||
|
- `cargo fmt --check`、`git diff --check`、関連 `cargo check` / E2E tests が pass する。
|
||||||
|
|
||||||
|
## 関連
|
||||||
|
|
||||||
|
- `00001KSKBP9YG`: E2E harness first slice。
|
||||||
|
- `00001KV0TJVN5`: E2E binary provider / env isolation follow-up。
|
||||||
195
.yoi/tickets/00001KV0YK5S0/thread.md
Normal file
195
.yoi/tickets/00001KV0YK5S0/thread.md
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
<!-- event: create author: orchestrator at: 2026-06-13T16:56:11Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: orchestrator at: 2026-06-13T16:56:22Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
ユーザーが E2E を完全に clean な tmp environment で実行し、毎回 cleanup することを明示した。要件は workspace/HOME/XDG/runtime/artifacts の fixture 分離、host Pod/runtime registry 非参照、fixture-created Pod metadata の fixture-local 化、成功/失敗時 cleanup と artifact 記録。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T16:56:22Z from: planning to: ready reason: user_authorized_clean_tmp_e2e_isolation field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket planning が完了しました。state planning -> ready。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-06-13T16:56:31Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `yoi ticket` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-06-13T16:56:50Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- ユーザーが E2E harness の clean tmp environment / cleanup を明示した。
|
||||||
|
- Ticket は `queued` で、要件・受け入れ条件は具体的。既存 `00001KSKBP9YG` / `00001KV0TJVN5` の follow-up で、主な変更面は `tests/e2e` に bounded される。
|
||||||
|
- Blocking relation はなく、既存 queued Tickets とは変更面が異なる。Orchestrator worktree は clean。
|
||||||
|
- Risk は test/runtime isolation だが、binding invariants(host runtime registry 非参照、fixture-local Pod metadata、cleanup artifact 記録)が明確なため実装可能。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- 新規 Ticket body / related records。
|
||||||
|
- `00001KV0TJVN5` の post-merge concern: host/global runtime registry を見た可能性、fixture `workspace-orchestrator` metadata、runtime isolation assertion 不足。
|
||||||
|
- Existing E2E code map: `tests/e2e/src/lib.rs` の fixture setup / `PanelHarness::spawn` / `run_yoi_capture` / artifact handling。
|
||||||
|
- Orchestrator workspace state: clean。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- E2E harness が毎回 clean な tmp workspace/data/runtime environment で実行され、host Pod/runtime registry を見ず、成功・失敗に関係なく fixture temp を cleanup するようにする。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- Tested `yoi` process は host runtime/data/session/worktree/Pod registry を見ない。
|
||||||
|
- Fixture-created Pods (`workspace`, `workspace-orchestrator` 等) は fixture-local。
|
||||||
|
- Failure diagnostics は `target/e2e-artifacts` にコピーして残し、fixture temp root は cleanup する。
|
||||||
|
- Existing binary provider、env credential isolation、mouse capture guard、quit pending barrier を壊さない。
|
||||||
|
- Root/original workspace では作業しない。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- `cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` が clean tmp env を使い、終了後に fixture temp root を残さない。
|
||||||
|
- Artifact に fixture workspace/data/runtime paths、cleanup result、env/runtime policy が残る。
|
||||||
|
- Test/assertion で Panel が host live Pods / host runtime registry を見ないことを確認する。
|
||||||
|
- Host `XDG_RUNTIME_DIR` 等が設定されていても tested `yoi` は fixture runtime だけを見る。
|
||||||
|
- `cargo fmt --check` / `git diff --check` / relevant checks / E2E tests が pass。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- `tempfile::TempDir` / custom guard / artifact snapshot strategy の具体設計は Coder が選んでよい。
|
||||||
|
- Runtime dir は explicit `XDG_RUNTIME_DIR` fixture path でも、Yoi-specific fixture runtime env でもよいが、host fallback を使わないことを artifact/assertion で示す。
|
||||||
|
- Failure artifact は fixture tree 全体ではなく必要 subset copy でよい。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Yoi runtime path discovery に explicit test runtime dir を渡す public/production surface 変更が必要になる場合。
|
||||||
|
- Cleanup と artifact preservation が両立しない design constraint がある場合。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T16:56:58Z from: queued to: inprogress reason: orchestrator_acceptance_user_requested_clean_tmp_isolation field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
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 -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
外部 Reviewer Pod 起動を試みたが、この Orchestrator の現在の delegation scope では child launch validation が要求する root workspace read grant を再委譲できず、Reviewer Pod を起動できなかった。そのため Orchestrator が実装 diff・validation・artifact を直接確認した。
|
||||||
|
|
||||||
|
確認した実装:
|
||||||
|
- `tests/e2e/src/lib.rs` に per-scenario `FixtureWorkspace` / `TempDir` 管理を追加。
|
||||||
|
- fixture root 配下に `workspace`、`home`、`data`、`state`、`config`、`runtime` を作り、tested `yoi` には `env_clear()` + fixture-local `XDG_RUNTIME_DIR` を渡す。
|
||||||
|
- `PanelHarness::spawn` と fixture setup command は host `XDG_RUNTIME_DIR` / host provider env を継承しない。
|
||||||
|
- fixture snapshot / cleanup report / fixture metadata を `target/e2e-artifacts/<run>/` に保存し、fixture temp root は cleanup 後に存在しないことを記録する。
|
||||||
|
- tests は hostile host `XDG_RUNTIME_DIR` を設定した状態でも fixture runtime policy を使う。
|
||||||
|
- existing binary provider、mouse capture guard、quit pending barrier、env credential isolation は維持されている。
|
||||||
|
|
||||||
|
Validation performed by Orchestrator:
|
||||||
|
- `cargo fmt --check`: PASS
|
||||||
|
- `git diff --check`: PASS
|
||||||
|
- `cargo check -p yoi-e2e --all-targets --features e2e`: PASS
|
||||||
|
- `XDG_RUNTIME_DIR=/tmp/yoi-e2e-host-runtime-leak-test 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`: PASS
|
||||||
|
- artifact spot-check: recent `cleanup.json` records `cleanup_success: true` and `fixture_root_exists_after: false`。
|
||||||
|
- `target/e2e-tmp` の直下に residual fixture temp root が残っていないことを確認。
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Failure artifact は fixture snapshot を保存するため、host への leak ではなく `target/e2e-artifacts` に診断情報が残る。
|
||||||
|
- Fixture-created `workspace` / `workspace-orchestrator` Pod metadata は fixture data dir / snapshot に閉じる。host runtime/data registry を読む経路は fixture `XDG_RUNTIME_DIR` + env clear により遮断されている。
|
||||||
|
|
||||||
|
Decision: approve.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-06-13T17:33:46Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Integration outcome: merged and validated.
|
||||||
|
|
||||||
|
- Implementation branch: `ticket-00001KV0YK5S0-e2e-tmp-isolation`
|
||||||
|
- Implementation commit: `07e754ce test: isolate e2e tmp runtime fixtures`
|
||||||
|
- Review: Orchestrator direct review `approve`(Reviewer Pod は現 delegated scope の child launch validation 制約で起動できなかったため、Orchestrator が diff / validation / artifact を確認)
|
||||||
|
- Orchestrator merge commit: `20184eeb merge: e2e tmp isolation`
|
||||||
|
|
||||||
|
Implemented:
|
||||||
|
- E2E fixture now creates per-scenario clean temp root under `target/e2e-tmp` with isolated workspace, HOME, XDG data/state/config, and explicit fixture `XDG_RUNTIME_DIR`。
|
||||||
|
- Tested `yoi` subprocesses continue to use `env_clear()` and now receive fixture-local runtime dir, preventing host runtime / Pod registry observation。
|
||||||
|
- Fixture metadata, run metadata, fixture snapshot, and cleanup report are persisted under `target/e2e-artifacts/<run>` before temp cleanup。
|
||||||
|
- Fixture temp roots are removed after scenario completion, and cleanup reports record `cleanup_success` plus `fixture_root_exists_after`。
|
||||||
|
- Existing binary provider, `YOI_E2E_BIN` override, credential env isolation, mouse capture guard, and quit pending barrier remain intact。
|
||||||
|
|
||||||
|
Orchestrator validation after merge:
|
||||||
|
- `cargo fmt --check`: PASS
|
||||||
|
- `git diff --check`: PASS
|
||||||
|
- `cargo check -p yoi-e2e --all-targets --features e2e`: PASS
|
||||||
|
- `XDG_RUNTIME_DIR=/tmp/yoi-e2e-host-runtime-leak-test 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`: PASS(2 tests)
|
||||||
|
- Artifact spot-check: latest `cleanup.json` files record `cleanup_success: true` and `fixture_root_exists_after: false`。
|
||||||
|
- `target/e2e-tmp` direct child count after validation: 0。
|
||||||
|
|
||||||
|
Residual notes:
|
||||||
|
- Persistent diagnostic artifacts intentionally remain under `target/e2e-artifacts`。
|
||||||
|
- Fixture snapshots may contain fixture-local `workspace` / `workspace-orchestrator` metadata, but those live under the copied artifact snapshot, not host runtime/data state。
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Mark Ticket `done` and clean up implementation worktree/branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-06-13T17:33:53Z from: inprogress to: done reason: merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
E2E tmp/runtime isolation follow-up was reviewed, merged into the Orchestrator branch as `20184eeb`, and validated in the Orchestrator worktree. Panel E2E now uses clean per-scenario tmp workspace/data/runtime fixtures, preserves artifacts under `target/e2e-artifacts`, removes fixture temp roots after runs, and does not inherit host runtime/credential environment. Ticket implementation work is done; closure remains separate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -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)?;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user