196 lines
6.8 KiB
Rust
196 lines
6.8 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, "src/sausage/README.md", "World source")?;
|
|
write(
|
|
&bundle,
|
|
"etc/sausage/config.yaml",
|
|
include_str!("../../../config/config.yaml"),
|
|
)?;
|
|
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("PI_PROXY_BASE_URL", "https://proxy.test")
|
|
.env("PI_PROXY_API_KEY", "fixture-key")
|
|
.stdin(std::process::Stdio::null())
|
|
.output()?;
|
|
assert!(
|
|
initialized.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&initialized.stderr)
|
|
);
|
|
let world = World::open(&destination)?;
|
|
assert!(destination.join("world/bin/git").exists());
|
|
let config = sausage::harness::config::Config::read(&world.root).await?;
|
|
assert_eq!(config.proxy.base_url, "https://proxy.test");
|
|
assert_eq!(config.proxy.api_key, "fixture-key");
|
|
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(())
|
|
}
|
|
|
|
#[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"),
|
|
)?;
|
|
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
|
|
}
|