refactor: rename pod crate to worker
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
Owns:
|
||||
|
||||
- one-shot Pod socket client behavior
|
||||
- one-shot Worker socket client behavior
|
||||
- request/reply delivery mechanics
|
||||
- runtime command construction below the product façade
|
||||
- shared attach/status probing helpers used by higher layers
|
||||
@@ -16,15 +16,15 @@ Owns:
|
||||
Does not own:
|
||||
|
||||
- product command names (`yoi`)
|
||||
- Pod state authority (`pod`, `pod-store`, `session-store`)
|
||||
- Worker state authority (`worker`, `pod-store`, `session-store`)
|
||||
- UI rendering (`tui`)
|
||||
- Engine turn semantics (`llm-engine`)
|
||||
|
||||
## Design notes
|
||||
|
||||
The client boundary lets `tui` and `yoi` share Pod communication without making library crates depend on the product binary. Socket clients should drain connect-time snapshot/alert traffic before sending a method or deciding status.
|
||||
The client boundary lets `tui` and `yoi` share Worker communication without making library crates depend on the product binary. Socket clients should drain connect-time snapshot/alert traffic before sending a method or deciding status.
|
||||
|
||||
## See also
|
||||
|
||||
- [`../../docs/design/pod-session-state.md`](../../docs/design/pod-session-state.md)
|
||||
- [`../../docs/design/worker-session-state.md`](../../docs/design/worker-session-state.md)
|
||||
- [`../../docs/design/overview.md`](../../docs/design/overview.md)
|
||||
|
||||
+11
-11
@@ -1,28 +1,28 @@
|
||||
//! Pod プロトコルを喋るクライアント。
|
||||
//! Worker プロトコルを喋るクライアント。
|
||||
//!
|
||||
//! - [`PodClient`]: 既存 pod の Unix ソケットへ接続して `Method` を送り、
|
||||
//! - [`WorkerClient`]: 既存 worker の Unix ソケットへ接続して `Method` を送り、
|
||||
//! `Event` を受け取る低レベル接続。
|
||||
//! - [`spawn`]: pod バイナリをサブプロセスとして起動し、`YOI-READY`
|
||||
//! - [`spawn`]: worker バイナリをサブプロセスとして起動し、`YOI-READY`
|
||||
//! ハンドシェイクが終わるまで待つフロー。subprocess を立ち上げる必要が
|
||||
//! ない呼び出し側 (=既存 pod に attach する場合) は使わなくてよい。
|
||||
//! ない呼び出し側 (=既存 worker に attach する場合) は使わなくてよい。
|
||||
//!
|
||||
//! TUI / GUI / E2E ハーネスはこの crate に依存して protocol を喋る。
|
||||
|
||||
mod pod_client;
|
||||
pub mod runtime_command;
|
||||
pub mod spawn;
|
||||
pub mod ticket_role;
|
||||
mod worker_client;
|
||||
|
||||
pub use runtime_command::PodRuntimeCommand;
|
||||
pub use runtime_command::WorkerRuntimeCommand;
|
||||
|
||||
pub use pod_client::PodClient;
|
||||
pub use spawn::{
|
||||
PodProcessLaunchConfig, PodProcessLaunchOptions, SpawnConfig, SpawnError, SpawnReady,
|
||||
spawn_pod, spawn_pod_with_options,
|
||||
SpawnConfig, SpawnError, SpawnReady, WorkerProcessLaunchConfig, WorkerProcessLaunchOptions,
|
||||
spawn_worker, spawn_worker_with_options,
|
||||
};
|
||||
pub use ticket_role::{
|
||||
TicketRef, TicketRoleLaunchContext, TicketRoleLaunchError, TicketRoleLaunchOptions,
|
||||
TicketRoleLaunchPlan, TicketRoleLaunchResult, TicketRolePreRunWarning, launch_ticket_role_pod,
|
||||
launch_ticket_role_pod_with_options, plan_ticket_role_launch,
|
||||
TicketRoleLaunchPlan, TicketRoleLaunchResult, TicketRolePreRunWarning,
|
||||
launch_ticket_role_worker, launch_ticket_role_worker_with_options, plan_ticket_role_launch,
|
||||
plan_ticket_role_launch_with_config,
|
||||
};
|
||||
pub use worker_client::WorkerClient;
|
||||
|
||||
@@ -6,12 +6,12 @@ use std::path::{Path, PathBuf};
|
||||
const POD_RUNTIME_COMMAND_ENV: &str = "YOI_POD_RUNTIME_COMMAND";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PodRuntimeCommand {
|
||||
pub struct WorkerRuntimeCommand {
|
||||
pub program: PathBuf,
|
||||
pub prefix_args: Vec<OsString>,
|
||||
}
|
||||
|
||||
impl PodRuntimeCommand {
|
||||
impl WorkerRuntimeCommand {
|
||||
pub fn new(program: impl Into<PathBuf>, prefix_args: Vec<OsString>) -> Self {
|
||||
Self {
|
||||
program: program.into(),
|
||||
@@ -24,15 +24,15 @@ impl PodRuntimeCommand {
|
||||
}
|
||||
|
||||
pub fn for_executable(program: impl Into<PathBuf>) -> Self {
|
||||
Self::new(program, vec![OsString::from("pod")])
|
||||
Self::new(program, vec![OsString::from("worker")])
|
||||
}
|
||||
|
||||
/// Resolve the Pod runtime command used for subprocess launches.
|
||||
/// Resolve the Worker runtime command used for subprocess launches.
|
||||
///
|
||||
/// The default launch path is always the current `yoi` executable plus
|
||||
/// the unified `pod` prefix argument. During development, a non-empty
|
||||
/// the unified `worker` prefix argument. During development, a non-empty
|
||||
/// `YOI_POD_RUNTIME_COMMAND` value replaces only the executable path;
|
||||
/// the `pod` prefix is still added here and the env value is not parsed as a
|
||||
/// the `worker` prefix is still added here and the env value is not parsed as a
|
||||
/// shell command.
|
||||
pub fn resolve() -> io::Result<Self> {
|
||||
Self::resolve_from_env_value(
|
||||
@@ -74,7 +74,7 @@ impl PodRuntimeCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PodRuntimeCommand {
|
||||
impl fmt::Display for WorkerRuntimeCommand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.program.display())?;
|
||||
for arg in &self.prefix_args {
|
||||
@@ -89,14 +89,14 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn yoi_binary_defaults_to_pod_prefix() {
|
||||
let command = PodRuntimeCommand::for_executable("/opt/yoi/bin/yoi");
|
||||
fn yoi_binary_defaults_to_worker_prefix() {
|
||||
let command = WorkerRuntimeCommand::for_executable("/opt/yoi/bin/yoi");
|
||||
|
||||
assert_eq!(command.program(), Path::new("/opt/yoi/bin/yoi"));
|
||||
assert_eq!(command.prefix_args(), [OsString::from("pod")]);
|
||||
assert_eq!(command.prefix_args(), [OsString::from("worker")]);
|
||||
assert_eq!(
|
||||
command.argv_with(["--pod", "agent"]),
|
||||
vec!["pod", "--pod", "agent"]
|
||||
command.argv_with(["--worker", "agent"]),
|
||||
vec!["worker", "--worker", "agent"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>()
|
||||
@@ -104,14 +104,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_runtime_executable_gets_pod_prefix() {
|
||||
let command = PodRuntimeCommand::for_executable("/opt/yoi/bin/custom-runtime");
|
||||
fn any_runtime_executable_gets_worker_prefix() {
|
||||
let command = WorkerRuntimeCommand::for_executable("/opt/yoi/bin/custom-runtime");
|
||||
|
||||
assert_eq!(command.program(), Path::new("/opt/yoi/bin/custom-runtime"));
|
||||
assert_eq!(command.prefix_args(), [OsString::from("pod")]);
|
||||
assert_eq!(command.prefix_args(), [OsString::from("worker")]);
|
||||
assert_eq!(
|
||||
command.argv_with(["--pod", "agent"]),
|
||||
vec!["pod", "--pod", "agent"]
|
||||
command.argv_with(["--worker", "agent"]),
|
||||
vec!["worker", "--worker", "agent"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>()
|
||||
@@ -120,43 +120,43 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn resolve_uses_current_exe_when_override_is_unset() {
|
||||
let command = PodRuntimeCommand::resolve_from_env_value(None, || {
|
||||
let command = WorkerRuntimeCommand::resolve_from_env_value(None, || {
|
||||
Ok(PathBuf::from("/opt/yoi/bin/yoi"))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
PodRuntimeCommand::for_executable("/opt/yoi/bin/yoi")
|
||||
WorkerRuntimeCommand::for_executable("/opt/yoi/bin/yoi")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_uses_current_exe_when_override_is_empty() {
|
||||
let command = PodRuntimeCommand::resolve_from_env_value(Some(OsString::new()), || {
|
||||
let command = WorkerRuntimeCommand::resolve_from_env_value(Some(OsString::new()), || {
|
||||
Ok(PathBuf::from("/opt/yoi/bin/yoi"))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
PodRuntimeCommand::for_executable("/opt/yoi/bin/yoi")
|
||||
WorkerRuntimeCommand::for_executable("/opt/yoi/bin/yoi")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_override_replaces_only_program_and_keeps_pod_prefix() {
|
||||
let command = PodRuntimeCommand::resolve_from_env_value(
|
||||
fn resolve_override_replaces_only_program_and_keeps_worker_prefix() {
|
||||
let command = WorkerRuntimeCommand::resolve_from_env_value(
|
||||
Some(OsString::from("/tmp/rebuilt yoi")),
|
||||
|| panic!("override must not inspect current_exe"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(command.program(), Path::new("/tmp/rebuilt yoi"));
|
||||
assert_eq!(command.prefix_args(), [OsString::from("pod")]);
|
||||
assert_eq!(command.prefix_args(), [OsString::from("worker")]);
|
||||
assert_eq!(
|
||||
command.argv_with(["--pod", "agent"]),
|
||||
vec!["pod", "--pod", "agent"]
|
||||
command.argv_with(["--worker", "agent"]),
|
||||
vec!["worker", "--worker", "agent"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
+64
-61
@@ -1,13 +1,13 @@
|
||||
//! Pod runtime command をサブプロセスとして立ち上げ、`YOI-READY` を待つ
|
||||
//! Worker runtime command をサブプロセスとして立ち上げ、`YOI-READY` を待つ
|
||||
//! ハンドシェイク。
|
||||
//!
|
||||
//! - 親プロセス (TUI / GUI / E2E) は profile/default/typed restore flags を
|
||||
//! 指定してこの関数に渡す。pod はそれを受けて socket を bind し、stderr に
|
||||
//! 指定してこの関数に渡す。worker はそれを受けて socket を bind し、stderr に
|
||||
//! `YOI-READY\t<name>\t<socket>` を吐く。
|
||||
//! - 待機中の stderr 行は `progress` コールバック越しに呼び出し側へ流す。
|
||||
//! UI の進捗表示や E2E のログ収集はここで賄う。
|
||||
//! - `kill_on_drop = false` + `process_group(0)` により、親プロセス
|
||||
//! ライフサイクルから切り離した detached pod を作る。ready 後の lifecycle
|
||||
//! ライフサイクルから切り離した detached worker を作る。ready 後の lifecycle
|
||||
//! 管理は runtime ディレクトリ / socket を介して行う。
|
||||
|
||||
use std::io;
|
||||
@@ -15,7 +15,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::PodRuntimeCommand;
|
||||
use crate::WorkerRuntimeCommand;
|
||||
use tokio::process::Command;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -23,14 +23,14 @@ const READY_PREFIX: &str = "YOI-READY\t";
|
||||
const READY_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PodProcessLaunchConfig {
|
||||
pub runtime_command: PodRuntimeCommand,
|
||||
/// `pod.name` として使う識別子。runtime ディレクトリ
|
||||
/// (`manifest::paths::pod_runtime_dir`) の解決と、ready 行に乗る
|
||||
pub struct WorkerProcessLaunchConfig {
|
||||
pub runtime_command: WorkerRuntimeCommand,
|
||||
/// `worker.name` として使う識別子。runtime ディレクトリ
|
||||
/// (`manifest::paths::worker_runtime_dir`) の解決と、ready 行に乗る
|
||||
/// 名前との突き合わせに使う。
|
||||
pub pod_name: String,
|
||||
/// Optional reusable Profile selector. Pod identity is always supplied
|
||||
/// separately with `--pod`; profile selection must not imply a name.
|
||||
pub worker_name: String,
|
||||
/// Optional reusable Profile selector. Worker identity is always supplied
|
||||
/// separately with `--worker`; profile selection must not imply a name.
|
||||
pub profile: Option<String>,
|
||||
/// Explicit runtime workspace root. The child receives it via
|
||||
/// `--workspace` so startup does not infer workspace identity from the
|
||||
@@ -46,7 +46,7 @@ pub struct PodProcessLaunchConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct PodProcessLaunchOptions {
|
||||
pub struct WorkerProcessLaunchOptions {
|
||||
/// Extra child CLI arguments supplied by an upper resolver layer. The
|
||||
/// low-level launch config intentionally does not model Ticket IDs,
|
||||
/// Ticket roles, orchestration roles, executable authority, or raw
|
||||
@@ -54,7 +54,7 @@ pub struct PodProcessLaunchOptions {
|
||||
pub extra_args: Vec<String>,
|
||||
}
|
||||
|
||||
impl PodProcessLaunchOptions {
|
||||
impl WorkerProcessLaunchOptions {
|
||||
pub fn with_hidden_arg(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
self.extra_args.extend([name.into(), value.into()]);
|
||||
self
|
||||
@@ -65,11 +65,11 @@ impl PodProcessLaunchOptions {
|
||||
}
|
||||
}
|
||||
|
||||
pub type SpawnConfig = PodProcessLaunchConfig;
|
||||
pub type SpawnConfig = WorkerProcessLaunchConfig;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SpawnReady {
|
||||
pub pod_name: String,
|
||||
pub worker_name: String,
|
||||
pub socket_path: PathBuf,
|
||||
}
|
||||
|
||||
@@ -79,10 +79,10 @@ pub enum SpawnError {
|
||||
/// runtime ディレクトリが解決できなかった (環境変数未設定等)。
|
||||
RuntimeDirUnavailable,
|
||||
PodLaunchFailed {
|
||||
command: PodRuntimeCommand,
|
||||
command: WorkerRuntimeCommand,
|
||||
source: io::Error,
|
||||
},
|
||||
PodExitedEarly {
|
||||
WorkerExitedEarly {
|
||||
stderr_tail: String,
|
||||
},
|
||||
Timeout,
|
||||
@@ -98,18 +98,18 @@ impl std::fmt::Display for SpawnError {
|
||||
),
|
||||
Self::PodLaunchFailed { command, source } => write!(
|
||||
f,
|
||||
"failed to launch pod runtime command `{command}`: {source}"
|
||||
"failed to launch worker runtime command `{command}`: {source}"
|
||||
),
|
||||
Self::PodExitedEarly { stderr_tail } => {
|
||||
Self::WorkerExitedEarly { stderr_tail } => {
|
||||
if stderr_tail.is_empty() {
|
||||
write!(f, "pod exited before becoming ready")
|
||||
write!(f, "worker exited before becoming ready")
|
||||
} else {
|
||||
write!(f, "pod exited before becoming ready: {stderr_tail}")
|
||||
write!(f, "worker exited before becoming ready: {stderr_tail}")
|
||||
}
|
||||
}
|
||||
Self::Timeout => write!(
|
||||
f,
|
||||
"pod did not become ready within {}s",
|
||||
"worker did not become ready within {}s",
|
||||
READY_TIMEOUT.as_secs()
|
||||
),
|
||||
}
|
||||
@@ -120,7 +120,7 @@ impl std::error::Error for SpawnError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io(error) | Self::PodLaunchFailed { source: error, .. } => Some(error),
|
||||
Self::RuntimeDirUnavailable | Self::PodExitedEarly { .. } | Self::Timeout => None,
|
||||
Self::RuntimeDirUnavailable | Self::WorkerExitedEarly { .. } | Self::Timeout => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,10 @@ impl From<io::Error> for SpawnError {
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_args(config: &PodProcessLaunchConfig, options: &PodProcessLaunchOptions) -> Vec<String> {
|
||||
fn runtime_args(
|
||||
config: &WorkerProcessLaunchConfig,
|
||||
options: &WorkerProcessLaunchOptions,
|
||||
) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"--workspace".to_string(),
|
||||
config.workspace_root.display().to_string(),
|
||||
@@ -140,11 +143,11 @@ fn runtime_args(config: &PodProcessLaunchConfig, options: &PodProcessLaunchOptio
|
||||
args.extend([
|
||||
"--session".to_string(),
|
||||
id.to_string(),
|
||||
"--pod".to_string(),
|
||||
config.pod_name.clone(),
|
||||
"--worker".to_string(),
|
||||
config.worker_name.clone(),
|
||||
]);
|
||||
} else {
|
||||
args.extend(["--pod".to_string(), config.pod_name.clone()]);
|
||||
args.extend(["--worker".to_string(), config.worker_name.clone()]);
|
||||
if let Some(profile) = &config.profile {
|
||||
args.extend(["--profile".to_string(), profile.clone()]);
|
||||
}
|
||||
@@ -153,32 +156,32 @@ fn runtime_args(config: &PodProcessLaunchConfig, options: &PodProcessLaunchOptio
|
||||
args
|
||||
}
|
||||
|
||||
/// pod を spawn し、`YOI-READY` ハンドシェイクが終わるまで待つ。
|
||||
/// worker を spawn し、`YOI-READY` ハンドシェイクが終わるまで待つ。
|
||||
///
|
||||
/// `progress` は ready 行を見つけるまでに観測した stderr の各行で呼ばれる
|
||||
/// (ready 行自体は除外される)。UI の表示更新や E2E ログ取得に使う。
|
||||
pub async fn spawn_pod<F>(
|
||||
config: PodProcessLaunchConfig,
|
||||
pub async fn spawn_worker<F>(
|
||||
config: WorkerProcessLaunchConfig,
|
||||
progress: F,
|
||||
) -> Result<SpawnReady, SpawnError>
|
||||
where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
spawn_pod_with_options(config, PodProcessLaunchOptions::default(), progress).await
|
||||
spawn_worker_with_options(config, WorkerProcessLaunchOptions::default(), progress).await
|
||||
}
|
||||
|
||||
pub async fn spawn_pod_with_options<F>(
|
||||
config: PodProcessLaunchConfig,
|
||||
options: PodProcessLaunchOptions,
|
||||
pub async fn spawn_worker_with_options<F>(
|
||||
config: WorkerProcessLaunchConfig,
|
||||
options: WorkerProcessLaunchOptions,
|
||||
mut progress: F,
|
||||
) -> Result<SpawnReady, SpawnError>
|
||||
where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
let pod_runtime_dir = manifest::paths::pod_runtime_dir(&config.pod_name)
|
||||
let worker_runtime_dir = manifest::paths::worker_runtime_dir(&config.worker_name)
|
||||
.ok_or(SpawnError::RuntimeDirUnavailable)?;
|
||||
std::fs::create_dir_all(&pod_runtime_dir).map_err(SpawnError::Io)?;
|
||||
let stderr_path = pod_runtime_dir.join("stderr.log");
|
||||
std::fs::create_dir_all(&worker_runtime_dir).map_err(SpawnError::Io)?;
|
||||
let stderr_path = worker_runtime_dir.join("stderr.log");
|
||||
let stderr_file = std::fs::File::create(&stderr_path).map_err(SpawnError::Io)?;
|
||||
|
||||
let mut command = Command::new(config.runtime_command.program());
|
||||
@@ -200,9 +203,9 @@ where
|
||||
})?;
|
||||
|
||||
// Default `kill_on_drop = false` plus `process_group(0)` makes this
|
||||
// a detached Pod once startup succeeds: dropping the handle does not
|
||||
// a detached Worker once startup succeeds: dropping the handle does not
|
||||
// terminate it, and terminal-generated signals for the parent's
|
||||
// process group do not hit the Pod. Runtime state/socket files are
|
||||
// process group do not hit the Worker. Runtime state/socket files are
|
||||
// the source of truth after that point.
|
||||
let ready = match wait_for_ready_file(&mut progress, &stderr_path, &mut child).await {
|
||||
Ok(ready) => ready,
|
||||
@@ -240,10 +243,10 @@ where
|
||||
for line in content[offset..].lines() {
|
||||
if let Some(rest) = line.strip_prefix(READY_PREFIX) {
|
||||
let mut parts = rest.splitn(2, '\t');
|
||||
let pod_name = parts.next().unwrap_or("").to_string();
|
||||
let worker_name = parts.next().unwrap_or("").to_string();
|
||||
let socket_str = parts.next().unwrap_or("").to_string();
|
||||
if pod_name.is_empty() || socket_str.is_empty() {
|
||||
return Err(SpawnError::PodExitedEarly {
|
||||
if worker_name.is_empty() || socket_str.is_empty() {
|
||||
return Err(SpawnError::WorkerExitedEarly {
|
||||
stderr_tail: format!("malformed ready line: {line}"),
|
||||
});
|
||||
}
|
||||
@@ -258,7 +261,7 @@ where
|
||||
)
|
||||
.await?;
|
||||
return Ok(SpawnReady {
|
||||
pod_name,
|
||||
worker_name,
|
||||
socket_path,
|
||||
});
|
||||
}
|
||||
@@ -274,11 +277,11 @@ where
|
||||
tokio::select! {
|
||||
status = child.wait() => {
|
||||
let _ = status;
|
||||
// Pod は exit 直前に最終 stderr 行を flush することがある。
|
||||
// Worker は exit 直前に最終 stderr 行を flush することがある。
|
||||
// child.wait() が解決した後に再読みして、原因行を取りこ
|
||||
// ぼさず PodExitedEarly に載せる。
|
||||
// ぼさず WorkerExitedEarly に載せる。
|
||||
drain_stderr_into_tail(stderr_path, &mut tail, &mut offset).await;
|
||||
return Err(SpawnError::PodExitedEarly {
|
||||
return Err(SpawnError::WorkerExitedEarly {
|
||||
stderr_tail: tail.into_string(),
|
||||
});
|
||||
}
|
||||
@@ -310,7 +313,7 @@ async fn wait_for_socket(
|
||||
status = child.wait() => {
|
||||
let _ = status;
|
||||
drain_stderr_into_tail(stderr_path, tail, offset).await;
|
||||
return Err(SpawnError::PodExitedEarly {
|
||||
return Err(SpawnError::WorkerExitedEarly {
|
||||
stderr_tail: tail.as_string(),
|
||||
});
|
||||
}
|
||||
@@ -363,10 +366,10 @@ mod tests {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
|
||||
fn base_config() -> PodProcessLaunchConfig {
|
||||
PodProcessLaunchConfig {
|
||||
runtime_command: PodRuntimeCommand::new("/bin/yoi", vec![OsString::from("pod")]),
|
||||
pod_name: "explicit-pod".to_string(),
|
||||
fn base_config() -> WorkerProcessLaunchConfig {
|
||||
WorkerProcessLaunchConfig {
|
||||
runtime_command: WorkerRuntimeCommand::new("/bin/yoi", vec![OsString::from("worker")]),
|
||||
worker_name: "explicit-worker".to_string(),
|
||||
profile: Some("project:companion".to_string()),
|
||||
workspace_root: PathBuf::from("/work/other-project"),
|
||||
cwd: None,
|
||||
@@ -375,14 +378,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_args_keep_workspace_pod_and_profile_separate() {
|
||||
fn runtime_args_keep_workspace_worker_and_profile_separate() {
|
||||
assert_eq!(
|
||||
runtime_args(&base_config(), &PodProcessLaunchOptions::default()),
|
||||
runtime_args(&base_config(), &WorkerProcessLaunchOptions::default()),
|
||||
vec![
|
||||
"--workspace",
|
||||
"/work/other-project",
|
||||
"--pod",
|
||||
"explicit-pod",
|
||||
"--worker",
|
||||
"explicit-worker",
|
||||
"--profile",
|
||||
"project:companion",
|
||||
]
|
||||
@@ -394,14 +397,14 @@ mod tests {
|
||||
let mut config = base_config();
|
||||
config.resume_from = Some(Uuid::nil());
|
||||
assert_eq!(
|
||||
runtime_args(&config, &PodProcessLaunchOptions::default()),
|
||||
runtime_args(&config, &WorkerProcessLaunchOptions::default()),
|
||||
vec![
|
||||
"--workspace",
|
||||
"/work/other-project",
|
||||
"--session",
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
"--pod",
|
||||
"explicit-pod",
|
||||
"--worker",
|
||||
"explicit-worker",
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -414,14 +417,14 @@ mod tests {
|
||||
assert_eq!(
|
||||
runtime_args(
|
||||
&config,
|
||||
&PodProcessLaunchOptions::default()
|
||||
&WorkerProcessLaunchOptions::default()
|
||||
.with_hidden_arg("--ticket-role", "orchestrator"),
|
||||
),
|
||||
vec![
|
||||
"--workspace",
|
||||
"/work/other-project",
|
||||
"--pod",
|
||||
"explicit-pod",
|
||||
"--worker",
|
||||
"explicit-worker",
|
||||
"--profile",
|
||||
"project:companion",
|
||||
"--ticket-role",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Ticket-role Pod launch planning and execution.
|
||||
//! Ticket-role Worker launch planning and execution.
|
||||
//!
|
||||
//! This module keeps Ticket role configuration, generated first-run input, and
|
||||
//! host-side Pod spawning behind the `client` crate so UI callers do not need to
|
||||
//! depend on `pod` internals.
|
||||
//! host-side Worker spawning behind the `client` crate so UI callers do not need to
|
||||
//! depend on `worker` internals.
|
||||
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -15,8 +15,8 @@ pub use ticket::config::TicketRole;
|
||||
use ticket::config::{TicketConfig, TicketConfigError, TicketRoleLaunchConfigError};
|
||||
|
||||
use crate::{
|
||||
PodClient, PodProcessLaunchConfig, PodProcessLaunchOptions, PodRuntimeCommand, SpawnError,
|
||||
SpawnReady, spawn_pod_with_options,
|
||||
SpawnError, SpawnReady, WorkerClient, WorkerProcessLaunchConfig, WorkerProcessLaunchOptions,
|
||||
WorkerRuntimeCommand, spawn_worker_with_options,
|
||||
};
|
||||
|
||||
const MAX_FIELD_CHARS: usize = 8_000;
|
||||
@@ -37,7 +37,7 @@ impl TicketRef {
|
||||
}
|
||||
}
|
||||
|
||||
fn pod_name_seed(&self) -> Option<&str> {
|
||||
fn worker_name_seed(&self) -> Option<&str> {
|
||||
non_empty(self.id.as_deref())
|
||||
}
|
||||
|
||||
@@ -55,14 +55,17 @@ impl TicketRef {
|
||||
/// Auditable panel handoff target included in a Ticket Intake launch.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketIntakeHandoff {
|
||||
pub orchestrator_pod: String,
|
||||
pub workspace_orchestrator_worker: String,
|
||||
pub workspace_label: String,
|
||||
}
|
||||
|
||||
impl TicketIntakeHandoff {
|
||||
pub fn new(orchestrator_pod: impl Into<String>, workspace_label: impl Into<String>) -> Self {
|
||||
pub fn new(
|
||||
workspace_orchestrator_worker: impl Into<String>,
|
||||
workspace_label: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
orchestrator_pod: orchestrator_pod.into(),
|
||||
workspace_orchestrator_worker: workspace_orchestrator_worker.into(),
|
||||
workspace_label: workspace_label.into(),
|
||||
}
|
||||
}
|
||||
@@ -70,7 +73,11 @@ impl TicketIntakeHandoff {
|
||||
fn append_submit_lines(&self, out: &mut String) {
|
||||
out.push_str("\nPanel handoff:\n");
|
||||
push_bounded_bullet(out, "workspace", &self.workspace_label);
|
||||
push_bounded_bullet(out, "workspace_orchestrator_pod", &self.orchestrator_pod);
|
||||
push_bounded_bullet(
|
||||
out,
|
||||
"workspace_workspace_orchestrator_worker",
|
||||
&self.workspace_orchestrator_worker,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +89,7 @@ pub struct TicketRoleLaunchContext {
|
||||
pub original_workspace_root: Option<PathBuf>,
|
||||
pub target_workspace_root: Option<PathBuf>,
|
||||
pub role: TicketRole,
|
||||
pub pod_name: Option<String>,
|
||||
pub worker_name: Option<String>,
|
||||
pub ticket: Option<TicketRef>,
|
||||
pub user_instruction: Option<String>,
|
||||
pub intake_handoff: Option<TicketIntakeHandoff>,
|
||||
@@ -102,7 +109,7 @@ impl TicketRoleLaunchContext {
|
||||
original_workspace_root: None,
|
||||
target_workspace_root: None,
|
||||
role,
|
||||
pod_name: None,
|
||||
worker_name: None,
|
||||
ticket: None,
|
||||
user_instruction: None,
|
||||
intake_handoff: None,
|
||||
@@ -156,7 +163,7 @@ pub struct TicketRoleLaunchPlan {
|
||||
pub target_workspace_root: PathBuf,
|
||||
pub implementation_worktree_root: PathBuf,
|
||||
pub role: TicketRole,
|
||||
pub pod_name: String,
|
||||
pub worker_name: String,
|
||||
pub profile: String,
|
||||
pub workflow: String,
|
||||
pub launch_prompt_ref: Option<String>,
|
||||
@@ -172,14 +179,14 @@ impl TicketRoleLaunchPlan {
|
||||
|
||||
pub fn spawn_config(
|
||||
&self,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
) -> Result<PodProcessLaunchConfig, TicketRoleLaunchError> {
|
||||
runtime_command: WorkerRuntimeCommand,
|
||||
) -> Result<WorkerProcessLaunchConfig, TicketRoleLaunchError> {
|
||||
if self.profile == "inherit" {
|
||||
return Err(TicketRoleLaunchError::UnsupportedInheritProfile);
|
||||
}
|
||||
Ok(PodProcessLaunchConfig {
|
||||
Ok(WorkerProcessLaunchConfig {
|
||||
runtime_command,
|
||||
pod_name: self.pod_name.clone(),
|
||||
worker_name: self.worker_name.clone(),
|
||||
profile: Some(self.profile.clone()),
|
||||
workspace_root: self.workspace_root.clone(),
|
||||
cwd: self.cwd.clone(),
|
||||
@@ -187,8 +194,8 @@ impl TicketRoleLaunchPlan {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn spawn_options(&self) -> PodProcessLaunchOptions {
|
||||
PodProcessLaunchOptions::default()
|
||||
pub fn spawn_options(&self) -> WorkerProcessLaunchOptions {
|
||||
WorkerProcessLaunchOptions::default()
|
||||
.with_hidden_arg("--ticket-role", self.role.as_str().to_string())
|
||||
}
|
||||
}
|
||||
@@ -208,7 +215,7 @@ pub struct TicketRoleLaunchResult {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketRoleLaunchAcceptanceEvidence {
|
||||
pub pod_name: String,
|
||||
pub worker_name: String,
|
||||
pub accepted_run_segments: usize,
|
||||
pub event: TicketRoleLaunchAcceptanceEvent,
|
||||
}
|
||||
@@ -233,8 +240,8 @@ pub struct TicketRoleLaunchOptions {
|
||||
}
|
||||
|
||||
impl TicketRoleLaunchOptions {
|
||||
pub fn with_pre_run_peer_registration(mut self, pod_name: impl Into<String>) -> Self {
|
||||
self.pre_run_peer_registrations.push(pod_name.into());
|
||||
pub fn with_pre_run_peer_registration(mut self, worker_name: impl Into<String>) -> Self {
|
||||
self.pre_run_peer_registrations.push(worker_name.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -253,30 +260,30 @@ pub enum TicketRoleLaunchError {
|
||||
selector: String,
|
||||
message: String,
|
||||
},
|
||||
#[error("Ticket role Pod name must not be empty")]
|
||||
EmptyPodName,
|
||||
#[error("Ticket role Worker name must not be empty")]
|
||||
EmptyWorkerName,
|
||||
#[error(
|
||||
"Ticket role profile 'inherit' cannot be used for top-level launch execution; configure a concrete role profile selector"
|
||||
)]
|
||||
UnsupportedInheritProfile,
|
||||
#[error(transparent)]
|
||||
Spawn(#[from] SpawnError),
|
||||
#[error("failed to connect to spawned Ticket role Pod at {}: {source}", .socket_path.display())]
|
||||
#[error("failed to connect to spawned Ticket role Worker at {}: {source}", .socket_path.display())]
|
||||
Connect {
|
||||
socket_path: PathBuf,
|
||||
#[source]
|
||||
source: io::Error,
|
||||
},
|
||||
#[error("failed to send first run input to spawned Ticket role Pod: {source}")]
|
||||
#[error("failed to send first run input to spawned Ticket role Worker: {source}")]
|
||||
SendRun {
|
||||
#[source]
|
||||
source: io::Error,
|
||||
},
|
||||
#[error("Ticket role Pod rejected first run input with {code:?}: {message}")]
|
||||
#[error("Ticket role Worker rejected first run input with {code:?}: {message}")]
|
||||
RunRejected { code: ErrorCode, message: String },
|
||||
#[error("Ticket role Pod closed before confirming first run acceptance")]
|
||||
#[error("Ticket role Worker closed before confirming first run acceptance")]
|
||||
RunAcceptanceClosed,
|
||||
#[error("timed out waiting for Ticket role Pod to confirm first run acceptance")]
|
||||
#[error("timed out waiting for Ticket role Worker to confirm first run acceptance")]
|
||||
RunAcceptanceTimeout,
|
||||
}
|
||||
|
||||
@@ -303,12 +310,17 @@ pub fn plan_ticket_role_launch_with_config(
|
||||
.launch_prompt
|
||||
.as_ref()
|
||||
.map(|prompt| prompt.as_str().to_string());
|
||||
let pod_name = match context.pod_name.as_deref().map(str::trim) {
|
||||
Some("") => return Err(TicketRoleLaunchError::EmptyPodName),
|
||||
let worker_name = match context.worker_name.as_deref().map(str::trim) {
|
||||
Some("") => return Err(TicketRoleLaunchError::EmptyWorkerName),
|
||||
Some(name) => name.to_string(),
|
||||
None => default_pod_name(context.role, context.ticket.as_ref()),
|
||||
None => default_worker_name(context.role, context.ticket.as_ref()),
|
||||
};
|
||||
validate_ticket_role_profile(context.role, &profile, &context.workspace_root, &pod_name)?;
|
||||
validate_ticket_role_profile(
|
||||
context.role,
|
||||
&profile,
|
||||
&context.workspace_root,
|
||||
&worker_name,
|
||||
)?;
|
||||
let prompt = build_launch_prompt(&context);
|
||||
|
||||
let original_workspace_root = context.original_workspace_root().to_path_buf();
|
||||
@@ -322,7 +334,7 @@ pub fn plan_ticket_role_launch_with_config(
|
||||
target_workspace_root,
|
||||
implementation_worktree_root,
|
||||
role: context.role,
|
||||
pod_name,
|
||||
worker_name,
|
||||
profile,
|
||||
workflow: workflow.clone(),
|
||||
launch_prompt_ref,
|
||||
@@ -339,7 +351,7 @@ fn validate_ticket_role_profile(
|
||||
role: TicketRole,
|
||||
profile: &str,
|
||||
workspace_root: &std::path::Path,
|
||||
pod_name: &str,
|
||||
worker_name: &str,
|
||||
) -> Result<(), TicketRoleLaunchError> {
|
||||
let selector = ProfileSelector::parse_cli(profile);
|
||||
let registry = ProfileDiscovery::for_cwd(workspace_root)
|
||||
@@ -354,7 +366,7 @@ fn validate_ticket_role_profile(
|
||||
.resolve_from_registry(
|
||||
&selector,
|
||||
®istry,
|
||||
ProfileResolveOptions::with_pod_name(pod_name),
|
||||
ProfileResolveOptions::with_worker_name(worker_name),
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|source| TicketRoleLaunchError::ProfileResolution {
|
||||
@@ -364,17 +376,17 @@ fn validate_ticket_role_profile(
|
||||
})
|
||||
}
|
||||
|
||||
/// Spawn the Pod, connect to its socket, send the first `Method::Run` input,
|
||||
/// and wait for bounded acceptance evidence from the Pod event stream.
|
||||
pub async fn launch_ticket_role_pod<F>(
|
||||
/// Spawn the Worker, connect to its socket, send the first `Method::Run` input,
|
||||
/// and wait for bounded acceptance evidence from the Worker event stream.
|
||||
pub async fn launch_ticket_role_worker<F>(
|
||||
context: TicketRoleLaunchContext,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
runtime_command: WorkerRuntimeCommand,
|
||||
progress: F,
|
||||
) -> Result<TicketRoleLaunchResult, TicketRoleLaunchError>
|
||||
where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
launch_ticket_role_pod_with_options(
|
||||
launch_ticket_role_worker_with_options(
|
||||
context,
|
||||
runtime_command,
|
||||
progress,
|
||||
@@ -383,11 +395,11 @@ where
|
||||
.await
|
||||
}
|
||||
|
||||
/// Spawn the Pod, run bounded pre-run launch options while it is still idle,
|
||||
/// Spawn the Worker, run bounded pre-run launch options while it is still idle,
|
||||
/// then send the first `Method::Run` input and wait for acceptance evidence.
|
||||
pub async fn launch_ticket_role_pod_with_options<F>(
|
||||
pub async fn launch_ticket_role_worker_with_options<F>(
|
||||
context: TicketRoleLaunchContext,
|
||||
runtime_command: PodRuntimeCommand,
|
||||
runtime_command: WorkerRuntimeCommand,
|
||||
progress: F,
|
||||
options: TicketRoleLaunchOptions,
|
||||
) -> Result<TicketRoleLaunchResult, TicketRoleLaunchError>
|
||||
@@ -397,8 +409,8 @@ where
|
||||
let plan = plan_ticket_role_launch(context)?;
|
||||
let spawn_config = plan.spawn_config(runtime_command)?;
|
||||
let spawn_options = plan.spawn_options();
|
||||
let ready = spawn_pod_with_options(spawn_config, spawn_options, progress).await?;
|
||||
let mut client = PodClient::connect(&ready.socket_path)
|
||||
let ready = spawn_worker_with_options(spawn_config, spawn_options, progress).await?;
|
||||
let mut client = WorkerClient::connect(&ready.socket_path)
|
||||
.await
|
||||
.map_err(|source| TicketRoleLaunchError::Connect {
|
||||
socket_path: ready.socket_path.clone(),
|
||||
@@ -408,7 +420,7 @@ where
|
||||
let acceptance_event =
|
||||
wait_for_run_acceptance(&mut client, &plan.run_segments, RUN_ACCEPTANCE_TIMEOUT).await?;
|
||||
let acceptance_evidence = TicketRoleLaunchAcceptanceEvidence {
|
||||
pod_name: ready.pod_name.clone(),
|
||||
worker_name: ready.worker_name.clone(),
|
||||
accepted_run_segments: plan.run_segments.len(),
|
||||
event: acceptance_event,
|
||||
};
|
||||
@@ -421,7 +433,7 @@ where
|
||||
}
|
||||
|
||||
async fn run_pre_run_options_then_send_run(
|
||||
client: &mut PodClient,
|
||||
client: &mut WorkerClient,
|
||||
plan: &TicketRoleLaunchPlan,
|
||||
options: &TicketRoleLaunchOptions,
|
||||
) -> Result<Vec<TicketRolePreRunWarning>, TicketRoleLaunchError> {
|
||||
@@ -439,7 +451,7 @@ async fn run_pre_run_options_then_send_run(
|
||||
}
|
||||
|
||||
async fn perform_pre_run_peer_registrations(
|
||||
client: &mut PodClient,
|
||||
client: &mut WorkerClient,
|
||||
peer_names: &[String],
|
||||
timeout: Duration,
|
||||
) -> Vec<TicketRolePreRunWarning> {
|
||||
@@ -447,7 +459,7 @@ async fn perform_pre_run_peer_registrations(
|
||||
for peer_name in peer_names {
|
||||
if peer_name.trim().is_empty() {
|
||||
warnings.push(TicketRolePreRunWarning {
|
||||
message: "pre-run peer registration skipped: peer Pod name is empty".to_string(),
|
||||
message: "pre-run peer registration skipped: peer Worker name is empty".to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -459,7 +471,7 @@ async fn perform_pre_run_peer_registrations(
|
||||
}
|
||||
|
||||
async fn pre_run_register_peer(
|
||||
client: &mut PodClient,
|
||||
client: &mut WorkerClient,
|
||||
peer_name: &str,
|
||||
timeout: Duration,
|
||||
) -> Result<(), String> {
|
||||
@@ -503,7 +515,7 @@ async fn pre_run_register_peer(
|
||||
}
|
||||
|
||||
async fn wait_for_run_acceptance(
|
||||
client: &mut PodClient,
|
||||
client: &mut WorkerClient,
|
||||
expected_segments: &[Segment],
|
||||
timeout: Duration,
|
||||
) -> Result<TicketRoleLaunchAcceptanceEvent, TicketRoleLaunchError> {
|
||||
@@ -593,10 +605,10 @@ fn append_operation_targets(out: &mut String, context: &TicketRoleLaunchContext)
|
||||
);
|
||||
}
|
||||
|
||||
fn default_pod_name(role: TicketRole, ticket: Option<&TicketRef>) -> String {
|
||||
fn default_worker_name(role: TicketRole, ticket: Option<&TicketRef>) -> String {
|
||||
let mut name = format!("ticket-{}", role.as_str());
|
||||
if let Some(seed) = ticket.and_then(TicketRef::pod_name_seed) {
|
||||
let suffix = sanitise_pod_name_component(seed);
|
||||
if let Some(seed) = ticket.and_then(TicketRef::worker_name_seed) {
|
||||
let suffix = sanitise_worker_name_component(seed);
|
||||
if !suffix.is_empty() {
|
||||
name.push('-');
|
||||
name.push_str(&suffix);
|
||||
@@ -605,7 +617,7 @@ fn default_pod_name(role: TicketRole, ticket: Option<&TicketRef>) -> String {
|
||||
name.chars().take(MAX_POD_NAME_CHARS).collect()
|
||||
}
|
||||
|
||||
fn sanitise_pod_name_component(value: &str) -> String {
|
||||
fn sanitise_worker_name_component(value: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut last_was_dash = false;
|
||||
for ch in value.trim().chars() {
|
||||
@@ -680,7 +692,7 @@ fn non_empty(value: Option<&str>) -> Option<&str> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use protocol::{Greeting, PodStatus};
|
||||
use protocol::{Greeting, WorkerStatus};
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader};
|
||||
use tokio::net::UnixListener;
|
||||
@@ -723,7 +735,7 @@ mod tests {
|
||||
Event::Snapshot {
|
||||
entries: vec![],
|
||||
greeting: Greeting {
|
||||
pod_name: "ticket-intake".to_string(),
|
||||
worker_name: "ticket-intake".to_string(),
|
||||
cwd: "/tmp".to_string(),
|
||||
provider: "test".to_string(),
|
||||
model: "test".to_string(),
|
||||
@@ -732,7 +744,7 @@ mod tests {
|
||||
context_window: 0,
|
||||
context_tokens: 0,
|
||||
},
|
||||
status: PodStatus::Idle,
|
||||
status: WorkerStatus::Idle,
|
||||
in_flight: protocol::InFlightSnapshot::default(),
|
||||
}
|
||||
}
|
||||
@@ -745,7 +757,7 @@ mod tests {
|
||||
target_workspace_root: workspace.to_path_buf(),
|
||||
implementation_worktree_root: workspace.join(".worktree"),
|
||||
role: TicketRole::Intake,
|
||||
pod_name: "ticket-intake".to_string(),
|
||||
worker_name: "ticket-intake".to_string(),
|
||||
profile: "project:intake".to_string(),
|
||||
workflow: "ticket-intake-workflow".to_string(),
|
||||
launch_prompt_ref: None,
|
||||
@@ -758,7 +770,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn pre_run_peer_registration_is_sent_before_first_run_submission() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let socket_path = temp.path().join("pod.sock");
|
||||
let socket_path = temp.path().join("worker.sock");
|
||||
let listener = UnixListener::bind(&socket_path).unwrap();
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.unwrap();
|
||||
@@ -793,7 +805,7 @@ mod tests {
|
||||
}
|
||||
});
|
||||
|
||||
let mut client = PodClient::connect(&socket_path).await.unwrap();
|
||||
let mut client = WorkerClient::connect(&socket_path).await.unwrap();
|
||||
let options = TicketRoleLaunchOptions::default()
|
||||
.with_pre_run_peer_registration("workspace-orchestrator");
|
||||
let warnings = run_pre_run_options_then_send_run(
|
||||
@@ -811,7 +823,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn pre_run_peer_registration_failure_warns_but_still_sends_run() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let socket_path = temp.path().join("pod.sock");
|
||||
let socket_path = temp.path().join("worker.sock");
|
||||
let listener = UnixListener::bind(&socket_path).unwrap();
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.unwrap();
|
||||
@@ -842,7 +854,7 @@ mod tests {
|
||||
));
|
||||
});
|
||||
|
||||
let mut client = PodClient::connect(&socket_path).await.unwrap();
|
||||
let mut client = WorkerClient::connect(&socket_path).await.unwrap();
|
||||
let options = TicketRoleLaunchOptions::default()
|
||||
.with_pre_run_peer_registration("workspace-orchestrator");
|
||||
let warnings = run_pre_run_options_then_send_run(
|
||||
@@ -863,7 +875,7 @@ mod tests {
|
||||
fn default_config_role_launch_plan_requires_explicit_role_config() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let mut context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Coder);
|
||||
context.ticket = Some(TicketRef::id("Ticket Role Pod Launcher"));
|
||||
context.ticket = Some(TicketRef::id("Ticket Role Worker Launcher"));
|
||||
|
||||
let err = plan_ticket_role_launch(context).unwrap_err();
|
||||
|
||||
@@ -1007,7 +1019,7 @@ profile = "builtin:default"
|
||||
plan.profile = "inherit".to_string();
|
||||
|
||||
let err = plan
|
||||
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
|
||||
.spawn_config(WorkerRuntimeCommand::for_executable("/bin/yoi"))
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
@@ -1030,14 +1042,14 @@ workflow = "ticket-review-workflow"
|
||||
"#,
|
||||
);
|
||||
let mut context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Reviewer);
|
||||
context.pod_name = Some("reviewer-fixed".to_string());
|
||||
context.ticket = Some(TicketRef::id("20260605-190330-ticket-role-pod-launcher"));
|
||||
context.worker_name = Some("reviewer-fixed".to_string());
|
||||
context.ticket = Some(TicketRef::id("20260605-190330-ticket-role-worker-launcher"));
|
||||
context.user_instruction = Some("Review the submitted implementation.".to_string());
|
||||
|
||||
let plan = plan_ticket_role_launch(context).unwrap();
|
||||
let text = text_segment(&plan);
|
||||
|
||||
assert_eq!(plan.pod_name, "reviewer-fixed");
|
||||
assert_eq!(plan.worker_name, "reviewer-fixed");
|
||||
assert_eq!(plan.profile, "builtin:default");
|
||||
assert_eq!(plan.workflow, "ticket-review-workflow");
|
||||
assert_eq!(
|
||||
@@ -1055,13 +1067,13 @@ workflow = "ticket-review-workflow"
|
||||
assert!(!text.contains("Role: reviewer"));
|
||||
assert!(!text.contains("system_instruction"));
|
||||
assert!(text.contains("Target Ticket:"));
|
||||
assert!(text.contains("id: 20260605-190330-ticket-role-pod-launcher"));
|
||||
assert!(text.contains("id: 20260605-190330-ticket-role-worker-launcher"));
|
||||
assert!(text.contains("Action instruction:"));
|
||||
assert!(text.contains("Review the submitted implementation."));
|
||||
let spawn = plan
|
||||
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
|
||||
.spawn_config(WorkerRuntimeCommand::for_executable("/bin/yoi"))
|
||||
.unwrap();
|
||||
assert_eq!(spawn.pod_name, "reviewer-fixed");
|
||||
assert_eq!(spawn.worker_name, "reviewer-fixed");
|
||||
assert_eq!(spawn.profile.as_deref(), Some("builtin:default"));
|
||||
assert_eq!(spawn.workspace_root, temp.path());
|
||||
assert!(spawn.cwd.is_none());
|
||||
@@ -1102,7 +1114,10 @@ workflow = "ticket-review-workflow"
|
||||
let handoff_plan = plan_ticket_role_launch(handoff_intake).unwrap();
|
||||
let handoff_text = text_segment(&handoff_plan);
|
||||
assert!(handoff_text.contains("Panel handoff:"));
|
||||
assert!(handoff_text.contains("workspace_orchestrator_pod: panel-orchestrator-demo"));
|
||||
assert!(
|
||||
handoff_text
|
||||
.contains("workspace_workspace_orchestrator_worker: panel-orchestrator-demo")
|
||||
);
|
||||
assert!(handoff_text.contains("workspace: Demo workspace"));
|
||||
assert!(!handoff_text.contains("created_or_updated_ticket_id"));
|
||||
assert!(!handoff_text.contains("Ticket tool surface"));
|
||||
@@ -1125,15 +1140,15 @@ workflow = "ticket-review-workflow"
|
||||
assert!(!orchestrator_text.contains("role_cwd"));
|
||||
|
||||
let mut coder = TicketRoleLaunchContext::new(temp.path(), TicketRole::Coder);
|
||||
coder.ticket = Some(TicketRef::id("20260605-190330-ticket-role-pod-launcher"));
|
||||
coder.ticket = Some(TicketRef::id("20260605-190330-ticket-role-worker-launcher"));
|
||||
coder.worktree_path = Some(PathBuf::from("/tmp/yoi-code"));
|
||||
coder.branch = Some("work/ticket-role-pod-launcher".into());
|
||||
coder.branch = Some("work/ticket-role-worker-launcher".into());
|
||||
coder.validation = vec!["cargo test -p client ticket_role".into()];
|
||||
coder.report_expectations = vec!["implementation report with validation".into()];
|
||||
let coder_plan = plan_ticket_role_launch(coder).unwrap();
|
||||
let coder_text = text_segment(&coder_plan);
|
||||
assert!(coder_text.contains("path: /tmp/yoi-code"));
|
||||
assert!(coder_text.contains("branch: work/ticket-role-pod-launcher"));
|
||||
assert!(coder_text.contains("branch: work/ticket-role-worker-launcher"));
|
||||
assert!(coder_text.contains("cargo test -p client ticket_role"));
|
||||
assert!(coder_text.contains("implementation report with validation"));
|
||||
assert!(!coder_text.contains("provided child worktree/branch"));
|
||||
@@ -1141,14 +1156,14 @@ workflow = "ticket-review-workflow"
|
||||
assert!(!coder_text.contains("Do not merge, push"));
|
||||
|
||||
let mut reviewer = TicketRoleLaunchContext::new(temp.path(), TicketRole::Reviewer);
|
||||
reviewer.ticket = Some(TicketRef::id("20260605-190330-ticket-role-pod-launcher"));
|
||||
reviewer.ticket = Some(TicketRef::id("20260605-190330-ticket-role-worker-launcher"));
|
||||
reviewer.worktree_path = Some(PathBuf::from("/tmp/yoi-review"));
|
||||
reviewer.branch = Some("work/ticket-role-pod-launcher".into());
|
||||
reviewer.branch = Some("work/ticket-role-worker-launcher".into());
|
||||
reviewer.report_expectations = vec!["approve or request changes".into()];
|
||||
let reviewer_plan = plan_ticket_role_launch(reviewer).unwrap();
|
||||
let reviewer_text = text_segment(&reviewer_plan);
|
||||
assert!(reviewer_text.contains("path: /tmp/yoi-review"));
|
||||
assert!(reviewer_text.contains("branch: work/ticket-role-pod-launcher"));
|
||||
assert!(reviewer_text.contains("branch: work/ticket-role-worker-launcher"));
|
||||
assert!(reviewer_text.contains("approve or request changes"));
|
||||
assert!(!reviewer_text.contains("read-only by default"));
|
||||
assert!(!reviewer_text.contains("Orchestrator-side integration"));
|
||||
@@ -1177,7 +1192,7 @@ workflow = "ticket-review-workflow"
|
||||
);
|
||||
assert_eq!(plan.target_workspace_root, temp.path().join("target"));
|
||||
let spawn_config = plan
|
||||
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
|
||||
.spawn_config(WorkerRuntimeCommand::for_executable("/bin/yoi"))
|
||||
.unwrap();
|
||||
assert_eq!(spawn_config.workspace_root, temp.path());
|
||||
assert_eq!(spawn_config.cwd, None);
|
||||
@@ -1194,15 +1209,15 @@ workflow = "ticket-review-workflow"
|
||||
assert!(!text.contains("Orchestrator implementation integration guidance"));
|
||||
}
|
||||
#[test]
|
||||
fn caller_provided_pod_name_is_used_exactly() {
|
||||
fn caller_provided_worker_name_is_used_exactly() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_builtin_role_config(temp.path(), &[TicketRole::Intake]);
|
||||
let mut context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake);
|
||||
context.pod_name = Some("custom-intake-pod".into());
|
||||
context.worker_name = Some("custom-intake-worker".into());
|
||||
|
||||
let plan = plan_ticket_role_launch(context).unwrap();
|
||||
|
||||
assert_eq!(plan.pod_name, "custom-intake-pod");
|
||||
assert_eq!(plan.worker_name, "custom-intake-worker");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,13 +7,13 @@ use tokio::net::UnixStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
pub struct PodClient {
|
||||
pub struct WorkerClient {
|
||||
writer: JsonLineWriter<tokio::io::WriteHalf<UnixStream>>,
|
||||
event_rx: mpsc::Receiver<Event>,
|
||||
reader_task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl PodClient {
|
||||
impl WorkerClient {
|
||||
pub async fn connect(path: &Path) -> Result<Self, io::Error> {
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
let (reader, writer) = tokio::io::split(stream);
|
||||
@@ -50,7 +50,7 @@ impl PodClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PodClient {
|
||||
impl Drop for WorkerClient {
|
||||
fn drop(&mut self) {
|
||||
self.reader_task.abort();
|
||||
}
|
||||
@@ -61,7 +61,7 @@ mod tests {
|
||||
use std::io::ErrorKind;
|
||||
use std::time::Duration;
|
||||
|
||||
use protocol::{PodStatus, Segment};
|
||||
use protocol::{Segment, WorkerStatus};
|
||||
use tempfile::tempdir;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::UnixListener;
|
||||
@@ -91,13 +91,13 @@ mod tests {
|
||||
let mut writer = JsonLineWriter::new(stream);
|
||||
writer
|
||||
.write(&Event::Status {
|
||||
status: PodStatus::Idle,
|
||||
status: WorkerStatus::Idle,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let mut client = PodClient::connect(&socket_path).await.unwrap();
|
||||
let mut client = WorkerClient::connect(&socket_path).await.unwrap();
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), client.next_event())
|
||||
.await
|
||||
@@ -105,7 +105,7 @@ mod tests {
|
||||
assert!(matches!(
|
||||
event,
|
||||
Some(Event::Status {
|
||||
status: PodStatus::Idle
|
||||
status: WorkerStatus::Idle
|
||||
})
|
||||
));
|
||||
server.await.unwrap();
|
||||
@@ -122,7 +122,7 @@ mod tests {
|
||||
reader.next::<Method>().await.unwrap()
|
||||
});
|
||||
|
||||
let mut client = PodClient::connect(&socket_path).await.unwrap();
|
||||
let mut client = WorkerClient::connect(&socket_path).await.unwrap();
|
||||
let method = Method::Run {
|
||||
input: vec![Segment::text("hello")],
|
||||
};
|
||||
@@ -155,7 +155,7 @@ mod tests {
|
||||
});
|
||||
|
||||
for _ in 0..16 {
|
||||
let client = PodClient::connect(&socket_path).await.unwrap();
|
||||
let client = WorkerClient::connect(&socket_path).await.unwrap();
|
||||
drop(client);
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ mod tests {
|
||||
.await;
|
||||
});
|
||||
|
||||
let client = PodClient::connect(&socket_path).await.unwrap();
|
||||
let client = WorkerClient::connect(&socket_path).await.unwrap();
|
||||
tokio::task::yield_now().await;
|
||||
drop(client);
|
||||
|
||||
Reference in New Issue
Block a user