summaryrefslogtreecommitdiff
path: root/tests/integration/cli_status.rs
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2026-02-15 21:27:00 +0100
committerDawid Rycerz <dawid@rycerz.xyz>2026-02-15 21:27:00 +0100
commitce0dbf6b249956700c6a1705bf4ad85a09d53e8c (patch)
treed7c3236807cfbf75d7f3a355eb5df5a5e2cc4ad7 /tests/integration/cli_status.rs
parent064a1d01c5c14f5ecc032fa9b8346a4a88b893f6 (diff)
feat: witryna 0.2.0HEADv0.2.0main
Switch, cleanup, and status CLI commands. Persistent build state via state.json. Post-deploy hooks on success and failure with WITRYNA_BUILD_STATUS. Dependency diet (axum→tiny_http, clap→argh, tracing→log). Drop built-in rate limiting. Nix flake with NixOS module. Arch Linux PKGBUILD. Centralized version management. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'tests/integration/cli_status.rs')
-rw-r--r--tests/integration/cli_status.rs544
1 files changed, 421 insertions, 123 deletions
diff --git a/tests/integration/cli_status.rs b/tests/integration/cli_status.rs
index 25135fb..4eb50a4 100644
--- a/tests/integration/cli_status.rs
+++ b/tests/integration/cli_status.rs
@@ -1,3 +1,4 @@
+use std::fmt::Write as _;
use std::process::Stdio;
use tempfile::TempDir;
use tokio::process::Command;
@@ -12,17 +13,16 @@ fn witryna_bin() -> std::path::PathBuf {
}
/// Write a minimal witryna.toml config for status tests.
-async fn write_status_config(
- dir: &std::path::Path,
- sites: &[&str],
- log_dir: &std::path::Path,
-) -> std::path::PathBuf {
+async fn write_status_config(dir: &std::path::Path, sites: &[&str]) -> std::path::PathBuf {
let base_dir = dir.join("data");
+ let log_dir = dir.join("logs");
tokio::fs::create_dir_all(&base_dir).await.unwrap();
+ tokio::fs::create_dir_all(&log_dir).await.unwrap();
let mut sites_toml = String::new();
for name in sites {
- sites_toml.push_str(&format!(
+ write!(
+ sites_toml,
r#"
[[sites]]
name = "{name}"
@@ -30,7 +30,8 @@ repo_url = "https://example.com/{name}.git"
branch = "main"
webhook_token = "unused"
"#
- ));
+ )
+ .unwrap();
}
let config_path = dir.join("witryna.toml");
@@ -48,63 +49,43 @@ log_level = "info"
config_path
}
-/// Write a fake build log with a valid header.
-async fn write_test_build_log(
- log_dir: &std::path::Path,
+/// Write a state.json for a site with the new format.
+async fn write_state_json(
+ base_dir: &std::path::Path,
site_name: &str,
- timestamp: &str,
- status: &str,
- commit: &str,
- image: &str,
- duration: &str,
+ current: &str,
+ builds: &[serde_json::Value],
) {
- let site_log_dir = log_dir.join(site_name);
- tokio::fs::create_dir_all(&site_log_dir).await.unwrap();
-
- let content = format!(
- "=== BUILD LOG ===\n\
- Site: {site_name}\n\
- Timestamp: {timestamp}\n\
- Git Commit: {commit}\n\
- Image: {image}\n\
- Duration: {duration}\n\
- Status: {status}\n\
- \n\
- === STDOUT ===\n\
- build output\n\
- \n\
- === STDERR ===\n"
- );
+ let state_dir = base_dir.join("builds").join(site_name);
+ tokio::fs::create_dir_all(&state_dir).await.unwrap();
+
+ let content = serde_json::json!({
+ "current": current,
+ "builds": builds,
+ });
- let log_file = site_log_dir.join(format!("{timestamp}.log"));
- tokio::fs::write(&log_file, content).await.unwrap();
+ let state_path = state_dir.join("state.json");
+ tokio::fs::write(&state_path, content.to_string())
+ .await
+ .unwrap();
}
-/// Write a fake hook log with a valid header.
-async fn write_test_hook_log(
- log_dir: &std::path::Path,
- site_name: &str,
- timestamp: &str,
+/// Create a build entry JSON value.
+fn build_entry(
status: &str,
-) {
- let site_log_dir = log_dir.join(site_name);
- tokio::fs::create_dir_all(&site_log_dir).await.unwrap();
-
- let content = format!(
- "=== HOOK LOG ===\n\
- Site: {site_name}\n\
- Timestamp: {timestamp}\n\
- Command: hook-cmd\n\
- Duration: 1s\n\
- Status: {status}\n\
- \n\
- === STDOUT ===\n\
- \n\
- === STDERR ===\n"
- );
-
- let log_file = site_log_dir.join(format!("{timestamp}-hook.log"));
- tokio::fs::write(&log_file, content).await.unwrap();
+ timestamp: &str,
+ git_commit: &str,
+ duration: &str,
+ log: &str,
+) -> serde_json::Value {
+ serde_json::json!({
+ "status": status,
+ "timestamp": timestamp,
+ "started_at": "2026-02-10T12:00:00Z",
+ "git_commit": git_commit,
+ "duration": duration,
+ "log": log,
+ })
}
// ---------------------------------------------------------------------------
@@ -114,13 +95,10 @@ async fn write_test_hook_log(
#[tokio::test]
async fn cli_status_no_builds() {
let tempdir = TempDir::new().unwrap();
- let log_dir = tempdir.path().join("logs");
- tokio::fs::create_dir_all(&log_dir).await.unwrap();
-
- let config_path = write_status_config(tempdir.path(), &["empty-site"], &log_dir).await;
+ let config_path = write_status_config(tempdir.path(), &["empty-site"]).await;
let output = Command::new(witryna_bin())
- .args(["--config", config_path.to_str().unwrap(), "status"])
+ .args(["status", "--config", config_path.to_str().unwrap()])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
@@ -139,24 +117,25 @@ async fn cli_status_no_builds() {
#[tokio::test]
async fn cli_status_single_build() {
let tempdir = TempDir::new().unwrap();
- let log_dir = tempdir.path().join("logs");
- tokio::fs::create_dir_all(&log_dir).await.unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["my-site"]).await;
- write_test_build_log(
- &log_dir,
+ write_state_json(
+ &base_dir,
"my-site",
"20260126-143000-123456",
- "success",
- "abc123d",
- "node:20-alpine",
- "45s",
+ &[build_entry(
+ "success",
+ "20260126-143000-123456",
+ "abc123d",
+ "45s",
+ "/logs/my-site/20260126-143000-123456.log",
+ )],
)
.await;
- let config_path = write_status_config(tempdir.path(), &["my-site"], &log_dir).await;
-
let output = Command::new(witryna_bin())
- .args(["--config", config_path.to_str().unwrap(), "status"])
+ .args(["status", "--config", config_path.to_str().unwrap()])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
@@ -174,28 +153,29 @@ async fn cli_status_single_build() {
#[tokio::test]
async fn cli_status_json_output() {
let tempdir = TempDir::new().unwrap();
- let log_dir = tempdir.path().join("logs");
- tokio::fs::create_dir_all(&log_dir).await.unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["json-site"]).await;
- write_test_build_log(
- &log_dir,
+ write_state_json(
+ &base_dir,
"json-site",
"20260126-143000-123456",
- "success",
- "abc123d",
- "node:20-alpine",
- "45s",
+ &[build_entry(
+ "success",
+ "20260126-143000-123456",
+ "abc123d",
+ "45s",
+ "/logs/json-site/20260126-143000-123456.log",
+ )],
)
.await;
- let config_path = write_status_config(tempdir.path(), &["json-site"], &log_dir).await;
-
let output = Command::new(witryna_bin())
.args([
- "--config",
- config_path.to_str().unwrap(),
"status",
"--json",
+ "--config",
+ config_path.to_str().unwrap(),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@@ -217,41 +197,43 @@ async fn cli_status_json_output() {
#[tokio::test]
async fn cli_status_site_filter() {
let tempdir = TempDir::new().unwrap();
- let log_dir = tempdir.path().join("logs");
- tokio::fs::create_dir_all(&log_dir).await.unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["site-a", "site-b"]).await;
- // Create logs for two sites
- write_test_build_log(
- &log_dir,
+ write_state_json(
+ &base_dir,
"site-a",
"20260126-143000-000000",
- "success",
- "aaa1111",
- "alpine:latest",
- "10s",
+ &[build_entry(
+ "success",
+ "20260126-143000-000000",
+ "aaa1111",
+ "10s",
+ "/logs/a.log",
+ )],
)
.await;
- write_test_build_log(
- &log_dir,
+ write_state_json(
+ &base_dir,
"site-b",
"20260126-150000-000000",
- "success",
- "bbb2222",
- "alpine:latest",
- "20s",
+ &[build_entry(
+ "success",
+ "20260126-150000-000000",
+ "bbb2222",
+ "20s",
+ "/logs/b.log",
+ )],
)
.await;
- let config_path = write_status_config(tempdir.path(), &["site-a", "site-b"], &log_dir).await;
-
let output = Command::new(witryna_bin())
.args([
- "--config",
- config_path.to_str().unwrap(),
"status",
- "--site",
"site-a",
+ "--config",
+ config_path.to_str().unwrap(),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@@ -271,33 +253,340 @@ async fn cli_status_site_filter() {
#[tokio::test]
async fn cli_status_hook_failed() {
let tempdir = TempDir::new().unwrap();
- let log_dir = tempdir.path().join("logs");
- tokio::fs::create_dir_all(&log_dir).await.unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["hook-site"]).await;
- // Build succeeded, but hook failed
- write_test_build_log(
- &log_dir,
+ write_state_json(
+ &base_dir,
"hook-site",
"20260126-143000-123456",
- "success",
- "abc123d",
- "alpine:latest",
- "12s",
+ &[build_entry(
+ "hook failed",
+ "20260126-143000-123456",
+ "abc123d",
+ "12s",
+ "/logs/hook-site/20260126-143000-123456.log",
+ )],
)
.await;
- write_test_hook_log(
- &log_dir,
- "hook-site",
+ let output = Command::new(witryna_bin())
+ .args(["status", "--config", config_path.to_str().unwrap()])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(output.status.success(), "should exit 0");
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ assert!(
+ stdout.contains("hook failed"),
+ "should show 'hook failed', got: {stdout}"
+ );
+}
+
+#[tokio::test]
+async fn cli_status_building_shows_in_progress() {
+ let tempdir = TempDir::new().unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["building-site"]).await;
+
+ let started_at = witryna::time::format_rfc3339(std::time::SystemTime::now());
+
+ let state_dir = base_dir.join("builds").join("building-site");
+ tokio::fs::create_dir_all(&state_dir).await.unwrap();
+ let content = serde_json::json!({
+ "current": "",
+ "builds": [{
+ "status": "building",
+ "timestamp": "20260210-120000-000000",
+ "started_at": started_at,
+ "git_commit": "",
+ "duration": "",
+ "log": "/logs/building-site/20260210-120000-000000.log",
+ }],
+ });
+ tokio::fs::write(state_dir.join("state.json"), content.to_string())
+ .await
+ .unwrap();
+
+ let output = Command::new(witryna_bin())
+ .args(["status", "--config", config_path.to_str().unwrap()])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(output.status.success(), "should exit 0");
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ assert!(
+ stdout.contains("building"),
+ "should show 'building' status, got: {stdout}"
+ );
+}
+
+#[tokio::test]
+async fn cli_status_json_includes_building_state() {
+ let tempdir = TempDir::new().unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["json-building"]).await;
+
+ let started_at = witryna::time::format_rfc3339(std::time::SystemTime::now());
+
+ let state_dir = base_dir.join("builds").join("json-building");
+ tokio::fs::create_dir_all(&state_dir).await.unwrap();
+ let content = serde_json::json!({
+ "current": "",
+ "builds": [{
+ "status": "building",
+ "timestamp": "20260210-120000-000000",
+ "started_at": started_at,
+ "git_commit": "",
+ "duration": "",
+ "log": "/logs/json-building/20260210-120000-000000.log",
+ }],
+ });
+ tokio::fs::write(state_dir.join("state.json"), content.to_string())
+ .await
+ .unwrap();
+
+ let output = Command::new(witryna_bin())
+ .args([
+ "status",
+ "--json",
+ "--config",
+ config_path.to_str().unwrap(),
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(output.status.success(), "should exit 0");
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
+ let arr = parsed.as_array().unwrap();
+ assert_eq!(arr.len(), 1);
+ assert_eq!(arr[0]["site_name"], "json-building");
+ assert_eq!(arr[0]["status"], "building");
+}
+
+// ---------------------------------------------------------------------------
+// current_build / "+" marker tests
+// ---------------------------------------------------------------------------
+
+#[tokio::test]
+async fn cli_status_marks_current_build() {
+ let tempdir = TempDir::new().unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["my-site"]).await;
+
+ write_state_json(
+ &base_dir,
+ "my-site",
+ "20260126-143000-123456",
+ &[build_entry(
+ "success",
+ "20260126-143000-123456",
+ "abc123d",
+ "45s",
+ "/logs/my-site/20260126-143000-123456.log",
+ )],
+ )
+ .await;
+
+ let output = Command::new(witryna_bin())
+ .args(["status", "--config", config_path.to_str().unwrap()])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(output.status.success(), "should exit 0");
+ let stdout = String::from_utf8_lossy(&output.stdout);
+
+ let data_line = stdout
+ .lines()
+ .find(|l| l.contains("my-site"))
+ .expect("should have my-site row");
+ assert!(
+ data_line.starts_with('+'),
+ "current build row should start with '+', got: {data_line}"
+ );
+}
+
+#[tokio::test]
+async fn cli_status_no_current_no_marker() {
+ let tempdir = TempDir::new().unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["my-site"]).await;
+
+ write_state_json(
+ &base_dir,
+ "my-site",
+ "", // no current
+ &[build_entry(
+ "success",
+ "20260126-143000-123456",
+ "abc123d",
+ "45s",
+ "/logs/my-site/20260126-143000-123456.log",
+ )],
+ )
+ .await;
+
+ let output = Command::new(witryna_bin())
+ .args(["status", "--config", config_path.to_str().unwrap()])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(output.status.success(), "should exit 0");
+ let stdout = String::from_utf8_lossy(&output.stdout);
+
+ for line in stdout.lines().skip(1) {
+ assert!(
+ !line.starts_with('+'),
+ "no row should have '+' without current, got: {line}"
+ );
+ }
+}
+
+#[tokio::test]
+async fn cli_status_json_includes_current_build() {
+ let tempdir = TempDir::new().unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["json-site"]).await;
+
+ write_state_json(
+ &base_dir,
+ "json-site",
+ "20260126-143000-123456",
+ &[build_entry(
+ "success",
+ "20260126-143000-123456",
+ "abc123d",
+ "45s",
+ "/logs/json-site/20260126-143000-123456.log",
+ )],
+ )
+ .await;
+
+ let output = Command::new(witryna_bin())
+ .args([
+ "status",
+ "--json",
+ "--config",
+ config_path.to_str().unwrap(),
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(output.status.success(), "should exit 0");
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
+ let arr = parsed.as_array().unwrap();
+ assert_eq!(arr.len(), 1);
+ assert_eq!(
+ arr[0]["current_build"], "20260126-143000-123456",
+ "JSON should include current_build field"
+ );
+}
+
+#[tokio::test]
+async fn cli_status_single_site_shows_all_builds() {
+ let tempdir = TempDir::new().unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["my-site"]).await;
+
+ write_state_json(
+ &base_dir,
+ "my-site",
"20260126-143000-123456",
- "failed (exit code 1)",
+ &[
+ build_entry(
+ "failed",
+ "20260126-150000-000000",
+ "def4567",
+ "30s",
+ "/logs/2.log",
+ ),
+ build_entry(
+ "success",
+ "20260126-143000-123456",
+ "abc123d",
+ "45s",
+ "/logs/1.log",
+ ),
+ ],
)
.await;
- let config_path = write_status_config(tempdir.path(), &["hook-site"], &log_dir).await;
+ // Single-site view
+ let output = Command::new(witryna_bin())
+ .args([
+ "status",
+ "my-site",
+ "--config",
+ config_path.to_str().unwrap(),
+ ])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await
+ .unwrap();
+
+ assert!(output.status.success(), "should exit 0");
+ let stdout = String::from_utf8_lossy(&output.stdout);
+
+ let data_lines: Vec<&str> = stdout.lines().skip(1).collect();
+ assert_eq!(data_lines.len(), 2, "should have 2 build rows");
+
+ // First row: failed (no marker — current points to a different timestamp)
+ assert!(
+ !data_lines[0].starts_with('+'),
+ "failed build should not have '+': {}",
+ data_lines[0]
+ );
+ // Second row: success (has marker — matches current)
+ assert!(
+ data_lines[1].starts_with('+'),
+ "current build should have '+': {}",
+ data_lines[1]
+ );
+}
+
+#[tokio::test]
+async fn cli_status_failed_build_shows_failed() {
+ let tempdir = TempDir::new().unwrap();
+ let base_dir = tempdir.path().join("data");
+ let config_path = write_status_config(tempdir.path(), &["fail-site"]).await;
+
+ write_state_json(
+ &base_dir,
+ "fail-site",
+ "",
+ &[build_entry(
+ "failed",
+ "20260126-160000-000000",
+ "def4567",
+ "2m 0s",
+ "/logs/fail-site/20260126-160000-000000.log",
+ )],
+ )
+ .await;
let output = Command::new(witryna_bin())
- .args(["--config", config_path.to_str().unwrap(), "status"])
+ .args(["status", "--config", config_path.to_str().unwrap()])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
@@ -307,7 +596,16 @@ async fn cli_status_hook_failed() {
assert!(output.status.success(), "should exit 0");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
- stdout.contains("hook failed"),
- "should show 'hook failed', got: {stdout}"
+ stdout.contains("failed"),
+ "should show 'failed' (not a long error), got: {stdout}"
+ );
+ // Verify that status column is clean (no long error string breaking the table)
+ let data_line = stdout
+ .lines()
+ .find(|l| l.contains("fail-site"))
+ .expect("should have fail-site row");
+ assert!(
+ data_line.contains("def4567"),
+ "should show commit in correct column, got: {data_line}"
);
}