mod common;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::{Duration, Instant};
use camel_cli::compile::runtime::{ArtifactArgs, EmbeddedRequest};
use camel_cli::compile::trailer;
use common::{KillOnDrop, drain_to_buffer, send_signal};
const CHILD_ENV: &str = "CAMEL_COMPILED_ARTIFACT_CHILD";
const ROUTE_DOC: &str = "\
routes:
- id: demo
from: timer:tick?period=300
steps:
- to: log:demo
";
const JOB_DOC: &str = "\
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: direct:transform
body: ping
routes:
- id: job-transform
from: direct:transform
steps:
- set_body:
value: job-done
";
const FAILING_JOB_DOC: &str = "\
execute:
mode: one-shot
timeout: 60s
send:
to: direct:boom
routes:
- id: job-fail
from: direct:boom
steps:
- to: direct:missing-consumer
";
const ENV_DOC: &str = "\
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: direct:transform
body: ping
routes:
- id: job-transform
from: direct:transform
steps:
- set_body:
value: ${env:DEPLOY_GREETING}
";
const MULTI_CONFIG: &str = "\
include = [\"conf/base.toml\"]
routes = [\"routes/*.yaml\"]
[default]
log_level = \"info\"
";
const MULTI_JOB_CONFIG: &str = "\
include = [\"conf/base.toml\"]
[default]
log_level = \"info\"
";
const MULTI_INCLUDE: &str = "[default]\ndrain_timeout_ms = 5000\n";
const MULTI_ENTRY_ROUTE: &str = "\
routes:
- id: alpha
from: timer:tick?period=200
steps:
- set_body:
value: alpha-marker
- to: log:alpha
";
const MULTI_INDEXED_ROUTE: &str = "\
routes:
- id: beta
from: timer:tick?period=200
steps:
- set_body:
value: beta-marker
- to: log:beta
";
const MULTI_JOB_DOC: &str = "\
routeFiles:
- routes/transform.yaml
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: direct:transform
body: ping
";
const MULTI_ENV_JOB_DOC: &str = "\
routeFiles:
- routes/greet.yaml
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: direct:transform
body: ping
";
const MULTI_JOB_ROUTE: &str = "\
routes:
- id: job-transform
from: direct:transform
steps:
- set_body:
value: multi-job-done
";
const MULTI_ENV_ROUTE: &str = "\
routes:
- id: greet-transform
from: direct:transform
steps:
- set_body:
value: ${env:DEPLOY_GREETING}
";
const ARG_DOC: &str = "\
args:
value:
default: hello
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: direct:transform
body: \"${arg:value}\"
routes:
- id: job-arg
from: direct:transform
";
const REQUIRED_ARG_DOC: &str = "\
args:
value:
required: true
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: direct:transform
body: \"${arg:value}\"
routes:
- id: job-arg
from: direct:transform
";
const TYPED_DEFAULT_ARG_DOC: &str = "\
args:
count:
type: int
default: \"007\"
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: \"direct:${arg:count}\"
body: ping
routes:
- id: job-count
from: direct:7
";
const BAD_TYPED_DEFAULT_DOC: &str = "\
args:
count:
type: int
default: \"abc\"
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: \"direct:${arg:count}\"
body: ping
routes:
- id: job-count
from: direct:7
";
const MALFORMED_DECLARATION_DOC: &str = "\
args:
count:
requried: true
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: \"direct:${arg:count}\"
body: ping
routes:
- id: job-count
from: direct:7
";
const STRUCTURE_INVALID_WELL_DECLARED_DOC: &str = "\
wat: oops
args:
count:
type: int
default: \"007\"
execute:
mode: one-shot
timeout: 60s
capture-reply: true
send:
to: \"direct:${arg:count}\"
body: ping
routes:
- id: job-count
from: direct:7
";
fn compile(dir: &Path, doc: &str, artifact: &str, envs: &[(&str, &str)]) -> Output {
compile_full(dir, doc, artifact, envs, None, &[])
}
fn compile_full(
dir: &Path,
doc: &str,
artifact: &str,
envs: &[(&str, &str)],
config: Option<&str>,
profiles: &[&str],
) -> Output {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_camel"));
cmd.env_clear()
.envs(envs.iter().copied())
.current_dir(dir)
.args(["compile", doc, "-o", artifact]);
if let Some(config) = config {
cmd.arg("--config").arg(config);
}
for profile in profiles {
cmd.arg("--profile").arg(profile);
}
cmd.output().expect("spawn `camel compile`")
}
struct Fixture {
route: PathBuf,
job: PathBuf,
failing_job: PathBuf,
env: PathBuf,
arg: PathBuf,
required_arg: PathBuf,
multi_route: PathBuf,
multi_job: PathBuf,
multi_env: PathBuf,
typed_arg: PathBuf,
}
static FIXTURE: OnceLock<Fixture> = OnceLock::new();
fn fixture_dir() -> PathBuf {
std::env::temp_dir().join(format!("camel-compiled-fixture-{}", std::process::id()))
}
fn spawn_reaper(dir: &Path) {
let dir = dir.to_string_lossy().into_owned();
let pid = std::process::id().to_string();
let _ = Command::new("sh")
.arg("-c")
.arg(format!(
"while kill -0 {pid} 2>/dev/null; do sleep 1; done; rm -rf -- '{dir}'"
))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
fn sweep_stale_fixtures() {
let Ok(entries) = std::fs::read_dir(std::env::temp_dir()) else {
return;
};
for entry in entries.flatten() {
let file_name = entry.file_name();
let Some(name) = file_name.to_str() else {
continue;
};
let Some(pid_str) = name.strip_prefix("camel-compiled-fixture-") else {
continue;
};
let Ok(pid) = pid_str.parse::<u32>() else {
continue;
};
if pid == std::process::id() {
continue;
}
let alive = Command::new("kill")
.arg("-0")
.arg(pid.to_string())
.status()
.map(|s| s.success())
.unwrap_or(false);
if !alive {
let _ = std::fs::remove_dir_all(entry.path());
}
}
}
fn fixture() -> &'static Fixture {
FIXTURE.get_or_init(|| {
sweep_stale_fixtures();
let dir = fixture_dir();
if dir.exists() {
std::fs::remove_dir_all(&dir).expect("remove stale fixture dir");
}
std::fs::create_dir_all(&dir).expect("create fixture dir");
spawn_reaper(&dir);
let compile_one =
|doc_name: &str, doc: &str, artifact: &str, envs: &[(&str, &str)]| -> PathBuf {
std::fs::write(dir.join(doc_name), doc).expect("write document");
let output = compile(&dir, doc_name, artifact, envs);
assert_eq!(
output.status.code(),
Some(0),
"document must compile: {}",
String::from_utf8_lossy(&output.stderr)
);
dir.join(artifact)
};
let route = compile_one("app.yaml", ROUTE_DOC, "route.bin", &[]);
let job = compile_one("ingest.job.yaml", JOB_DOC, "job.bin", &[]);
let failing_job = compile_one("fail.job.yaml", FAILING_JOB_DOC, "fail.bin", &[]);
let env = compile_one(
"greet.job.yaml",
ENV_DOC,
"env.bin",
&[("DEPLOY_GREETING", "compile-secret-value")],
);
let arg = compile_one("args.job.yaml", ARG_DOC, "arg.bin", &[]);
let required_arg = compile_one("reqargs.job.yaml", REQUIRED_ARG_DOC, "req.bin", &[]);
let compile_multi = |subdir: &str,
config: &str,
entry: &str,
entry_doc: &str,
route_path: &str,
route_doc: &str,
artifact: &str,
envs: &[(&str, &str)]|
-> PathBuf {
let root = dir.join(subdir);
std::fs::create_dir_all(root.join("conf")).expect("mkdir conf");
std::fs::create_dir_all(root.join("routes")).expect("mkdir routes");
std::fs::write(root.join("Camel.toml"), config).expect("write config");
std::fs::write(root.join("conf").join("base.toml"), MULTI_INCLUDE)
.expect("write include");
std::fs::write(root.join(route_path), route_doc).expect("write indexed route");
std::fs::write(root.join(entry), entry_doc).expect("write entry document");
let output = compile_full(&root, entry, artifact, envs, Some("Camel.toml"), &[]);
assert_eq!(
output.status.code(),
Some(0),
"multi-document compile must succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
root.join(artifact)
};
let multi_route = compile_multi(
"multi-route",
MULTI_CONFIG,
"multi-app.yaml",
MULTI_ENTRY_ROUTE,
"routes/beta.yaml",
MULTI_INDEXED_ROUTE,
"multi-route.bin",
&[],
);
let multi_job = compile_multi(
"multi-job",
MULTI_JOB_CONFIG,
"ingest-m.job.yaml",
MULTI_JOB_DOC,
"routes/transform.yaml",
MULTI_JOB_ROUTE,
"multi-job.bin",
&[],
);
let multi_env = compile_multi(
"multi-env",
MULTI_JOB_CONFIG,
"greet-m.job.yaml",
MULTI_ENV_JOB_DOC,
"routes/greet.yaml",
MULTI_ENV_ROUTE,
"multi-env.bin",
&[("DEPLOY_GREETING", "compile-secret-value")],
);
let typed_arg = compile_one("typed.job.yaml", TYPED_DEFAULT_ARG_DOC, "typed.bin", &[]);
Fixture {
route,
job,
failing_job,
env,
arg,
required_arg,
multi_route,
multi_job,
multi_env,
typed_arg,
}
})
}
fn deploy_artifact(artifact: &Path) -> (tempfile::TempDir, PathBuf) {
let deploy_dir = tempfile::tempdir().expect("deploy tempdir");
let target = deploy_dir.path().join("app.bin");
if std::fs::hard_link(artifact, &target).is_err() {
std::fs::copy(artifact, &target).expect("copy artifact");
}
(deploy_dir, target)
}
fn run_child() -> i32 {
let artifact = std::env::var(CHILD_ENV).expect("child env names the artifact");
let argv: Vec<String> = std::env::args().skip_while(|a| a != "--").skip(1).collect();
let bytes = std::fs::read(&artifact).expect("child reads the artifact");
let decoded = match trailer::decode_artifact(&bytes) {
Ok(Some(decoded)) => decoded,
Ok(None) => {
eprintln!("compiled artifact integrity error: no terminal marker");
return 2;
}
Err(e) => {
eprintln!("compiled artifact integrity error: {e}");
return 2;
}
};
let args = match ArtifactArgs::parse(&argv) {
Ok(args) => args,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let request = match decoded {
trailer::DecodedArtifact::V1(v1) => EmbeddedRequest::from_trailer(v1, args),
trailer::DecodedArtifact::V2(v2) => EmbeddedRequest::from_v2(v2, args),
};
let request = match request {
Ok(request) => request,
Err(e) => {
eprintln!("compiled artifact integrity error: {e}");
return 2;
}
};
tokio::runtime::Runtime::new()
.expect("tokio runtime")
.block_on(async { camel_cli::compile::runtime::run_embedded_document_code(request).await })
}
fn child_guard() {
if std::env::var(CHILD_ENV).is_ok() {
std::process::exit(run_child());
}
}
fn spawn_child_output(
test: &str,
dir: &Path,
artifact: &Path,
argv: &[&str],
envs: &[(&str, &str)],
) -> (i32, String, String) {
let mut cmd = Command::new(std::env::current_exe().expect("current test exe"));
cmd.env(CHILD_ENV, artifact)
.envs(envs.iter().copied())
.current_dir(dir)
.args(["--exact", test, "--nocapture", "--"])
.args(argv)
.stdin(Stdio::null())
.output()
.expect("spawn harness child (to completion)")
.into_code_and_strings()
}
trait CodeAndStrings {
fn into_code_and_strings(self) -> (i32, String, String);
}
impl CodeAndStrings for std::process::Output {
fn into_code_and_strings(self) -> (i32, String, String) {
(
self.status.code().unwrap_or(-1),
String::from_utf8_lossy(&self.stdout).into_owned(),
String::from_utf8_lossy(&self.stderr).into_owned(),
)
}
}
fn spawn_child(
test: &str,
dir: &Path,
artifact: &Path,
argv: &[&str],
envs: &[(&str, &str)],
) -> KillOnDrop {
let mut cmd = Command::new(std::env::current_exe().expect("current test exe"));
cmd.env(CHILD_ENV, artifact)
.envs(envs.iter().copied())
.current_dir(dir)
.args(["--exact", test, "--nocapture", "--"])
.args(argv)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
KillOnDrop(cmd.spawn().expect("spawn harness child"))
}
struct Drained {
out: Arc<Mutex<String>>,
err: Arc<Mutex<String>>,
}
impl Drained {
fn captured(&self) -> String {
format!(
"stdout:\n{}\nstderr:\n{}",
self.out.lock().expect("stdout lock").clone(),
self.err.lock().expect("stderr lock").clone()
)
}
}
fn spawn_drained(child: &mut Child) -> Drained {
let out = Arc::new(Mutex::new(String::new()));
let err = Arc::new(Mutex::new(String::new()));
let stdout = child.stdout.take().expect("child stdout piped");
let stderr = child.stderr.take().expect("child stderr piped");
thread::spawn({
let buf = Arc::clone(&out);
move || drain_to_buffer(stdout, buf)
});
thread::spawn({
let buf = Arc::clone(&err);
move || drain_to_buffer(stderr, buf)
});
Drained { out, err }
}
fn wait_for_marker(drained: &Drained, marker: &str, timeout: Duration) -> bool {
let start = Instant::now();
loop {
if drained.out.lock().expect("stdout lock").contains(marker)
|| drained.err.lock().expect("stderr lock").contains(marker)
{
return true;
}
if start.elapsed() >= timeout {
return false;
}
thread::sleep(Duration::from_millis(20));
}
}
fn wait_exit_code(child: &mut KillOnDrop, timeout: Duration) -> i32 {
let start = Instant::now();
loop {
match child.0.try_wait() {
Ok(Some(status)) => return status.code().unwrap_or(-1),
Ok(None) => {
if start.elapsed() >= timeout {
let _ = child.0.kill();
let _ = child.0.wait();
return -1;
}
thread::sleep(Duration::from_millis(25));
}
Err(e) => panic!("try_wait failed: {e}"),
}
}
}
fn graceful_shutdown(child: &mut KillOnDrop, drained: &Drained, test: &str) -> i32 {
assert!(
wait_for_marker(drained, "context started", Duration::from_secs(60)),
"artifact must boot through the embedded document: {}",
drained.captured()
);
send_signal(&child.0, "-TERM");
let code = wait_exit_code(child, Duration::from_secs(30));
assert_eq!(code, 0, "SIGTERM must shut down gracefully: {}", test);
code
}
#[test]
fn compiled_route_runs_without_source_tree() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().route);
assert!(!deploy.path().join("app.yaml").exists(), "no source doc");
assert!(!deploy.path().join("Camel.toml").exists(), "no config");
let mut child = spawn_child(
"compiled_route_runs_without_source_tree",
deploy.path(),
&artifact,
&[],
&[],
);
let drained = spawn_drained(&mut child);
graceful_shutdown(
&mut child,
&drained,
"compiled_route_runs_without_source_tree",
);
}
#[test]
fn compiled_job_uses_existing_outcome_report() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().job);
let (code, stdout, stderr) = spawn_child_output(
"compiled_job_uses_existing_outcome_report",
deploy.path(),
&artifact,
&["--report", "report.json"],
&[],
);
assert_eq!(
code, 0,
"completed job must exit 0;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(deploy.path().join("report.json"))
.expect("job report must be written"),
)
.expect("job report is JSON");
assert_eq!(report["outcome"], "Completed", "report: {report}");
assert_eq!(
report["document"], "compiled://ingest.job.yaml",
"report: {report}"
);
assert_eq!(report["mode"], "one-shot", "report: {report}");
assert_eq!(report["terminated_early"], false, "report: {report}");
assert_eq!(report["reply"]["body"], "job-done", "report: {report}");
let (deploy, artifact) = deploy_artifact(&fixture().failing_job);
let (code, stdout, stderr) = spawn_child_output(
"compiled_job_uses_existing_outcome_report",
deploy.path(),
&artifact,
&["--report", "fail-report.json"],
&[],
);
assert_eq!(
code, 1,
"pipeline failure must exit 1;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(deploy.path().join("fail-report.json"))
.expect("failed job report must be written"),
)
.expect("job report is JSON");
assert_eq!(report["outcome"], "Failed", "report: {report}");
assert!(
report["error"].as_str().is_some_and(|e| !e.is_empty()),
"report: {report}"
);
}
#[test]
fn compiled_artifact_resolves_deploy_environment() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().env);
let artifact_bytes = std::fs::read(&artifact).expect("artifact exists");
assert!(
artifact_bytes
.windows(b"${env:DEPLOY_GREETING}".len())
.any(|w| w == b"${env:DEPLOY_GREETING}"),
"artifact must keep the env expression"
);
assert!(
!artifact_bytes
.windows(b"compile-secret-value".len())
.any(|w| w == b"compile-secret-value"),
"artifact must not embed the compile-time value"
);
let (code, stdout, stderr) = spawn_child_output(
"compiled_artifact_resolves_deploy_environment",
deploy.path(),
&artifact,
&["--report", "env-report.json"],
&[("DEPLOY_GREETING", "deploy-value")],
);
assert_eq!(
code, 0,
"job must complete;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(deploy.path().join("env-report.json")).expect("report written"),
)
.expect("report is JSON");
assert_eq!(
report["reply"]["body"], "deploy-value",
"route must observe the deployment value: {report}"
);
}
#[test]
fn compiled_job_uses_declared_default() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().arg);
let (code, stdout, stderr) = spawn_child_output(
"compiled_job_uses_declared_default",
deploy.path(),
&artifact,
&["--report", "arg-report.json"],
&[],
);
assert_eq!(
code, 0,
"default resolution must complete;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let artifact_report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(deploy.path().join("arg-report.json"))
.expect("artifact report written"),
)
.expect("artifact report is JSON");
assert_eq!(
artifact_report["outcome"], "Completed",
"report: {artifact_report}"
);
assert_eq!(
artifact_report["reply"]["body"], "hello",
"the embedded default must fill ${{arg:value}}: {artifact_report}"
);
std::fs::write(deploy.path().join("args.job.yaml"), ARG_DOC).expect("write source doc");
let (code, stdout, stderr) = common::run_binary(
deploy.path(),
Path::new(env!("CARGO_BIN_EXE_camel")),
&["job", "args.job.yaml", "--report", "job-report.json"],
&[],
);
assert_eq!(
code, 0,
"normal job run must complete;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let job_report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(deploy.path().join("job-report.json"))
.expect("job report written"),
)
.expect("job report is JSON");
assert_eq!(
artifact_report["reply"]["body"], job_report["reply"]["body"],
"default resolution parity: artifact vs normal job; {job_report}"
);
}
#[test]
fn compiled_job_rejects_required_without_default() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().required_arg);
let (code, stdout, stderr) = spawn_child_output(
"compiled_job_rejects_required_without_default",
deploy.path(),
&artifact,
&["--report", "report.json"],
&[],
);
assert_eq!(
code, 2,
"required without default must exit 2;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("value") && combined.contains("required"),
"diagnostic must name the argument: {combined}"
);
assert!(
combined.contains("default"),
"diagnostic must point at declaring a default: {combined}"
);
assert!(
!combined.contains("pass --arg"),
"artifact diagnostic must not suggest the unavailable --arg surface: {combined}"
);
assert!(!combined.contains("context started"), "no boot: {combined}");
assert!(
!deploy.path().join("report.json").exists(),
"a rejected startup writes no report"
);
}
#[test]
fn compiled_job_rejects_arg_flag() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().arg);
let (code, stdout, stderr) = spawn_child_output(
"compiled_job_rejects_arg_flag",
deploy.path(),
&artifact,
&["--arg", "value=other"],
&[],
);
assert_eq!(
code, 2,
"--arg must be rejected as unknown;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("--arg"),
"must name the rejected argument: {combined}"
);
assert!(!combined.contains("context started"), "no boot: {combined}");
}
#[test]
fn compiled_job_coerces_typed_default() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().typed_arg);
let (code, stdout, stderr) = spawn_child_output(
"compiled_job_coerces_typed_default",
deploy.path(),
&artifact,
&["--report", "typed-report.json"],
&[],
);
assert_eq!(
code, 0,
"typed default must coerce and complete;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let artifact_report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(deploy.path().join("typed-report.json"))
.expect("artifact report written"),
)
.expect("artifact report is JSON");
assert_eq!(
artifact_report["outcome"], "Completed",
"report: {artifact_report}"
);
assert_eq!(
artifact_report["reply"]["body"], "ping",
"the coerced target must route to the `direct:7` consumer: {artifact_report}"
);
std::fs::write(deploy.path().join("typed.job.yaml"), TYPED_DEFAULT_ARG_DOC)
.expect("write source doc");
let (code, stdout, stderr) = common::run_binary(
deploy.path(),
Path::new(env!("CARGO_BIN_EXE_camel")),
&["job", "typed.job.yaml", "--report", "typed-job-report.json"],
&[],
);
assert_eq!(
code, 0,
"normal job run must complete;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let job_report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(deploy.path().join("typed-job-report.json"))
.expect("job report written"),
)
.expect("job report is JSON");
assert_eq!(job_report["outcome"], "Completed", "report: {job_report}");
assert_eq!(
artifact_report["reply"]["body"], job_report["reply"]["body"],
"identical send target: artifact vs normal job; {job_report}"
);
}
#[test]
fn compile_rejects_bad_typed_default() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("bad.job.yaml"), BAD_TYPED_DEFAULT_DOC).expect("write document");
let output = compile(dir.path(), "bad.job.yaml", "bad.bin", &[]);
assert_eq!(
output.status.code(),
Some(2),
"compile must reject the bad typed default: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("count") && stderr.contains("int") && stderr.contains("abc"),
"diagnostic must name the argument, the expected type, and the raw value: {stderr}"
);
assert!(
!dir.path().join("bad.bin").exists(),
"a rejected compile must produce no artifact"
);
assert!(
!dir.path().join("bad.bin.tmp").exists(),
"a rejected compile must leave no partial artifact"
);
}
#[test]
fn compile_rejects_malformed_declaration() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("typo.job.yaml"), MALFORMED_DECLARATION_DOC)
.expect("write document");
let output = compile(dir.path(), "typo.job.yaml", "typo.bin", &[]);
assert_eq!(
output.status.code(),
Some(2),
"compile must reject the malformed declaration: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("requried") && stderr.contains("count"),
"unknown-field diagnostic must name the field and the argument: {stderr}"
);
assert!(
!dir.path().join("typo.bin").exists(),
"a rejected compile must produce no artifact"
);
assert!(
!dir.path().join("typo.bin.tmp").exists(),
"a rejected compile must leave no partial artifact"
);
}
#[test]
fn compile_allows_structure_invalid_but_well_declared_job() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("loose.job.yaml"),
STRUCTURE_INVALID_WELL_DECLARED_DOC,
)
.expect("write document");
let output = compile(dir.path(), "loose.job.yaml", "loose.bin", &[]);
assert_eq!(
output.status.code(),
Some(0),
"compile must run declaration checks ONLY;\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
dir.path().join("loose.bin").is_file(),
"the accepted compile must write the artifact"
);
}
#[test]
fn compiled_artifact_does_not_extract_or_watch() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().route);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(deploy.path(), std::fs::Permissions::from_mode(0o555))
.expect("chmod read-only");
}
let mut child = spawn_child(
"compiled_artifact_does_not_extract_or_watch",
deploy.path(),
&artifact,
&[],
&[],
);
let drained = spawn_drained(&mut child);
assert!(
wait_for_marker(&drained, "context started", Duration::from_secs(60)),
"artifact must boot on a read-only root: {}",
drained.captured()
);
let all_output = format!(
"{}{}",
drained.out.lock().expect("stdout lock"),
drained.err.lock().expect("stderr lock")
);
assert!(
!all_output.contains("hot-reload watching"),
"the watcher must never activate: {all_output}"
);
send_signal(&child.0, "-TERM");
let code = wait_exit_code(&mut child, Duration::from_secs(30));
assert_eq!(code, 0, "graceful shutdown on read-only root");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(deploy.path(), std::fs::Permissions::from_mode(0o755))
.expect("restore writable for cleanup");
}
let mut entries: Vec<String> = std::fs::read_dir(deploy.path())
.expect("read deploy dir")
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
entries.sort();
assert_eq!(
entries,
vec!["app.bin".to_string()],
"no extraction or other writes: {entries:?}"
);
}
#[test]
fn compiled_route_report_writes_status_json() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().route);
let mut child = spawn_child(
"compiled_route_report_writes_status_json",
deploy.path(),
&artifact,
&["--report", "status.json"],
&[],
);
let drained = spawn_drained(&mut child);
graceful_shutdown(
&mut child,
&drained,
"compiled_route_report_writes_status_json",
);
let report = std::fs::read_to_string(deploy.path().join("status.json"))
.expect("route status report must be written");
assert_eq!(
report.trim(),
r#"{"kind":"route","status":"completed","error":null}"#,
"exact RouteReport JSON"
);
}
#[cfg(unix)]
fn make_executable(path: &Path) {
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
.expect("chmod executable");
}
#[test]
fn trailer_free_binary_keeps_normal_cli() {
let camel = PathBuf::from(env!("CARGO_BIN_EXE_camel"));
let dir = tempfile::tempdir().expect("tempdir");
let (code, stdout, stderr) = common::run_binary(dir.path(), &camel, &["--version"], &[]);
assert_eq!(
code, 0,
"plain `--version` exits 0;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.trim().starts_with("camel "),
"Clap version output: {stdout}"
);
let (code, stdout, stderr) = common::run_binary(dir.path(), &camel, &["--watch"], &[]);
assert_eq!(
code, 2,
"unknown flag is Clap misuse;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stderr.starts_with("error:"),
"Clap error fingerprint: {stderr}"
);
}
#[test]
fn artifact_manifest_exits_without_boot() {
let (deploy, artifact) = deploy_artifact(&fixture().route);
let (code, stdout, stderr) = common::run_binary(deploy.path(), &artifact, &["--manifest"], &[]);
assert_eq!(
code, 0,
"--manifest exits 0;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let manifest: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("stdout is manifest JSON");
assert_eq!(manifest["kind"], "route", "manifest: {manifest}");
assert_eq!(manifest["source_name"], "app.yaml", "manifest: {manifest}");
assert_eq!(
manifest["runtime_version"],
camel_cli::compile::manifest::RUNTIME_VERSION,
"manifest: {manifest}"
);
assert!(
manifest["components"]
.as_array()
.is_some_and(|c| c.iter().any(|s| s.as_str() == Some("timer"))),
"embedded components listed: {manifest}"
);
assert!(
manifest["env_names"].as_array().is_some(),
"required env names listed: {manifest}"
);
assert!(
manifest["listeners"].as_array().is_some(),
"listener declarations listed: {manifest}"
);
let all = format!("{stdout}{stderr}");
assert!(!all.contains("context started"), "no route boot: {all}");
}
#[test]
fn artifact_manifest_lists_virtual_store_without_boot() {
let (deploy, artifact) = deploy_artifact(&fixture().multi_route);
let (code, stdout, stderr) = common::run_binary(deploy.path(), &artifact, &["--manifest"], &[]);
assert_eq!(
code, 0,
"--manifest exits 0;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let manifest: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("stdout is manifest JSON");
assert_eq!(manifest["manifest_schema"], 2, "manifest: {manifest}");
assert_eq!(manifest["kind"], "route", "manifest: {manifest}");
assert_eq!(
manifest["source_name"], "multi-app.yaml",
"manifest: {manifest}"
);
assert_eq!(
manifest["runtime_version"],
camel_cli::compile::manifest::RUNTIME_VERSION,
"manifest: {manifest}"
);
let files = manifest["embedded_files"]
.as_array()
.expect("embedded_files array");
let listed: Vec<(String, String)> = files
.iter()
.map(|f| {
(
f["path"].as_str().expect("path").to_string(),
f["kind"].as_str().expect("kind").to_string(),
)
})
.collect();
assert_eq!(
listed,
vec![
("Camel.toml".to_string(), "config".to_string()),
("conf/base.toml".to_string(), "include".to_string()),
("multi-app.yaml".to_string(), "route".to_string()),
("routes/beta.yaml".to_string(), "route".to_string()),
],
"every embedded logical path is listed: {manifest}"
);
let all = format!("{stdout}{stderr}");
assert!(!all.contains("context started"), "no route boot: {all}");
}
#[test]
fn artifact_rejects_unknown_positional_and_duplicate_args() {
let (deploy, artifact) = deploy_artifact(&fixture().multi_route);
let cases: &[(&[&str], &str)] = &[
(&["--help", "--version"], "--version"),
(&["--report", "a.json", "--report", "b.json"], "--report"),
(&["--report"], "--report"),
(&["--watch"], "--watch"),
(&["routes.yaml"], "routes.yaml"),
];
for (argv, named) in cases {
let (code, stdout, stderr) = common::run_binary(deploy.path(), &artifact, argv, &[]);
assert_eq!(
code, 2,
"argv {argv:?} must exit 2;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains(named),
"argv {argv:?} must name the rejected argument: {combined}"
);
assert!(!combined.contains("context started"), "no boot: {combined}");
}
}
#[test]
fn artifact_rejects_marked_corruption() {
let (deploy, artifact) = deploy_artifact(&fixture().route);
let valid = std::fs::read(&artifact).expect("artifact bytes");
let mut corrupt_data = valid.clone();
let data_end = corrupt_data.len() - trailer::FOOTER_LEN;
corrupt_data[data_end - 1] ^= 0xFF;
let mut corrupt_footer = valid.clone();
corrupt_footer[data_end + 28] ^= 0xFF;
for (name, bytes) in [("data", corrupt_data), ("footer", corrupt_footer)] {
let path = deploy.path().join(format!("corrupt-{name}.bin"));
std::fs::write(&path, bytes).expect("write corrupt artifact");
#[cfg(unix)]
make_executable(&path);
let (code, stdout, stderr) = common::run_binary(deploy.path(), &path, &[], &[]);
assert_eq!(
code, 2,
"corrupt {name} must fail closed;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("integrity error"),
"corrupt {name} must carry an integrity diagnostic: {combined}"
);
assert!(!combined.contains("context started"), "no boot: {combined}");
}
}
#[test]
fn artifact_rejects_v2_corruption_and_unknown_schemas() {
use camel_cli::compile::store::{
StoreDocument, StoreEntryKind, StoreIndex, VirtualDocumentStore,
};
use camel_cli::compile::trailer::{TrailerKind, TrailerV2};
let (deploy, artifact) = deploy_artifact(&fixture().multi_route);
let valid = std::fs::read(&artifact).expect("artifact bytes");
let data_end = valid.len() - trailer::FOOTER_LEN_V2;
let footer = &valid[data_end..];
let le = |range: std::ops::Range<usize>| {
u64::from_le_bytes(footer[range].try_into().expect("length field"))
};
let total = (le(12..20) + le(20..28) + le(28..36)) as usize;
let content_start = data_end - total;
let image = valid[..content_start - trailer::MAGIC.len()].to_vec();
let run_rejected = |name: &str, bytes: &[u8], diagnostic: &str| {
let path = deploy.path().join(name);
std::fs::write(&path, bytes).expect("write rejected artifact");
#[cfg(unix)]
make_executable(&path);
let (code, stdout, stderr) = common::run_binary(deploy.path(), &path, &[], &[]);
assert_eq!(
code, 2,
"{name} must fail closed;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("integrity error"),
"{name} must carry the integrity diagnostic: {combined}"
);
assert!(
combined.contains(diagnostic),
"{name} must name the failure: {combined}"
);
assert!(
!combined.contains("context started"),
"{name} must not boot: {combined}"
);
drop(std::fs::remove_file(&path));
};
let content_len = le(12..20) as usize;
let mut corrupt_content = valid.clone();
corrupt_content[content_start + content_len - 1] ^= 0xFF;
run_rejected(
"corrupt-content.bin",
&corrupt_content,
"trailer checksum mismatch",
);
let mut corrupt_footer = valid.clone();
corrupt_footer[data_end + 40] ^= 0xFF;
run_rejected(
"corrupt-footer.bin",
&corrupt_footer,
"trailer checksum mismatch",
);
let route_text = "routes:\n - id: demo\n from: timer:tick?period=300\n steps:\n - to: log:demo\n";
let store = VirtualDocumentStore::build(
"app.yaml",
&[StoreDocument {
path: "app.yaml".to_string(),
kind: StoreEntryKind::Route,
bytes: route_text.as_bytes().to_vec(),
}],
&[],
&["app.yaml".to_string()],
)
.expect("valid store builds");
let manifest = camel_cli::compile::manifest::derive_for_store(
&store,
TrailerKind::Route,
&[("app.yaml".to_string(), route_text.to_string())],
)
.expect("manifest derives");
let mut bad_index: StoreIndex = store.index.clone();
bad_index.store_schema = 99;
let mut bytes = image.clone();
bytes.extend_from_slice(&trailer::encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content.clone(),
index: bad_index.encode_canonical().expect("canonical index"),
manifest: manifest.to_canonical_json().into_bytes(),
}));
run_rejected("schema99-index.bin", &bytes, "unsupported store schema 99");
let mut manifest_value: serde_json::Value =
serde_json::from_str(&manifest.to_canonical_json()).expect("manifest JSON");
manifest_value["manifest_schema"] = serde_json::json!(99);
let mut bytes = image;
bytes.extend_from_slice(&trailer::encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content.clone(),
index: store.index.encode_canonical().expect("canonical index"),
manifest: serde_json::to_string(&manifest_value)
.expect("manifest JSON")
.into_bytes(),
}));
run_rejected(
"schema99-manifest.bin",
&bytes,
"unsupported manifest schema 99",
);
}
#[test]
fn artifact_truncated_without_marker_keeps_clap_fallback() {
let (deploy, artifact) = deploy_artifact(&fixture().route);
let mut bytes = std::fs::read(&artifact).expect("artifact bytes");
bytes.truncate(bytes.len() - trailer::MAGIC.len());
assert_eq!(
trailer::decode(&bytes),
Ok(None),
"truncation must remove the marker"
);
let path = deploy.path().join("truncated.bin");
std::fs::write(&path, bytes).expect("write truncated artifact");
#[cfg(unix)]
make_executable(&path);
let (code, stdout, stderr) = common::run_binary(deploy.path(), &path, &["--watch"], &[]);
assert_eq!(
code, 2,
"Clap misuse exits 2;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stderr.starts_with("error:"),
"unchanged Clap fallback: {stderr}"
);
let (deploy, artifact) = deploy_artifact(&fixture().multi_route);
let mut bytes = std::fs::read(&artifact).expect("artifact bytes");
bytes.truncate(bytes.len() - trailer::MAGIC.len());
assert!(
matches!(trailer::decode_artifact(&bytes), Ok(None)),
"truncation must remove the v2 marker"
);
let path = deploy.path().join("truncated-v2.bin");
std::fs::write(&path, bytes).expect("write truncated v2 artifact");
#[cfg(unix)]
make_executable(&path);
let (code, stdout, stderr) = common::run_binary(deploy.path(), &path, &["--watch"], &[]);
assert_eq!(
code, 2,
"Clap misuse exits 2 on truncated v2;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stderr.starts_with("error:"),
"unchanged Clap fallback for truncated v2: {stderr}"
);
}
#[test]
fn artifact_help_and_version_exit_zero() {
let (deploy, artifact) = deploy_artifact(&fixture().route);
let (code, stdout, stderr) = common::run_binary(deploy.path(), &artifact, &["--help"], &[]);
assert_eq!(
code, 0,
"--help exits 0;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("camel compiled artifact usage"),
"artifact usage text: {stdout}"
);
let (code, stdout, stderr) = common::run_binary(deploy.path(), &artifact, &["--version"], &[]);
assert_eq!(
code, 0,
"--version exits 0;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert_eq!(
stdout.trim(),
format!("camel {}", camel_cli::compile::manifest::RUNTIME_VERSION),
"artifact version line"
);
for stream in [&stdout, &stderr] {
assert!(!stream.contains("context started"), "no boot: {stream}");
}
}
#[test]
fn compiled_multidocument_route_runs_without_source_tree() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().multi_route);
for absent in ["multi-app.yaml", "Camel.toml", "routes", "conf"] {
assert!(
!deploy.path().join(absent).exists(),
"no source/config tree: {absent} must not exist"
);
}
let mut child = spawn_child(
"compiled_multidocument_route_runs_without_source_tree",
deploy.path(),
&artifact,
&[],
&[],
);
let drained = spawn_drained(&mut child);
assert!(
wait_for_marker(&drained, "context started", Duration::from_secs(60)),
"artifact must boot without its source tree: {}",
drained.captured()
);
assert!(
wait_for_marker(&drained, "alpha-marker", Duration::from_secs(30)),
"entry-document route must execute: {}",
drained.captured()
);
assert!(
wait_for_marker(&drained, "beta-marker", Duration::from_secs(30)),
"indexed route file must execute: {}",
drained.captured()
);
send_signal(&child.0, "-TERM");
let code = wait_exit_code(&mut child, Duration::from_secs(30));
assert_eq!(
code,
0,
"graceful shutdown after full multi-document run: {}",
drained.captured()
);
}
#[test]
fn compiled_job_uses_embedded_route_plan_and_report() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().multi_job);
for absent in ["ingest-m.job.yaml", "Camel.toml", "routes", "conf"] {
assert!(
!deploy.path().join(absent).exists(),
"no source/config tree: {absent} must not exist"
);
}
let (code, stdout, stderr) = spawn_child_output(
"compiled_job_uses_embedded_route_plan_and_report",
deploy.path(),
&artifact,
&["--report", "report.json"],
&[],
);
assert_eq!(
code, 0,
"multi-document job must complete;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(deploy.path().join("report.json"))
.expect("job report must be written"),
)
.expect("job report is JSON");
assert_eq!(report["outcome"], "Completed", "report: {report}");
assert_eq!(
report["document"], "compiled://ingest-m.job.yaml",
"virtual entry-point identity: {report}"
);
assert_eq!(report["mode"], "one-shot", "report: {report}");
assert_eq!(
report["reply"]["body"], "multi-job-done",
"indexed route file must drive the pipeline: {report}"
);
}
#[test]
fn compiled_multidocument_resolves_deployment_environment() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().multi_env);
let artifact_bytes = std::fs::read(&artifact).expect("artifact exists");
assert!(
artifact_bytes
.windows(b"${env:DEPLOY_GREETING}".len())
.any(|w| w == b"${env:DEPLOY_GREETING}"),
"artifact must keep the env expression"
);
assert!(
!artifact_bytes
.windows(b"compile-secret-value".len())
.any(|w| w == b"compile-secret-value"),
"artifact must not embed the compile-time value"
);
let (code, stdout, stderr) = spawn_child_output(
"compiled_multidocument_resolves_deployment_environment",
deploy.path(),
&artifact,
&["--report", "env-report.json"],
&[("DEPLOY_GREETING", "deploy-value")],
);
assert_eq!(
code, 0,
"job must complete;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(deploy.path().join("env-report.json")).expect("report written"),
)
.expect("report is JSON");
assert_eq!(
report["reply"]["body"], "deploy-value",
"route must observe the deployment value only: {report}"
);
}
#[test]
fn compiled_multidocument_does_not_extract_glob_or_watch() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().multi_route);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(deploy.path(), std::fs::Permissions::from_mode(0o555))
.expect("chmod read-only");
}
let mut child = spawn_child(
"compiled_multidocument_does_not_extract_glob_or_watch",
deploy.path(),
&artifact,
&[],
&[],
);
let drained = spawn_drained(&mut child);
assert!(
wait_for_marker(&drained, "context started", Duration::from_secs(60)),
"artifact must boot on a read-only root: {}",
drained.captured()
);
let all_output = format!(
"{}{}",
drained.out.lock().expect("stdout lock"),
drained.err.lock().expect("stderr lock")
);
assert!(
all_output.contains("virtual store"),
"the virtual-store loading seam must be visible: {all_output}"
);
assert!(
!all_output.contains("loading routes from patterns"),
"no glob discovery may run: {all_output}"
);
assert!(
!all_output.contains("hot-reload watching"),
"the watcher must never activate: {all_output}"
);
send_signal(&child.0, "-TERM");
let code = wait_exit_code(&mut child, Duration::from_secs(30));
assert_eq!(code, 0, "graceful shutdown on read-only root");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(deploy.path(), std::fs::Permissions::from_mode(0o755))
.expect("restore writable for cleanup");
}
let mut entries: Vec<String> = std::fs::read_dir(deploy.path())
.expect("read deploy dir")
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
entries.sort();
assert_eq!(
entries,
vec!["app.bin".to_string()],
"no extraction or other writes: {entries:?}"
);
}
#[test]
fn compiled_multidocument_ignores_post_compile_decoy() {
child_guard();
let (deploy, artifact) = deploy_artifact(&fixture().multi_route);
std::fs::create_dir_all(deploy.path().join("routes")).expect("mkdir decoy routes");
std::fs::write(
deploy.path().join("routes").join("decoy.yaml"),
"routes:\n - id: decoy\n from: timer:tick?period=100\n steps:\n - set_body:\n value: decoy-marker\n - to: log:decoy\n",
)
.expect("write decoy route");
let mut child = spawn_child(
"compiled_multidocument_ignores_post_compile_decoy",
deploy.path(),
&artifact,
&[],
&[],
);
let drained = spawn_drained(&mut child);
assert!(
wait_for_marker(&drained, "alpha-marker", Duration::from_secs(60)),
"embedded entry route must execute: {}",
drained.captured()
);
assert!(
wait_for_marker(&drained, "beta-marker", Duration::from_secs(30)),
"embedded indexed route must execute: {}",
drained.captured()
);
thread::sleep(Duration::from_millis(500));
let all_output = format!(
"{}{}",
drained.out.lock().expect("stdout lock"),
drained.err.lock().expect("stderr lock")
);
assert!(
!all_output.contains("decoy-marker"),
"the decoy route must never load: {all_output}"
);
send_signal(&child.0, "-TERM");
let code = wait_exit_code(&mut child, Duration::from_secs(30));
assert_eq!(code, 0, "graceful shutdown with decoy present");
}
#[test]
fn compiled_v1_artifact_uses_single_entry_adapter() {
child_guard();
let deploy = tempfile::tempdir().expect("deploy tempdir");
let manifest =
camel_cli::compile::manifest::derive("app.yaml", trailer::TrailerKind::Route, ROUTE_DOC)
.expect("v1 manifest derives");
let v1 = trailer::Trailer {
kind: trailer::TrailerKind::Route,
payload: ROUTE_DOC.as_bytes().to_vec(),
manifest: manifest.to_legacy_json().into_bytes(),
};
let artifact = deploy.path().join("app.bin");
std::fs::write(&artifact, trailer::encode(&v1)).expect("write v1 artifact");
let mut child = spawn_child(
"compiled_v1_artifact_uses_single_entry_adapter",
deploy.path(),
&artifact,
&[],
&[],
);
let drained = spawn_drained(&mut child);
assert!(
wait_for_marker(&drained, "context started", Duration::from_secs(60)),
"v1 artifact must boot: {}",
drained.captured()
);
let all_output = format!(
"{}{}",
drained.out.lock().expect("stdout lock"),
drained.err.lock().expect("stderr lock")
);
assert!(
all_output.contains("loading routes from compiled://app.yaml"),
"the v1 single-document seam must serve the run: {all_output}"
);
assert!(
!all_output.contains("virtual store"),
"v1 artifacts keep the single-entry adapter path: {all_output}"
);
send_signal(&child.0, "-TERM");
let code = wait_exit_code(&mut child, Duration::from_secs(30));
assert_eq!(code, 0, "graceful shutdown on the v1 path");
}
#[test]
fn compiled_runtime_rejects_invalid_store_before_boot() {
child_guard();
use camel_cli::compile::store::{
StoreDocument, StoreEntryKind, StoreIndex, VirtualDocumentStore,
};
use camel_cli::compile::trailer::{TrailerKind, TrailerV2};
let deploy = tempfile::tempdir().expect("deploy tempdir");
let route_text = "routes:\n - id: demo\n from: timer:tick?period=300\n steps:\n - to: log:demo\n";
let store = VirtualDocumentStore::build(
"app.yaml",
&[
StoreDocument {
path: "app.yaml".to_string(),
kind: StoreEntryKind::Route,
bytes: route_text.as_bytes().to_vec(),
},
StoreDocument {
path: "Camel.toml".to_string(),
kind: StoreEntryKind::Config,
bytes: b"this is not = = valid toml [[\n".to_vec(),
},
],
&["Camel.toml".to_string()],
&["app.yaml".to_string()],
)
.expect("structurally valid store builds");
let manifest = camel_cli::compile::manifest::derive_for_store(
&store,
TrailerKind::Route,
&[("app.yaml".to_string(), route_text.to_string())],
)
.expect("manifest derives");
let artifact_bytes = trailer::encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content.clone(),
index: store.index.encode_canonical().expect("canonical index"),
manifest: manifest.to_canonical_json().into_bytes(),
});
let artifact = deploy.path().join("invalid.bin");
std::fs::write(&artifact, artifact_bytes).expect("write invalid artifact");
let (code, stdout, stderr) = spawn_child_output(
"compiled_runtime_rejects_invalid_store_before_boot",
deploy.path(),
&artifact,
&[],
&[],
);
let combined = format!("{stdout}{stderr}");
assert_eq!(
code, 2,
"invalid store must fail closed with exit 2;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
combined.contains("Camel.toml"),
"the diagnostic must name the malformed configuration: {combined}"
);
assert!(
!combined.contains("not available in this build"),
"the interim bridge message must be gone: {combined}"
);
assert!(
!combined.contains("context started"),
"no boot may happen: {combined}"
);
let valid_store = VirtualDocumentStore::build(
"app.yaml",
&[
StoreDocument {
path: "app.yaml".to_string(),
kind: StoreEntryKind::Route,
bytes: route_text.as_bytes().to_vec(),
},
StoreDocument {
path: "Camel.toml".to_string(),
kind: StoreEntryKind::Config,
bytes: b"[profiles.default]\n".to_vec(),
},
],
&["Camel.toml".to_string()],
&["app.yaml".to_string()],
)
.expect("structurally valid store builds");
let valid_manifest = camel_cli::compile::manifest::derive_for_store(
&valid_store,
TrailerKind::Route,
&[("app.yaml".to_string(), route_text.to_string())],
)
.expect("manifest derives");
fn schema_99(index: &mut StoreIndex) {
index.store_schema = 99;
}
fn missing_plan_reference(index: &mut StoreIndex) {
index
.source_plan
.references
.push("routes/ghost.yaml".to_string());
}
fn job_entry_point(index: &mut StoreIndex) {
for entry in &mut index.entries {
if entry.path == index.entry_point {
entry.kind = StoreEntryKind::Job;
}
}
}
for (label, diagnostic, mutate) in [
(
"unknown-store-schema",
"unsupported store schema 99",
schema_99 as fn(&mut StoreIndex),
),
(
"missing-plan-reference",
"store reference to missing entry \"routes/ghost.yaml\"",
missing_plan_reference,
),
(
"entry-point-kind-mismatch",
"store reference \"app.yaml\" names a job entry, expected route",
job_entry_point,
),
] {
let mut index = valid_store.index.clone();
mutate(&mut index);
let artifact_bytes = trailer::encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: valid_store.content.clone(),
index: index.encode_canonical().expect("mutated index encodes"),
manifest: valid_manifest.to_canonical_json().into_bytes(),
});
let artifact = deploy.path().join(format!("{label}.bin"));
std::fs::write(&artifact, artifact_bytes).expect("write corrupted artifact");
let (code, stdout, stderr) = spawn_child_output(
"compiled_runtime_rejects_invalid_store_before_boot",
deploy.path(),
&artifact,
&[],
&[],
);
let combined = format!("{stdout}{stderr}");
assert_eq!(
code, 2,
"{label} must fail closed with exit 2;\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
combined.contains(diagnostic),
"{label} must name the store failure: {combined}"
);
assert!(
!combined.contains("checksum mismatch"),
"{label} must not fail on integrity (the artifact is re-sealed): {combined}"
);
assert!(
!combined.contains("context started"),
"{label} must boot zero routes: {combined}"
);
}
}