use std::path::{Path, PathBuf};
use std::process::Command;
fn flux() -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_flux"));
cmd.env("NO_COLOR", "1");
cmd
}
fn temp_project(tag: &str) -> PathBuf {
let mut dir = std::env::temp_dir();
dir.push(format!("flux-it-{}-{}", tag, std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write(dir: &Path, name: &str, contents: &str) {
std::fs::write(dir.join(name), contents).unwrap();
}
fn run(dir: &Path, args: &[&str]) -> (String, bool) {
let out = flux().args(args).current_dir(dir).output().unwrap();
let mut s = String::from_utf8_lossy(&out.stdout).into_owned();
s.push_str(&String::from_utf8_lossy(&out.stderr));
(s, out.status.success())
}
#[test]
fn init_detects_rust_and_writes_config() {
let dir = temp_project("init");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"widget\"\nversion = \"0.1.0\"\n",
);
let (out, ok) = run(&dir, &["init"]);
assert!(ok, "init should succeed: {out}");
assert!(out.contains("Initializing project"), "{out}");
assert!(out.contains("AI agents configured"), "{out}");
let cfg = std::fs::read_to_string(dir.join(".flux")).unwrap();
assert!(cfg.contains("project \"widget\""), "{cfg}");
assert!(cfg.contains("language rust"), "{cfg}");
assert!(cfg.contains("cargo build --release"), "{cfg}");
let platform = std::fs::read_to_string(dir.join("flux.yaml")).unwrap();
assert!(platform.contains("name: widget"), "{platform}");
assert!(dir.join(".flux.d").join("agents").is_dir());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn build_runs_pipeline_then_caches() {
let dir = temp_project("build");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"c\"\nversion = \"0.1.0\"\n",
);
write(&dir, "src.txt", "hello");
write(
&dir,
".flux",
"project \"c\"\nlanguage rust\npipeline {\n step build { command \"echo compiling\" }\n}\n",
);
let (first, ok1) = run(&dir, &["build"]);
assert!(ok1, "first build should succeed: {first}");
assert!(
first.contains("compiling"),
"should run the command: {first}"
);
assert!(first.contains("Build completed"), "{first}");
let (second, ok2) = run(&dir, &["build"]);
assert!(ok2, "second build should succeed: {second}");
assert!(
second.contains("cached"),
"second build should be cached: {second}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn failing_step_stops_pipeline_with_nonzero_exit() {
let dir = temp_project("fail");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"f\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"f\"\nlanguage rust\npipeline {\n step build { command \"exit 3\" }\n step test { command \"echo should-not-run\" }\n}\n",
);
let (out, ok) = run(&dir, &["build"]);
assert!(!ok, "build should fail: {out}");
assert!(out.contains("Build failed"), "{out}");
assert!(
!out.contains("should-not-run"),
"later steps must not run: {out}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn dependency_graph_runs_needs_and_propagates_failure() {
let dir = temp_project("dag");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"d\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"d\"\nlanguage rust\npipeline {\n\
step frontend { command \"echo fe\" }\n\
step backend { command \"echo be\" }\n\
step tests { needs [ frontend, backend ] command \"exit 1\" }\n\
step package { needs tests command \"echo packaging\" }\n\
}\n",
);
let (out, ok) = run(&dir, &["build"]);
assert!(!ok, "build should fail when tests fail: {out}");
assert!(out.contains("dependency graph"), "{out}");
assert!(!out.contains("packaging"), "dependent must not run: {out}");
assert!(out.contains("skipped (dependency failed)"), "{out}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn secret_is_injected_into_step_env() {
let dir = temp_project("secret");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"s\"\nversion = \"0.1.0\"\n",
);
let echo = if cfg!(windows) {
"echo token=%TOKEN%"
} else {
"echo token=$TOKEN"
};
write(
&dir,
".flux",
&format!(
"project \"s\"\nlanguage rust\nsecret TOKEN\npipeline {{\n\
step show {{ command \"{echo}\" env [ TOKEN ] }}\n\
}}\n"
),
);
let (set_out, set_ok) = run(&dir, &["secret", "set", "TOKEN", "abc123"]);
assert!(set_ok, "secret set should succeed: {set_out}");
let (out, ok) = run(&dir, &["build"]);
assert!(ok, "build should succeed: {out}");
assert!(out.contains("token=abc123"), "secret not injected: {out}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cyclic_pipeline_is_rejected() {
let dir = temp_project("cycle");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"c\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"pipeline {\n\
step a { needs b command \"echo a\" }\n\
step b { needs a command \"echo b\" }\n\
}\n",
);
let (out, ok) = run(&dir, &["build"]);
assert!(!ok, "cyclic pipeline must be rejected: {out}");
assert!(out.contains("cycle"), "{out}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn intelligent_cache_rebuilds_only_affected() {
let dir = temp_project("intel");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"m\"\nversion = \"0.1.0\"\n",
);
std::fs::create_dir_all(dir.join("frontend")).unwrap();
std::fs::create_dir_all(dir.join("backend")).unwrap();
write(&dir, "frontend/a.ts", "v1");
write(&dir, "backend/b.rs", "v1");
write(
&dir,
".flux",
"project \"m\"\nlanguage rust\npipeline {\n\
step frontend { command \"echo fe\" inputs [ \"frontend/**\" ] }\n\
step backend { command \"echo be\" inputs [ \"backend/**\" ] }\n\
step all { needs [ frontend, backend ] command \"echo all\" }\n\
}\n",
);
let (_, ok1) = run(&dir, &["build"]);
assert!(ok1);
write(&dir, "frontend/a.ts", "v2");
let (out, ok2) = run(&dir, &["build"]);
assert!(ok2, "{out}");
assert!(
out.contains("backend (cached"),
"backend should stay cached: {out}"
);
assert!(out.contains("fe"), "frontend should rebuild: {out}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn module_use_splices_reusable_steps() {
let dir = temp_project("module");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"a\"\nversion = \"0.1.0\"\n",
);
std::fs::create_dir_all(dir.join("modules")).unwrap();
write(
&dir,
"modules/base.flux",
"pipeline {\n step compile { command \"echo compiling\" }\n step test { command \"echo testing\" }\n}\n",
);
write(
&dir,
".flux",
"project \"a\"\nlanguage rust\npipeline {\n use base\n step package { command \"echo packaging\" }\n}\n",
);
let (out, ok) = run(&dir, &["build"]);
assert!(ok, "{out}");
for step in ["compile", "test", "package"] {
assert!(out.contains(step), "module step '{step}' missing: {out}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn workspace_builds_only_affected_members() {
let dir = temp_project("ws");
for member in ["shared", "auth"] {
let mroot = dir.join(member);
std::fs::create_dir_all(&mroot).unwrap();
std::fs::write(
mroot.join("Cargo.toml"),
format!("[package]\nname=\"{member}\"\nversion=\"0.1.0\"\n"),
)
.unwrap();
std::fs::write(
mroot.join(".flux"),
format!("project \"{member}\"\nlanguage rust\npipeline {{ step build {{ command \"echo build-{member}\" }} }}\n"),
)
.unwrap();
std::fs::write(mroot.join("src.txt"), "v1").unwrap();
}
write(
&dir,
"flux.workspace",
"workspace \"w\"\nmember shared { path \"shared\" }\nmember auth { path \"auth\" needs [ shared ] }\n",
);
let (out1, ok1) = run(&dir, &["workspace", "build"]);
assert!(ok1, "{out1}");
assert!(
out1.contains("build-shared") && out1.contains("build-auth"),
"{out1}"
);
std::fs::write(dir.join("auth").join("src.txt"), "v2").unwrap();
let (out2, ok2) = run(&dir, &["workspace", "build"]);
assert!(ok2, "{out2}");
assert!(
out2.contains("shared skipped (unchanged)"),
"shared should be skipped: {out2}"
);
assert!(out2.contains("build-auth"), "auth should rebuild: {out2}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn policy_blocks_ci_when_violated() {
let dir = temp_project("policy");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"p\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"p\"\nlanguage rust\npolicy prod { require tests require security }\n\
pipeline { step build { command \"echo building\" } }\n",
);
let (out, ok) = run(&dir, &["ci"]);
assert!(!ok, "ci must be blocked by policy: {out}");
assert!(out.contains("Policy violations"), "{out}");
assert!(
!out.contains("building"),
"the pipeline must not run: {out}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn init_template_writes_tailored_pipeline() {
let dir = temp_project("tpl");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"api\"\nversion = \"0.1.0\"\n",
);
let (out, ok) = run(&dir, &["init", "rust-api"]);
assert!(ok, "{out}");
let cfg = std::fs::read_to_string(dir.join(".flux")).unwrap();
assert!(
cfg.contains("environment { image \"rust:latest\" }"),
"{cfg}"
);
assert!(cfg.contains("target kubernetes"), "{cfg}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn validate_accepts_good_and_rejects_bad() {
let dir = temp_project("validate");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"v\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"v\"\nlanguage rust\npipeline { step build { command \"cargo build\" } }\n",
);
let (out, ok) = run(&dir, &["validate"]);
assert!(ok, "valid .flux should pass: {out}");
assert!(out.contains(".flux is valid"), "{out}");
write(
&dir,
".flux",
"project \"v\"\npipeline { step build { commnd \"x\" } }\n",
);
let (bad, bad_ok) = run(&dir, &["validate"]);
assert!(!bad_ok, "invalid .flux must fail: {bad}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn format_is_idempotent() {
let dir = temp_project("fmt");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"f\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"f\"\nlanguage rust\npipeline{step build{command \"cargo build\"}step test{needs build command \"cargo test\"}}\n",
);
let (_, ok) = run(&dir, &["format"]);
assert!(ok);
let (out2, ok2) = run(&dir, &["format", "--check"]);
assert!(ok2, "formatted file should pass --check: {out2}");
assert!(out2.contains("already formatted"), "{out2}");
let cfg = std::fs::read_to_string(dir.join(".flux")).unwrap();
assert!(cfg.contains(" step build {"), "{cfg}");
assert!(cfg.contains("needs [ build ]"), "{cfg}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn explain_describes_the_pipeline() {
let dir = temp_project("explain");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"e\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"e\"\nlanguage rust\npipeline {\n\
step build { command \"cargo build\" }\n\
step test { needs build command \"cargo test\" }\n\
}\n",
);
let (out, ok) = run(&dir, &["explain"]);
assert!(ok, "{out}");
assert!(out.contains("2 step"), "{out}");
assert!(out.contains("after build"), "{out}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn plugin_search_filters_catalog() {
let dir = temp_project("psearch");
let (out, ok) = run(&dir, &["plugin", "search", "docker"]);
assert!(ok, "{out}");
assert!(out.contains("docker"), "{out}");
assert!(
!out.contains("terraform"),
"unrelated plugin should not match: {out}"
);
for builtin in ["rust", "node", "python"] {
let (out, ok) = run(&dir, &["plugin", "search", builtin]);
assert!(ok, "{out}");
assert!(
out.contains(builtin),
"built-in '{builtin}' should be found: {out}"
);
assert!(
!out.contains("no matches"),
"built-in '{builtin}' should not report no matches: {out}"
);
assert!(
out.contains("built-in"),
"built-in '{builtin}' should be marked as such: {out}"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
fn rust_project(tag: &str) -> PathBuf {
let dir = temp_project(tag);
write(
&dir,
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n\n[dependencies]\nanyhow = \"1\"\n",
);
std::fs::create_dir_all(dir.join("src")).unwrap();
write(&dir, "src/main.rs", "pub fn go() {}\nfn main() {}\n");
dir
}
#[test]
fn project_reports_intelligence_and_writes_knowledge_graph() {
let dir = rust_project("project");
let (out, ok) = run(&dir, &["project"]);
assert!(ok, "{out}");
assert!(out.contains("Repository Intelligence"), "{out}");
assert!(out.contains("Health"), "{out}");
let arch = dir.join(".flux-cache/knowledge/architecture.json");
assert!(arch.is_file(), "architecture.json should exist");
let arch = std::fs::read_to_string(arch).unwrap();
assert!(arch.contains("\"components\""), "{arch}");
let (jout, jok) = run(&dir, &["project", "--json"]);
assert!(jok, "{jout}");
assert!(jout.contains("\"project\""), "{jout}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn deps_json_emits_report() {
let dir = rust_project("depsjson");
let (out, ok) = run(&dir, &["deps", "--json"]);
assert!(ok, "deps --json should succeed: {out}");
assert!(out.contains("\"total\""), "{out}");
assert!(out.contains("\"outdatedNote\""), "{out}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn agent_list_and_run_maintenance() {
let dir = rust_project("agent");
let (list, ok) = run(&dir, &["agent", "list"]);
assert!(ok, "{list}");
assert!(list.contains("maintenance"), "{list}");
assert!(list.contains("reviewer"), "{list}");
let (run_out, ok) = run(&dir, &["agent", "run", "maintenance"]);
assert!(ok, "{run_out}");
assert!(run_out.contains("Flux Maintenance"), "{run_out}");
assert!(
dir.join(".flux-cache/reports/maintenance.md").is_file(),
"the agent should write a report"
);
let (bad, ok) = run(&dir, &["agent", "run", "nope"]);
assert!(!ok, "unknown agent should fail: {bad}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn ask_context_prints_bundle() {
let dir = rust_project("ask");
let (out, ok) = run(&dir, &["ask", "--context"]);
assert!(ok, "{out}");
assert!(out.contains("Project: demo"), "{out}");
assert!(out.contains("## Components"), "{out}");
let (ans, ok) = run(&dir, &["ask", "explain this repository"]);
assert!(ok, "{ans}");
assert!(ans.contains("offline heuristic"), "{ans}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn github_init_writes_workflow() {
let dir = rust_project("gh");
let (out, ok) = run(&dir, &["github", "init"]);
assert!(ok, "{out}");
assert!(dir.join(".github/workflows/flux.yml").is_file());
assert!(dir.join(".github/pull_request_template.md").is_file());
let wf = std::fs::read_to_string(dir.join(".github/workflows/flux.yml")).unwrap();
assert!(wf.contains("flux verify"), "{wf}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn docs_generate_then_check_is_in_sync() {
let dir = rust_project("docs");
let (chk, ok) = run(&dir, &["docs", "--check"]);
assert!(!ok, "check should fail before generation: {chk}");
let (gen, ok) = run(&dir, &["docs"]);
assert!(ok, "{gen}");
assert!(dir.join("docs/commands.md").is_file());
assert!(dir.join("docs/manifest.json").is_file());
let (chk2, ok) = run(&dir, &["docs", "--check"]);
assert!(ok, "check should pass after generation: {chk2}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn dashboard_writes_self_contained_html() {
let dir = rust_project("dash");
let (out, ok) = run(&dir, &["dashboard"]);
assert!(ok, "{out}");
let html = dir.join(".flux-cache/reports/dashboard.html");
assert!(html.is_file());
let html = std::fs::read_to_string(html).unwrap();
assert!(html.starts_with("<!doctype html>"), "{}", &html[..40]);
assert!(html.contains("demo"), "should name the project");
assert!(
!html.contains("http://") && !html.contains("https://"),
"must not reference the network"
);
let _ = std::fs::remove_dir_all(&dir);
}