//! Core world extensions execute through the snapshot loader and the actual VFS. use sausage::{ exec::{Command, Exec, Options, Stream}, world::World, }; use std::{path::Path, sync::Arc}; const BUNDLE: &str = include_str!("../../js/core-ext-test-bundle.js"); struct Author(Arc); #[async_trait::async_trait] impl Exec for Author { async fn exec( &self, command: Command, mut options: Options, output: &mut (dyn for<'a> FnMut(Stream, &'a [u8]) + Send), ) -> anyhow::Result { for (key, value) in [ ("GIT_AUTHOR_NAME", "Test"), ("GIT_AUTHOR_EMAIL", "test@example.com"), ("GIT_COMMITTER_NAME", "Test"), ("GIT_COMMITTER_EMAIL", "test@example.com"), ] { options.env.push((key.into(), value.into())); } self.0.exec(command, options, output).await } } fn copy_tree(from: &Path, to: &Path) -> anyhow::Result<()> { std::fs::create_dir_all(to)?; for entry in std::fs::read_dir(from)? { let entry = entry?; let dest = to.join(entry.file_name()); if entry.file_type()?.is_dir() { copy_tree(&entry.path(), &dest)?; } else { std::fs::copy(entry.path(), dest)?; } } Ok(()) } fn write(root: &Path, path: &str, text: &str) -> anyhow::Result<()> { let path = root.join(path); std::fs::create_dir_all(path.parent().unwrap())?; std::fs::write(path, text)?; Ok(()) } #[tokio::test(flavor = "current_thread")] async fn init_seeds_config_tools_and_plain_source() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; let bundle = dir.path().join("bundle"); write(&bundle, "bin/unused.wasm", "not a component")?; write(&bundle, "src/sausage/README.md", "World source")?; write( &bundle, "etc/sausage/config.yaml", include_str!("../../../config/config.yaml"), )?; write(&bundle, "srv/www/index.html", "World page")?; let destination = dir.path().join("new world"); let initialized = std::process::Command::new(env!("CARGO_BIN_EXE_sausage")) .arg("init") .arg(&destination) .arg("--userland") .arg(&bundle) .env("OPENAI_BASE_URL", "https://provider.test") .env("OPENAI_API_KEY", "fixture-key") .stdin(std::process::Stdio::null()) .output()?; assert!( initialized.status.success(), "{}", String::from_utf8_lossy(&initialized.stderr) ); assert!(initialized.stdout.is_empty()); let stderr = String::from_utf8(initialized.stderr)?; let lines: Vec<_> = stderr.lines().collect(); assert_eq!( lines, [ format!("Create world folder: {}", destination.display()), "Copy tools: 1".into(), "Create /src/sausage".into(), "Seed /etc and /srv/www".into(), format!( "Write config: {}", destination.join("world/etc/sausage/config.yaml").display() ), "Next steps:".into(), format!( " Edit {} to configure providers and API keys.", destination.join("world/etc/sausage/config.yaml").display() ), format!(" sausage run '{}'", destination.display()), ] ); assert!(!stderr.contains(['\r', '\x1b'])); assert_eq!( std::fs::read_to_string(destination.join("world/srv/www/index.html"))?, "World page" ); let world = World::open(&destination)?; assert!(destination.join("world/bin/unused").exists()); let config = world .root .open("/etc/sausage/config.yaml", Default::default()) .await?; let config = String::from_utf8(config.read(0, 65536).await?)?; assert!(!config.contains("https://provider.test")); assert!(!config.contains("fixture-key")); assert_eq!(config, include_str!("../../../config/config.yaml")); let source = destination.join("world/src/sausage"); assert!(!source.join(".git").exists()); assert_eq!( std::fs::read_to_string(source.join("README.md"))?, "World source" ); assert!( World::init(&destination, &bundle) .await .err() .unwrap() .to_string() .contains("already exists") ); Ok(()) } #[test] fn init_errors_name_the_failed_step() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; let bundle = dir.path().join("bundle"); for (name, expected) in [ ("missing", "Copy tools failed"), ("invalid", "Create /src/sausage failed"), ] { if name == "invalid" { write(&bundle, "bin/git.wasm", "not a component")?; write(&bundle, "src", "not a directory")?; } let output = std::process::Command::new(env!("CARGO_BIN_EXE_sausage")) .arg("init") .arg(dir.path().join(name)) .arg("--userland") .arg(&bundle) .output()?; assert!(!output.status.success()); assert!(output.stdout.is_empty()); let stderr = String::from_utf8(output.stderr)?; assert!(stderr.contains(expected), "{stderr}"); assert!(!stderr.contains("Next steps:"), "{stderr}"); assert!(!stderr.contains("Precompile"), "{stderr}"); } Ok(()) } #[test] fn run_does_not_print_init_progress() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; let output = std::process::Command::new(env!("CARGO_BIN_EXE_sausage")) .arg("run") .arg(dir.path()) .output()?; assert!(!output.status.success()); assert!(output.stdout.is_empty()); let stderr = String::from_utf8(output.stderr)?; assert!(stderr.starts_with("Error: No world at "), "{stderr}"); assert!(!stderr.contains("Create world folder")); assert!(!stderr.contains("Next steps:")); Ok(()) } #[tokio::test(flavor = "current_thread")] async fn core_extensions() -> anyhow::Result<()> { let repo = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); let target = crate::common::build_git(); let dir = tempfile::tempdir()?; let world = World::open(dir.path())?; let disk = dir.path().join("world"); std::fs::create_dir_all(disk.join("bin"))?; for binary in ["git", "tar"] { std::fs::copy( target.join(format!("wasm32-wasip2/release/{binary}.wasm")), disk.join(format!("bin/{binary}.wasm")), )?; } let source = disk.join("src/sausage"); for name in ["core", "models", "prompt", "conversations"] { copy_tree( &repo.join("extensions").join(name), &source.join("extensions").join(name), )?; } write( &disk, "etc/sausage/config.yaml", include_str!("../../../config/config.yaml"), )?; copy_tree( &repo.join("config/roster"), &disk.join("etc/sausage/roster"), )?; write( &source, "skills/builtin/SKILL.md", "---\nname: builtin-skill\ndescription: Builtin skill\n---\nInstructions", )?; write( &source, "extensions/fixture/server.ts", r#"import {defineExtension,defineTool} from '@earendil-works/pi-durable'; import {obj} from '@earendil-works/pi-schema'; import {State} from '../conversations/api.ts'; export const order=500; export default host=>{host.implement({name:'models.pin'},{pin:async()=>globalThis.modelPin});host.onOpen(async()=>{globalThis.core={selection:host.need({name:'models'}),conversations:host.need({name:'conversations'}),State,metadata:host.metadata}});return defineExtension({name:'fixture',tools:['read','write'].map(name=>defineTool({name,description:name,parameters:obj({}),execute:async()=>({output:[]})}))})};"#, )?; write(&disk, "AGENTS.md", "Outside instructions")?; write(&disk, "project/.git/HEAD", "ref: refs/heads/main")?; write(&disk, "project/AGENTS.md", "Root instructions")?; write(&disk, "project/sub/AGENTS.md", "Nested instructions")?; write( &disk, "project/.sausage/skills/project/SKILL.md", "---\nname: project-skill\ndescription: Project skill\n---\nInstructions", )?; assert!( std::process::Command::new("git") .args(["init", "-b", "main"]) .arg(&source) .status()? .success() ); assert!( std::process::Command::new("git") .args(["add", "."]) .current_dir(&source) .status()? .success() ); assert!( std::process::Command::new("git") .args([ "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--no-gpg-sign", "-m", "core fixtures" ]) .current_dir(&source) .status()? .success() ); let exec: Arc = Arc::new(Author(sausage::process::exec(World { root: world.root.clone(), host_dir: world.host_dir.clone(), processes: world.processes.clone(), })?)); sausage::harness::run_module(world, exec, BUNDLE).await }