use std::path::PathBuf;
use std::process::Stdio;
use async_trait::async_trait;
use tracing::{info, warn};
use super::ports::{PortResult, UpstreamPortError, UpstreamVerifier, VerificationOutcome};
const OUTPUT_TAIL_LIMIT: usize = 8 * 1024;
pub struct CommandVerifier {
command: String,
cwd: PathBuf,
}
impl CommandVerifier {
pub fn new(command: impl Into<String>, cwd: impl Into<PathBuf>) -> Self {
Self {
command: command.into(),
cwd: cwd.into(),
}
}
}
fn tail(text: &str, limit: usize) -> String {
if text.len() <= limit {
return text.to_string();
}
let mut start = text.len() - limit;
while start < text.len() && !text.is_char_boundary(start) {
start += 1;
}
text[start..].to_string()
}
#[async_trait]
impl UpstreamVerifier for CommandVerifier {
async fn verify(&self) -> PortResult<VerificationOutcome> {
info!(
command = %self.command,
cwd = %self.cwd.display(),
"Running upstream verification command"
);
let mut cmd = crate::shell_command::build_login_shell_command(&self.command);
cmd.current_dir(&self.cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd
.output()
.await
.map_err(|e| UpstreamPortError::new("upstream verification command", e.to_string()))?;
if output.status.success() {
return Ok(VerificationOutcome::passed());
}
let combined = format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
warn!(
command = %self.command,
exit_code = ?output.status.code(),
"Upstream verification command failed"
);
Ok(VerificationOutcome::failed(tail(
&combined,
OUTPUT_TAIL_LIMIT,
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn upstream_integration_bounds_verification_output_tail() {
let long = "x".repeat(OUTPUT_TAIL_LIMIT * 2);
assert_eq!(tail(&long, OUTPUT_TAIL_LIMIT).len(), OUTPUT_TAIL_LIMIT);
assert_eq!(tail("short", OUTPUT_TAIL_LIMIT), "short");
}
#[test]
fn upstream_integration_tail_respects_char_boundaries() {
let text = "あいうえお";
let trimmed = tail(text, 7);
assert!(text.ends_with(&trimmed));
assert!(trimmed.len() <= 7);
}
}