refactor: rename pod crate to worker

This commit is contained in:
2026-06-26 00:05:57 +09:00
parent 4c677640f4
commit 6c59fe927b
194 changed files with 6637 additions and 6146 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
client = { workspace = true }
memory = { workspace = true }
manifest = { workspace = true }
pod = { workspace = true }
worker = { workspace = true }
pod-store = { workspace = true }
session-store = { workspace = true }
session-analytics = { workspace = true }
+2 -2
View File
@@ -2,7 +2,7 @@
## Role
`yoi` is the product CLI facade. It owns the installed binary name, top-level argument parsing, normal TUI launch, and product subcommands such as `yoi pod` and `yoi memory lint`.
`yoi` is the product CLI facade. It owns the installed binary name, top-level argument parsing, normal TUI launch, and product subcommands such as `yoi worker` and `yoi memory lint`.
## Boundaries
@@ -15,7 +15,7 @@ Owns:
Does not own:
- Pod runtime internals (`pod`)
- Worker runtime internals (`worker`)
- socket client mechanics (`client`)
- model turn orchestration (`llm-engine`)
- TUI rendering/runtime implementation (`tui`)
+80 -70
View File
@@ -2,16 +2,16 @@ mod mcp_cli;
mod memory_lint;
mod objective_cli;
mod plugin_cli;
mod pod_cleanup_cli;
mod session_cli;
mod ticket_cli;
mod worker_cleanup_cli;
use std::ffi::OsString;
use std::fmt;
use std::path::PathBuf;
use std::process::{Command, ExitCode};
use client::PodRuntimeCommand;
use client::WorkerRuntimeCommand;
use memory_lint::{LintCliOptions, LintStatus};
use session_store::SegmentId;
use tui::{LaunchMode, LaunchOptions};
@@ -26,11 +26,11 @@ enum Mode {
Plugin(plugin_cli::PluginCliCommand),
Objective(objective_cli::ObjectiveCli),
Session(session_cli::SessionCli),
PodCleanup(pod_cleanup_cli::PodCleanupCli),
WorkerCleanup(worker_cleanup_cli::WorkerCleanupCli),
Ticket(ticket_cli::TicketCli),
WorkspaceHelp,
WorkspaceServe(Vec<String>),
PodRuntime(Vec<String>),
WorkerRuntime(Vec<String>),
Keys,
SetupModel,
Tui {
@@ -127,16 +127,16 @@ async fn main() -> ExitCode {
ExitCode::FAILURE
}
},
Mode::PodCleanup(cli) => match pod_cleanup_cli::run(cli).await {
Mode::WorkerCleanup(cli) => match worker_cleanup_cli::run(cli).await {
Ok(output) => {
print!("{}", output.stdout);
match output.status {
pod_cleanup_cli::PodCleanupCliStatus::Success => ExitCode::SUCCESS,
pod_cleanup_cli::PodCleanupCliStatus::Failure => ExitCode::FAILURE,
worker_cleanup_cli::WorkerCleanupCliStatus::Success => ExitCode::SUCCESS,
worker_cleanup_cli::WorkerCleanupCliStatus::Failure => ExitCode::FAILURE,
}
}
Err(e) => {
eprintln!("yoi pod: {e}");
eprintln!("yoi worker: {e}");
ExitCode::FAILURE
}
},
@@ -153,17 +153,17 @@ async fn main() -> ExitCode {
ExitCode::FAILURE
}
},
Mode::PodRuntime(args) => pod::entrypoint::run_cli_from("yoi pod", args).await,
Mode::WorkerRuntime(args) => worker::entrypoint::run_cli_from("yoi worker", args).await,
Mode::Keys => tui::keys::launch().await,
Mode::SetupModel => tui::setup_model::launch().await,
Mode::Tui {
mode,
workspace_root,
} => {
let runtime_command = match PodRuntimeCommand::resolve() {
let runtime_command = match WorkerRuntimeCommand::resolve() {
Ok(command) => command,
Err(e) => {
eprintln!("yoi: failed to resolve Pod runtime command: {e}");
eprintln!("yoi: failed to resolve Worker runtime command: {e}");
return ExitCode::FAILURE;
}
};
@@ -194,7 +194,7 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
if args.is_empty() {
return Ok(Mode::Tui {
mode: LaunchMode::Spawn {
pod_name: None,
worker_name: None,
profile: None,
},
workspace_root: current_dir()?,
@@ -204,13 +204,13 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
match args[0].as_str() {
"--help" | "-h" => return Ok(Mode::Help),
"resume" => return parse_resume_args(&args[1..]),
"pod" => {
if let Some(cli) = pod_cleanup_cli::parse_pod_management_args(&args[1..])
"worker" => {
if let Some(cli) = worker_cleanup_cli::parse_worker_management_args(&args[1..])
.map_err(|e| ParseError(e.to_string()))?
{
return Ok(Mode::PodCleanup(cli));
return Ok(Mode::WorkerCleanup(cli));
}
return Ok(Mode::PodRuntime(args[1..].to_vec()));
return Ok(Mode::WorkerRuntime(args[1..].to_vec()));
}
"objective" => {
let objective_cli = objective_cli::parse_objective_args(&args[1..])
@@ -284,7 +284,7 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
let mut workspace_root = current_dir()?;
let mut session = None;
let mut pod_name = None;
let mut worker_name = None;
let mut socket_override = None;
let mut profile = None;
@@ -299,14 +299,14 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
session = Some(parse_session_id(value)?);
i += 2;
}
"--pod" => {
"--worker" => {
let value = args
.get(i + 1)
.ok_or_else(|| ParseError("--pod requires a value".to_string()))?;
.ok_or_else(|| ParseError("--worker requires a value".to_string()))?;
if value.starts_with('-') {
return Err(ParseError("--pod requires a value".to_string()));
return Err(ParseError("--worker requires a value".to_string()));
}
pod_name = Some(value.clone());
worker_name = Some(value.clone());
i += 2;
}
"--socket" => {
@@ -347,12 +347,12 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
session = Some(parse_session_id(value)?);
i += 1;
}
arg if arg.starts_with("--pod=") => {
let value = arg.trim_start_matches("--pod=");
arg if arg.starts_with("--worker=") => {
let value = arg.trim_start_matches("--worker=");
if value.is_empty() {
return Err(ParseError("--pod requires a value".to_string()));
return Err(ParseError("--worker requires a value".to_string()));
}
pod_name = Some(value.to_string());
worker_name = Some(value.to_string());
i += 1;
}
arg if arg.starts_with("--socket=") => {
@@ -384,7 +384,7 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
}
value => {
return Err(ParseError(format!(
"unknown command `{value}`; use --pod <NAME> to open a Pod by name"
"unknown command `{value}`; use --worker <NAME> to open a Worker by name"
)));
}
}
@@ -395,19 +395,19 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
"--profile can only be used for fresh spawn".to_string(),
));
}
if socket_override.is_some() && pod_name.is_none() {
return Err(ParseError("--socket requires --pod".to_string()));
if socket_override.is_some() && worker_name.is_none() {
return Err(ParseError("--socket requires --worker".to_string()));
}
if socket_override.is_some() && session.is_some() {
return Err(ParseError(
"--socket can only be used with --pod attach mode".to_string(),
"--socket can only be used with --worker attach mode".to_string(),
));
}
if let Some(profile) = profile {
return Ok(Mode::Tui {
mode: LaunchMode::Spawn {
pod_name,
worker_name,
profile: Some(profile),
},
workspace_root,
@@ -415,14 +415,14 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
}
if let Some(id) = session {
return Ok(Mode::Tui {
mode: LaunchMode::ResumeWithSession { id, pod_name },
mode: LaunchMode::ResumeWithSession { id, worker_name },
workspace_root,
});
}
if let Some(pod_name) = pod_name {
if let Some(worker_name) = worker_name {
return Ok(Mode::Tui {
mode: LaunchMode::PodName {
pod_name,
mode: LaunchMode::WorkerName {
worker_name,
socket_override,
},
workspace_root,
@@ -430,7 +430,7 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
}
Ok(Mode::Tui {
mode: LaunchMode::Spawn {
pod_name: None,
worker_name: None,
profile: None,
},
workspace_root,
@@ -901,7 +901,7 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
fn print_help() {
println!(
"yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi pod delete <NAME> [--force] [--dry-run]\n yoi pod prune --older-than <DURATION> [--force] [--dry-run]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n yoi ticket <COMMAND> [OPTIONS]\n yoi workspace serve [OPTIONS]\n yoi plugin new <rust-component-tool|rust-component-service> <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Pod chat/client surface (default, --pod, yoi resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n --workspace <PATH> Runtime workspace root for default Console/--pod (defaults to cwd)\n --pod <NAME> Open the Pod Console by name (attach/restore/create)\n --socket <PATH> Attach a Pod Console to a specific socket with --pod\n --session <UUID> Resume a specific session segment in the Pod Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
"yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi worker [WORKER_OPTIONS]\n yoi worker delete <NAME> [--force] [--dry-run]\n yoi worker prune --older-than <DURATION> [--force] [--dry-run]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n yoi ticket <COMMAND> [OPTIONS]\n yoi workspace serve [OPTIONS]\n yoi plugin new <rust-component-tool|rust-component-service> <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Worker chat/client surface (default, --worker, yoi resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n --workspace <PATH> Runtime workspace root for default Console/--worker (defaults to cwd)\n --worker <NAME> Open the Worker Console by name (attach/restore/create)\n --socket <PATH> Attach a Worker Console to a specific socket with --worker\n --session <UUID> Resume a specific session segment in the Worker Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
);
}
@@ -913,7 +913,7 @@ fn print_workspace_help() {
fn print_resume_help() {
println!(
"yoi resume\n\nUsage:\n yoi resume [--workspace <PATH>] [--all]\n\nOptions:\n --workspace <PATH> Open the Pod Console picker scoped to this workspace (defaults to cwd)\n --all Open the Pod Console picker across this host/data dir\n -h, --help Print help\n"
"yoi resume\n\nUsage:\n yoi resume [--workspace <PATH>] [--all]\n\nOptions:\n --workspace <PATH> Open the Worker Console picker scoped to this workspace (defaults to cwd)\n --all Open the Worker Console picker across this host/data dir\n -h, --help Print help\n"
);
}
@@ -928,20 +928,20 @@ mod tests {
use super::*;
#[test]
fn parse_pod_name_mode() {
match parse_args_from(["--pod", "agent", "--socket", "/tmp/agent.sock"]).unwrap() {
fn parse_worker_name_mode() {
match parse_args_from(["--worker", "agent", "--socket", "/tmp/agent.sock"]).unwrap() {
Mode::Tui {
mode:
LaunchMode::PodName {
pod_name,
LaunchMode::WorkerName {
worker_name,
socket_override,
},
..
} => {
assert_eq!(pod_name, "agent");
assert_eq!(worker_name, "agent");
assert_eq!(socket_override, Some(PathBuf::from("/tmp/agent.sock")));
}
_ => panic!("expected PodName mode"),
_ => panic!("expected WorkerName mode"),
}
}
@@ -994,35 +994,37 @@ mod tests {
}
#[test]
fn parse_pod_subcommand_uses_runtime_mode() {
match parse_args_from(["pod", "--pod", "agent", "--profile", "default"]).unwrap() {
Mode::PodRuntime(args) => assert_eq!(args, ["--pod", "agent", "--profile", "default"]),
_ => panic!("expected PodRuntime mode"),
fn parse_worker_subcommand_uses_runtime_mode() {
match parse_args_from(["worker", "--worker", "agent", "--profile", "default"]).unwrap() {
Mode::WorkerRuntime(args) => {
assert_eq!(args, ["--worker", "agent", "--profile", "default"])
}
_ => panic!("expected WorkerRuntime mode"),
}
}
#[test]
fn parse_pod_delete_uses_cleanup_mode() {
match parse_args_from(["pod", "delete", "agent", "--dry-run"]).unwrap() {
Mode::PodCleanup(pod_cleanup_cli::PodCleanupCli::Delete(options)) => {
fn parse_worker_delete_uses_cleanup_mode() {
match parse_args_from(["worker", "delete", "agent", "--dry-run"]).unwrap() {
Mode::WorkerCleanup(worker_cleanup_cli::WorkerCleanupCli::Delete(options)) => {
assert_eq!(options.name, "agent");
assert!(options.dry_run);
assert!(!options.force);
}
_ => panic!("expected Pod cleanup delete mode"),
_ => panic!("expected Worker cleanup delete mode"),
}
}
#[test]
fn parse_pod_prune_uses_cleanup_mode() {
match parse_args_from(["pod", "prune", "--older-than", "30d"]).unwrap() {
Mode::PodCleanup(pod_cleanup_cli::PodCleanupCli::Prune(options)) => {
fn parse_worker_prune_uses_cleanup_mode() {
match parse_args_from(["worker", "prune", "--older-than", "30d"]).unwrap() {
Mode::WorkerCleanup(worker_cleanup_cli::WorkerCleanupCli::Prune(options)) => {
assert_eq!(
options.older_than,
std::time::Duration::from_secs(30 * 24 * 60 * 60)
);
}
_ => panic!("expected Pod cleanup prune mode"),
_ => panic!("expected Worker cleanup prune mode"),
}
}
@@ -1092,20 +1094,20 @@ mod tests {
}
#[test]
fn parse_literal_pod_name_still_available_with_flag() {
match parse_args_from(["--pod", "pod"]).unwrap() {
fn parse_literal_worker_name_still_available_with_flag() {
match parse_args_from(["--worker", "worker"]).unwrap() {
Mode::Tui {
mode:
LaunchMode::PodName {
pod_name,
LaunchMode::WorkerName {
worker_name,
socket_override,
},
..
} => {
assert_eq!(pod_name, "pod");
assert_eq!(worker_name, "worker");
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
_ => panic!("expected WorkerName mode"),
}
}
@@ -1238,7 +1240,7 @@ mod tests {
match parse_args_from([
"--session",
&segment_id.to_string(),
"--pod",
"--worker",
"explicit-name",
])
.unwrap()
@@ -1247,14 +1249,14 @@ mod tests {
mode:
LaunchMode::ResumeWithSession {
id,
pod_name: Some(pod_name),
worker_name: Some(worker_name),
},
..
} => {
assert_eq!(id, segment_id);
assert_eq!(pod_name, "explicit-name");
assert_eq!(worker_name, "explicit-name");
}
_ => panic!("expected ResumeWithSession mode with explicit pod name"),
_ => panic!("expected ResumeWithSession mode with explicit worker name"),
}
}
@@ -1264,7 +1266,11 @@ mod tests {
(vec!["-r".to_string()], "unknown argument: -r"),
(vec!["--resume".to_string()], "unknown argument: --resume"),
(
vec!["--pod".to_string(), "agent".to_string(), "-r".to_string()],
vec![
"--worker".to_string(),
"agent".to_string(),
"-r".to_string(),
],
"unknown argument: -r",
),
];
@@ -1291,16 +1297,20 @@ mod tests {
"/tmp/other-workspace",
"--profile",
"project:companion",
"--pod",
"--worker",
"agent",
])
.unwrap()
{
Mode::Tui {
mode: LaunchMode::Spawn { pod_name, profile },
mode:
LaunchMode::Spawn {
worker_name,
profile,
},
workspace_root,
} => {
assert_eq!(pod_name, Some("agent".to_string()));
assert_eq!(worker_name, Some("agent".to_string()));
assert_eq!(profile, Some("project:companion".to_string()));
assert_eq!(workspace_root, PathBuf::from("/tmp/other-workspace"));
}
@@ -1350,7 +1360,7 @@ mod tests {
}
#[test]
fn parse_dashboard_word_is_not_an_alias_or_pod_name() {
fn parse_dashboard_word_is_not_an_alias_or_worker_name() {
let err = parse_args_from(["dashboard"]).unwrap_err();
assert_eq!(err.to_string(), "unknown command `dashboard`");
}
+2 -2
View File
@@ -14,7 +14,7 @@ pub(crate) type Result<T> = std::result::Result<T, Box<dyn Error>>;
const MAX_SERVERS: usize = 128;
const MAX_DIAGNOSTICS: usize = 48;
const MAX_TEXT_CHARS: usize = 240;
const MCP_STATIC_NOT_LIVE_REASON: &str = "CLI inspection reads resolved static MCP config only; provider-discovered state is unavailable without live Pod/runtime MCP state, and this command does not start MCP server processes.";
const MCP_STATIC_NOT_LIVE_REASON: &str = "CLI inspection reads resolved static MCP config only; provider-discovered state is unavailable without live Worker/runtime MCP state, and this command does not start MCP server processes.";
#[derive(Clone, Debug, Default)]
pub(crate) struct McpCliArgs {
@@ -146,7 +146,7 @@ fn inspect_static_config(args: &McpCliArgs) -> StaticConfigSnapshot {
.with_workspace_base(&workspace)
.resolve(
&selector,
ProfileResolveOptions::with_pod_name("mcp-inspect"),
ProfileResolveOptions::with_worker_name("mcp-inspect"),
) {
Ok(resolved) => {
let mut diagnostics = vec![DiagnosticReport::info(
+3 -3
View File
@@ -15,8 +15,8 @@ use manifest::plugin::{
read_plugin_package_file, resolve_enabled_plugins, write_plugin_package_file,
};
use manifest::{ProfileResolveOptions, ProfileResolver, ProfileSelector, paths};
use pod::feature::plugin::{PluginStaticInspection, inspect_resolved_plugin_static};
use serde::Serialize;
use worker::feature::plugin::{PluginStaticInspection, inspect_resolved_plugin_static};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
@@ -944,7 +944,7 @@ fn load_plugin_config(args: &PluginCliArgs, workspace: &Path) -> Result<PluginCo
.with_workspace_base(workspace)
.resolve(
&selector,
ProfileResolveOptions::with_pod_name("plugin-inspect"),
ProfileResolveOptions::with_worker_name("plugin-inspect"),
)?;
Ok(resolved.manifest.plugins)
}
@@ -1480,8 +1480,8 @@ impl ItemBuilder {
mod tests {
use super::*;
use manifest::plugin::{PluginEnablementConfig, PluginExactVersion, PluginGrantConfig};
use pod::feature::plugin::{PluginPermissionEligibility, PluginRuntimeEligibility};
use tempfile::tempdir;
use worker::feature::plugin::{PluginPermissionEligibility, PluginRuntimeEligibility};
#[test]
fn active_list_and_show_json_are_structured_and_non_executing() {
+11 -11
View File
@@ -4,10 +4,10 @@ use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use manifest::paths;
use pod_store::{FsPodStore, PodMetadataStore};
use pod_store::{FsWorkerStore, WorkerMetadataStore};
use session_store::{FsStore, SessionId, Store};
use crate::pod_cleanup_cli::parse_duration;
use crate::worker_cleanup_cli::parse_duration;
const MAX_REPORT_ITEMS: usize = 50;
@@ -203,7 +203,7 @@ pub fn run_prune_with_roots(
));
}
let session_store = FsStore::new(data_dir.join("sessions")).map_err(to_error)?;
let pod_store = FsPodStore::new(data_dir.join("pods")).map_err(to_error)?;
let pod_store = FsWorkerStore::new(data_dir.join("pods")).map_err(to_error)?;
let referenced_sessions = referenced_sessions(&pod_store)?;
let cutoff = options
.older_than
@@ -241,7 +241,7 @@ pub fn run_prune_with_roots(
index,
"kept",
*session_id,
"referenced by pod metadata",
"referenced by worker metadata",
);
continue;
}
@@ -315,7 +315,7 @@ pub fn run_prune_with_roots(
})
}
fn referenced_sessions(pod_store: &FsPodStore) -> Result<BTreeSet<SessionId>, SessionCliError> {
fn referenced_sessions(pod_store: &FsWorkerStore) -> Result<BTreeSet<SessionId>, SessionCliError> {
let mut sessions = BTreeSet::new();
for name in pod_store.list_names().map_err(to_error)? {
let metadata = pod_store
@@ -323,7 +323,7 @@ fn referenced_sessions(pod_store: &FsPodStore) -> Result<BTreeSet<SessionId>, Se
.map_err(to_error)?
.ok_or_else(|| {
SessionCliError(format!(
"pod metadata for `{name}` disappeared while checking references"
"worker metadata for `{name}` disappeared while checking references"
))
})?;
if let Some(active) = metadata.active {
@@ -352,13 +352,13 @@ fn to_error<E: fmt::Display>(error: E) -> SessionCliError {
}
pub fn help_text() -> &'static str {
"yoi session\n\nUsage:\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n\nOptions:\n --json Emit a machine-readable JSON analytics report\n --unreferenced Prune only Sessions not referenced by Pod metadata\n --older-than Optional explicit age threshold for unreferenced cleanup (units: s, m, h, d, w)\n --force Perform deletion after safety checks\n --dry-run Report only, even with --force\n -h, --help Print help\n"
"yoi session\n\nUsage:\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n\nOptions:\n --json Emit a machine-readable JSON analytics report\n --unreferenced Prune only Sessions not referenced by Worker metadata\n --older-than Optional explicit age threshold for unreferenced cleanup (units: s, m, h, d, w)\n --force Perform deletion after safety checks\n --dry-run Report only, even with --force\n -h, --help Print help\n"
}
#[cfg(test)]
mod tests {
use super::*;
use pod_store::{PodActiveSegmentRef, PodMetadata};
use pod_store::{WorkerActiveSegmentRef, WorkerMetadata};
use session_store::{Store, new_segment_id, new_session_id};
use std::io::Write;
@@ -441,7 +441,7 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let data_dir = tmp.path().join("data");
let session_store = FsStore::new(data_dir.join("sessions")).unwrap();
let pod_store = FsPodStore::new(data_dir.join("pods")).unwrap();
let pod_store = FsWorkerStore::new(data_dir.join("pods")).unwrap();
let referenced_session = new_session_id();
let referenced_segment = new_segment_id();
let orphan_session = new_session_id();
@@ -453,9 +453,9 @@ mod tests {
.create_segment(orphan_session, orphan_segment, &[])
.unwrap();
pod_store
.write(&PodMetadata::new(
.write(&WorkerMetadata::new(
"agent",
Some(PodActiveSegmentRef::active_segment(
Some(WorkerActiveSegmentRef::active_segment(
referenced_session,
referenced_segment,
)),
@@ -4,72 +4,76 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use manifest::paths;
use pod_store::{FsPodStore, PodMetadata, PodMetadataStore, validate_pod_name};
use pod_store::{FsWorkerStore, WorkerMetadata, WorkerMetadataStore, validate_worker_name};
const MAX_REPORT_ITEMS: usize = 50;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PodCleanupCli {
pub enum WorkerCleanupCli {
Help,
Delete(PodDeleteOptions),
Prune(PodPruneOptions),
Delete(WorkerDeleteOptions),
Prune(WorkerPruneOptions),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PodDeleteOptions {
pub struct WorkerDeleteOptions {
pub name: String,
pub force: bool,
pub dry_run: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PodPruneOptions {
pub struct WorkerPruneOptions {
pub older_than: Duration,
pub force: bool,
pub dry_run: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PodCleanupCliOutput {
pub struct WorkerCleanupCliOutput {
pub stdout: String,
pub status: PodCleanupCliStatus,
pub status: WorkerCleanupCliStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PodCleanupCliStatus {
pub enum WorkerCleanupCliStatus {
Success,
Failure,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PodCleanupCliError(String);
pub struct WorkerCleanupCliError(String);
impl fmt::Display for PodCleanupCliError {
impl fmt::Display for WorkerCleanupCliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for PodCleanupCliError {}
impl std::error::Error for WorkerCleanupCliError {}
pub fn parse_pod_management_args(
pub fn parse_worker_management_args(
args: &[String],
) -> Result<Option<PodCleanupCli>, PodCleanupCliError> {
) -> Result<Option<WorkerCleanupCli>, WorkerCleanupCliError> {
let Some((subcommand, rest)) = args.split_first() else {
return Ok(None);
};
match subcommand.as_str() {
"delete" => parse_delete_args(rest).map(PodCleanupCli::Delete).map(Some),
"prune" => parse_prune_args(rest).map(PodCleanupCli::Prune).map(Some),
"help" => Ok(Some(PodCleanupCli::Help)),
"--help" | "-h" => Ok(Some(PodCleanupCli::Help)),
"delete" => parse_delete_args(rest)
.map(WorkerCleanupCli::Delete)
.map(Some),
"prune" => parse_prune_args(rest)
.map(WorkerCleanupCli::Prune)
.map(Some),
"help" => Ok(Some(WorkerCleanupCli::Help)),
"--help" | "-h" => Ok(Some(WorkerCleanupCli::Help)),
_ => Ok(None),
}
}
fn parse_delete_args(args: &[String]) -> Result<PodDeleteOptions, PodCleanupCliError> {
fn parse_delete_args(args: &[String]) -> Result<WorkerDeleteOptions, WorkerCleanupCliError> {
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
return Err(PodCleanupCliError(delete_help_text().to_string()));
return Err(WorkerCleanupCliError(delete_help_text().to_string()));
}
let mut name = None;
let mut force = false;
@@ -86,36 +90,37 @@ fn parse_delete_args(args: &[String]) -> Result<PodDeleteOptions, PodCleanupCliE
break;
}
value if value.starts_with('-') => {
return Err(PodCleanupCliError(format!(
"unknown yoi pod delete option `{value}`"
return Err(WorkerCleanupCliError(format!(
"unknown yoi worker delete option `{value}`"
)));
}
positional => set_name(&mut name, positional)?,
}
}
let name = name
.ok_or_else(|| PodCleanupCliError("yoi pod delete requires an explicit Pod name".into()))?;
validate_pod_name(&name).map_err(|e| PodCleanupCliError(e.to_string()))?;
Ok(PodDeleteOptions {
let name = name.ok_or_else(|| {
WorkerCleanupCliError("yoi worker delete requires an explicit Worker name".into())
})?;
validate_worker_name(&name).map_err(|e| WorkerCleanupCliError(e.to_string()))?;
Ok(WorkerDeleteOptions {
name,
force,
dry_run,
})
}
fn set_name(name: &mut Option<String>, value: &str) -> Result<(), PodCleanupCliError> {
fn set_name(name: &mut Option<String>, value: &str) -> Result<(), WorkerCleanupCliError> {
if name.is_some() {
return Err(PodCleanupCliError(
"yoi pod delete accepts exactly one Pod name".into(),
return Err(WorkerCleanupCliError(
"yoi worker delete accepts exactly one Worker name".into(),
));
}
*name = Some(value.to_string());
Ok(())
}
fn parse_prune_args(args: &[String]) -> Result<PodPruneOptions, PodCleanupCliError> {
fn parse_prune_args(args: &[String]) -> Result<WorkerPruneOptions, WorkerCleanupCliError> {
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
return Err(PodCleanupCliError(prune_help_text().to_string()));
return Err(WorkerCleanupCliError(prune_help_text().to_string()));
}
let mut older_than = None;
let mut force = false;
@@ -131,10 +136,10 @@ fn parse_prune_args(args: &[String]) -> Result<PodPruneOptions, PodCleanupCliErr
index += 1;
} else if arg == "--older-than" {
let value = args.get(index + 1).ok_or_else(|| {
PodCleanupCliError("--older-than requires a duration value".into())
WorkerCleanupCliError("--older-than requires a duration value".into())
})?;
if value.starts_with('-') {
return Err(PodCleanupCliError(
return Err(WorkerCleanupCliError(
"--older-than requires a duration value".into(),
));
}
@@ -142,47 +147,47 @@ fn parse_prune_args(args: &[String]) -> Result<PodPruneOptions, PodCleanupCliErr
index += 2;
} else if let Some(value) = arg.strip_prefix("--older-than=") {
if value.is_empty() {
return Err(PodCleanupCliError(
return Err(WorkerCleanupCliError(
"--older-than requires a duration value".into(),
));
}
older_than = Some(parse_duration(value)?);
index += 1;
} else if arg.starts_with('-') {
return Err(PodCleanupCliError(format!(
"unknown yoi pod prune option `{arg}`"
return Err(WorkerCleanupCliError(format!(
"unknown yoi worker prune option `{arg}`"
)));
} else {
return Err(PodCleanupCliError(format!(
"yoi pod prune does not accept positional argument `{arg}`"
return Err(WorkerCleanupCliError(format!(
"yoi worker prune does not accept positional argument `{arg}`"
)));
}
}
let older_than = older_than.ok_or_else(|| {
PodCleanupCliError("yoi pod prune requires --older-than <DURATION>".into())
WorkerCleanupCliError("yoi worker prune requires --older-than <DURATION>".into())
})?;
Ok(PodPruneOptions {
Ok(WorkerPruneOptions {
older_than,
force,
dry_run,
})
}
pub fn parse_duration(value: &str) -> Result<Duration, PodCleanupCliError> {
pub fn parse_duration(value: &str) -> Result<Duration, WorkerCleanupCliError> {
let split = value
.find(|ch: char| !ch.is_ascii_digit())
.unwrap_or(value.len());
let (amount, unit) = value.split_at(split);
if amount.is_empty() || unit.is_empty() {
return Err(PodCleanupCliError(format!(
return Err(WorkerCleanupCliError(format!(
"duration `{value}` must use an explicit unit: s, m, h, d, or w"
)));
}
let amount = amount
.parse::<u64>()
.map_err(|_| PodCleanupCliError(format!("invalid duration amount `{value}`")))?;
.map_err(|_| WorkerCleanupCliError(format!("invalid duration amount `{value}`")))?;
if amount == 0 {
return Err(PodCleanupCliError(
return Err(WorkerCleanupCliError(
"duration must be greater than zero".into(),
));
}
@@ -193,7 +198,7 @@ pub fn parse_duration(value: &str) -> Result<Duration, PodCleanupCliError> {
"d" | "day" | "days" => amount.saturating_mul(60 * 60 * 24),
"w" | "week" | "weeks" => amount.saturating_mul(60 * 60 * 24 * 7),
_ => {
return Err(PodCleanupCliError(format!(
return Err(WorkerCleanupCliError(format!(
"unknown duration unit `{unit}` in `{value}`"
)));
}
@@ -201,98 +206,98 @@ pub fn parse_duration(value: &str) -> Result<Duration, PodCleanupCliError> {
Ok(Duration::from_secs(seconds))
}
pub async fn run(cli: PodCleanupCli) -> Result<PodCleanupCliOutput, PodCleanupCliError> {
pub async fn run(cli: WorkerCleanupCli) -> Result<WorkerCleanupCliOutput, WorkerCleanupCliError> {
let data_dir = paths::data_dir()
.ok_or_else(|| PodCleanupCliError("failed to resolve Yoi data directory".into()))?;
.ok_or_else(|| WorkerCleanupCliError("failed to resolve Yoi data directory".into()))?;
let runtime_dir = paths::runtime_dir()
.ok_or_else(|| PodCleanupCliError("failed to resolve Yoi runtime directory".into()))?;
.ok_or_else(|| WorkerCleanupCliError("failed to resolve Yoi runtime directory".into()))?;
run_with_roots(cli, data_dir, runtime_dir).await
}
pub async fn run_with_roots(
cli: PodCleanupCli,
cli: WorkerCleanupCli,
data_dir: PathBuf,
runtime_dir: PathBuf,
) -> Result<PodCleanupCliOutput, PodCleanupCliError> {
) -> Result<WorkerCleanupCliOutput, WorkerCleanupCliError> {
match cli {
PodCleanupCli::Help => Ok(PodCleanupCliOutput {
WorkerCleanupCli::Help => Ok(WorkerCleanupCliOutput {
stdout: help_text().to_string(),
status: PodCleanupCliStatus::Success,
status: WorkerCleanupCliStatus::Success,
}),
PodCleanupCli::Delete(options) => run_delete(options, data_dir, runtime_dir).await,
PodCleanupCli::Prune(options) => run_prune(options, data_dir, runtime_dir).await,
WorkerCleanupCli::Delete(options) => run_delete(options, data_dir, runtime_dir).await,
WorkerCleanupCli::Prune(options) => run_prune(options, data_dir, runtime_dir).await,
}
}
async fn run_delete(
options: PodDeleteOptions,
options: WorkerDeleteOptions,
data_dir: PathBuf,
runtime_dir: PathBuf,
) -> Result<PodCleanupCliOutput, PodCleanupCliError> {
let store = FsPodStore::new(data_dir.join("pods")).map_err(to_error)?;
) -> Result<WorkerCleanupCliOutput, WorkerCleanupCliError> {
let store = FsWorkerStore::new(data_dir.join("pods")).map_err(to_error)?;
let metadata = store.read_by_name(&options.name).map_err(to_error)?;
let Some(metadata) = metadata else {
return Ok(PodCleanupCliOutput {
return Ok(WorkerCleanupCliOutput {
stdout: format!(
"yoi pod delete\nstatus: refused\npod: {}\nreason: pod metadata is missing\n",
"yoi worker delete\nstatus: refused\npod: {}\nreason: worker metadata is missing\n",
options.name
),
status: PodCleanupCliStatus::Failure,
status: WorkerCleanupCliStatus::Failure,
});
};
let probe = probe_pod_liveness(&runtime_dir, &options.name).await;
let probe = probe_worker_liveness(&runtime_dir, &options.name).await;
if let Some(reason) = probe.refusal_reason() {
return Ok(PodCleanupCliOutput {
return Ok(WorkerCleanupCliOutput {
stdout: format!(
"yoi pod delete\nstatus: refused\npod: {}\nreason: {}\nsocket: {}\n",
"yoi worker delete\nstatus: refused\npod: {}\nreason: {}\nsocket: {}\n",
options.name,
reason,
probe.socket_path.display()
),
status: PodCleanupCliStatus::Failure,
status: WorkerCleanupCliStatus::Failure,
});
}
let delete = options.force && !options.dry_run;
let mut stdout = String::new();
stdout.push_str("yoi pod delete\n");
stdout.push_str("yoi worker delete\n");
stdout.push_str(if delete {
"mode: force\n"
} else {
"mode: dry-run\n"
});
stdout.push_str(&format!("pod: {}\n", options.name));
stdout.push_str(&format!("worker: {}\n", options.name));
describe_metadata(&mut stdout, &metadata);
if delete {
store.delete_by_name(&options.name).map_err(to_error)?;
stdout.push_str("deleted: pod metadata\n");
stdout.push_str("deleted: worker metadata\n");
stdout.push_str("preserved: session logs/history\n");
} else {
stdout.push_str("would_delete: pod metadata\n");
stdout.push_str("would_delete: worker metadata\n");
stdout.push_str("would_preserve: session logs/history\n");
stdout
.push_str("note: pass --force to delete metadata; --dry-run keeps report-only mode\n");
}
Ok(PodCleanupCliOutput {
Ok(WorkerCleanupCliOutput {
stdout,
status: PodCleanupCliStatus::Success,
status: WorkerCleanupCliStatus::Success,
})
}
async fn run_prune(
options: PodPruneOptions,
options: WorkerPruneOptions,
data_dir: PathBuf,
runtime_dir: PathBuf,
) -> Result<PodCleanupCliOutput, PodCleanupCliError> {
let store = FsPodStore::new(data_dir.join("pods")).map_err(to_error)?;
) -> Result<WorkerCleanupCliOutput, WorkerCleanupCliError> {
let store = FsWorkerStore::new(data_dir.join("pods")).map_err(to_error)?;
let names = store.list_names().map_err(to_error)?;
let cutoff = SystemTime::now()
.checked_sub(options.older_than)
.ok_or_else(|| PodCleanupCliError("--older-than duration is too large".into()))?;
.ok_or_else(|| WorkerCleanupCliError("--older-than duration is too large".into()))?;
let delete = options.force && !options.dry_run;
let mut stdout = String::new();
stdout.push_str("yoi pod prune\n");
stdout.push_str("yoi worker prune\n");
stdout.push_str(if delete {
"mode: force\n"
} else {
@@ -334,7 +339,7 @@ async fn run_prune(
);
continue;
}
let probe = probe_pod_liveness(&runtime_dir, name).await;
let probe = probe_worker_liveness(&runtime_dir, name).await;
if let Some(reason) = probe.refusal_reason() {
refused += 1;
push_item_line(&mut stdout, index, "refused", name, &reason);
@@ -348,7 +353,7 @@ async fn run_prune(
index,
"deleted",
name,
"old pod metadata; session logs/history preserved",
"old worker metadata; session logs/history preserved",
);
} else {
would_delete += 1;
@@ -367,17 +372,17 @@ async fn run_prune(
stdout
.push_str("note: pass --force to delete metadata; --dry-run keeps report-only mode\n");
}
Ok(PodCleanupCliOutput {
Ok(WorkerCleanupCliOutput {
stdout,
status: if refused > 0 {
PodCleanupCliStatus::Failure
WorkerCleanupCliStatus::Failure
} else {
PodCleanupCliStatus::Success
WorkerCleanupCliStatus::Success
},
})
}
fn describe_metadata(stdout: &mut String, metadata: &PodMetadata) {
fn describe_metadata(stdout: &mut String, metadata: &WorkerMetadata) {
match metadata.active.as_ref() {
Some(active) => stdout.push_str(&format!(
"active_session: {}\nactive_segment: {}\n",
@@ -393,12 +398,12 @@ fn describe_metadata(stdout: &mut String, metadata: &PodMetadata) {
fn metadata_modified_at(
root: Option<&Path>,
pod_name: &str,
worker_name: &str,
) -> Result<Option<SystemTime>, io::Error> {
let Some(root) = root else {
return Ok(None);
};
let path = root.join(pod_name).join("metadata.json");
let path = root.join(worker_name).join("metadata.json");
match std::fs::metadata(path) {
Ok(metadata) => metadata.modified().map(Some),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
@@ -431,16 +436,16 @@ impl LivenessProbe {
fn refusal_reason(&self) -> Option<String> {
match &self.result {
LivenessResult::NotReachable => None,
LivenessResult::Reachable => Some("pod is live/reachable".into()),
LivenessResult::Reachable => Some("worker is live/reachable".into()),
LivenessResult::Uncertain(reason) => Some(format!(
"pod liveness is uncertain; refusing destructive metadata cleanup ({reason})"
"worker liveness is uncertain; refusing destructive metadata cleanup ({reason})"
)),
}
}
}
async fn probe_pod_liveness(runtime_dir: &Path, pod_name: &str) -> LivenessProbe {
let socket_path = runtime_dir.join(pod_name).join("sock");
async fn probe_worker_liveness(runtime_dir: &Path, worker_name: &str) -> LivenessProbe {
let socket_path = runtime_dir.join(worker_name).join("sock");
let result = probe_socket(&socket_path).await;
LivenessProbe {
socket_path,
@@ -474,26 +479,26 @@ async fn probe_socket(_socket_path: &Path) -> LivenessResult {
LivenessResult::Uncertain("Unix socket probing is unavailable on this platform".into())
}
fn to_error<E: fmt::Display>(error: E) -> PodCleanupCliError {
PodCleanupCliError(error.to_string())
fn to_error<E: fmt::Display>(error: E) -> WorkerCleanupCliError {
WorkerCleanupCliError(error.to_string())
}
pub fn help_text() -> &'static str {
"yoi pod\n\nUsage:\n yoi pod delete <NAME> [--force] [--dry-run]\n yoi pod prune --older-than <DURATION> [--force] [--dry-run]\n yoi pod [POD_OPTIONS]\n\nDescription:\n delete/prune are safe Pod metadata cleanup commands. `pod delete` removes only name-keyed Pod metadata and never removes session logs/history. Live or uncertain Pod liveness is refused. Without --force the command reports only.\n\nDuration units: s, m, h, d, w\n\nOptions:\n --force Perform deletion after safety checks\n --dry-run Report only, even with --force\n --older-than Required explicit age threshold for prune\n -h, --help Print help\n"
"yoi worker\n\nUsage:\n yoi worker delete <NAME> [--force] [--dry-run]\n yoi worker prune --older-than <DURATION> [--force] [--dry-run]\n yoi worker [WORKER_OPTIONS]\n\nDescription:\n delete/prune are safe Worker metadata cleanup commands. `worker delete` removes only name-keyed Worker metadata and never removes session logs/history. Live or uncertain Worker liveness is refused. Without --force the command reports only.\n\nDuration units: s, m, h, d, w\n\nOptions:\n --force Perform deletion after safety checks\n --dry-run Report only, even with --force\n --older-than Required explicit age threshold for prune\n -h, --help Print help\n"
}
fn delete_help_text() -> &'static str {
"usage: yoi pod delete <NAME> [--force] [--dry-run]"
"usage: yoi worker delete <NAME> [--force] [--dry-run]"
}
fn prune_help_text() -> &'static str {
"usage: yoi pod prune --older-than <DURATION> [--force] [--dry-run]"
"usage: yoi worker prune --older-than <DURATION> [--force] [--dry-run]"
}
#[cfg(test)]
mod tests {
use super::*;
use pod_store::PodActiveSegmentRef;
use pod_store::WorkerActiveSegmentRef;
use session_store::{Store, new_segment_id, new_session_id};
fn string_args(args: &[&str]) -> Vec<String> {
@@ -501,14 +506,18 @@ mod tests {
}
#[test]
fn parse_pod_delete_command() {
let cli =
parse_pod_management_args(&string_args(&["delete", "agent", "--force", "--dry-run"]))
.unwrap()
.unwrap();
fn parse_worker_delete_command() {
let cli = parse_worker_management_args(&string_args(&[
"delete",
"agent",
"--force",
"--dry-run",
]))
.unwrap()
.unwrap();
assert_eq!(
cli,
PodCleanupCli::Delete(PodDeleteOptions {
WorkerCleanupCli::Delete(WorkerDeleteOptions {
name: "agent".into(),
force: true,
dry_run: true,
@@ -517,8 +526,8 @@ mod tests {
}
#[test]
fn parse_pod_prune_requires_explicit_threshold() {
let err = parse_pod_management_args(&string_args(&["prune"])).unwrap_err();
fn parse_worker_prune_requires_explicit_threshold() {
let err = parse_worker_management_args(&string_args(&["prune"])).unwrap_err();
assert!(err.to_string().contains("--older-than"));
}
@@ -534,7 +543,7 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let data_dir = tmp.path().join("data");
let runtime_dir = tmp.path().join("run");
let pod_store = FsPodStore::new(data_dir.join("pods")).unwrap();
let pod_store = FsWorkerStore::new(data_dir.join("pods")).unwrap();
let session_store = session_store::FsStore::new(data_dir.join("sessions")).unwrap();
let session_id = new_session_id();
let segment_id = new_segment_id();
@@ -542,14 +551,16 @@ mod tests {
.create_segment(session_id, segment_id, &[])
.unwrap();
pod_store
.write(&PodMetadata::new(
.write(&WorkerMetadata::new(
"agent",
Some(PodActiveSegmentRef::active_segment(session_id, segment_id)),
Some(WorkerActiveSegmentRef::active_segment(
session_id, segment_id,
)),
))
.unwrap();
let output = run_with_roots(
PodCleanupCli::Delete(PodDeleteOptions {
WorkerCleanupCli::Delete(WorkerDeleteOptions {
name: "agent".into(),
force: true,
dry_run: false,
@@ -560,8 +571,8 @@ mod tests {
.await
.unwrap();
assert_eq!(output.status, PodCleanupCliStatus::Success);
assert!(output.stdout.contains("deleted: pod metadata"));
assert_eq!(output.status, WorkerCleanupCliStatus::Success);
assert!(output.stdout.contains("deleted: worker metadata"));
assert!(pod_store.read_by_name("agent").unwrap().is_none());
assert!(session_store.exists(session_id, segment_id).unwrap());
}
@@ -571,11 +582,13 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let data_dir = tmp.path().join("data");
let runtime_dir = tmp.path().join("run");
let pod_store = FsPodStore::new(data_dir.join("pods")).unwrap();
pod_store.write(&PodMetadata::new("agent", None)).unwrap();
let pod_store = FsWorkerStore::new(data_dir.join("pods")).unwrap();
pod_store
.write(&WorkerMetadata::new("agent", None))
.unwrap();
let output = run_with_roots(
PodCleanupCli::Delete(PodDeleteOptions {
WorkerCleanupCli::Delete(WorkerDeleteOptions {
name: "agent".into(),
force: false,
dry_run: false,
@@ -586,7 +599,7 @@ mod tests {
.await
.unwrap();
assert_eq!(output.status, PodCleanupCliStatus::Success);
assert_eq!(output.status, WorkerCleanupCliStatus::Success);
assert!(output.stdout.contains("mode: dry-run"));
assert!(pod_store.read_by_name("agent").unwrap().is_some());
}
@@ -599,13 +612,15 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let data_dir = tmp.path().join("data");
let runtime_dir = tmp.path().join("run");
let pod_store = FsPodStore::new(data_dir.join("pods")).unwrap();
pod_store.write(&PodMetadata::new("agent", None)).unwrap();
let pod_store = FsWorkerStore::new(data_dir.join("pods")).unwrap();
pod_store
.write(&WorkerMetadata::new("agent", None))
.unwrap();
std::fs::create_dir_all(runtime_dir.join("agent")).unwrap();
let listener = UnixListener::bind(runtime_dir.join("agent/sock")).unwrap();
let output = run_with_roots(
PodCleanupCli::Delete(PodDeleteOptions {
WorkerCleanupCli::Delete(WorkerDeleteOptions {
name: "agent".into(),
force: true,
dry_run: false,
@@ -617,7 +632,7 @@ mod tests {
.unwrap();
drop(listener);
assert_eq!(output.status, PodCleanupCliStatus::Failure);
assert_eq!(output.status, WorkerCleanupCliStatus::Failure);
assert!(output.stdout.contains("status: refused"));
assert!(pod_store.read_by_name("agent").unwrap().is_some());
}