From 62283e7733dd3cd62ac07eb7e429a1ec2728c7b5 Mon Sep 17 00:00:00 2001 From: Surma Date: Sun, 8 Nov 2026 03:15:15 +0000 Subject: [PATCH] Attribute world commits to conversations and restore temporary directories --- extensions/workspaces/lander.ts | 2 +- host/src/process/mod.rs | 9 +++++++++ host/src/world.rs | 4 ++++ host/tests/it/git_runtime.rs | 31 +++++++++++++++++++++++++++++++ host/tests/it/process.rs | 18 ++++++++++++++++++ host/tests/it/workspaces.rs | 24 +++++++++++++++++++++++- 6 files changed, 86 insertions(+), 2 deletions(-) diff --git a/extensions/workspaces/lander.ts b/extensions/workspaces/lander.ts index e26e5c0..d4a650e 100644 --- a/extensions/workspaces/lander.ts +++ b/extensions/workspaces/lander.ts @@ -34,7 +34,7 @@ export function lander(host:any,workspaces:any) { const remote=host.settings().lander?.push if(remote)await run(path,'fetch',remote,entry.target) if(remote)await run(path,'merge','--no-edit','FETCH_HEAD') - await run(path,'merge','--no-ff','--no-edit','-m',`Merge from #${entry.conversationId} into ${entry.target}`,entry.tip) + await run(path,'merge','--no-ff','--no-edit','-m',`Merge from #${entry.conversationId} into ${entry.target}\n\nSausage-Conversation: ${entry.conversationId}`,entry.tip) const sha=await run(path,'rev-parse','HEAD') for(const configured of host.settings().lander?.checks??[]) { const check=typeof configured==='string'?{command:configured}:configured diff --git a/host/src/process/mod.rs b/host/src/process/mod.rs index 3181925..f3ebf15 100644 --- a/host/src/process/mod.rs +++ b/host/src/process/mod.rs @@ -349,6 +349,15 @@ impl Runtime { .env .push(("PATH".into(), "/usr/local/bin:/bin".into())); } + // Keep process attribution aligned with the nearest conversation, including children. + if let Some(conversation) = conversations.first() { + request + .env + .retain(|(name, _)| name != "SAUSAGE_CONVERSATION"); + request + .env + .push(("SAUSAGE_CONVERSATION".into(), conversation.clone())); + } request.cwd = sausage_vfs::resolve("/", &request.cwd); request.env.retain(|(name, _)| name != "PWD"); request.env.push(("PWD".into(), request.cwd.clone())); diff --git a/host/src/world.rs b/host/src/world.rs index f740b10..70cf941 100644 --- a/host/src/world.rs +++ b/host/src/world.rs @@ -42,6 +42,7 @@ impl World { } /// Copy an optional bundle into a new world. Existing worlds retain their programs. + /// Ensure `/tmp`, `/run`, and `/var/tmp` exist on every startup. /// Each `.wasm` tool keeps its independent file and gains a relative `/bin/` symlink. /// `/dev` exposes generated null, zero, and random byte devices in every world. /// Missing `/etc/gitconfig` defaults to Sausage . Local repository config overrides it. @@ -50,6 +51,9 @@ impl World { let host_dir = dir.join("host"); let fresh = !root_dir.exists(); std::fs::create_dir_all(root_dir.join("usr/local/bin"))?; + for path in ["tmp", "run", "var/tmp"] { + std::fs::create_dir_all(root_dir.join(path))?; + } if fresh && let Some(bundle) = userland { for folder in ["bin", "src"] { copy_tree(&bundle.join(folder), &root_dir.join(folder))?; diff --git a/host/tests/it/git_runtime.rs b/host/tests/it/git_runtime.rs index 17fae59..aea49a8 100644 --- a/host/tests/it/git_runtime.rs +++ b/host/tests/it/git_runtime.rs @@ -125,6 +125,37 @@ async fn run(fixture: &Fixture, script: &str) -> String { assert_eq!(code, 0, "{}", String::from_utf8_lossy(&output)); String::from_utf8(output).unwrap() } +#[tokio::test(flavor = "current_thread")] +async fn conversation_bash_commit_inherits_nearest_attribution() { + let fixture = fixture(); + let mut options = options(); + options.conversations = vec!["158".into(), "79".into()]; + options + .env + .push(("SAUSAGE_CONVERSATION".into(), "wrong".into())); + let mut output = Vec::new(); + let code = fixture + .runtime + .exec( + Command::Shell("sh -c 'git commit --allow-empty --no-gpg-sign -m attributed'".into()), + options, + &mut |_, bytes| output.extend_from_slice(bytes), + ) + .await + .unwrap(); + assert_eq!(code, 0, "{}", String::from_utf8_lossy(&output)); + let commit = std::process::Command::new("git") + .args(["log", "worker", "-1", "--format=%B"]) + .current_dir(fixture._temp.path().join("world/src/demo")) + .output() + .unwrap(); + assert!(commit.status.success()); + let text = String::from_utf8(commit.stdout).unwrap(); + assert!(text.contains("Sausage-Conversation: 158"), "{text}"); + assert!(!text.contains("Sausage-Conversation: 79"), "{text}"); + assert!(!text.contains("Sausage-Conversation: wrong"), "{text}"); +} + async fn edit(fixture: &Fixture) { let file = fixture .root diff --git a/host/tests/it/process.rs b/host/tests/it/process.rs index 23dac35..1c35792 100644 --- a/host/tests/it/process.rs +++ b/host/tests/it/process.rs @@ -57,6 +57,24 @@ fn world_copies_programs_and_nested_source_only_once() -> anyhow::Result<()> { Ok(()) } +#[test] +fn world_restores_temporary_directories_on_startup() -> anyhow::Result<()> { + let temp = tempfile::tempdir()?; + for restart in [false, true] { + let world = World::open(temp.path())?; + for path in ["tmp", "run", "var/tmp"] { + let directory = temp.path().join("world").join(path); + assert!( + directory.is_dir(), + "missing /{path} after restart={restart}" + ); + std::fs::remove_dir(&directory)?; + } + drop(world); + } + Ok(()) +} + mod features { //! Acceptance coverage for preview1, identity, inherited IO, services, HTTP, and bundle aliases. use super::*; diff --git a/host/tests/it/workspaces.rs b/host/tests/it/workspaces.rs index 4f7a801..bdf9a39 100644 --- a/host/tests/it/workspaces.rs +++ b/host/tests/it/workspaces.rs @@ -207,7 +207,29 @@ async fn coordination() -> anyhow::Result<()> { World::open(dir.path())?; prepare(dir.path())?; let _server = serve(dir.path())?; - run(dir.path(), "normal").await + run(dir.path(), "normal").await?; + let output = Command::new("git") + .args(["log", "-1", "--format=%s%n%B"]) + .current_dir(dir.path().join("world/project")) + .output()?; + assert!(output.status.success()); + let message = String::from_utf8(output.stdout)?; + let submitter = message + .lines() + .next() + .unwrap() + .strip_prefix("Merge from #") + .unwrap() + .split_whitespace() + .next() + .unwrap(); + assert!( + message + .lines() + .any(|line| line == format!("Sausage-Conversation: {submitter}")), + "{message}" + ); + Ok(()) } #[tokio::test(flavor = "current_thread")] async fn restart_child() -> anyhow::Result<()> {