//! P1 integration: bundle contents, source commits, extension loading, and real shell calls. use sausage::{harness::run_module, world::World}; use sausage_vfs::Open; use std::path::PathBuf; fn bundle() -> anyhow::Result { if let Some(bundle) = std::env::var_os("SAUSAGE_USERLAND") { return Ok(PathBuf::from(bundle)); } let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".."); let bundle = crate::common::target_dir().join("bundle"); let _guard = crate::common::BUILD.lock().unwrap(); let key = crate::common::bundle_key(&repo)?; let stamp = bundle.join(".source-key"); if !bundle.join("bin/sh.wasm").exists() || !bundle.join("etc/sausage/config.yaml").exists() || std::fs::read_to_string(&stamp).ok().as_deref() != Some(key.as_str()) { if bundle.exists() { std::fs::remove_dir_all(&bundle)?; } assert!( std::process::Command::new("nu") .arg(repo.join("userland/bundle.nu")) .arg(&bundle) .status()? .success() ); std::fs::write(stamp, key)?; } Ok(bundle) } #[tokio::test(flavor = "current_thread")] async fn bundle_copy_and_real_shell() -> anyhow::Result<()> { let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".."); let bundle = bundle()?; let dir = tempfile::tempdir()?; let world = World::init(dir.path(), &bundle).await?; assert!(world.root.stat("/bin/sh.wasm", true).await?.size > 0); let snapshot = dir.path().join("world/src/sausage"); for path in [ "config/config.yaml", "skills/README.md", "extensions/README.md", ] { assert!(snapshot.join(path).is_file(), "missing bundled {path}"); } let git = |path: &std::path::Path, args: &[&str]| -> anyhow::Result { let output = std::process::Command::new("git") .arg("-C") .arg(path) .args(args) .output()?; anyhow::ensure!( output.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&output.stderr) ); Ok(String::from_utf8(output.stdout)?.trim().to_owned()) }; let message = git(&snapshot, &["log", "-1", "--format=%s"])?; assert_eq!(message, "Initial world source"); for path in ["config", "skills", "extensions"] { let difference = std::process::Command::new("git") .args(["diff", "--no-index", "--exit-code", "--"]) .arg(repo.join(path)) .arg(snapshot.join(path)) .output()?; assert!( difference.status.success(), "bundled {path} must match the source files: {}{}", String::from_utf8_lossy(&difference.stdout), String::from_utf8_lossy(&difference.stderr), ); } use std::os::unix::fs::MetadataExt; for entry in std::fs::read_dir(dir.path().join("world/bin"))? { let entry = entry?; if entry.file_type()?.is_symlink() { assert_eq!( std::fs::read_link(entry.path())?, PathBuf::from(format!("{}.wasm", entry.file_name().to_string_lossy())) ); } else { assert_eq!(entry.path().extension().unwrap(), "wasm"); assert_eq!(entry.metadata()?.nlink(), 1); } } assert!(world.root.stat("/usr/local/bin", true).await?.directory); let writable = world .root .open( "/bin/forbidden", Open { write: true, create: true, ..Default::default() }, ) .await; assert!(writable.is_ok()); let exec = sausage::process::exec(World { root: world.root.clone(), host_dir: world.host_dir.clone(), processes: world.processes.clone(), })?; use sausage::exec::{Command, Options, Stream}; for (script, expected) in [ ("command -v ls", "/bin/ls\n"), ("ls", "bin"), ("git --version", "git version"), ("printf 'a\\n' | xargs echo", "a\n"), ("env FOO=1 printenv FOO", "1\n"), ] { let mut out = Vec::new(); let code = exec .exec( Command::Shell(script.into()), Options::default(), &mut |stream, bytes| { if matches!(stream, Stream::Stdout) { out.extend_from_slice(bytes); } }, ) .await?; assert_eq!(code, 0, "{script}"); assert!(String::from_utf8(out)?.contains(expected), "{script}"); } for (script, expected) in [ ( "echo x > /dev/null && cat < /dev/null && ls /bin/git && printf 'a\\nb\\n' | xargs -I{} echo [{}]", "/bin/git\n[a]\n[b]\n", ), ("test -x /bin/git && echo executable", "executable\n"), ("head -c 4 /dev/zero | wc -c", "4\n"), ("head -c 4 /dev/urandom | wc -c", "4\n"), ("echo x > /dev/null 2>&1; cat < /dev/null", ""), ] { let mut out = Vec::new(); let code = exec .exec( Command::Shell(script.into()), Options::default(), &mut |stream, bytes| { if matches!(stream, Stream::Stdout) { out.extend_from_slice(bytes); } }, ) .await?; assert_eq!(code, 0, "{script}"); assert_eq!(out, expected.as_bytes(), "{script}"); } for utility in [ "tail -n 5", "head -n 2", "wc -l", "sort", "uniq", "cut -c 1", ] { for script in [ format!("printf 'g\\nb\\nb\\na\\nf\\ne\\nd\\n' | {utility}"), format!("printf '' | {utility}"), format!("printf 'g\\nb\\n' | {utility} | head -n 0"), ] { let native = std::process::Command::new("bash") .args(["-c", &script]) .output()?; assert!(native.status.success(), "{script}: {:?}", native.stderr); let mut out = Vec::new(); let mut err = Vec::new(); let code = exec .exec( Command::Shell(script.clone()), Options { timeout: Some(std::time::Duration::from_secs(15)), ..Options::default() }, &mut |stream, bytes| match stream { Stream::Stdout => out.extend_from_slice(bytes), Stream::Stderr => err.extend_from_slice(bytes), }, ) .await?; assert_eq!(code, 0, "{script}: {}", String::from_utf8_lossy(&err)); assert_eq!(out, native.stdout, "{script}"); } } let mut history = Vec::new(); assert_eq!( exec.exec( Command::Shell("cd /src/sausage && git log --oneline".into()), Options::default(), &mut |stream, bytes| { if matches!(stream, Stream::Stdout) { history.extend_from_slice(bytes); } }, ) .await?, 0, ); let history = String::from_utf8(history)?; assert_eq!(history.lines().count(), 1); assert!(history.contains("Initial world source")); let mut status = Vec::new(); assert_eq!( exec.exec( Command::Shell("cd /src/sausage && git status --short".into()), Options::default(), &mut |stream, bytes| { if matches!(stream, Stream::Stdout) { status.extend_from_slice(bytes); } }, ) .await?, 0, ); assert!(status.is_empty(), "{}", String::from_utf8_lossy(&status)); let source = format!( "globalThis.testP1 = true;\n{}", include_str!("../../js/test-bundle.js") ); run_module(world, exec.clone(), &source).await?; assert_eq!( std::fs::read_to_string(dir.path().join("world/test.txt"))?, "after\n" ); assert_eq!( std::fs::read_to_string(dir.path().join("world/parallel/a"))?, "a" ); assert_eq!( std::fs::read_to_string(dir.path().join("world/parallel/b"))?, "b" ); let spills = std::fs::read_dir(dir.path().join("world/tmp"))?.collect::, _>>()?; assert_eq!(spills.len(), 1); let actual = std::fs::read_to_string(spills[0].path())?; let expected: String = (1..=10000).map(|n| format!("{n}\n")).collect(); assert_eq!(actual, expected); let source_cat = std::fs::read(bundle.join("bin/cat.wasm"))?; let head = std::fs::read(dir.path().join("world/bin/head.wasm"))?; let mut out = Vec::new(); assert_eq!( exec.exec( Command::Shell( "cp /bin/true.wasm /bin/cat.wasm; cat /test.txt; head -n 1 /test.txt".into() ), Options::default(), &mut |stream, bytes| { if matches!(stream, Stream::Stdout) { out.extend_from_slice(bytes); } } ) .await?, 0 ); assert_eq!(out, b"after\n"); assert_eq!(std::fs::read(bundle.join("bin/cat.wasm"))?, source_cat); assert_eq!(std::fs::read(dir.path().join("world/bin/head.wasm"))?, head); assert_eq!( exec.exec( Command::Shell("cp /bin/true.wasm /usr/local/bin/head.wasm; head /test.txt".into()), Options::default(), &mut |_, bytes| assert!(bytes.is_empty()) ) .await?, 0 ); let changed = std::fs::read(dir.path().join("world/bin/cat.wasm"))?; let reopened = World::open_with_userland( dir.path(), Some(std::path::Path::new("/nonexistent-bundle")), )?; assert_eq!( reopened .root .open("/bin/cat.wasm", Open::default()) .await? .read(0, changed.len()) .await?, changed ); // An unbundled world remains usable by process tests and file-only callers. let unbundled = tempfile::tempdir()?; let world = World::open(unbundled.path())?; assert!(world.root.stat("/bin/sh", true).await.is_err()); Ok(()) } /// Preserve command timings in captured test output when the loaded-machine regression fails. struct LoaderTiming(std::sync::Arc); #[async_trait::async_trait] impl sausage::exec::Exec for LoaderTiming { async fn exec( &self, command: sausage::exec::Command, options: sausage::exec::Options, output: &mut (dyn for<'a> FnMut(sausage::exec::Stream, &'a [u8]) + Send), ) -> anyhow::Result { let label = format!("{command:?}"); let budget = options.timeout; let started = std::time::Instant::now(); let result = self.0.exec(command, options, output).await; eprintln!( "Loader {label}: deadline {budget:?}, elapsed {:?}, result {result:?}", started.elapsed() ); result } } #[tokio::test(flavor = "current_thread")] async fn bundled_extensions_load() -> anyhow::Result<()> { let bundle = bundle()?; let dir = tempfile::tempdir()?; let world = World::init(dir.path(), &bundle).await?; let root = world.root.clone(); let exec = sausage::process::exec(World { root: world.root.clone(), host_dir: world.host_dir.clone(), processes: world.processes.clone(), })?; // Use the compiled production loader without the console entrypoint or provider bootstrap. let (library, _) = include_str!("../../js/bundle.js") .split_once("// main.ts") .expect("console bundle entrypoint"); let source = format!( r#"{library} const bundleModels = createModels(); const bundleRegistry = createRegistry(); bundleRegistry.install(CodingTools2); bundleRegistry.install(CredentialRequests); const bundleStorage = await openStorage(globalThis.storagePath); const bundleHarness = await Harness.open(bundleStorage, {{ models: bundleModels, registry: bundleRegistry, settings: {{ get extensions() {{ return bundleRegistry.snapshot().installed(); }} }} }}, BACKGROUND_CONTEXT); await bundleHarness.root(BACKGROUND_CONTEXT); const bundleLoader = await loadExtensions(bundleHarness, bundleRegistry, bundleStorage, bundleModels, BACKGROUND_CONTEXT, [CodingTools2, CredentialRequests]); if (bundleLoader.state().error) throw new Error(bundleLoader.state().error); const snapshotEntries=await fs2.list(`/run/sausage/extensions/${{bundleLoader.state().good}}`); if(JSON.stringify(snapshotEntries.map(entry=>entry.name)) !== '["extensions"]') throw new Error(JSON.stringify(snapshotEntries)); await bundleLoader.close(); await bundleHarness.close(BACKGROUND_CONTEXT); "# ); run_module(world, std::sync::Arc::new(LoaderTiming(exec)), &source).await?; let status = root.open("/proc/extensions", Open::default()).await?; let status: serde_json::Value = serde_json::from_slice(&status.read(0, 65536).await?)?; let commit = std::fs::read_to_string(dir.path().join("world/src/sausage/.git/refs/heads/main"))?; assert_eq!(status["good"].as_str(), Some(commit.trim()), "{status}"); assert!(status["error"].is_null(), "{status}"); Ok(()) }