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);
}
fn echo_token() -> &'static str {
if cfg!(windows) {
"echo token=%TOKEN%"
} else {
"echo token=$TOKEN"
}
}
#[test]
fn secret_is_injected_by_the_secrets_field() {
let dir = temp_project("secret");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"s\"\nversion = \"0.1.0\"\n",
);
let echo = echo_token();
write(
&dir,
".flux",
&format!(
"project \"s\"\nlanguage rust\nsecret TOKEN\npipeline {{\n\
step show {{ command \"{echo}\" secrets [ 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 deprecated_env_field_still_injects_and_warns() {
let dir = temp_project("envdep");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"s\"\nversion = \"0.1.0\"\n",
);
let echo = echo_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, "{set_out}");
let (out, ok) = run(&dir, &["build"]);
assert!(ok, "a deprecated field must not break the build: {out}");
assert!(out.contains("token=abc123"), "secret not injected: {out}");
assert!(out.contains("warning:"), "no deprecation warning: {out}");
assert!(out.contains("'env' field is deprecated"), "{out}");
assert!(out.contains("line 5"), "{out}");
let (fmt_out, fmt_ok) = run(&dir, &["format"]);
assert!(fmt_ok, "{fmt_out}");
let cfg = std::fs::read_to_string(dir.join(".flux")).unwrap();
assert!(cfg.contains("secrets [ TOKEN ]"), "{cfg}");
assert!(!cfg.contains("env [ TOKEN ]"), "{cfg}");
let (after, after_ok) = run(&dir, &["validate"]);
assert!(after_ok, "{after}");
assert!(!after.contains("warning:"), "{after}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn removed_keywords_are_parse_errors_naming_their_replacement() {
let dir = temp_project("retired");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"r\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"r\"\npipeline { step build { command \"echo x\" pool \"gpu\" } }\n",
);
let (pool_out, pool_ok) = run(&dir, &["validate"]);
assert!(!pool_ok, "step 'pool' must be rejected: {pool_out}");
assert!(pool_out.contains("'pool' was removed"), "{pool_out}");
assert!(pool_out.contains("runners"), "{pool_out}");
write(
&dir,
".flux",
"project \"r\"\nimport shared-ci\npipeline { step build { command \"echo x\" } }\n",
);
let (imp_out, imp_ok) = run(&dir, &["validate"]);
assert!(!imp_ok, "top-level 'import' must be rejected: {imp_out}");
assert!(imp_out.contains("directive was removed"), "{imp_out}");
assert!(imp_out.contains("use <name>"), "{imp_out}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn only_if_namespace_is_closed_and_documented() {
let dir = temp_project("onlyif");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"o\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"o\"\npipeline {\n\
step a { command \"echo a\" only_if branch != \"\" }\n\
step b { command \"echo b\" only_if tag == \"v1\" }\n\
step c { command \"echo c\" only_if flux_env == \"default\" }\n\
}\n",
);
let (good, good_ok) = run(&dir, &["validate"]);
assert!(good_ok, "every documented variable must parse: {good}");
write(
&dir,
".flux",
"project \"o\"\npipeline { step a { command \"echo a\" only_if brunch == \"main\" } }\n",
);
let (bad, bad_ok) = run(&dir, &["validate"]);
assert!(!bad_ok, "an unknown variable must be rejected: {bad}");
assert!(bad.contains("unknown only_if variable"), "{bad}");
for var in ["branch", "tag", "flux_env"] {
assert!(bad.contains(var), "error should list '{var}': {bad}");
}
let _ = std::fs::remove_dir_all(&dir);
}
fn sleep_cmd(secs: u32) -> String {
if cfg!(windows) {
format!("ping -n {} 127.0.0.1 >nul", secs + 1)
} else {
format!("sleep {secs}")
}
}
#[test]
fn build_streams_output_attributed_to_each_step() {
let dir = temp_project("stream");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"s\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"s\"\nlanguage rust\npipeline {\n\
step frontend { command \"echo bundling\" }\n\
step backend { command \"echo warming 1>&2\" }\n\
step tests { needs [ frontend, backend ] command \"echo 3 passed\" }\n\
}\n",
);
let (out, ok) = run(&dir, &["build"]);
assert!(ok, "{out}");
assert!(
out.contains("frontend \u{2502} bundling"),
"stdout not attributed: {out}"
);
assert!(
out.contains("backend \u{2506} warming"),
"stderr not attributed: {out}"
);
assert!(out.contains("tests \u{2502} 3 passed"), "{out}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_step_timeout_kills_the_command_and_fails_the_build() {
let dir = temp_project("timeout");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"t\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
&format!(
"project \"t\"\nlanguage rust\npipeline {{\n\
step hang {{ command \"{}\" timeout 1 }}\n\
step after {{ needs hang command \"echo should-not-run\" }}\n\
}}\n",
sleep_cmd(60)
),
);
let start = std::time::Instant::now();
let (out, ok) = run(&dir, &["build"]);
let elapsed = start.elapsed();
assert!(!ok, "a timed-out build must exit non-zero: {out}");
assert!(
out.contains("timed out after 1s"),
"the reason must be reported: {out}"
);
assert!(out.contains("timed out"), "{out}");
assert!(
!out.contains("should-not-run"),
"dependents of a killed step must be skipped: {out}"
);
assert!(
elapsed < std::time::Duration::from_secs(30),
"the command should have been killed at 1s, not waited out: {elapsed:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pipeline_timeout_is_the_default_and_steps_override_it() {
let dir = temp_project("timeoutdefault");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"t\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
&format!(
"project \"t\"\nlanguage rust\npipeline {{\n\
timeout 1\n\
step patient {{ command \"{} && echo patient-done\" timeout \"2m\" }}\n\
step impatient {{ needs patient command \"{}\" }}\n\
}}\n",
sleep_cmd(2),
sleep_cmd(60)
),
);
let (out, ok) = run(&dir, &["build"]);
assert!(!ok, "{out}");
assert!(out.contains("step timeout: 1s"), "{out}");
assert!(
out.contains("patient-done"),
"a step with its own longer timeout must finish: {out}"
);
assert!(
out.contains("impatient") && out.contains("timed out after 1s"),
"a step inheriting the pipeline timeout must be killed: {out}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn parallel_caps_how_many_steps_run_at_once() {
let dir = temp_project("parallel");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"p\"\nversion = \"0.1.0\"\n",
);
let sleep = sleep_cmd(1);
write(
&dir,
".flux",
&format!(
"project \"p\"\nlanguage rust\npipeline {{\n\
parallel 1\n\
step a {{ command \"echo a-start && {sleep} && echo a-end\" }}\n\
step b {{ command \"echo b-start && {sleep} && echo b-end\" }}\n\
step c {{ command \"echo c-start && {sleep} && echo c-end\" }}\n\
step all {{ needs [ a, b, c ] command \"echo all\" }}\n\
}}\n"
),
);
let (out, ok) = run(&dir, &["build"]);
assert!(ok, "{out}");
assert!(out.contains("up to 1 step in parallel"), "{out}");
let at = |needle: &str| {
out.find(needle)
.unwrap_or_else(|| panic!("missing '{needle}' in:\n{out}"))
};
for (first, second) in [("a-end", "b-start"), ("b-end", "c-start")] {
assert!(
at(first) < at(second),
"with parallel 1, '{first}' must precede '{second}':\n{out}"
);
}
let (wide, wide_ok) = run(&dir, &["build", "--parallel", "3"]);
assert!(wide_ok, "{wide}");
assert!(wide.contains("up to 3 steps in parallel"), "{wide}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn format_round_trips_timeout_and_parallel() {
let dir = temp_project("fmttimeout");
write(
&dir,
"Cargo.toml",
"[package]\nname = \"f\"\nversion = \"0.1.0\"\n",
);
write(
&dir,
".flux",
"project \"f\"\npipeline{parallel 2 timeout \"10m\" step build{command \"cargo build\" timeout 90}step serve{command \"./serve\" timeout off}}\n",
);
let (out, ok) = run(&dir, &["format"]);
assert!(ok, "{out}");
let cfg = std::fs::read_to_string(dir.join(".flux")).unwrap();
assert!(cfg.contains(" timeout \"10m\""), "{cfg}");
assert!(cfg.contains(" parallel 2"), "{cfg}");
assert!(cfg.contains(" timeout \"90s\""), "{cfg}");
assert!(cfg.contains(" timeout off"), "{cfg}");
let (check, check_ok) = run(&dir, &["format", "--check"]);
assert!(check_ok, "the formatted file must be stable: {check}");
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);
}