use std::future::Future;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use harness::registry::PluginRegistry;
use llm_lib::attempt::VerifyReport;
use llm_lib::edit_plan::{AnchoredEdit, EditPlan, FileEditPlan};
use plugin_host::{WasmDetector, WasmT0};
fn noop(_: *const ()) {}
fn clone_noop(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &VTABLE)
}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone_noop, noop, noop, noop);
fn block_on<F: Future>(mut fut: F) -> F::Output {
let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
let mut cx = Context::from_waker(&waker);
let mut fut = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => std::thread::yield_now(),
}
}
}
fn build_fixture(name: &str) -> Option<PathBuf> {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let fixture_dir = manifest_dir.join("tests/guest").join(name);
let manifest_path = fixture_dir.join("Cargo.toml");
let Some(sdk) = std::env::var_os("WASI_SDK_PATH") else {
eprintln!(
"wasmtime_backend: пропускаю наскрізний тест — не задано WASI_SDK_PATH. \
`wasm32-wasip3` потребує wasi-libc, якої rustup не постачає (доккоментар \
build_fixture); візьми wasi-sdk 34-rc.3+ і вкажи корінь у цій змінній"
);
return None;
};
let sysroot = PathBuf::from(&sdk).join("share/wasi-sysroot/lib/wasm32-wasip3");
let crt = sysroot.join("crt1-reactor.o");
if !crt.is_file() {
eprintln!(
"wasmtime_backend: пропускаю наскрізний тест — {} не знайдено; \
WASI_SDK_PATH вказує на SDK без p3-сисрута (потрібен wasi-sdk 34-rc.3+)",
crt.display()
);
return None;
}
let rustflags = format!(
"-C link-self-contained=no -L {} -C link-arg={}",
sysroot.display(),
crt.display()
);
let status = Command::new("cargo")
.env("RUSTFLAGS", &rustflags)
.args([
"+nightly",
"build",
"-Z",
"build-std=std,panic_abort",
"--target",
"wasm32-wasip3",
"--release",
"--manifest-path",
])
.arg(&manifest_path)
.status();
match status {
Ok(status) if status.success() => {}
Ok(status) => {
eprintln!(
"wasmtime_backend: пропускаю наскрізний тест — `cargo +nightly build --target \
wasm32-wasip3` для {name} завершився кодом {status:?} (немає nightly чи \
компонента `rust-src`, немає мережі до crates.io, чи інша причина \
середовища — доккоментар модуля, розділ «Чому тест може себе пропустити»)"
);
return None;
}
Err(err) => {
eprintln!(
"wasmtime_backend: пропускаю наскрізний тест — не вдалось спавнити cargo: {err}"
);
return None;
}
}
let wasm_path = fixture_dir
.join("target/wasm32-wasip3/release")
.join(format!("{name}.wasm"));
if !wasm_path.is_file() {
eprintln!(
"wasmtime_backend: пропускаю — {} не з'явився після успішного build",
wasm_path.display()
);
return None;
}
Some(wasm_path)
}
#[test]
fn detector_denies_write_and_uses_a_fresh_instance_per_call() {
let Some(wasm_path) = build_fixture("detector_fixture") else {
return;
};
let repo_root = tempfile::tempdir().expect("tempdir");
let detect_fn = WasmDetector::from_file(repo_root.path(), &wasm_path)
.expect("WasmDetector::from_file")
.into_detect_fn();
let first = block_on((detect_fn)()).expect("перший виклик detect()");
let second = block_on((detect_fn)()).expect("другий виклик detect()");
assert_eq!(first.len(), 1);
assert_eq!(second.len(), 1);
assert!(
first[0].message.starts_with("calls=1;"),
"перший виклик: {}",
first[0].message
);
assert!(
second[0].message.starts_with("calls=1;"),
"другий виклик мусив побачити СВІЖИЙ інстанс (calls=1, не 2) — \
інакше wasmtime-бекенд закешував Instance між викликами, порушуючи \
§3.16 правило 3: {}",
second[0].message
);
for report in [&first[0], &second[0]] {
assert!(
report.message.contains("write: denied"),
"guest не мав отримати файлової capability: {}",
report.message
);
assert!(
!report.message.contains("BUG"),
"запис МАВ провалитись — guest не має жодного preopen: {}",
report.message
);
}
assert!(
!repo_root.path().join("pwned.txt").exists(),
"guest створив файл — межа §3.9 не спрацювала"
);
}
#[test]
fn wasm_detector_plugs_into_plugin_registry_like_the_in_process_backend() {
let Some(wasm_path) = build_fixture("detector_fixture") else {
return;
};
let repo_root = tempfile::tempdir().expect("tempdir");
let detect_fn = WasmDetector::from_file(repo_root.path(), &wasm_path)
.expect("WasmDetector::from_file")
.into_detect_fn();
let verify_fn: harness::registry::VerifyFn = Arc::new(|| {
Box::pin(async {
VerifyReport {
ok: true,
output: String::new(),
infra_error: false,
}
})
});
let registry = PluginRegistry::new(detect_fn, verify_fn, None, None);
let checkout_1 = registry.checkout_detector();
let checkout_2 = registry.checkout_detector();
assert_eq!(
checkout_2.generation,
checkout_1.generation + 1,
"реєстр і далі бампає generation на кожен checkout — незалежно від бекенда"
);
let violations = block_on((checkout_1.value)()).expect("detect()");
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].file, PathBuf::from("src/a.rs"));
}
#[test]
fn t0_end_to_end_returns_the_fixed_edit_plan() {
let Some(wasm_path) = build_fixture("t0_fixture") else {
return;
};
let repo_root = tempfile::tempdir().expect("tempdir");
let plan_fn = WasmT0::from_file(repo_root.path(), &wasm_path)
.expect("WasmT0::from_file")
.into_t0_fn();
let plan = block_on((plan_fn)());
let expected = EditPlan {
files: vec![FileEditPlan::Anchored {
path: PathBuf::from("src/a.rs"),
edits: vec![AnchoredEdit {
anchor: "1a2".to_string(),
line: 3,
new_text: Some("fixed by t0".to_string()),
}],
}],
};
assert_eq!(
plan, expected,
"guest-план мусить пережити roundtrip host ← guest bindgen! незмінним"
);
}