use std::path::{Path, PathBuf};
use std::process::Command;
const WAT_PROVIDER: &str = r#"(component
(import "host:env/dep@0.1.0" (instance $dep
(export "get" (func (result u32)))
))
(alias export $dep "get" (func $f))
(instance $out (export "get" (func $f)))
(export "my:providers/a@0.1.0" (instance $out))
)"#;
const WAT_CONSUMER: &str = r#"(component
(import "my:providers/a@0.1.0" (instance $a
(export "get" (func (result u32)))
))
(alias export $a "get" (func $f))
(instance $out (export "get" (func $f)))
(export "my:consumer/app@0.1.0" (instance $out))
)"#;
fn write_compose_components(dir: &Path) -> (PathBuf, PathBuf) {
let provider = dir.join("provider.wasm");
let consumer = dir.join("consumer.wasm");
std::fs::write(
&provider,
wat::parse_str(WAT_PROVIDER).expect("compile provider"),
)
.expect("write provider");
std::fs::write(
&consumer,
wat::parse_str(WAT_CONSUMER).expect("compile consumer"),
)
.expect("write consumer");
(provider, consumer)
}
fn splicer_in(dir: &Path) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_splicer"));
cmd.current_dir(dir);
cmd
}
fn assert_valid_wasm(bytes: &[u8]) {
let mut v = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
v.validate_all(bytes)
.expect("composed bytes should validate");
}
#[test]
fn compose_default_writes_only_composed_wasm() {
let dir = tempfile::tempdir().unwrap();
let (a, b) = write_compose_components(dir.path());
let out = splicer_in(dir.path())
.arg("compose")
.arg(&a)
.arg(&b)
.output()
.unwrap();
assert!(
out.status.success(),
"splicer compose failed: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
out.stdout.is_empty(),
"default mode should be silent on stdout, got: {}",
String::from_utf8_lossy(&out.stdout)
);
let composed = dir.path().join("composed.wasm");
assert!(composed.exists(), "composed.wasm should exist");
assert!(
!dir.path().join("output.wac").exists(),
"output.wac should not be written in default mode"
);
let bytes = std::fs::read(&composed).unwrap();
assert_valid_wasm(&bytes);
}
#[test]
fn compose_plan_writes_wac_only_and_prints_command() {
let dir = tempfile::tempdir().unwrap();
let (a, b) = write_compose_components(dir.path());
let out = splicer_in(dir.path())
.arg("compose")
.arg(&a)
.arg(&b)
.arg("--plan")
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("wac compose "),
"stdout should start with `wac compose ...` repro command, got: {stdout}"
);
assert!(stdout.contains("--dep"), "stdout should list deps");
assert!(
dir.path().join("output.wac").exists(),
"--plan should persist output.wac"
);
assert!(
!dir.path().join("composed.wasm").exists(),
"--plan should skip in-process compose"
);
}
#[test]
fn compose_emit_wac_bare_persists_both() {
let dir = tempfile::tempdir().unwrap();
let (a, b) = write_compose_components(dir.path());
let out = splicer_in(dir.path())
.arg("compose")
.arg(&a)
.arg(&b)
.arg("--emit-wac")
.output()
.unwrap();
assert!(out.status.success());
assert!(dir.path().join("composed.wasm").exists());
assert!(dir.path().join("output.wac").exists());
}
#[test]
fn compose_emit_wac_custom_path() {
let dir = tempfile::tempdir().unwrap();
let (a, b) = write_compose_components(dir.path());
let out = splicer_in(dir.path())
.arg("compose")
.arg(&a)
.arg(&b)
.arg("--emit-wac")
.arg("custom.wac")
.output()
.unwrap();
assert!(out.status.success());
assert!(dir.path().join("custom.wac").exists());
assert!(!dir.path().join("output.wac").exists());
assert!(dir.path().join("composed.wasm").exists());
}
#[test]
fn compose_output_path_override() {
let dir = tempfile::tempdir().unwrap();
let (a, b) = write_compose_components(dir.path());
let out = splicer_in(dir.path())
.arg("compose")
.arg(&a)
.arg(&b)
.arg("-o")
.arg("myapp.wasm")
.output()
.unwrap();
assert!(out.status.success());
assert!(dir.path().join("myapp.wasm").exists());
assert!(!dir.path().join("composed.wasm").exists());
}
const CHAIN1_YAML_REL: &str = "tests/component-interposition/splicer-rules/chain1.yaml";
const CHAIN1_WASM_REL: &str = "tests/component-interposition/compositions/chain1.wasm";
const PRINTER_MDL_REL: &str = "tests/component-interposition/fixtures/printer_mdl.comp.wasm";
fn manifest_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn chain1_fixture_present() -> bool {
let m = manifest_dir();
m.join(CHAIN1_YAML_REL).exists()
&& m.join(CHAIN1_WASM_REL).exists()
&& m.join(PRINTER_MDL_REL).exists()
}
fn stage_chain1_fixture(dir: &Path) {
let m = manifest_dir();
std::fs::copy(m.join(CHAIN1_YAML_REL), dir.join("chain1.yaml")).unwrap();
std::fs::copy(m.join(CHAIN1_WASM_REL), dir.join("chain1.wasm")).unwrap();
std::fs::create_dir_all(dir.join("fixtures")).unwrap();
std::fs::copy(
m.join(PRINTER_MDL_REL),
dir.join("fixtures/printer_mdl.comp.wasm"),
)
.unwrap();
}
const CHAINED_WASM_REL: &str = "tests/component-interposition/fixtures/chained.wasm";
const ADDER_MDL_A_REL: &str = "tests/component-interposition/fixtures/adder_mdl_a.comp.wasm";
fn typed_mismatch_fixture_present() -> bool {
let m = manifest_dir();
m.join(CHAINED_WASM_REL).exists() && m.join(ADDER_MDL_A_REL).exists()
}
fn stage_typed_mismatch_fixture(dir: &Path) {
let m = manifest_dir();
std::fs::copy(m.join(CHAINED_WASM_REL), dir.join("chained.wasm")).unwrap();
std::fs::copy(m.join(ADDER_MDL_A_REL), dir.join("adder_mdl_a.comp.wasm")).unwrap();
let yaml = r#"version: 1
rules:
- between:
interface: "wasi:http/handler@0.3.0-rc-2026-01-06"
inner:
name: srv-b
outer:
name: srv-a
inject:
- name: mismatched
path: "./adder_mdl_a.comp.wasm"
"#;
std::fs::write(dir.join("mismatch.yaml"), yaml).unwrap();
}
#[test]
fn splice_compose_failure_preserves_wac_and_prints_repro() {
if !typed_mismatch_fixture_present() {
eprintln!("skipping: typed-mismatch fixture not checked out");
return;
}
let dir = tempfile::tempdir().unwrap();
stage_typed_mismatch_fixture(dir.path());
let out = splicer_in(dir.path())
.arg("splice")
.arg("mismatch.yaml")
.arg("chained.wasm")
.arg("--skip-type-check")
.output()
.unwrap();
assert!(
!out.status.success(),
"typed-mismatched splice should fail at compose time"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("in-process compose failed"),
"error should label the failure: {stderr}"
);
assert!(
stderr.contains("WAC preserved at:"),
"error should name the persisted WAC path: {stderr}"
);
assert!(
stderr.contains("Splits preserved at:"),
"error should name the persisted splits path: {stderr}"
);
assert!(
stderr.contains("Reproduce standalone with:"),
"error should include a repro section header: {stderr}"
);
assert!(
stderr.contains("wac compose "),
"repro command should be the literal `wac compose ...`: {stderr}"
);
}
#[test]
fn splice_failure_does_not_pollute_cwd() {
if !chain1_fixture_present() {
eprintln!("skipping: chain1 fixture not checked out");
return;
}
let dir = tempfile::tempdir().unwrap();
stage_chain1_fixture(dir.path());
let _ = splicer_in(dir.path())
.arg("splice")
.arg("chain1.yaml")
.arg("chain1.wasm")
.output()
.unwrap();
assert!(!dir.path().join("output.wac").exists());
assert!(!dir.path().join("composed.wasm").exists());
assert!(!dir.path().join("splits").exists());
}
#[test]
fn splice_plan_works_even_when_compose_would_fail() {
if !typed_mismatch_fixture_present() {
eprintln!("skipping: typed-mismatch fixture not checked out");
return;
}
let dir = tempfile::tempdir().unwrap();
stage_typed_mismatch_fixture(dir.path());
let out = splicer_in(dir.path())
.arg("splice")
.arg("mismatch.yaml")
.arg("chained.wasm")
.arg("--skip-type-check")
.arg("--plan")
.output()
.unwrap();
assert!(
out.status.success(),
"--plan skips compose, so it should succeed: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.starts_with("wac compose "));
assert!(dir.path().join("output.wac").exists());
assert!(dir.path().join("splits").exists());
assert!(!dir.path().join("composed.wasm").exists());
}