Files
sausage/host/tests/it/p1.rs
T

356 lines
13 KiB
Rust

//! 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<PathBuf> {
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("src/sausage/agents/models.json").exists()
|| std::fs::read_to_string(&stamp).ok().as_deref() != Some(key.as_str())
{
if stamp.exists() {
std::fs::remove_file(&stamp)?;
}
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::open_with_userland(dir.path(), Some(&bundle))?;
assert!(world.root.stat("/bin/sh.wasm", true).await?.size > 0);
let snapshot = dir.path().join("world/src/sausage");
for path in [
"agents/models.json",
"agents/ultimate.md",
"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<String> {
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"])?;
let revision = message.strip_prefix("Bundle source ").unwrap();
for path in ["agents", "skills", "extensions"] {
assert_eq!(
git(&snapshot, &["rev-parse", &format!("HEAD:{path}")])?,
git(&repo, &["rev-parse", &format!("{revision}:{path}")])?,
"bundled {path} must match the source commit"
);
}
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("Bundle 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::<Result<Vec<_>, _>>()?;
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<dyn sausage::exec::Exec>);
#[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<i32> {
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::open_with_userland(dir.path(), Some(&bundle))?;
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 model proxy.
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(())
}