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

276 lines
9.7 KiB
Rust

//! 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<dyn Exec>);
#[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<i32> {
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_one_source_commit() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let bundle = dir.path().join("bundle");
let target = crate::common::build_git();
std::fs::create_dir_all(bundle.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
bundle.join("bin/git.wasm"),
)?;
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: 2".into(),
"Create /src/sausage".into(),
"Seed /etc and /srv/www".into(),
format!(
"Write config: {}",
destination.join("world/etc/sausage/config.yaml").display()
),
"Create the first commit in /src/sausage".into(),
"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/git").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");
for (args, expected) in [
(vec!["symbolic-ref", "HEAD"], "refs/heads/main"),
(vec!["rev-list", "--count", "HEAD"], "1"),
] {
let result = std::process::Command::new("git")
.args(args)
.current_dir(&source)
.output()?;
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
assert_eq!(String::from_utf8(result.stdout)?.trim(), expected);
}
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 source repository failed"),
] {
if name == "invalid" {
write(&bundle, "bin/git.wasm", "not a component")?;
write(&bundle, "src/sausage/README.md", "Source")?;
}
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<dyn Exec> = 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
}