use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::provider_v3::error::{Error, Result};
use crate::provider_v3::plan::BundleBinding;
use crate::provider_v3::reason::WireReason;
use crate::provider_v3::vocabulary::{Command, Operation, TargetScope};
const BUNDLE_FLAGS: &[&str] = &[
"--bundle",
"--bundle-format",
"--bundle-digest",
"--artifact-digest",
"--bundle-size",
];
pub struct Usage {
pub command: Command,
pub required: &'static [&'static str],
pub optional: &'static [(&'static str, &'static str)],
pub note: &'static str,
}
const fn plan_usage(command: Command) -> Usage {
Usage {
command,
required: &[
"--target",
"--json",
"--operation",
"--provider-release-digest",
"--operation-id",
"--expires-at",
],
optional: &[
(
"--prefix",
"where a program lives; required by every software_* operation",
),
("--backup-ref", "which slot a restore returns to"),
(
"--capture-mode",
"complete_native captures the entire declared native surface",
),
("--permission-profile", "a profile this build declares"),
(
"--software-version",
"exactly one pinned version, when not the current one",
),
(
"--bundle …",
"the five bundle flags, for install and replace",
),
(
"--target-scope",
"which scope this target is; accepted and not yet acted on",
),
(
"--instruction-section",
"marked user-global instruction bytes for patch_instruction_region",
),
],
note: "Produce a plan. Always pure: reads the target and the local disk, opens no socket.",
}
}
#[must_use]
pub const fn usage(command: Command) -> Usage {
match command {
Command::ProviderInfo => Usage {
command,
required: &[],
optional: &[],
note: "Report capabilities. Takes no arguments at all, not even --json.",
},
Command::Status => Usage {
command,
required: &["--target", "--json"],
optional: &[(
"--target-scope",
"which scope to measure the target under; absent, the target's own record decides",
)],
note: "Report the target's current state. Never changes it.",
},
Command::RecoverOperation => Usage {
command,
required: &["--target", "--json"],
optional: &[],
note: "Resolve an interrupted operation. Reads the journal to know what it is resolving.",
},
Command::ValidateBundle => Usage {
command,
required: &[
"--target",
"--json",
"--bundle",
"--bundle-format",
"--bundle-digest",
"--artifact-digest",
"--bundle-size",
],
optional: &[],
note: "Check a bundle against the exact claim that named it. Touches nothing.",
},
Command::PlanOperation => plan_usage(command),
Command::ApplyOperation => Usage {
command,
required: &[
"--target",
"--json",
"--plan",
"--plan-digest",
"--provider-release-digest",
],
optional: &[
(
"--prefix",
"where a program lives; required by every software_* operation",
),
(
"--software-artifact",
"one per software_artifacts entry, in the plan's order",
),
(
"--bundle …",
"the five bundle flags, for install and replace",
),
],
note: "Apply one exact plan under the target lock. --plan is the plan object, \
written canonically -- not the envelope the planner printed around it.",
},
Command::Launch => Usage {
command,
required: &["--target", "--json", "--prefix"],
optional: &[(
"-- <args>",
"everything after a bare -- goes to the product verbatim",
)],
note: "Start the exact executable a software install placed. Never a name found on PATH.",
},
}
}
#[must_use]
pub fn render_usage(command: Command) -> String {
use std::fmt::Write as _;
let shape = usage(command);
let mut out = format!("{command}\n\n {}\n", shape.note);
if !shape.required.is_empty() {
out.push_str("\nRequired:\n");
for flag in shape.required {
let _ = writeln!(out, " {flag}");
}
}
if !shape.optional.is_empty() {
out.push_str("\nOptional:\n");
for (flag, why) in shape.optional {
let _ = writeln!(out, " {flag:<22} {why}");
}
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Invocation {
ProviderInfo,
ValidateBundle {
target: PathBuf,
bundle: Bundle,
},
PlanOperation {
target: PathBuf,
request: PlanRequest,
},
ApplyOperation {
target: PathBuf,
plan_path: PathBuf,
plan_digest: String,
provider_release_digest: String,
bundle: Option<Bundle>,
prefix: Option<PathBuf>,
software_artifacts: Vec<PathBuf>,
},
RecoverOperation {
target: PathBuf,
},
Status {
target: PathBuf,
target_scope: Option<TargetScope>,
},
Launch {
target: PathBuf,
prefix: Option<PathBuf>,
arguments: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bundle {
pub path: PathBuf,
pub binding: BundleBinding,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlanRequest {
pub operation: Operation,
pub provider_release_digest: String,
pub operation_id: String,
pub expires_at: String,
pub backup_ref: Option<String>,
pub capture_mode: Option<String>,
pub permission_profile: Option<String>,
pub bundle: Option<Bundle>,
pub prefix: Option<PathBuf>,
pub software_version: Option<String>,
pub target_scope: Option<TargetScope>,
pub instruction_section: Option<String>,
}
fn take_target_scope(flags: &mut Flags) -> Result<Option<TargetScope>> {
let Some(named) = flags.take_optional("--target-scope") else {
return Ok(None);
};
TargetScope::parse(&named).map(Some).ok_or_else(|| {
Error::refuse(
WireReason::UnsupportedOperation,
format!(
"{named:?} is not a target scope this build knows; it knows {}",
TargetScope::ALL
.iter()
.map(|scope| scope.as_str())
.collect::<Vec<_>>()
.join(", ")
),
)
})
}
impl Invocation {
#[must_use]
pub fn target(&self) -> Option<&PathBuf> {
match self {
Self::ProviderInfo => None,
Self::ValidateBundle { target, .. }
| Self::PlanOperation { target, .. }
| Self::ApplyOperation { target, .. }
| Self::RecoverOperation { target }
| Self::Status { target, .. }
| Self::Launch { target, .. } => Some(target),
}
}
#[must_use]
pub const fn command(&self) -> Command {
match self {
Self::ProviderInfo => Command::ProviderInfo,
Self::ValidateBundle { .. } => Command::ValidateBundle,
Self::PlanOperation { .. } => Command::PlanOperation,
Self::ApplyOperation { .. } => Command::ApplyOperation,
Self::RecoverOperation { .. } => Command::RecoverOperation,
Self::Status { .. } => Command::Status,
Self::Launch { .. } => Command::Launch,
}
}
}
pub fn parse<I, S>(arguments: I) -> Result<Invocation>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let tokens: Vec<String> = arguments.into_iter().map(Into::into).collect();
let Some(name) = tokens.first() else {
return Err(local("no command was given"));
};
let Some(command) = Command::parse(name) else {
return Err(local(format!(
"{name:?} is not a provider protocol v3 command"
)));
};
let rest = tokens.get(1..).unwrap_or_default();
if !command.takes_target() {
if !rest.is_empty() {
return Err(local(format!("{command} takes no arguments")));
}
return Ok(Invocation::ProviderInfo);
}
let (mine, passthrough) = Flags::split_passthrough(rest);
let mut flags = Flags::parse(&mine)?;
let missing: Vec<&str> = usage(command)
.required
.iter()
.copied()
.filter(|flag| !flags.holds(flag))
.collect();
if !missing.is_empty() {
return Err(local(format!(
"{command} is missing {}; run `{command} --help` for what it takes",
missing.join(", ")
)));
}
let target = PathBuf::from(flags.take_required("--target")?);
if !flags.take_switch("--json") {
return Err(local(format!("{command} requires --json")));
}
let invocation = match command {
Command::ProviderInfo => return Err(local("provider-info never reaches this branch")),
Command::Status => Invocation::Status {
target,
target_scope: take_target_scope(&mut flags)?,
},
Command::RecoverOperation => Invocation::RecoverOperation { target },
Command::Launch => Invocation::Launch {
target,
prefix: flags.take_prefix()?,
arguments: passthrough,
},
Command::ValidateBundle => {
let Some(bundle) = flags.take_bundle()? else {
return Err(local("validate-bundle requires a bundle"));
};
Invocation::ValidateBundle { target, bundle }
}
Command::PlanOperation => {
let operation_name = flags.take_required("--operation")?;
let Some(operation) = Operation::parse(&operation_name) else {
return Err(Error::refuse(
WireReason::UnsupportedOperation,
format!("{operation_name:?} is not an operation this protocol defines"),
));
};
Invocation::PlanOperation {
target,
request: PlanRequest {
operation,
provider_release_digest: flags.take_required("--provider-release-digest")?,
operation_id: flags.take_required("--operation-id")?,
expires_at: flags.take_required("--expires-at")?,
backup_ref: flags.take_optional("--backup-ref"),
capture_mode: flags.take_optional("--capture-mode"),
permission_profile: flags.take_optional("--permission-profile"),
bundle: flags.take_bundle()?,
prefix: flags.take_prefix()?,
software_version: flags.take_optional("--software-version"),
target_scope: take_target_scope(&mut flags)?,
instruction_section: flags.take_optional("--instruction-section"),
},
}
}
Command::ApplyOperation => Invocation::ApplyOperation {
target,
plan_path: PathBuf::from(flags.take_required("--plan")?),
plan_digest: flags.take_required("--plan-digest")?,
provider_release_digest: flags.take_required("--provider-release-digest")?,
bundle: flags.take_bundle()?,
prefix: flags.take_prefix()?,
software_artifacts: flags
.take_repeated("--software-artifact")
.into_iter()
.map(PathBuf::from)
.collect(),
},
};
flags.require_exhausted()?;
Ok(invocation)
}
fn local(detail: impl Into<String>) -> Error {
Error::refuse(WireReason::ProviderUnavailable, detail)
}
struct Flags {
values: BTreeMap<String, Vec<String>>,
switches: Vec<String>,
}
const REPEATABLE: &[&str] = &["--software-artifact"];
fn looks_like_flag(token: &str) -> bool {
token
.strip_prefix("--")
.is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_alphabetic()))
}
impl Flags {
fn split_passthrough(tokens: &[String]) -> (Vec<String>, Vec<String>) {
match tokens.iter().position(|token| token == "--") {
Some(at) => (tokens[..at].to_vec(), tokens[at + 1..].to_vec()),
None => (tokens.to_vec(), Vec::new()),
}
}
fn parse(tokens: &[String]) -> Result<Self> {
let mut values: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut switches = Vec::new();
let mut index = 0;
while index < tokens.len() {
let Some(token) = tokens.get(index) else {
break;
};
if !token.starts_with("--") {
return Err(local(format!("{token:?} is not a flag")));
}
if token == "--json" {
if switches.iter().any(|switch| switch == token) {
return Err(local("--json was given twice"));
}
switches.push(token.clone());
index += 1;
continue;
}
let Some(value) = tokens.get(index + 1) else {
return Err(local(format!("{token} has no value")));
};
if looks_like_flag(value) {
return Err(local(format!("{token} has no value")));
}
let seen = values.entry(token.clone()).or_default();
if !seen.is_empty() && !REPEATABLE.contains(&token.as_str()) {
return Err(local(format!("{token} was given twice")));
}
seen.push(value.clone());
index += 2;
}
Ok(Self { values, switches })
}
fn holds(&self, name: &str) -> bool {
if name == "--json" {
return self.switches.iter().any(|switch| switch == name);
}
self.values.contains_key(name)
}
fn take_required(&mut self, name: &str) -> Result<String> {
self.take_optional(name)
.ok_or_else(|| local(format!("{name} is required")))
}
fn take_optional(&mut self, name: &str) -> Option<String> {
self.values.remove(name)?.into_iter().next()
}
fn take_repeated(&mut self, name: &str) -> Vec<String> {
self.values.remove(name).unwrap_or_default()
}
fn take_switch(&mut self, name: &str) -> bool {
if let Some(position) = self.switches.iter().position(|switch| switch == name) {
self.switches.remove(position);
return true;
}
false
}
fn take_bundle(&mut self) -> Result<Option<Bundle>> {
let present = BUNDLE_FLAGS
.iter()
.filter(|flag| self.values.contains_key(**flag))
.count();
if present == 0 {
return Ok(None);
}
if present != BUNDLE_FLAGS.len() {
return Err(local(
"a bundle is named by all five of --bundle, --bundle-format, \
--bundle-digest, --artifact-digest and --bundle-size",
));
}
let path = PathBuf::from(self.take_required("--bundle")?);
let bundle_format = self.take_required("--bundle-format")?;
let bundle_digest = self.take_required("--bundle-digest")?;
let artifact_digest = self.take_required("--artifact-digest")?;
let raw_size = self.take_required("--bundle-size")?;
let bundle_size: u64 = raw_size.parse().map_err(|_| {
local(format!(
"--bundle-size {raw_size:?} is not a decimal byte count"
))
})?;
Ok(Some(Bundle {
path,
binding: BundleBinding {
bundle_format,
bundle_digest,
artifact_digest,
bundle_size,
},
}))
}
fn take_prefix(&mut self) -> Result<Option<PathBuf>> {
let Some(text) = self.take_optional("--prefix") else {
return Ok(None);
};
let path = PathBuf::from(&text);
if !path.is_absolute() {
return Err(local(format!("--prefix {text:?} is not an absolute path")));
}
Ok(Some(path))
}
fn require_exhausted(&self) -> Result<()> {
if let Some((name, _)) = self.values.iter().next() {
return Err(local(format!("{name} is not an argument of this command")));
}
if let Some(switch) = self.switches.first() {
return Err(local(format!(
"{switch} is not an argument of this command"
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use super::*;
const DIGEST: &str = "sha256:2222222222222222222222222222222222222222222222222222222222222222";
fn bundle_flags() -> Vec<String> {
[
"--bundle",
"/tmp/bundle.zip",
"--bundle-format",
"ai-stp-bundle/1",
"--bundle-digest",
DIGEST,
"--artifact-digest",
DIGEST,
"--bundle-size",
"4096",
]
.iter()
.map(|s| (*s).to_owned())
.collect()
}
fn with_target(command: &str, extra: &[&str]) -> Vec<String> {
let mut tokens = vec![
command.to_owned(),
"--target".to_owned(),
"/tmp/target".to_owned(),
"--json".to_owned(),
];
tokens.extend(extra.iter().map(|s| (*s).to_owned()));
tokens
}
fn complete(command: Command) -> Vec<String> {
let temporary = |name: &str| {
std::env::temp_dir()
.join(name)
.to_string_lossy()
.into_owned()
};
let mut tokens = vec![command.as_str().to_owned()];
for flag in usage(command).required {
tokens.push((*flag).to_owned());
if *flag == "--json" {
continue;
}
tokens.push(match *flag {
"--target" => temporary("target"),
"--operation" => "install".to_owned(),
"--bundle" | "--plan" => temporary("file"),
"--bundle-format" => "ai-stp-bundle/1".to_owned(),
"--bundle-size" => "4096".to_owned(),
"--operation-id" => "operation_00000000000000000000000".to_owned(),
"--expires-at" => "2027-01-01T00:00:00.000Z".to_owned(),
"--prefix" => temporary("prefix"),
_ => DIGEST.to_owned(),
});
}
if command == Command::PlanOperation {
tokens.extend(bundle_flags());
}
tokens
}
#[test]
fn every_flag_the_table_calls_required_is_one_the_parser_demands() {
for command in Command::ALL.iter().copied().filter(|c| c.takes_target()) {
let whole = complete(command);
parse(whole.clone())
.unwrap_or_else(|e| panic!("{command}: a complete invocation was refused: {e}"));
for flag in usage(command).required {
let mut without = Vec::new();
let mut skip = false;
for token in &whole {
if skip {
skip = false;
continue;
}
if token == flag {
skip = *flag != "--json";
continue;
}
without.push(token.clone());
}
let error = parse(without)
.err()
.unwrap_or_else(|| panic!("{command} was accepted without {flag}"));
assert!(
error.detail().contains(flag),
"{command} without {flag} refused without naming it: {}",
error.detail()
);
}
}
}
#[test]
fn one_refusal_names_every_missing_argument() {
let error = parse(["plan-operation", "--target", "/tmp/target"]).unwrap_err();
for flag in [
"--json",
"--operation",
"--provider-release-digest",
"--operation-id",
"--expires-at",
] {
assert!(
error.detail().contains(flag),
"the refusal did not name {flag}: {}",
error.detail()
);
}
assert!(
error.detail().contains("--help"),
"it does not say how to ask"
);
}
#[test]
fn every_command_can_be_asked_what_it_takes() {
for command in Command::ALL {
let rendered = render_usage(*command);
assert!(rendered.contains(command.as_str()));
assert!(!usage(*command).note.is_empty());
for flag in usage(*command).required {
assert!(
rendered.contains(flag),
"{command} help omits its own required {flag}"
);
}
}
}
#[test]
fn provider_info_takes_neither_target_nor_json() {
assert_eq!(parse(["provider-info"]).unwrap(), Invocation::ProviderInfo);
assert!(parse(["provider-info", "--target", "/tmp/x"]).is_err());
assert!(parse(["provider-info", "--json"]).is_err());
}
#[test]
fn every_other_command_requires_both() {
for command in Command::ALL.iter().filter(|c| c.takes_target()) {
let name = command.as_str();
assert!(parse([name]).is_err(), "{name} accepted no target");
assert!(
parse([name, "--target", "/tmp/target"]).is_err(),
"{name} accepted no --json"
);
}
}
#[test]
fn status_and_recover_carry_nothing_else() {
assert_eq!(
parse(with_target("status", &[])).unwrap(),
Invocation::Status {
target: PathBuf::from("/tmp/target"),
target_scope: None,
}
);
assert!(
parse(with_target(
"recover-operation",
&["--target-scope", "project"]
))
.is_err(),
"recovery reads its scope from the journal, never from argv"
);
assert_eq!(
parse(with_target("recover-operation", &[])).unwrap(),
Invocation::RecoverOperation {
target: PathBuf::from("/tmp/target")
}
);
}
#[test]
fn status_may_be_asked_about_a_scope() {
assert_eq!(
parse(with_target("status", &["--target-scope", "project"])).unwrap(),
Invocation::Status {
target: PathBuf::from("/tmp/target"),
target_scope: Some(TargetScope::Project),
}
);
let error = parse(with_target("status", &["--target-scope", "galaxy"])).unwrap_err();
assert!(
error.detail().contains("not a target scope"),
"{}",
error.detail()
);
}
#[test]
fn a_full_plan_request_parses_every_field() {
let mut tokens = with_target(
"plan-operation",
&[
"--operation",
"install",
"--provider-release-digest",
DIGEST,
"--operation-id",
"operation_01TEST",
"--expires-at",
"2026-08-23T15:00:00Z",
"--backup-ref",
"slot-000000000001",
"--permission-profile",
"default",
],
);
tokens.extend(bundle_flags());
let Invocation::PlanOperation { target, request } = parse(tokens).unwrap() else {
panic!("expected a plan invocation");
};
assert_eq!(target, PathBuf::from("/tmp/target"));
assert_eq!(request.operation, Operation::Install);
assert_eq!(request.operation_id, "operation_01TEST");
assert_eq!(request.backup_ref.as_deref(), Some("slot-000000000001"));
assert_eq!(request.permission_profile.as_deref(), Some("default"));
let bundle = request.bundle.unwrap();
assert_eq!(bundle.path, PathBuf::from("/tmp/bundle.zip"));
assert_eq!(bundle.binding.bundle_size, 4096);
}
#[test]
fn a_plan_without_the_optional_parts_still_parses() {
let tokens = with_target(
"plan-operation",
&[
"--operation",
"backup",
"--provider-release-digest",
DIGEST,
"--operation-id",
"operation_01TEST",
"--expires-at",
"2026-08-23T15:00:00Z",
],
);
let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
panic!("expected a plan invocation");
};
assert_eq!(request.operation, Operation::Backup);
assert!(request.bundle.is_none());
assert!(request.backup_ref.is_none());
}
#[test]
fn an_operation_outside_the_closed_set_is_refused_by_its_contract_reason() {
let tokens = with_target(
"plan-operation",
&[
"--operation",
"reformat",
"--provider-release-digest",
DIGEST,
"--operation-id",
"operation_01TEST",
"--expires-at",
"2026-08-23T15:00:00Z",
],
);
let error = parse(tokens).unwrap_err();
assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation));
}
#[test]
fn patch_instruction_region_parses_the_marked_section() {
let tokens = with_target(
"plan-operation",
&[
"--operation",
"patch_instruction_region",
"--provider-release-digest",
DIGEST,
"--operation-id",
"operation_01TEST",
"--expires-at",
"2026-08-23T15:00:00Z",
"--instruction-section",
":::begin-ai-stp\nhello\n:::end-ai-stp\n",
],
);
let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
panic!("expected a plan invocation");
};
assert_eq!(request.operation, Operation::PatchInstructionRegion);
assert_eq!(
request.instruction_section.as_deref(),
Some(":::begin-ai-stp\nhello\n:::end-ai-stp\n")
);
}
#[test]
fn patch_instruction_region_parses_yaml_frontmatter_before_markers() {
let section = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nhello\n:::end-ai-stp\n";
let tokens = with_target(
"plan-operation",
&[
"--operation",
"patch_instruction_region",
"--provider-release-digest",
DIGEST,
"--instruction-section",
section,
"--operation-id",
"operation_01TEST",
"--expires-at",
"2026-08-23T15:00:00Z",
],
);
let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
panic!("expected a plan invocation");
};
assert_eq!(request.instruction_section.as_deref(), Some(section));
}
#[test]
fn a_scope_this_build_cannot_yet_use_is_still_accepted() {
for named in TargetScope::ALL {
let tokens = with_target(
"plan-operation",
&[
"--operation",
"backup",
"--provider-release-digest",
DIGEST,
"--operation-id",
"operation_01TEST",
"--expires-at",
"2026-08-23T15:00:00Z",
"--target-scope",
named.as_str(),
],
);
let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
panic!("plan-operation did not parse with --target-scope {named:?}");
};
assert_eq!(request.target_scope, Some(*named));
}
}
#[test]
fn a_scope_this_build_does_not_know_is_refused_by_name() {
let tokens = with_target(
"plan-operation",
&[
"--operation",
"backup",
"--provider-release-digest",
DIGEST,
"--operation-id",
"operation_01TEST",
"--expires-at",
"2026-08-23T15:00:00Z",
"--target-scope",
"machine_root",
],
);
let error = parse(tokens).unwrap_err();
assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation));
let said = error.to_string();
assert!(said.contains("machine_root"), "{said}");
for known in TargetScope::ALL {
assert!(said.contains(known.as_str()), "{said} omits {known:?}");
}
}
#[test]
fn a_plan_without_a_scope_still_parses_and_says_so() {
let tokens = with_target(
"plan-operation",
&[
"--operation",
"backup",
"--provider-release-digest",
DIGEST,
"--operation-id",
"operation_01TEST",
"--expires-at",
"2026-08-23T15:00:00Z",
],
);
let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
panic!("plan-operation without a scope did not parse");
};
assert_eq!(request.target_scope, None);
}
#[test]
fn apply_binds_the_plan_path_and_its_digest() {
let mut tokens = with_target(
"apply-operation",
&[
"--plan",
"/tmp/plan.json",
"--plan-digest",
DIGEST,
"--provider-release-digest",
DIGEST,
],
);
tokens.extend(bundle_flags());
let Invocation::ApplyOperation {
plan_path,
plan_digest,
bundle,
..
} = parse(tokens).unwrap()
else {
panic!("expected an apply invocation");
};
assert_eq!(plan_path, PathBuf::from("/tmp/plan.json"));
assert_eq!(plan_digest, DIGEST);
assert!(bundle.is_some());
}
#[test]
fn a_partial_bundle_is_refused_rather_than_completed() {
let mut partial = bundle_flags();
partial.truncate(partial.len() - 2);
let error =
parse([with_target("validate-bundle", &[]), partial.clone()].concat()).unwrap_err();
assert!(
error.detail().contains("--bundle-size"),
"{}",
error.detail()
);
let plan = parse(
[
with_target(
"plan-operation",
&[
"--operation",
"install",
"--provider-release-digest",
DIGEST,
"--operation-id",
"operation_00000000000000000000000",
"--expires-at",
"2027-01-01T00:00:00.000Z",
],
),
partial,
]
.concat(),
)
.unwrap_err();
assert!(plan.detail().contains("all five"), "{}", plan.detail());
}
#[test]
fn a_bundle_size_that_is_not_a_number_is_refused() {
let mut flags = bundle_flags();
let last = flags.len() - 1;
flags[last] = "four thousand".to_owned();
let tokens = [with_target("validate-bundle", &[]), flags].concat();
assert!(
parse(tokens)
.unwrap_err()
.detail()
.contains("decimal byte count")
);
}
#[test]
fn an_unknown_flag_is_refused_rather_than_ignored() {
let tokens = with_target("status", &["--dry-run", "true"]);
assert!(parse(tokens).unwrap_err().detail().contains("--dry-run"));
}
#[test]
fn the_v1_expected_target_digest_flag_is_not_a_v3_argument() {
let tokens = with_target(
"plan-operation",
&[
"--operation",
"install",
"--provider-release-digest",
DIGEST,
"--operation-id",
"operation_01TEST",
"--expires-at",
"2026-08-23T15:00:00Z",
"--expected-target-digest",
DIGEST,
],
);
assert!(
parse(tokens)
.unwrap_err()
.detail()
.contains("--expected-target-digest")
);
}
#[test]
fn a_repeated_flag_is_refused_rather_than_last_one_winning() {
let tokens = [
"status", "--target", "/tmp/a", "--target", "/tmp/b", "--json",
];
assert!(parse(tokens).unwrap_err().detail().contains("twice"));
}
#[test]
fn a_flag_with_no_value_is_refused() {
assert!(
parse(["status", "--target", "--json"])
.unwrap_err()
.detail()
.contains("no value")
);
assert!(
parse(["status", "--target"])
.unwrap_err()
.detail()
.contains("no value")
);
}
#[test]
fn order_within_the_accepted_set_does_not_change_the_result() {
let ordered = parse(with_target("status", &[])).unwrap();
let swapped = parse(["status", "--json", "--target", "/tmp/target"]).unwrap();
assert_eq!(ordered, swapped);
}
#[test]
fn an_unknown_command_is_refused_and_never_guessed_at() {
assert!(
parse(["plan"])
.unwrap_err()
.detail()
.contains("not a provider protocol v3")
);
assert!(
parse(Vec::<String>::new())
.unwrap_err()
.detail()
.contains("no command")
);
}
#[test]
fn a_parsed_invocation_reports_the_command_and_target_it_carries() {
let invocation = parse(with_target("status", &[])).unwrap();
assert_eq!(invocation.command(), Command::Status);
assert_eq!(invocation.target(), Some(&PathBuf::from("/tmp/target")));
assert_eq!(parse(["provider-info"]).unwrap().target(), None);
}
}