Merge v3-t59: one host test binary (host/tests/it), fixtures without /tmp (T59)

This commit is contained in:
2026-11-08 02:31:45 +00:00
70 changed files with 404 additions and 631 deletions
+14
View File
@@ -56,6 +56,20 @@ nix develop -c cargo build --manifest-path userland/Cargo.toml --release
`nix flake check` still describes v2 and is updated later in P1.
## Tests
Host integration tests share one executable:
```sh
cargo test -p sausage --test it
cargo test -p sausage --test it -- chain::
```
Use the required Nix environment and build slot wrapper for these commands.
Add test modules under `host/tests/it/` and declare them in `it/main.rs`.
Use fully qualified child test filters with `--exact`.
See `host/tests/README.md` for fixture and isolation details.
Tests must stay fast:
- Only a few tests may wait for a timing (grace periods, deadlines).
+5
View File
@@ -3,6 +3,11 @@ name = "sausage"
version = "0.3.0"
edition = "2024"
description = "A world of WASI programs and durable agents."
autotests = false
[[test]]
name = "it"
path = "tests/it/main.rs"
[[bin]]
name = "sausage"
+37
View File
@@ -0,0 +1,37 @@
# Host integration tests
All host integration tests share the `it` executable. Each module keeps its former file name.
```sh
cargo test -p sausage --test it
cargo test -p sausage --test it -- chain::
cargo test -p sausage --test it -- --test-threads=1
```
Use `nix develop -c` and the required build slot wrapper for these commands.
Set `CARGO_TARGET_DIR` to your executor's target directory.
The journal timing benchmark keeps its 10% threshold but does not run in the default suite.
Run it by hand on an idle machine:
```sh
cargo test --release -p sausage --test it -- --ignored undo_cost
```
Add new modules under `host/tests/it/` and declare them in `it/main.rs`.
Cargo does not discover separate test executables in `host/tests/`.
Fixture sources remain in `builder-fixture/`.
The bundle-copy regression preserves relative symlink targets, including broken links.
`common.rs` shares the target lookup and the cached userland Git build.
The goal/questions and harness API fixtures copy the complete extension tree through its shared helper.
A shared mutex protects the cwd-init output and the brush checkout during fixture builds.
The cwd-init build runs at most once per process when its output is absent.
The first parallel run reproduced a collision when two compiler processes wrote `init.o`.
Tests use temporary worlds and ephemeral listener ports.
Child processes receive environment variables through `Command`, not through changes to the test process.
SIGKILL parents use fully qualified test filters with `--exact`.
The `hook::hook_git` and `hook_git` registrations both existed before this move.
Both remain as wrappers around one shared helper to preserve the test count.
The shared `hook_guest` module now builds its fixture once per process.
@@ -6,7 +6,7 @@ use std::{
process::Command,
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/automations-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/automations-test-bundle.js");
fn copy_tree(source: &Path, destination: &Path) -> Result<()> {
std::fs::create_dir_all(destination)?;
for entry in std::fs::read_dir(source)? {
@@ -24,14 +24,7 @@ fn setup(directory: &Path) -> Result<()> {
let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
if !userland.join("cwd-init/init.o").is_file() {
ensure!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success(),
"cwd build failed"
);
crate::common::cwd_init();
}
ensure!(
Command::new("cargo")
@@ -57,11 +50,7 @@ fn setup(directory: &Path) -> Result<()> {
let release = target.join("wasm32-wasip2/release");
if !release.join("brush.wasm").is_file() {
ensure!(
Command::new("nu")
.arg("brush/build.nu")
.current_dir(&userland)
.status()?
.success(),
crate::common::build_brush()?.success(),
"shell build failed"
);
}
@@ -251,7 +240,7 @@ async fn fire_after_sigkill() -> Result<()> {
let directory = tempfile::tempdir()?;
setup(directory.path())?;
let mut child = Command::new(std::env::current_exe()?)
.args(["--exact", "automations_child", "--nocapture"])
.args(["--exact", "automations::automations_child", "--nocapture"])
.env("SAUSAGE_AUTOMATIONS_CHILD", directory.path())
.spawn()?;
let deadline = Instant::now() + Duration::from_secs(60);
@@ -13,13 +13,7 @@ fn programs() -> PathBuf {
static PATH: OnceLock<PathBuf> = OnceLock::new();
PATH.get_or_init(|| {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
assert!(
std::process::Command::new("nu")
.arg(root.join("userland/cwd-init/build.nu"))
.status()
.unwrap()
.success()
);
crate::common::cwd_init();
assert!(
std::process::Command::new("cargo")
.args(["build", "--release", "-p", "world-cargo"])
@@ -28,13 +22,7 @@ fn programs() -> PathBuf {
.unwrap()
.success()
);
assert!(
std::process::Command::new("nu")
.arg(root.join("userland/brush/build.nu"))
.status()
.unwrap()
.success()
);
assert!(crate::common::build_brush().unwrap().success());
PathBuf::from(std::env::var_os("CARGO_TARGET_DIR").expect("executor target required"))
.join("wasm32-wasip2/release")
})
@@ -108,23 +96,23 @@ async fn cargo_build_test_and_source_changes() -> anyhow::Result<()> {
for (path, bytes) in [
(
"Cargo.toml",
include_bytes!("builder-fixture/Cargo.toml").as_slice(),
include_bytes!("../builder-fixture/Cargo.toml").as_slice(),
),
(
"build.rs",
include_bytes!("builder-fixture/build.rs").as_slice(),
include_bytes!("../builder-fixture/build.rs").as_slice(),
),
(
"src/main.rs",
include_bytes!("builder-fixture/src/main.rs").as_slice(),
include_bytes!("../builder-fixture/src/main.rs").as_slice(),
),
(
"tests/integration.rs",
include_bytes!("builder-fixture/tests/integration.rs").as_slice(),
include_bytes!("../builder-fixture/tests/integration.rs").as_slice(),
),
(
"src/add.c",
include_bytes!("builder-fixture/src/add.c").as_slice(),
include_bytes!("../builder-fixture/src/add.c").as_slice(),
),
] {
std::fs::write(source.join(path), bytes)?;
+4 -15
View File
@@ -16,14 +16,7 @@ fn components() -> Result<PathBuf> {
let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
if !userland.join("cwd-init/init.o").is_file() {
ensure!(
std::process::Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success(),
"cwd fixture build failed"
);
crate::common::cwd_init();
}
ensure!(
std::process::Command::new("cargo")
@@ -48,11 +41,7 @@ fn components() -> Result<PathBuf> {
let release = target.join("wasm32-wasip2/release");
if !release.join("brush.wasm").is_file() {
ensure!(
std::process::Command::new("nu")
.arg("brush/build.nu")
.current_dir(&userland)
.status()?
.success(),
crate::common::build_brush()?.success(),
"shell fixture build failed"
);
}
@@ -101,7 +90,7 @@ async fn ancestry_reaches_the_real_hook() -> Result<()> {
},
runtime.clone(),
cred.clone(),
include_str!("../js/chain-test-bundle.js"),
include_str!("../../js/chain-test-bundle.js"),
)
.await?;
let records: serde_json::Value =
@@ -155,7 +144,7 @@ async fn ancestry_reaches_the_real_hook() -> Result<()> {
});
let source = format!(
"globalThis.chainReady = true; globalThis.chainPort = {port};\n{}",
include_str!("../js/chain-test-bundle.js")
include_str!("../../js/chain-test-bundle.js")
);
let result = tokio::time::timeout(
Duration::from_secs(90),
+81
View File
@@ -0,0 +1,81 @@
//! Shared fixture copies and builds. A mutex protects cwd-init and the brush checkout.
use std::{
path::{Path, PathBuf},
process::{Command, ExitStatus},
sync::{Mutex, OnceLock},
};
pub static BUILD: Mutex<()> = Mutex::new(());
pub fn copy_tree(source: &Path, destination: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(destination)?;
for entry in std::fs::read_dir(source)? {
let entry = entry?;
let target = destination.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_tree(&entry.path(), &target)?;
} else {
std::fs::copy(entry.path(), target)?;
}
}
Ok(())
}
pub fn target_dir() -> PathBuf {
std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| {
Path::new(env!("CARGO_TARGET_TMPDIR"))
.parent()
.unwrap()
.to_owned()
})
}
pub fn cwd_init() {
static INIT: OnceLock<()> = OnceLock::new();
INIT.get_or_init(|| {
let _guard = BUILD.lock().unwrap();
let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
if !userland.join("cwd-init/init.o").is_file() {
assert!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(userland)
.status()
.unwrap()
.success()
);
}
});
}
pub fn build_brush() -> std::io::Result<ExitStatus> {
let _guard = BUILD.lock().unwrap();
Command::new("nu")
.args(["--no-config-file", "brush/build.nu"])
.env("CARGO_TARGET_DIR", target_dir())
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland"))
.status()
}
pub fn build_git() -> &'static Path {
static GIT: OnceLock<PathBuf> = OnceLock::new();
GIT.get_or_init(|| {
let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
let target = target_dir();
cwd_init();
assert!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(&target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()
.unwrap()
.success()
);
target
})
}
@@ -4,7 +4,7 @@ use sausage::{
world::World,
};
use std::{path::Path, sync::Arc};
const BUNDLE: &str = include_str!("../js/core-ext-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/core-ext-test-bundle.js");
struct Author(Arc<dyn Exec>);
#[async_trait::async_trait]
impl Exec for Author {
@@ -47,28 +47,7 @@ fn write(root: &Path, path: &str, text: &str) -> anyhow::Result<()> {
#[tokio::test(flavor = "current_thread")]
async fn core_extensions() -> anyhow::Result<()> {
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let userland = repo.join("userland");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
if !userland.join("cwd-init/init.o").exists() {
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
}
assert!(
std::process::Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
let target = crate::common::build_git();
let dir = tempfile::tempdir()?;
let world = World::open(dir.path())?;
let disk = dir.path().join("world");
+2 -25
View File
@@ -1,7 +1,7 @@
//! World-loaded dream built-ins and messages survive a harness reopen.
use sausage::{harness::run_module, world::World};
use std::{path::Path, process::Command};
const BUNDLE: &str = include_str!("../js/dream-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/dream-test-bundle.js");
fn git(repo: &Path, args: &[&str]) -> anyhow::Result<String> {
let output = Command::new("git")
.args(args)
@@ -36,30 +36,7 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
let disk = dir.join("world");
let repo = disk.join("src/sausage");
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
anyhow::ensure!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success(),
"cwd-init build failed"
);
}
anyhow::ensure!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success(),
"world git build failed"
);
let target = crate::common::build_git();
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
@@ -20,7 +20,7 @@ async fn restart_child() -> anyhow::Result<()> {
let dir = std::path::Path::new(&dir);
let source = format!(
"globalThis.expectedState = 'approved';\n{}",
include_str!("../js/durable-test-bundle.js")
include_str!("../../js/durable-test-bundle.js")
);
run_module(
World::open(dir)?,
@@ -59,7 +59,7 @@ fn sigkill_preserves_request_wait() -> anyhow::Result<()> {
cred.unlocked()?,
)?;
let mut child = Command::new(std::env::current_exe()?)
.args(["--exact", "restart_child", "--nocapture"])
.args(["--exact", "durable::restart_child", "--nocapture"])
.env("DURABLE_RESTART_DIR", &dir)
.spawn()?;
let deadline = Instant::now() + Duration::from_secs(30);
@@ -117,7 +117,7 @@ fn sigkill_preserves_request_wait() -> anyhow::Result<()> {
)?;
for _ in 0..2 {
let mut resumed = Command::new(std::env::current_exe()?)
.args(["--exact", "restart_child", "--nocapture"])
.args(["--exact", "durable::restart_child", "--nocapture"])
.env("DURABLE_RESTART_DIR", &dir)
.spawn()?;
let deadline = Instant::now() + Duration::from_secs(30);
@@ -147,7 +147,7 @@ async fn proxy_child() -> anyhow::Result<()> {
let dir = std::path::Path::new(&dir);
let source = format!(
"globalThis.proxyOnly = true; const proxy = Deno.core.ops.op_model_proxy(); if (proxy.base_url !== 'https://proxy.invalid' || proxy.api_key !== 'faux-private-token') throw new Error('Wrong proxy item');\n{}",
include_str!("../js/durable-test-bundle.js")
include_str!("../../js/durable-test-bundle.js")
);
run_module(
World::open(dir)?,
@@ -172,7 +172,7 @@ fn proxy_bootstrap_once() -> anyhow::Result<()> {
let dir = std::env::temp_dir().join(format!("sausage-durable-proxy-{}", std::process::id()));
for start in 0..2 {
let status = std::process::Command::new(std::env::current_exe()?)
.args(["--exact", "proxy_child", "--nocapture"])
.args(["--exact", "durable::proxy_child", "--nocapture"])
.env("DURABLE_PROXY_DIR", &dir)
.env(
"PI_PROXY_BASE_URL",
@@ -259,7 +259,7 @@ async fn decisions_wake_idle_conversation() -> anyhow::Result<()> {
)?;
let source = format!(
"globalThis.expectedState = {state:?};\n{}",
include_str!("../js/durable-test-bundle.js")
include_str!("../../js/durable-test-bundle.js")
);
let decide = async {
let conversation = loop {
@@ -5,7 +5,7 @@ use std::{
process::Command,
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/events-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/events-test-bundle.js");
struct Author(std::sync::Arc<dyn sausage::exec::Exec>);
#[async_trait::async_trait]
impl sausage::exec::Exec for Author {
@@ -51,7 +51,7 @@ async fn restart_child() -> anyhow::Result<()> {
fn sigkill_between_emit_and_reaction() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let mut child = Command::new(std::env::current_exe()?)
.args(["--exact", "restart_child", "--nocapture"])
.args(["--exact", "events::restart_child", "--nocapture"])
.env("EVENTS_RESTART_DIR", dir.path())
.env("EVENTS_MODE", "emit")
.spawn()?;
@@ -68,7 +68,7 @@ fn sigkill_between_emit_and_reaction() -> anyhow::Result<()> {
child.wait()?;
for _ in 0..2 {
let mut child = Command::new(std::env::current_exe()?)
.args(["--exact", "restart_child", "--nocapture"])
.args(["--exact", "events::restart_child", "--nocapture"])
.env("EVENTS_RESTART_DIR", dir.path())
.env("EVENTS_MODE", "consume")
.spawn()?;
@@ -90,28 +90,7 @@ fn sigkill_between_emit_and_reaction() -> anyhow::Result<()> {
}
#[tokio::test(flavor = "current_thread")]
async fn world_extensions() -> anyhow::Result<()> {
let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
if !userland.join("cwd-init/init.o").is_file() {
assert!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
}
assert!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
let target = crate::common::build_git();
let dir = tempfile::tempdir()?;
let world = World::open(dir.path())?;
let disk = dir.path().join("world");
@@ -4,7 +4,7 @@ use sausage::{
world::World,
};
use std::{path::Path, sync::Arc};
const BUNDLE: &str = include_str!("../js/extensions-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/extensions-test-bundle.js");
struct Author(Arc<dyn Exec>);
#[async_trait::async_trait]
impl Exec for Author {
@@ -30,13 +30,7 @@ async fn commits_and_restart() -> anyhow::Result<()> {
let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
if !userland.join("cwd-init/init.o").is_file() {
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
crate::common::cwd_init();
}
assert!(
std::process::Command::new("cargo")
@@ -6,31 +6,15 @@ use sausage::{
#[tokio::test(flavor = "current_thread")]
async fn default_identity_and_which() -> anyhow::Result<()> {
let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let userland = repo.join("userland");
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
let userland = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
let target = crate::common::build_git();
assert!(
std::process::Command::new("cargo")
.args([
"build",
"--release",
"-p",
"git",
"-p",
"sausage-exec-tools",
"--locked"
])
.args(["build", "--release", "-p", "sausage-exec-tools", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
let target = std::path::PathBuf::from(std::env::var("CARGO_TARGET_DIR")?);
let bundle = tempfile::tempdir()?;
std::fs::create_dir_all(bundle.path().join("bin"))?;
std::fs::create_dir_all(bundle.path().join("src"))?;
@@ -14,39 +14,8 @@ use std::{
fn guests() -> &'static Path {
static BUILT: OnceLock<PathBuf> = OnceLock::new();
BUILT.get_or_init(|| {
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let target = PathBuf::from(std::env::var("CARGO_TARGET_DIR").unwrap());
for script in ["userland/cwd-init/build.nu", "userland/brush/build.nu"] {
if script == "userland/cwd-init/build.nu"
&& repo.join("userland/cwd-init/init.o").is_file()
{
continue;
}
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", script])
.current_dir(repo)
.status()
.unwrap()
.success()
);
}
assert!(
std::process::Command::new("cargo")
.args([
"build",
"--release",
"--locked",
"--target",
"wasm32-wasip2",
"-p",
"git"
])
.current_dir(repo.join("userland"))
.status()
.unwrap()
.success()
);
let target = crate::common::build_git();
assert!(crate::common::build_brush().unwrap().success());
target.join("wasm32-wasip2/release")
})
}
@@ -1,7 +1,7 @@
//! Goal and question-card server cases execute through the world loader.
use sausage::{harness::run_module, world::World};
use std::{path::Path, process::Command};
const BUNDLE: &str = include_str!("../js/goal-questions-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/goal-questions-test-bundle.js");
static PREPARE: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn prepare(dir: &Path) -> anyhow::Result<()> {
let _guard = PREPARE.lock().unwrap();
@@ -11,71 +11,19 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
return Ok(());
}
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
assert!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
}
assert!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
let target = crate::common::build_git();
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
disk.join("bin/git.wasm"),
)?;
for directory in [
"extensions/subagents",
"extensions/messages",
"extensions/lineage",
"extensions/models",
"extensions/goal",
"extensions/questions",
"extensions/automations",
"extensions/core",
"agents",
] {
std::fs::create_dir_all(repo.join(directory))?;
}
for directory in [
"extensions/subagents",
"extensions/messages",
"extensions/lineage",
"extensions/models",
"extensions/goal",
"extensions/questions",
"extensions/automations",
] {
for entry in std::fs::read_dir(source.join(directory))? {
let entry = entry?;
if entry.path().extension().is_some_and(|ext| ext == "ts") {
std::fs::copy(entry.path(), repo.join(directory).join(entry.file_name()))?;
}
}
}
crate::common::copy_tree(&source.join("extensions"), &repo.join("extensions"))?;
std::fs::create_dir_all(repo.join("agents"))?;
std::fs::create_dir_all(repo.join("extensions/probe"))?;
std::fs::write(
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.testHost = host; globalThis.subagentService = host.need({name:'subagents'}); globalThis.messageService = host.need({name:'messages'}); globalThis.lineageService = host.need({name:'lineage'}); host.implement({name:'conversations'}, {summaryModel: async () => globalThis.summaryRef, concludedIn: async () => new Set(), generatedTitle: async () => {}, setStanding: async () => {}, setAutomaticSummaries: async () => {}}); return {name:'probe'}; }",
)?;
std::fs::copy(
source.join("extensions/core/agents.ts"),
repo.join("extensions/core/agents.ts"),
)?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
@@ -45,7 +45,7 @@ async fn crash_child() -> anyhow::Result<()> {
let source = format!(
"globalThis.testCrash = {};\n{}",
serde_json::to_string(&phase)?,
include_str!("../js/test-bundle.js")
include_str!("../../js/test-bundle.js")
);
tokio::time::timeout(
std::time::Duration::from_secs(20),
@@ -69,7 +69,7 @@ fn sigkill_resumes_model_and_tool() -> anyhow::Result<()> {
let dir =
std::env::temp_dir().join(format!("sausage-crash-{}-{phase}", std::process::id()));
let mut child = Command::new(std::env::current_exe()?)
.args(["--exact", "crash_child", "--nocapture"])
.args(["--exact", "harness::crash_child", "--nocapture"])
.env("HARNESS_CRASH_DIR", &dir)
.env("HARNESS_CRASH_PHASE", phase)
.stdout(Stdio::piped())
@@ -92,7 +92,7 @@ fn sigkill_resumes_model_and_tool() -> anyhow::Result<()> {
child.wait()?;
ready?;
let resumed = Command::new(std::env::current_exe()?)
.args(["--exact", "crash_child", "--nocapture"])
.args(["--exact", "harness::crash_child", "--nocapture"])
.env("HARNESS_CRASH_DIR", &dir)
.env("HARNESS_CRASH_PHASE", "resume")
.status()?;
@@ -110,7 +110,7 @@ async fn file_tools_and_exec() -> anyhow::Result<()> {
let result = run_module(
World::open(&dir)?,
exec.clone(),
include_str!("../js/test-bundle.js"),
include_str!("../../js/test-bundle.js"),
)
.await;
result?;
@@ -2,7 +2,7 @@
use sausage::{harness::run_module_with_input, world::World};
use std::sync::Arc;
use std::{path::Path, process::Command};
const BUNDLE: &str = include_str!("../js/harness-api-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/harness-api-test-bundle.js");
static PREPARE: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn prepare(dir: &Path) -> anyhow::Result<()> {
let _guard = PREPARE.lock().unwrap();
@@ -15,13 +15,7 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
assert!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
crate::common::cwd_init();
}
assert!(
Command::new("cargo")
@@ -71,44 +65,13 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
target.join("wasm32-wasip2/release/curl.wasm"),
disk.join("bin/curl.wasm"),
)?;
for directory in [
"extensions/subagents",
"extensions/messages",
"extensions/lineage",
"extensions/models",
"extensions/goal",
"extensions/questions",
"extensions/automations",
"extensions/core",
"agents",
] {
std::fs::create_dir_all(repo.join(directory))?;
}
for directory in [
"extensions/subagents",
"extensions/messages",
"extensions/lineage",
"extensions/models",
"extensions/goal",
"extensions/questions",
"extensions/automations",
] {
for entry in std::fs::read_dir(source.join(directory))? {
let entry = entry?;
if entry.path().extension().is_some_and(|ext| ext == "ts") {
std::fs::copy(entry.path(), repo.join(directory).join(entry.file_name()))?;
}
}
}
crate::common::copy_tree(&source.join("extensions"), &repo.join("extensions"))?;
std::fs::create_dir_all(repo.join("agents"))?;
std::fs::create_dir_all(repo.join("extensions/probe"))?;
std::fs::write(
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.testHost = host; globalThis.subagentService = host.need({name:'subagents'}); globalThis.messageService = host.need({name:'messages'}); globalThis.lineageService = host.need({name:'lineage'}); host.implement({name:'conversations'}, {summaryModel: async () => globalThis.summaryRef, concludedIn: async () => new Set(), generatedTitle: async () => {}, setStanding: async () => {}, setAutomaticSummaries: async () => {}}); return {name:'probe'}; }",
)?;
std::fs::copy(
source.join("extensions/core/agents.ts"),
repo.join("extensions/core/agents.ts"),
)?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
@@ -13,16 +13,16 @@ use std::{
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/foundation-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/foundation-test-bundle.js");
#[test]
fn bundle_has_patched_hooks() {
for bundle in [BUNDLE, include_str!("../js/bundle.js")] {
for bundle in [BUNDLE, include_str!("../../js/bundle.js")] {
assert!(bundle.contains("handlers.beforeInput"));
assert!(bundle.contains("input: true"));
assert!(bundle.contains("#held"));
}
assert!(!include_str!("../js/patches/pi-durable-1.5.0.patch").contains("dist/env/node"));
assert!(!include_str!("../../js/patches/pi-durable-1.5.0.patch").contains("dist/env/node"));
}
async fn probe(case: &str) -> anyhow::Result<()> {
@@ -87,7 +87,11 @@ async fn headless_child() -> anyhow::Result<()> {
#[test]
fn channel_with_closed_stdin() -> anyhow::Result<()> {
let mut child = Command::new(std::env::current_exe()?)
.args(["--exact", "headless_child", "--nocapture"])
.args([
"--exact",
"harness_foundation::headless_child",
"--nocapture",
])
.env("HEADLESS_CHILD", "1")
.stdin(Stdio::null())
.spawn()?;
@@ -33,7 +33,7 @@ async fn shutdown_child() -> anyhow::Result<()> {
receiver,
concat!(
"globalThis.foundationCase = 'shutdown';\n",
include_str!("../js/foundation-test-bundle.js")
include_str!("../../js/foundation-test-bundle.js")
),
)
.await?;
@@ -79,7 +79,7 @@ fn sigterm_cancels_tool_and_resumes() -> anyhow::Result<()> {
)?;
let spawn = || {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "shutdown_child", "--nocapture"])
.args(["--exact", "harness_shutdown::shutdown_child", "--nocapture"])
.env("SHUTDOWN_WORLD", dir.path())
.stdin(Stdio::null())
.spawn()
+8 -2
View File
@@ -1,6 +1,12 @@
//! Real guest HTTP requests pass through caller identity, credential injection, and streaming transport.
mod hook_git;
mod hook_guest;
mod hook_git {
//! Preserve the Git push registration that also ran inside the former hook executable.
#[tokio::test]
async fn several_mb_git_push() {
crate::hook_git::push().await;
}
}
use super::hook_guest;
use sausage::{
cred::{
self,
@@ -10,8 +10,8 @@ use std::{
io::{Read, Write},
sync::Arc,
};
#[path = "hook_guest.rs"]
mod hook_guest;
use super::hook_guest;
#[tokio::test]
async fn one_api_for_pages_and_guest_requests() -> anyhow::Result<()> {
@@ -4,12 +4,7 @@ use sausage::{
process::Runtime,
world::World,
};
use std::{
io::Write,
path::Path,
process::Stdio,
sync::{Arc, OnceLock},
};
use std::{io::Write, path::Path, process::Stdio, sync::Arc};
fn git(cwd: &Path, args: &[&str]) {
assert!(
std::process::Command::new("git")
@@ -22,42 +17,10 @@ fn git(cwd: &Path, args: &[&str]) {
}
#[tokio::test]
async fn several_mb_git_push() {
static BUILT: OnceLock<std::path::PathBuf> = OnceLock::new();
let component = BUILT.get_or_init(|| {
let target = std::env::var_os("CARGO_TARGET_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| {
Path::new(env!("CARGO_TARGET_TMPDIR"))
.parent()
.unwrap()
.to_owned()
});
let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
if !userland.join("cwd-init/init.o").is_file() {
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()
.unwrap()
.success()
);
}
let manifest = userland.join("Cargo.toml");
assert!(
std::process::Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(manifest)
.current_dir(&userland)
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(&target)
.args(["-j", "2", "--locked"])
.status()
.unwrap()
.success()
);
target.join("wasm32-wasip2/release/git.wasm")
});
push().await;
}
pub async fn push() {
let component = crate::common::build_git().join("wasm32-wasip2/release/git.wasm");
let temp = tempfile::tempdir().unwrap();
let world = World::open(temp.path()).unwrap();
let root = temp.path().join("world");
@@ -12,12 +12,12 @@ fn write(path: PathBuf, text: &str) -> std::io::Result<()> {
pub fn component() -> &'static Path {
static PATH: OnceLock<PathBuf> = OnceLock::new();
PATH.get_or_init(|| {
let target = std::env::var_os("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|| Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap().to_owned());
let target = crate::common::target_dir();
let directory = Path::new(env!("CARGO_TARGET_TMPDIR")).join("hook-guest");
std::fs::create_dir_all(directory.join("src")).unwrap();
std::fs::create_dir_all(directory.join("wit/deps")).unwrap();
write(directory.join("wit/process.wit"), include_str!("../wit/process.wit")).unwrap();
write(directory.join("wit/deps/io.wit"), include_str!("../wit/deps/io.wit")).unwrap();
write(directory.join("wit/process.wit"), include_str!("../../wit/process.wit")).unwrap();
write(directory.join("wit/deps/io.wit"), include_str!("../../wit/deps/io.wit")).unwrap();
write(directory.join("Cargo.toml"), "[package]\nname='hook-guest'\nversion='0.1.0'\nedition='2024'\n[workspace]\n[dependencies]\nwstd='=0.6.8'\nanyhow='1'\nfutures-lite='2'\nwit-bindgen='0.46'\n").unwrap();
write(directory.join("src/main.rs"), r#"
use wstd::http::{Body, BodyExt, Client, Request};
+65
View File
@@ -0,0 +1,65 @@
//! Host integration tests share one executable. Modules retain their original test registrations.
mod api;
mod api_auth;
mod api_listener;
mod automations;
mod builder;
mod chain;
mod common;
mod core_ext;
mod cred;
mod cred_accounting;
mod cred_authorization;
mod cred_challenges;
mod cred_core;
mod cred_edits;
mod cred_entry;
mod cred_grants;
mod cred_maintenance;
mod cred_oauth2;
mod cred_policy;
mod cred_presentation;
mod cred_proofs;
mod cred_quickjs;
mod cred_quickjs_security;
mod cred_regressions;
mod cred_replay;
mod cred_security;
mod cred_sign;
mod cred_storage;
mod cred_summary;
mod cred_webauthn;
mod dream;
mod durable;
mod events;
mod extension_names;
mod extensions;
mod git_compat;
mod git_runtime;
mod goal_questions;
mod harness;
mod harness_api;
mod harness_foundation;
mod harness_shutdown;
mod hook;
mod hook_api;
mod hook_git;
mod hook_guest;
mod init;
mod messages;
mod net;
mod p1;
mod p2;
mod p3;
mod p4;
mod plan_todos;
mod process;
mod proxy_models;
mod subagents;
mod undo;
mod undo_api;
mod undo_cost;
mod undo_exec;
mod undo_harness;
mod watchdog;
mod workspaces;
@@ -5,7 +5,7 @@ use std::{
process::Command,
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/messages-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/messages-test-bundle.js");
static PREPARE: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn prepare(dir: &Path) -> anyhow::Result<()> {
let _guard = PREPARE.lock().unwrap();
@@ -15,28 +15,7 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
return Ok(());
}
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
assert!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
}
assert!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
let target = crate::common::build_git();
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
@@ -148,7 +127,7 @@ fn sigkill_after_reply_placement() -> anyhow::Result<()> {
prepare(dir.path())?;
let spawn = |mode: &str| {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "restart_child", "--nocapture"])
.args(["--exact", "messages::restart_child", "--nocapture"])
.env("MESSAGES_DIR", dir.path())
.env("MESSAGES_MODE", mode)
.spawn()
+1 -1
View File
@@ -1,5 +1,5 @@
//! Real components can accept TCP connections. Host port registration and host-only config deny access.
mod hook_guest;
use super::hook_guest;
use sausage::{
exec::{Command, Exec, Options},
process::{Runtime, net::NetPolicy},
+2 -1
View File
@@ -8,6 +8,7 @@ async fn bundle_copy_and_real_shell() -> anyhow::Result<()> {
let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
let bundle = PathBuf::from(std::env::var("CARGO_TARGET_DIR").unwrap()).join("bundle");
if !bundle.join("bin/sh.wasm").exists() {
let _guard = crate::common::BUILD.lock().unwrap();
assert!(
std::process::Command::new("nu")
.arg(repo.join("userland/bundle.nu"))
@@ -132,7 +133,7 @@ async fn bundle_copy_and_real_shell() -> anyhow::Result<()> {
assert!(status.is_empty(), "{}", String::from_utf8_lossy(&status));
let source = format!(
"globalThis.testP1 = true;\n{}",
include_str!("../js/test-bundle.js")
include_str!("../../js/test-bundle.js")
);
run_module(world, exec.clone(), &source).await?;
assert_eq!(
+4 -9
View File
@@ -166,9 +166,9 @@ impl Browser {
}
fn components() -> Result<PathBuf> {
let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).join("p2-userland");
let target = crate::common::target_dir();
if !userland.join("cwd-init/init.o").is_file() {
command(&userland, &["nu", "--no-config-file", "cwd-init/build.nu"])?;
crate::common::cwd_init();
}
let status = Command::new("cargo")
.current_dir(&userland)
@@ -192,12 +192,7 @@ fn components() -> Result<PathBuf> {
let release = target.join("wasm32-wasip2/release");
if !release.join("sh.wasm").is_file() {
ensure!(
Command::new("nu")
.arg("brush/build.nu")
.env("CARGO_TARGET_DIR", &target)
.current_dir(&userland)
.status()?
.success(),
crate::common::build_brush()?.success(),
"shell build failed"
);
std::fs::copy(release.join("brush.wasm"), release.join("sh.wasm"))?;
@@ -413,7 +408,7 @@ async fn request_approve_fetch_and_ssh_push() -> Result<()> {
browser.enter(&ssh_item, &key).await?;
let source = format!(
"globalThis.p2Http = {http_port};\n{}",
include_str!("../js/p2-test-bundle.js")
include_str!("../../js/p2-test-bundle.js")
);
let approve = async {
for item in [&http_item, &ssh_item] {
+5 -45
View File
@@ -1,11 +1,12 @@
//! P3 exit scenario across worker, check, and recorded fast-forward SIGKILL boundaries.
use crate::common::copy_tree;
use sausage::{harness::run_module, world::World};
use std::{
path::Path,
process::Command,
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/p3-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/p3-test-bundle.js");
fn git(repo: &Path, args: &[&str]) -> anyhow::Result<String> {
let output = Command::new("git")
.args(args)
@@ -22,48 +23,12 @@ fn git(repo: &Path, args: &[&str]) -> anyhow::Result<String> {
);
Ok(String::from_utf8(output.stdout)?.trim().into())
}
fn copy_tree(source: &Path, destination: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(destination)?;
for entry in std::fs::read_dir(source)? {
let entry = entry?;
let target = destination.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_tree(&entry.path(), &target)?;
} else {
std::fs::copy(entry.path(), target)?;
}
}
Ok(())
}
fn prepare(dir: &Path, all_extensions: bool) -> anyhow::Result<()> {
World::open(dir)?;
let disk = dir.join("world");
let repo = disk.join("src/sausage");
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
anyhow::ensure!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success(),
"cwd-init build failed"
);
}
anyhow::ensure!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success(),
"world git build failed"
);
let target = crate::common::build_git();
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
@@ -71,12 +36,7 @@ fn prepare(dir: &Path, all_extensions: bool) -> anyhow::Result<()> {
)?;
if !target.join("wasm32-wasip2/release/brush.wasm").exists() {
anyhow::ensure!(
Command::new("nu")
.arg("brush/build.nu")
.env("CARGO_TARGET_DIR", target)
.current_dir(&userland)
.status()?
.success(),
crate::common::build_brush()?.success(),
"brush build failed"
);
}
@@ -185,7 +145,7 @@ fn recovery(point: &str) -> anyhow::Result<()> {
}
let spawn = |mode: &str| {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "restart_child", "--nocapture"])
.args(["--exact", "p3::restart_child", "--nocapture"])
.env("P3_DIR", dir.path())
.env("P3_MODE", mode)
.spawn()
+6 -3
View File
@@ -26,7 +26,7 @@ fn prepare(dir: &Path) -> anyhow::Result<PathBuf> {
ensure!(revision.status.success(), "fixture revision");
if fs::read(bundle.join("revision")).ok().as_deref() != Some(revision.stdout.as_slice()) {
if !userland.join("cwd-init/init.o").exists() {
checked(Command::new("nu").arg(userland.join("cwd-init/build.nu")))?;
crate::common::cwd_init();
}
checked(Command::new("cargo").current_dir(&userland).args([
"build",
@@ -43,7 +43,10 @@ fn prepare(dir: &Path) -> anyhow::Result<PathBuf> {
"-p",
"sausage-coreutils",
]))?;
checked(Command::new("nu").arg(userland.join("brush/build.nu")))?;
ensure!(
crate::common::build_brush()?.success(),
"shell build failed"
);
fs::create_dir_all(bundle.join("bin"))?;
fs::create_dir_all(bundle.join("src"))?;
for (binary, name) in [
@@ -68,7 +71,7 @@ fn prepare(dir: &Path) -> anyhow::Result<PathBuf> {
fs::create_dir_all(repo.join("extensions/p4"))?;
fs::write(
repo.join("extensions/p4/server.ts"),
include_str!("../js/p4-test-bundle.js"),
include_str!("../../js/p4-test-bundle.js"),
)?;
for args in [
vec!["init", "-b", "main"],
@@ -1,32 +1,11 @@
//! The plan and todos ports run through real world extension snapshots and survive restart.
use sausage::world::World;
use std::path::Path;
const BUNDLE: &str = include_str!("../js/plan-todos-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/plan-todos-test-bundle.js");
#[tokio::test(flavor = "current_thread")]
async fn plan_todos_and_restart() -> anyhow::Result<()> {
let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
if !userland.join("cwd-init/init.o").is_file() {
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
}
assert!(
std::process::Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
let target = crate::common::build_git();
let dir = tempfile::tempdir()?;
let disk = dir.path().join("world");
let _world = World::open(dir.path())?;
@@ -21,6 +21,12 @@ fn world_copies_programs_and_nested_source_only_once() -> anyhow::Result<()> {
b"ref: refs/heads/main\n",
)?;
std::fs::write(bundle.join("src/sausage/README.md"), b"source")?;
for (name, target) in [
("readme-link", "README.md"),
("broken-link", "missing.wasm"),
] {
std::os::unix::fs::symlink(target, bundle.join("src/sausage").join(name))?;
}
let path = temp.path().join("world");
let _world = World::open_with_userland(&path, Some(&bundle))?;
assert_eq!(
@@ -32,6 +38,15 @@ fn world_copies_programs_and_nested_source_only_once() -> anyhow::Result<()> {
b"source"
);
assert!(path.join("world/src/sausage/.git/HEAD").is_file());
for (name, target) in [
("readme-link", "README.md"),
("broken-link", "missing.wasm"),
] {
assert_eq!(
std::fs::read_link(path.join("world/src/sausage").join(name))?,
Path::new(target)
);
}
std::fs::write(path.join("world/src/sausage/README.md"), b"agent patch")?;
let _world = World::open_with_userland(&path, Some(Path::new("/missing-bundle")))?;
assert_eq!(
@@ -375,10 +390,7 @@ fn build() {
"guest build failed: {args:?}"
);
};
run(
&["nu", "--no-config-file", "userland/cwd-init/build.nu"],
&root,
);
crate::common::cwd_init();
run(
&[
"cargo",
@@ -394,10 +406,7 @@ fn build() {
],
&root.join("userland"),
);
run(
&["nu", "--no-config-file", "userland/brush/build.nu"],
&root,
);
assert!(crate::common::build_brush().unwrap().success());
run(
&[
"cargo",
@@ -560,7 +569,13 @@ async fn corpus_matches_bash() {
.args(["-c", &script])
.output()
.unwrap();
assert_eq!(cli.status.code(), result.status.code());
assert_eq!(
cli.status.code(),
result.status.code(),
"CLI stdout: {}\nCLI stderr: {}",
String::from_utf8_lossy(&cli.stdout),
String::from_utf8_lossy(&cli.stderr)
);
assert_eq!(
cli.stdout,
expected.as_bytes(),
@@ -98,7 +98,7 @@ async fn provider_transport_and_prices() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let source = format!(
"globalThis.testProxy = {proxy};\n{}",
include_str!("../js/proxy-models-test-bundle.js")
include_str!("../../js/proxy-models-test-bundle.js")
);
let result =
sausage::harness::run_module(World::open(dir.path())?, Arc::new(NoExec), &source).await;
@@ -5,7 +5,7 @@ use std::{
process::Command,
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/subagents-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/subagents-test-bundle.js");
fn prepare(dir: &Path) -> anyhow::Result<()> {
let disk = dir.join("world");
let repo = disk.join("src/sausage");
@@ -13,28 +13,7 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
return Ok(());
}
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
assert!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
}
assert!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
let target = crate::common::build_git();
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
@@ -131,7 +110,7 @@ fn sigkill_between_deliver_and_report() -> anyhow::Result<()> {
prepare(dir.path())?;
let spawn = |mode: &str| {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "restart_child", "--nocapture"])
.args(["--exact", "subagents::restart_child", "--nocapture"])
.env("SUBAGENTS_DIR", dir.path())
.env("SUBAGENTS_MODE", mode)
.spawn()
@@ -6,19 +6,14 @@ use sausage::{
};
use sausage_vfs::{Host, Mounts, Process};
use std::{path::PathBuf, sync::Arc, time::Instant};
// This timing benchmark has a 10% threshold and is flaky on a loaded machine.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "Run this timing benchmark by hand on an idle machine"]
async fn status_cost() {
let target = PathBuf::from(std::env::var("CARGO_TARGET_DIR").unwrap());
if !target.join("wasm32-wasip2/release/git.wasm").is_file() {
let userland = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../userland");
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()
.unwrap()
.success()
);
crate::common::cwd_init();
assert!(
std::process::Command::new("cargo")
.args(["build", "--release", "-p", "git"])
@@ -39,14 +39,7 @@ async fn tool_call_commit_and_independent_change() {
|| !guest_dir.join("touch.wasm").is_file()
|| !guest_dir.join("undo.wasm").is_file()
{
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../userland"))
.status()
.unwrap()
.success()
);
crate::common::cwd_init();
assert!(
std::process::Command::new("cargo")
.args([
@@ -42,14 +42,7 @@ async fn command(runtime: &Arc<Runtime>, args: &[&str]) -> String {
async fn tools_restore_one_call_or_the_whole_turn() {
let target = PathBuf::from(std::env::var("CARGO_TARGET_DIR").unwrap());
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "userland/cwd-init/build.nu"])
.current_dir(repo)
.status()
.unwrap()
.success()
);
crate::common::cwd_init();
assert!(
std::process::Command::new("cargo")
.args([
@@ -71,15 +64,7 @@ async fn tools_restore_one_call_or_the_whole_turn() {
.unwrap()
.success()
);
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "userland/brush/build.nu"])
.env("CARGO_TARGET_DIR", &target)
.current_dir(repo)
.status()
.unwrap()
.success()
);
assert!(crate::common::build_brush().unwrap().success());
for since in [false, true] {
let temp = tempfile::tempdir().unwrap();
let world = World::open(temp.path()).unwrap();
@@ -112,7 +97,7 @@ async fn tools_restore_one_call_or_the_whole_turn() {
sausage::harness::run_module(
world,
Arc::new(runtime.clone()),
include_str!("../js/undo-harness-test-bundle.js"),
include_str!("../../js/undo-harness-test-bundle.js"),
)
.await
.unwrap();
@@ -5,7 +5,7 @@ use std::{
process::Command,
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/watchdog-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/watchdog-test-bundle.js");
static PREPARE: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn prepare(dir: &Path) -> anyhow::Result<()> {
let _guard = PREPARE.lock().unwrap();
@@ -15,28 +15,7 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
return Ok(());
}
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
assert!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
}
assert!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
let target = crate::common::build_git();
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
@@ -150,7 +129,7 @@ fn sigkill_during_durable_wait() -> anyhow::Result<()> {
prepare(dir.path())?;
let spawn = |mode: &str| {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "restart_child", "--nocapture"])
.args(["--exact", "watchdog::restart_child", "--nocapture"])
.env("WATCHDOG_DIR", dir.path())
.env("WATCHDOG_MODE", mode)
.spawn()
@@ -7,7 +7,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/workspaces-test-bundle.js");
const BUNDLE: &str = include_str!("../../js/workspaces-test-bundle.js");
fn git(path: &Path, args: &[&str]) -> anyhow::Result<()> {
assert!(
Command::new("git")
@@ -29,37 +29,9 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
return Ok(());
}
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
assert!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
}
assert!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
let target = crate::common::build_git();
if !target.join("wasm32-wasip2/release/brush.wasm").exists() {
assert!(
Command::new("nu")
.arg("brush/build.nu")
.env("CARGO_TARGET_DIR", target)
.current_dir(&userland)
.status()?
.success()
);
assert!(crate::common::build_brush()?.success());
}
std::fs::create_dir_all(disk.join("bin"))?;
for (binary, name) in [("git", "git"), ("brush", "sh")] {
@@ -252,7 +224,7 @@ fn sigkill_between_fast_forward_and_push() -> anyhow::Result<()> {
let _server = serve(dir.path())?;
let spawn = |mode: &str| {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "restart_child", "--nocapture"])
.args(["--exact", "workspaces::restart_child", "--nocapture"])
.env("WORKSPACES_DIR", dir.path())
.env("WORKSPACES_MODE", mode)
.spawn()
+8 -10
View File
@@ -27,17 +27,15 @@ Directory descriptors retain their original paths after a rename.
Hard links, symlink creation, file preallocation, signals, and sockets return `NOTSUP`.
Preview1 does not expose the platform's WASI 0.2 sockets.
The integration test uses the existing S6 and lab guests:
- esbuild v0.27.7: `/tmp/s6-esbuild/esbuild.wasm` and `/tmp/s6-esbuild/esbuild-native`.
- uutils 0.12.0: `/tmp/wasi-userland-lab/uu/coreutils-0.12.0-wasm32-wasip1/coreutils.wasm`.
- QuickJS-ng v0.18.0: `/tmp/wasi-userland-lab/qjs-wasi.wasm`.
The QuickJS-ng input matches its recorded release SHA-256.
The S6 fixture also requires `/tmp/s6-esbuild/app/node_modules`.
The integration test needs no prepared files under `/tmp`.
It uses the bundled `userland/qjs/qjs.wasm` for file operations and JavaScript execution.
It builds native and WASI esbuild from the pinned, vendored source under `userland/esbuild/source`.
Go reuses its persistent build cache. The test stores the executables under `CARGO_TARGET_TMPDIR/preview1-fixtures`.
It installs the S6 app dependencies with `npm ci` and the committed lockfile.
It stores local Wasmtime compilation artifacts in a persistent test cache.
Run it from the repository root:
The development shell supplies Go, Node.js, and npm.
Run it from the repository root with your executor's target directory and build slot:
```sh
CARGO_TARGET_DIR=target/preview1 nix develop -c cargo test -p sausage-vfs --test preview1 -- --nocapture
CARGO_BUILD_JOBS=2 CARGO_TARGET_DIR=/path/to/shared/target/executor nu /path/to/.board/slot.nu nix develop -c cargo test -p sausage-vfs --test preview1 -- --nocapture
```
+53 -47
View File
@@ -14,8 +14,9 @@ fn engine() -> &'static Engine {
static E: OnceLock<Engine> = OnceLock::new();
E.get_or_init(Engine::default)
}
fn module(path: &str) -> Module {
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("required guest {path}: {e}"));
fn module(path: &Path) -> Module {
let bytes =
std::fs::read(path).unwrap_or_else(|e| panic!("required guest {}: {e}", path.display()));
let mut hash = std::collections::hash_map::DefaultHasher::new();
bytes.hash(&mut hash);
engine().precompile_compatibility_hash().hash(&mut hash);
@@ -230,51 +231,30 @@ async fn poll_preserves_input_and_returns_clock() {
#[tokio::test(flavor = "multi_thread")]
async fn userland_files() {
let guest = module("/tmp/wasi-userland-lab/uu/coreutils-0.12.0-wasm32-wasip1/coreutils.wasm");
let qjs = module(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland/qjs/qjs.wasm"));
let root = Arc::new(Memory::default());
run(&guest, root.clone(), "/", &["coreutils", "mkdir", "/work"]).await;
run(
&guest,
root.clone(),
"/work",
&["coreutils", "touch", "input"],
)
.await;
let input = root
.open(
"/work/input",
Open {
write: true,
..Open::default()
},
)
.await
.unwrap();
input.write(0, b"hello preview1\n".to_vec()).await.unwrap();
run(
&guest,
root.clone(),
"/work",
&["coreutils", "cp", "input", "copy"],
)
.await;
let (cat, _) = run(&guest, root.clone(), "/work", &["coreutils", "cat", "copy"]).await;
assert_eq!(cat, "hello preview1\n");
let (ls, _) = run(
&guest,
root.clone(),
"/work",
&["coreutils", "ls", "-la", "."],
)
.await;
assert!(ls.contains("copy") && ls.contains("input"), "{ls}");
run(&guest, root.clone(), "/work", &["coreutils", "rm", "copy"]).await;
let script = r#"
if (os.mkdir('/work') !== 0) throw Error('mkdir');
let f = std.open('/work/input', 'w'); f.puts('hello preview1\n'); f.close();
f = std.open('/work/input', 'r'); let text = f.readAsString(); f.close();
let out = std.open('/work/copy', 'w'); out.puts(text); out.close();
f = std.open('/work/copy', 'r'); console.log(f.readAsString().trim()); f.close();
let [entries, error] = os.readdir('/work');
if (error) throw Error('readdir');
console.log(entries.sort().join(','));
if (os.stat('/work/copy')[0].size !== 15) throw Error('stat');
if (os.remove('/work/copy') !== 0) throw Error('remove');
"#;
let (out, _) = run(&qjs, root.clone(), "/", &["qjs", "--std", "-e", script]).await;
assert!(
out.starts_with("hello preview1\n") && out.contains("copy") && out.contains("input"),
"{out}"
);
assert_eq!(
root.stat("/work/copy", true).await.unwrap_err().kind(),
std::io::ErrorKind::NotFound
);
println!("PASS uutils mkdir touch cp cat ls -la rm");
let qjs = module("/tmp/wasi-userland-lab/qjs-wasi.wasm");
println!("PASS QuickJS mkdir create copy read stat readdir remove");
let script = b"let f = std.open('input', 'r'); let text = f.readAsString(); f.close(); let out = std.open('js-output', 'w'); out.puts(text.toUpperCase()); out.close(); console.log('QuickJS files ok');";
let file = root
.open(
@@ -308,13 +288,39 @@ async fn esbuild_bundle() {
std::fs::copy(fixture.join(name), dir.path().join(name)).unwrap();
}
assert!(
std::process::Command::new("cp")
.args(["-r", "/tmp/s6-esbuild/app/node_modules"])
.arg(dir.path())
std::process::Command::new("npm")
.args(["ci", "--ignore-scripts", "--no-audit", "--no-fund"])
.current_dir(dir.path())
.status()
.unwrap()
.success()
);
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland/esbuild/source");
let programs = Path::new(env!("CARGO_TARGET_TMPDIR")).join("preview1-fixtures");
std::fs::create_dir_all(&programs).unwrap();
for (name, wasm) in [("esbuild-native", false), ("esbuild.wasm", true)] {
let mut build = std::process::Command::new("go");
build
.current_dir(&source)
.env("GOPROXY", "off")
.env("GOSUMDB", "off")
.env("GOMAXPROCS", "2")
.args([
"build",
"-p",
"2",
"-mod=vendor",
"-trimpath",
"-ldflags=-s -w",
"-o",
])
.arg(programs.join(name))
.arg("./cmd/esbuild");
if wasm {
build.env("GOOS", "wasip1").env("GOARCH", "wasm");
}
assert!(build.status().unwrap().success(), "build {name}");
}
let flags = [
"app.tsx",
"--bundle",
@@ -327,7 +333,7 @@ async fn esbuild_bundle() {
"--log-level=error",
];
assert!(
std::process::Command::new("/tmp/s6-esbuild/esbuild-native")
std::process::Command::new(programs.join("esbuild-native"))
.current_dir(dir.path())
.args(flags)
.arg("--outfile=native.js")
@@ -336,7 +342,7 @@ async fn esbuild_bundle() {
.success()
);
let root = Arc::new(Host::new(dir.path(), Process::default()).unwrap());
let guest = module("/tmp/s6-esbuild/esbuild.wasm");
let guest = module(&programs.join("esbuild.wasm"));
let mut args = vec!["esbuild"];
args.extend(flags);
args.push("--outfile=host.js");