mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
Add git checkpointing for Docker and Daytona execution environments
Git checkpoint commits (stage-level snapshots, diff.patch, GitCheckpoint events) previously only ran for Local execution. This extends support to Docker (bind-mount uses host git, same as Local) and Daytona (runs git commands remotely via exec_command). - Add GitCheckpointMode enum (Host/Remote) replacing RunConfig.work_dir - Extract git_checkpoint_host/git_diff_host helpers from inline code - Add git_checkpoint_remote/git_diff_remote using exec_command - Enable git_clean check for Docker alongside Local - Add setup_daytona_git to create run branch in remote sandbox - Switch Daytona wrap_bash_command from quote-escaping to base64 encoding (matches TypeScript/Python/Ruby Daytona SDKs, avoids nested quote issues) - Add e2e tests for both Host mode and Remote mode (Daytona, live-tested) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b218c6b280
commit
9ef45204f6
8 changed files with 619 additions and 189 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -139,6 +139,7 @@ dependencies = [
|
|||
"assert_cmd",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64",
|
||||
"chrono",
|
||||
"clap",
|
||||
"daytona-api-client",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ dirs = "6"
|
|||
dialoguer.workspace = true
|
||||
daytona-sdk.workspace = true
|
||||
daytona-api-client.workspace = true
|
||||
base64.workspace = true
|
||||
scopeguard = "1"
|
||||
git2.workspace = true
|
||||
tokio-util.workspace = true
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use chrono::{Local, Utc};
|
|||
use arc_util::terminal::Styles;
|
||||
|
||||
use crate::checkpoint::Checkpoint;
|
||||
use crate::engine::{PipelineEngine, RunConfig};
|
||||
use crate::engine::{GitCheckpointMode, PipelineEngine, RunConfig};
|
||||
use crate::event::EventEmitter;
|
||||
use crate::handler::default_registry;
|
||||
use crate::interviewer::auto_approve::AutoApproveInterviewer;
|
||||
|
|
@ -107,10 +107,11 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
args.execution_env.or(toml_exec).unwrap_or_default()
|
||||
};
|
||||
let original_cwd = std::env::current_dir()?;
|
||||
let git_clean = if execution_env_kind_preview == ExecutionEnvKind::Local {
|
||||
crate::git::ensure_clean(&original_cwd).is_ok()
|
||||
} else {
|
||||
false
|
||||
let git_clean = match execution_env_kind_preview {
|
||||
ExecutionEnvKind::Local | ExecutionEnvKind::Docker => {
|
||||
crate::git::ensure_clean(&original_cwd).is_ok()
|
||||
}
|
||||
ExecutionEnvKind::Daytona => false,
|
||||
};
|
||||
|
||||
// 3. Create logs directory
|
||||
|
|
@ -346,6 +347,22 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
}
|
||||
});
|
||||
|
||||
// Set up git inside Daytona sandbox (if applicable)
|
||||
let (daytona_run_id, daytona_base_sha, daytona_branch) = if execution_env_kind == ExecutionEnvKind::Daytona {
|
||||
match setup_daytona_git(&*execution_env).await {
|
||||
Ok((rid, base, branch)) => (Some(rid), Some(base), Some(branch)),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{yellow}Warning:{reset} Daytona git setup failed ({e}), running without git checkpoints.",
|
||||
yellow = styles.yellow, reset = styles.reset,
|
||||
);
|
||||
(None, None, None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
// Run setup commands inside the execution environment (once, not per-stage)
|
||||
if !setup_commands.is_empty() {
|
||||
emitter.emit(&crate::event::PipelineEvent::SetupStarted { command_count: setup_commands.len() });
|
||||
|
|
@ -480,15 +497,24 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
let engine = PipelineEngine::with_interviewer(registry, Arc::clone(&emitter), interviewer, Arc::clone(&execution_env));
|
||||
|
||||
// 7. Execute
|
||||
let run_id = worktree_run_id.unwrap_or_else(|| ulid::Ulid::new().to_string());
|
||||
let run_id = worktree_run_id
|
||||
.or(daytona_run_id)
|
||||
.unwrap_or_else(|| ulid::Ulid::new().to_string());
|
||||
let config = RunConfig {
|
||||
logs_root: logs_dir.clone(),
|
||||
cancel_token: None,
|
||||
dry_run: dry_run_mode,
|
||||
run_id,
|
||||
work_dir: worktree_work_dir,
|
||||
base_sha: worktree_base_sha,
|
||||
run_branch: worktree_branch,
|
||||
git_checkpoint: match execution_env_kind {
|
||||
ExecutionEnvKind::Local | ExecutionEnvKind::Docker => {
|
||||
worktree_work_dir.map(GitCheckpointMode::Host)
|
||||
}
|
||||
ExecutionEnvKind::Daytona => {
|
||||
daytona_base_sha.as_ref().map(|_| GitCheckpointMode::Remote)
|
||||
}
|
||||
},
|
||||
base_sha: worktree_base_sha.or(daytona_base_sha),
|
||||
run_branch: worktree_branch.or(daytona_branch),
|
||||
};
|
||||
|
||||
let run_start = Instant::now();
|
||||
|
|
@ -616,6 +642,33 @@ fn setup_worktree(
|
|||
Ok((run_id, worktree_path.clone(), worktree_path, branch_name, base_sha))
|
||||
}
|
||||
|
||||
/// Set up git inside a Daytona sandbox for checkpoint commits.
|
||||
/// Returns (run_id, base_sha, branch_name) on success.
|
||||
async fn setup_daytona_git(
|
||||
exec_env: &dyn arc_agent::ExecutionEnvironment,
|
||||
) -> anyhow::Result<(String, String, String)> {
|
||||
// Get current HEAD as base SHA
|
||||
let sha_result = exec_env.exec_command("git rev-parse HEAD", 10_000, None, None, None).await
|
||||
.map_err(|e| anyhow::anyhow!("git rev-parse HEAD failed: {e}"))?;
|
||||
if sha_result.exit_code != 0 {
|
||||
anyhow::bail!("git rev-parse HEAD failed (exit {}): {}", sha_result.exit_code, sha_result.stderr);
|
||||
}
|
||||
let base_sha = sha_result.stdout.trim().to_string();
|
||||
|
||||
let run_id = ulid::Ulid::new().to_string();
|
||||
let branch_name = format!("arc/run/{run_id}");
|
||||
|
||||
// Create and checkout a run branch
|
||||
let checkout_cmd = format!("git checkout -b {branch_name}");
|
||||
let checkout_result = exec_env.exec_command(&checkout_cmd, 10_000, None, None, None).await
|
||||
.map_err(|e| anyhow::anyhow!("git checkout failed: {e}"))?;
|
||||
if checkout_result.exit_code != 0 {
|
||||
anyhow::bail!("git checkout -b failed (exit {}): {}", checkout_result.exit_code, checkout_result.stderr);
|
||||
}
|
||||
|
||||
Ok((run_id, base_sha, branch_name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -667,9 +667,13 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
|||
///
|
||||
/// The Daytona API uses direct exec (not a shell), so pipes, env vars,
|
||||
/// semicolons, etc. won't work without this wrapper.
|
||||
///
|
||||
/// Uses base64 encoding (matching the TypeScript/Python/Ruby Daytona SDKs)
|
||||
/// to avoid shell escaping issues with quotes and special characters.
|
||||
fn wrap_bash_command(command: &str) -> String {
|
||||
// Shell-quote by replacing ' with '\'' then wrapping in single quotes.
|
||||
format!("bash -c '{}'", command.replace('\'', "'\\''"))
|
||||
use base64::Engine;
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(command);
|
||||
format!("sh -c \"echo '{encoded}' | base64 -d | sh\"")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -685,24 +689,29 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_bash_simple() {
|
||||
assert_eq!(wrap_bash_command("echo hello"), "bash -c 'echo hello'");
|
||||
fn wrap_bash_uses_base64_encoding() {
|
||||
let wrapped = wrap_bash_command("echo hello");
|
||||
// Should use base64 pipe to sh
|
||||
assert!(wrapped.starts_with("sh -c \"echo '"), "should start with sh -c wrapper");
|
||||
assert!(wrapped.ends_with("' | base64 -d | sh\""), "should end with base64 -d | sh");
|
||||
// The base64 of "echo hello" is "ZWNobyBoZWxsbw=="
|
||||
assert!(wrapped.contains("ZWNobyBoZWxsbw=="), "should contain base64 of 'echo hello'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_bash_with_pipe() {
|
||||
assert_eq!(
|
||||
wrap_bash_command("ls | grep foo"),
|
||||
"bash -c 'ls | grep foo'"
|
||||
);
|
||||
fn wrap_bash_handles_single_quotes_safely() {
|
||||
// Single quotes in the original command are safely encoded in base64
|
||||
let wrapped = wrap_bash_command("echo 'hello world'");
|
||||
assert!(wrapped.starts_with("sh -c \"echo '"), "should use sh -c wrapper");
|
||||
// No raw single quotes from the original command should appear in the base64
|
||||
assert!(!wrapped.contains("hello world"), "original command should be base64 encoded, not literal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_bash_escapes_single_quotes() {
|
||||
assert_eq!(
|
||||
wrap_bash_command("echo 'hello world'"),
|
||||
"bash -c 'echo '\\''hello world'\\'''"
|
||||
);
|
||||
fn wrap_bash_handles_pipes() {
|
||||
let wrapped = wrap_bash_command("ls | grep foo");
|
||||
assert!(wrapped.starts_with("sh -c \"echo '"), "should use sh -c wrapper");
|
||||
assert!(wrapped.ends_with("' | base64 -d | sh\""), "should end with base64 -d | sh");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -465,6 +465,70 @@ fn is_terminal(node: &Node) -> bool {
|
|||
|
||||
// --- Pipeline engine ---
|
||||
|
||||
/// How git checkpointing should be performed for a pipeline run.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GitCheckpointMode {
|
||||
/// Run git commands on the host filesystem (local & Docker bind-mount).
|
||||
Host(PathBuf),
|
||||
/// Run git commands inside the remote execution environment via `exec_command`.
|
||||
Remote,
|
||||
}
|
||||
|
||||
/// Run a git checkpoint commit on the host filesystem (local/Docker bind-mount).
|
||||
async fn git_checkpoint_host(work_dir: PathBuf, run_id: String, node_id: String, status: String) -> Option<String> {
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
crate::git::checkpoint_commit(&work_dir, &run_id, &node_id, &status)
|
||||
}).await {
|
||||
Ok(Ok(sha)) => Some(sha),
|
||||
Ok(Err(_)) | Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a git diff on the host filesystem.
|
||||
async fn git_diff_host(work_dir: PathBuf, base: String) -> Option<String> {
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
crate::git::diff_against(&work_dir, &base)
|
||||
}).await {
|
||||
Ok(Ok(patch)) => Some(patch),
|
||||
Ok(Err(_)) | Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a git checkpoint commit inside a remote execution environment.
|
||||
async fn git_checkpoint_remote(exec_env: &dyn ExecutionEnvironment, run_id: &str, node_id: &str, status: &str) -> Option<String> {
|
||||
// Stage everything
|
||||
let add_result = exec_env.exec_command("git add -A", 30_000, None, None, None).await;
|
||||
if add_result.as_ref().map_or(true, |r| r.exit_code != 0) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Commit with arc identity
|
||||
let message = format!("arc({run_id}): {node_id} ({status})");
|
||||
let commit_cmd = format!(
|
||||
"git -c user.name=arc -c user.email=arc@local commit --allow-empty -m '{message}'"
|
||||
);
|
||||
let commit_result = exec_env.exec_command(&commit_cmd, 30_000, None, None, None).await;
|
||||
if commit_result.as_ref().map_or(true, |r| r.exit_code != 0) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the new HEAD SHA
|
||||
let sha_result = exec_env.exec_command("git rev-parse HEAD", 10_000, None, None, None).await;
|
||||
match sha_result {
|
||||
Ok(r) if r.exit_code == 0 => Some(r.stdout.trim().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a git diff inside a remote execution environment.
|
||||
async fn git_diff_remote(exec_env: &dyn ExecutionEnvironment, base: &str) -> Option<String> {
|
||||
let cmd = format!("git diff {base} HEAD");
|
||||
match exec_env.exec_command(&cmd, 30_000, None, None, None).await {
|
||||
Ok(r) if r.exit_code == 0 => Some(r.stdout),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for a pipeline run.
|
||||
pub struct RunConfig {
|
||||
pub logs_root: PathBuf,
|
||||
|
|
@ -472,8 +536,8 @@ pub struct RunConfig {
|
|||
pub dry_run: bool,
|
||||
/// Unique identifier for this pipeline run.
|
||||
pub run_id: String,
|
||||
/// Git worktree path for checkpoint commits.
|
||||
pub work_dir: Option<PathBuf>,
|
||||
/// Git checkpoint mode (None = no checkpointing).
|
||||
pub git_checkpoint: Option<GitCheckpointMode>,
|
||||
/// SHA of the commit the worktree branched from.
|
||||
pub base_sha: Option<String>,
|
||||
/// Git branch name for the run (e.g. `arc/run/{run_id}`).
|
||||
|
|
@ -714,7 +778,10 @@ impl PipelineEngine {
|
|||
run_id: run_id.clone(),
|
||||
base_sha: config.base_sha.clone(),
|
||||
run_branch: config.run_branch.clone(),
|
||||
worktree_dir: config.work_dir.as_ref().map(|p| p.display().to_string()),
|
||||
worktree_dir: match config.git_checkpoint {
|
||||
Some(GitCheckpointMode::Host(ref p)) => Some(p.display().to_string()),
|
||||
_ => None,
|
||||
},
|
||||
});
|
||||
self.inform(&format!("Pipeline started: {}", graph.name), "pipeline");
|
||||
|
||||
|
|
@ -801,7 +868,7 @@ impl PipelineEngine {
|
|||
|
||||
// Store run_id and work_dir in context for handlers
|
||||
context.set("internal.run_id", serde_json::json!(run_id));
|
||||
if let Some(ref wd) = config.work_dir {
|
||||
if let Some(GitCheckpointMode::Host(ref wd)) = config.git_checkpoint {
|
||||
context.set("internal.work_dir", serde_json::json!(wd.to_string_lossy().as_ref()));
|
||||
}
|
||||
|
||||
|
|
@ -1029,58 +1096,57 @@ impl PipelineEngine {
|
|||
});
|
||||
}
|
||||
|
||||
// Step 6b: Git checkpoint commit (when running in a worktree)
|
||||
if let Some(ref work_dir) = config.work_dir {
|
||||
let wd = work_dir.clone();
|
||||
// Step 6b: Git checkpoint commit
|
||||
if let Some(ref mode) = config.git_checkpoint {
|
||||
let rid = run_id.clone();
|
||||
let nid = node.id.clone();
|
||||
let status_str = outcome.status.to_string();
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
crate::git::checkpoint_commit(&wd, &rid, &nid, &status_str)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(sha)) => {
|
||||
checkpoint.git_commit_sha = Some(sha.clone());
|
||||
if let Err(e) = checkpoint.save(&checkpoint_path) {
|
||||
context.append_log(format!("checkpoint re-save with SHA failed: {e}"));
|
||||
}
|
||||
self.services.emitter.emit(&PipelineEvent::GitCheckpoint {
|
||||
run_id: run_id.clone(),
|
||||
node_id: node.id.clone(),
|
||||
status: outcome.status.to_string(),
|
||||
git_commit_sha: sha.clone(),
|
||||
});
|
||||
|
||||
// Save diff.patch for this stage
|
||||
let prev = last_git_sha.as_deref()
|
||||
.or(config.base_sha.as_deref())
|
||||
.unwrap_or(&sha);
|
||||
let diff_base = prev.to_string();
|
||||
let diff_wd = work_dir.clone();
|
||||
let diff_dest = node_dir(&config.logs_root, &node.id, visit).join("diff.patch");
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
crate::git::diff_against(&diff_wd, &diff_base)
|
||||
}).await {
|
||||
Ok(Ok(patch)) => {
|
||||
let _ = std::fs::write(&diff_dest, patch);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
context.append_log(format!("git diff failed: {e}"));
|
||||
}
|
||||
Err(e) => {
|
||||
context.append_log(format!("git diff task panicked: {e}"));
|
||||
}
|
||||
}
|
||||
let commit_result = match mode {
|
||||
GitCheckpointMode::Host(work_dir) => {
|
||||
git_checkpoint_host(work_dir.clone(), rid, nid, status_str).await
|
||||
}
|
||||
GitCheckpointMode::Remote => {
|
||||
git_checkpoint_remote(&*self.services.execution_env, &run_id, &node.id, &outcome.status.to_string()).await
|
||||
}
|
||||
};
|
||||
|
||||
last_git_sha = Some(sha);
|
||||
if let Some(sha) = commit_result {
|
||||
checkpoint.git_commit_sha = Some(sha.clone());
|
||||
if let Err(e) = checkpoint.save(&checkpoint_path) {
|
||||
context.append_log(format!("checkpoint re-save with SHA failed: {e}"));
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
context.append_log(format!("git checkpoint commit failed: {e}"));
|
||||
}
|
||||
Err(e) => {
|
||||
context.append_log(format!("git checkpoint commit task panicked: {e}"));
|
||||
self.services.emitter.emit(&PipelineEvent::GitCheckpoint {
|
||||
run_id: run_id.clone(),
|
||||
node_id: node.id.clone(),
|
||||
status: outcome.status.to_string(),
|
||||
git_commit_sha: sha.clone(),
|
||||
});
|
||||
|
||||
// Save diff.patch for this stage
|
||||
let prev = last_git_sha.as_deref()
|
||||
.or(config.base_sha.as_deref())
|
||||
.unwrap_or(&sha);
|
||||
let diff_base = prev.to_string();
|
||||
let diff_dest = node_dir(&config.logs_root, &node.id, visit).join("diff.patch");
|
||||
|
||||
let diff_result = match mode {
|
||||
GitCheckpointMode::Host(work_dir) => {
|
||||
git_diff_host(work_dir.clone(), diff_base).await
|
||||
}
|
||||
GitCheckpointMode::Remote => {
|
||||
git_diff_remote(&*self.services.execution_env, &diff_base).await
|
||||
}
|
||||
};
|
||||
if let Some(patch) = diff_result {
|
||||
let _ = std::fs::write(&diff_dest, patch);
|
||||
} else {
|
||||
context.append_log("git diff failed".to_string());
|
||||
}
|
||||
|
||||
last_git_sha = Some(sha);
|
||||
} else {
|
||||
context.append_log("git checkpoint commit failed".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1816,7 +1882,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1834,7 +1900,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1861,7 +1927,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1883,7 +1949,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1901,7 +1967,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1932,7 +1998,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1989,7 +2055,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2064,7 +2130,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2091,7 +2157,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2115,7 +2181,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2262,7 +2328,7 @@ mod tests {
|
|||
let dir = tempfile::tempdir().unwrap();
|
||||
let g = simple_graph();
|
||||
let engine = PipelineEngine::new(make_registry(), Arc::new(EventEmitter::new()), local_env());
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
engine.run(&g, &config).await.unwrap();
|
||||
|
||||
let manifest_path = dir.path().join("manifest.json");
|
||||
|
|
@ -2284,7 +2350,7 @@ mod tests {
|
|||
g.edges.push(Edge::new("start", "exit"));
|
||||
|
||||
let engine = PipelineEngine::new(make_registry(), Arc::new(EventEmitter::new()), local_env());
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
engine.run(&g, &config).await.unwrap();
|
||||
|
||||
let manifest_path = dir.path().join("manifest.json");
|
||||
|
|
@ -2320,7 +2386,7 @@ mod tests {
|
|||
let mut registry = make_registry();
|
||||
registry.register("always_fail", Box::new(AlwaysFailHandler));
|
||||
let engine = PipelineEngine::new(registry, Arc::new(EventEmitter::new()), local_env());
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
let outcome = engine.run(&g, &config).await.unwrap();
|
||||
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
|
|
@ -2356,7 +2422,7 @@ mod tests {
|
|||
let mut registry = make_registry();
|
||||
registry.register("always_fail", Box::new(AlwaysFailHandler));
|
||||
let engine = PipelineEngine::new(registry, Arc::new(EventEmitter::new()), local_env());
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
let result = engine.run(&g, &config).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
|
@ -2395,7 +2461,7 @@ mod tests {
|
|||
let mut registry = make_registry();
|
||||
registry.register("slow", Box::new(SlowHandler { sleep_ms: 500 }));
|
||||
let engine = PipelineEngine::new(registry, Arc::new(EventEmitter::new()), local_env());
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
let result = engine.run(&g, &config).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
|
|
@ -2429,7 +2495,7 @@ mod tests {
|
|||
let mut registry = make_registry();
|
||||
registry.register("slow", Box::new(SlowHandler { sleep_ms: 10 }));
|
||||
let engine = PipelineEngine::new(registry, Arc::new(EventEmitter::new()), local_env());
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
let outcome = engine.run(&g, &config).await.unwrap();
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
}
|
||||
|
|
@ -2460,7 +2526,7 @@ mod tests {
|
|||
let mut registry = make_registry();
|
||||
registry.register("slow", Box::new(SlowHandler { sleep_ms: 500 }));
|
||||
let engine = PipelineEngine::new(registry, Arc::new(EventEmitter::new()), local_env());
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config = RunConfig { logs_root: dir.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
let outcome = engine.run(&g, &config).await.unwrap();
|
||||
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
|
|
@ -2515,7 +2581,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2546,7 +2612,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2575,7 +2641,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2596,7 +2662,7 @@ mod tests {
|
|||
cancel_token: Some(cancel_token),
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2616,7 +2682,7 @@ mod tests {
|
|||
cancel_token: Some(cancel_token),
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2648,7 +2714,7 @@ mod tests {
|
|||
cancel_token: Some(cancel_token),
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2724,7 +2790,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2747,7 +2813,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: true,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2774,7 +2840,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: true,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2862,7 +2928,7 @@ mod tests {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ async fn start_pipeline(
|
|||
tokio::spawn(async move {
|
||||
let logs_root = std::env::temp_dir().join(format!("arc-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&logs_root).expect("failed to create logs directory");
|
||||
let config = RunConfig { logs_root, cancel_token: Some(cancel_token), dry_run: state_clone.dry_run, run_id: run_id_clone.clone(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config = RunConfig { logs_root, cancel_token: Some(cancel_token), dry_run: state_clone.dry_run, run_id: run_id_clone.clone(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
|
||||
let result = tokio::select! {
|
||||
result = engine.run(&graph, &config) => result,
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ async fn daytona_pipeline_artifact_offload_and_sync() {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -317,6 +317,170 @@ async fn daytona_pipeline_artifact_offload_and_sync() {
|
|||
// CLI Backend on Daytona — real CLI tools via exec_command
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use arc_attractor::engine::GitCheckpointMode;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git checkpoint E2E on Daytona (Remote mode)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Handler that writes a file via exec_command so git has something to commit.
|
||||
struct FileWriterHandler;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Handler for FileWriterHandler {
|
||||
async fn execute(
|
||||
&self,
|
||||
node: &Node,
|
||||
_context: &Context,
|
||||
_graph: &Graph,
|
||||
_logs_root: &Path,
|
||||
services: &arc_attractor::handler::EngineServices,
|
||||
) -> Result<Outcome, AttractorError> {
|
||||
let content = format!("output from {}", node.id);
|
||||
let cmd = format!("echo '{content}' > {}.txt", node.id);
|
||||
let _ = services.execution_env.exec_command(&cmd, 10_000, None, None, None).await;
|
||||
Ok(Outcome::success())
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up git inside a Daytona sandbox for checkpoint commits.
|
||||
/// Returns (run_id, base_sha, branch_name) on success.
|
||||
async fn setup_daytona_git(
|
||||
exec_env: &dyn ExecutionEnvironment,
|
||||
) -> (String, String, String) {
|
||||
// Get current HEAD as base SHA
|
||||
let sha_result = exec_env.exec_command("git rev-parse HEAD", 10_000, None, None, None).await
|
||||
.expect("git rev-parse HEAD should succeed");
|
||||
assert_eq!(sha_result.exit_code, 0, "git rev-parse HEAD failed: {}", sha_result.stderr);
|
||||
let base_sha = sha_result.stdout.trim().to_string();
|
||||
|
||||
let run_id = ulid::Ulid::new().to_string();
|
||||
let branch_name = format!("arc/run/{run_id}");
|
||||
|
||||
let checkout_cmd = format!("git checkout -b {branch_name}");
|
||||
let checkout_result = exec_env.exec_command(&checkout_cmd, 10_000, None, None, None).await
|
||||
.expect("git checkout should succeed");
|
||||
assert_eq!(
|
||||
checkout_result.exit_code, 0,
|
||||
"git checkout -b failed (exit {}): stdout={} stderr={}",
|
||||
checkout_result.exit_code, checkout_result.stdout, checkout_result.stderr
|
||||
);
|
||||
|
||||
(run_id, base_sha, branch_name)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn daytona_git_checkpoint_remote_emits_events() {
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(env);
|
||||
|
||||
// Install git if not available (the default ubuntu:22.04 image may not have it)
|
||||
let git_check = env.exec_command("git --version", 10_000, None, None, None).await;
|
||||
if git_check.as_ref().map_or(true, |r| r.exit_code != 0) {
|
||||
let install = env.exec_command(
|
||||
"apt-get update -qq && apt-get install -y -qq git >/dev/null 2>&1",
|
||||
120_000, None, None, None,
|
||||
).await.expect("apt-get install git should not error");
|
||||
assert_eq!(install.exit_code, 0, "git install failed: {}", install.stderr);
|
||||
}
|
||||
|
||||
// Set up git in the sandbox
|
||||
let (run_id, base_sha, branch_name) = setup_daytona_git(&*env).await;
|
||||
|
||||
// Pipeline: start -> work -> exit
|
||||
let mut graph = Graph::new("DaytonaGitCheckpoint");
|
||||
graph.attrs.insert(
|
||||
"goal".to_string(),
|
||||
AttrValue::String("Test Remote git checkpoint".to_string()),
|
||||
);
|
||||
|
||||
let mut start = Node::new("start");
|
||||
start.attrs.insert("shape".to_string(), AttrValue::String("Mdiamond".to_string()));
|
||||
graph.nodes.insert("start".to_string(), start);
|
||||
|
||||
let mut exit = Node::new("exit");
|
||||
exit.attrs.insert("shape".to_string(), AttrValue::String("Msquare".to_string()));
|
||||
graph.nodes.insert("exit".to_string(), exit);
|
||||
|
||||
let mut work = Node::new("work");
|
||||
work.attrs.insert("label".to_string(), AttrValue::String("Work".to_string()));
|
||||
graph.nodes.insert("work".to_string(), work);
|
||||
|
||||
graph.edges.push(Edge::new("start", "work"));
|
||||
graph.edges.push(Edge::new("work", "exit"));
|
||||
|
||||
// Set up event collection
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut emitter = EventEmitter::new();
|
||||
let events = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
{
|
||||
let events_clone = Arc::clone(&events);
|
||||
emitter.on_event(move |event| {
|
||||
events_clone.lock().unwrap().push(event.clone());
|
||||
});
|
||||
}
|
||||
|
||||
let mut registry = HandlerRegistry::new(Box::new(FileWriterHandler));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
|
||||
let engine = PipelineEngine::new(registry, Arc::new(emitter), env.clone());
|
||||
let config = RunConfig {
|
||||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id,
|
||||
git_checkpoint: Some(GitCheckpointMode::Remote),
|
||||
base_sha: Some(base_sha),
|
||||
run_branch: Some(branch_name),
|
||||
};
|
||||
|
||||
let outcome = engine.run(&graph, &config).await.expect("pipeline should succeed");
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
|
||||
// Assert GitCheckpoint events were emitted
|
||||
let events = events.lock().unwrap();
|
||||
let git_events: Vec<_> = events
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let arc_attractor::event::PipelineEvent::GitCheckpoint { node_id, git_commit_sha, .. } = e {
|
||||
Some((node_id.clone(), git_commit_sha.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
git_events.len() >= 2,
|
||||
"expected at least 2 GitCheckpoint events, got {}",
|
||||
git_events.len()
|
||||
);
|
||||
assert!(
|
||||
git_events.iter().all(|(_, sha)| sha.len() == 40 && sha.chars().all(|c| c.is_ascii_hexdigit())),
|
||||
"all SHAs should be 40-char hex, got: {git_events:?}"
|
||||
);
|
||||
|
||||
// Assert diff.patch was written for the work node
|
||||
let work_diff = dir.path().join("nodes").join("work").join("diff.patch");
|
||||
assert!(work_diff.exists(), "diff.patch should exist for work node");
|
||||
|
||||
// Verify checkpoint.json has git_commit_sha
|
||||
let checkpoint = Checkpoint::load(&dir.path().join("checkpoint.json"))
|
||||
.expect("checkpoint should load");
|
||||
assert!(
|
||||
checkpoint.git_commit_sha.is_some(),
|
||||
"checkpoint should have git_commit_sha"
|
||||
);
|
||||
|
||||
env.cleanup().await.unwrap();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI Backend on Daytona — real CLI tools via exec_command
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use arc_attractor::cli::cli_backend::CliBackend;
|
||||
use arc_attractor::handler::codergen::{CodergenBackend, CodergenResult};
|
||||
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ async fn end_to_end_linear_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -309,7 +309,7 @@ async fn end_to_end_branching_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -418,7 +418,7 @@ async fn end_to_end_human_gate_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -521,7 +521,7 @@ async fn goal_gate_routes_to_retry_target_on_failure() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -633,7 +633,7 @@ async fn goal_gate_routes_to_retry_target_when_present() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -945,7 +945,7 @@ async fn retry_on_failure_then_succeed() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1011,7 +1011,7 @@ async fn pipeline_with_many_nodes() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1329,7 +1329,7 @@ async fn smoke_test_with_mock_codergen_backend() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1427,7 +1427,7 @@ async fn end_to_end_parallel_fan_out_fan_in() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1534,7 +1534,7 @@ async fn resume_from_checkpoint_completes_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1626,7 +1626,7 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1660,7 +1660,7 @@ async fn graph_goal_in_context() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1690,7 +1690,7 @@ async fn event_streaming_lifecycle() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1760,7 +1760,7 @@ async fn context_flow_between_stages() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1803,7 +1803,7 @@ async fn tool_handler_e2e() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1864,7 +1864,7 @@ async fn auto_approve_interviewer_e2e() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1891,7 +1891,7 @@ async fn codergen_without_backend_simulated() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -1992,7 +1992,7 @@ async fn branching_loop_back_on_failure() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2076,7 +2076,7 @@ async fn human_gate_loops_back() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2128,7 +2128,7 @@ async fn scenario_ship_a_feature() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2208,7 +2208,7 @@ async fn scenario_parallel_expert_review() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2282,7 +2282,7 @@ async fn scenario_node_retries_on_retry_status() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2340,7 +2340,7 @@ async fn scenario_loop_restart_resets_context() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2402,7 +2402,7 @@ async fn scenario_bug_triage_router() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2452,7 +2452,7 @@ async fn scenario_crash_recovery() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2534,7 +2534,7 @@ async fn manager_loop_stop_condition_satisfied_e2e() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2584,7 +2584,7 @@ async fn manager_loop_max_cycles_exceeded_e2e() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2714,7 +2714,7 @@ async fn conditional_branching_success_fail_paths() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2762,7 +2762,7 @@ async fn edge_selection_condition_match_wins_over_weight() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2805,7 +2805,7 @@ async fn edge_selection_weight_breaks_ties() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2840,7 +2840,7 @@ async fn edge_selection_lexical_tiebreak() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2892,7 +2892,7 @@ async fn context_updates_visible_across_nodes() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2926,7 +2926,7 @@ async fn stylesheet_applies_model_override() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -2976,7 +2976,7 @@ async fn custom_handler_registration_and_execution() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -3037,7 +3037,7 @@ async fn integration_smoke_plan_implement_review_done() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -3526,7 +3526,7 @@ async fn sub_pipeline_e2e_through_engine() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -3672,7 +3672,7 @@ async fn manager_loop_with_child_observer_e2e() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -3795,7 +3795,7 @@ async fn graph_merge_e2e_through_engine() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -3939,7 +3939,7 @@ async fn fidelity_default_is_compact() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -3982,7 +3982,7 @@ async fn fidelity_graph_default_applied() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4024,7 +4024,7 @@ async fn fidelity_node_overrides_graph_default() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4072,7 +4072,7 @@ async fn fidelity_edge_overrides_node_and_graph() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4110,7 +4110,7 @@ async fn fidelity_full_produces_empty_preamble() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4155,7 +4155,7 @@ async fn fidelity_truncate_preamble_minimal() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4216,7 +4216,7 @@ async fn fidelity_summary_low_mode() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4272,7 +4272,7 @@ async fn fidelity_summary_medium_mode() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4328,7 +4328,7 @@ async fn fidelity_summary_high_mode() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4377,7 +4377,7 @@ async fn fidelity_full_sets_thread_id_in_context() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4437,7 +4437,7 @@ async fn fidelity_full_nodes_share_thread_id() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4504,7 +4504,7 @@ async fn fidelity_resume_degrades_full_to_summary_high() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4587,7 +4587,7 @@ async fn fidelity_resume_degrade_only_affects_first_hop() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4657,7 +4657,7 @@ async fn fidelity_resume_no_degrade_when_not_full() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4693,7 +4693,7 @@ async fn fidelity_stored_in_checkpoint_context() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4768,7 +4768,7 @@ async fn fidelity_precedence_multi_node_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4825,7 +4825,7 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -4873,7 +4873,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() {
|
|||
registry_low.register("exit", Box::new(ExitHandler));
|
||||
registry_low.register("fidelity_capture", Box::new(FidelityCapturingHandler { captures: captures_low.clone() }));
|
||||
let engine_low = PipelineEngine::new(registry_low, Arc::new(EventEmitter::new()), local_env());
|
||||
let config_low = RunConfig { logs_root: dir_low.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config_low = RunConfig { logs_root: dir_low.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
engine_low.run(&graph_low, &config_low).await.expect("run low");
|
||||
|
||||
{
|
||||
|
|
@ -4907,7 +4907,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() {
|
|||
registry_med.register("exit", Box::new(ExitHandler));
|
||||
registry_med.register("fidelity_capture", Box::new(FidelityCapturingHandler { captures: captures_med.clone() }));
|
||||
let engine_med = PipelineEngine::new(registry_med, Arc::new(EventEmitter::new()), local_env());
|
||||
let config_med = RunConfig { logs_root: dir_med.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), work_dir: None, base_sha: None, run_branch: None };
|
||||
let config_med = RunConfig { logs_root: dir_med.path().to_path_buf(), cancel_token: None, dry_run: false, run_id: "test-run".into(), git_checkpoint: None, base_sha: None, run_branch: None };
|
||||
engine_med.run(&graph_med, &config_med).await.expect("run med");
|
||||
|
||||
let preambles_med = captures_med.preambles.lock().unwrap();
|
||||
|
|
@ -4961,7 +4961,7 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5004,7 +5004,7 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5050,7 +5050,7 @@ async fn fidelity_edge_thread_id_override_in_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5097,7 +5097,7 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5151,7 +5151,7 @@ async fn fidelity_from_parsed_dot_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5193,7 +5193,7 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5252,7 +5252,7 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5325,7 +5325,7 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5514,7 +5514,7 @@ mod real_llm {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5622,7 +5622,7 @@ mod real_llm {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5760,7 +5760,7 @@ mod real_llm {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5864,7 +5864,7 @@ mod real_llm {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -5948,7 +5948,7 @@ async fn human_gate_freeform_only_routes_text() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6067,7 +6067,7 @@ async fn human_gate_freeform_with_fixed_choice_match() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6171,7 +6171,7 @@ async fn human_gate_freeform_fallback_on_unmatched_text() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6288,7 +6288,7 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6381,7 +6381,7 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6623,7 +6623,7 @@ async fn tool_hooks_pre_success_allows_pipeline_to_proceed() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6662,7 +6662,7 @@ async fn tool_hooks_pre_failure_skips_tool_call() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6704,7 +6704,7 @@ async fn tool_hooks_post_success_does_not_affect_outcome() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6738,7 +6738,7 @@ async fn tool_hooks_post_failure_does_not_block_pipeline() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6774,7 +6774,7 @@ async fn tool_hooks_graph_level_applies_to_all_nodes() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6813,7 +6813,7 @@ async fn tool_hooks_node_level_overrides_graph_level() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6862,7 +6862,7 @@ async fn tool_hooks_pre_receives_node_id_env_var() {
|
|||
logs_root: dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -6958,7 +6958,7 @@ async fn attractor_e2e_with_real_llm() {
|
|||
logs_root: logs_dir.path().to_path_buf(),
|
||||
cancel_token: None, dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -7065,7 +7065,7 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -7208,7 +7208,7 @@ async fn large_context_values_are_offloaded_to_artifact_store() {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -7383,7 +7383,7 @@ async fn artifact_pointers_rewritten_for_remote_execution_env() {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -7497,7 +7497,7 @@ async fn node_dir_uses_visit_count_on_revisit() {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -8063,7 +8063,7 @@ async fn full_pipeline_with_cli_backend_node() {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -8151,7 +8151,7 @@ async fn stylesheet_backend_property_routes_to_cli() {
|
|||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-run".into(),
|
||||
work_dir: None,
|
||||
git_checkpoint: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
};
|
||||
|
|
@ -8265,4 +8265,140 @@ fn parse_real_gemini_json() {
|
|||
assert_eq!(response.text, "4");
|
||||
assert_eq!(response.input_tokens, 123);
|
||||
assert_eq!(response.output_tokens, 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git checkpoint e2e — Host mode (Docker / Local)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use arc_attractor::engine::GitCheckpointMode;
|
||||
|
||||
/// End-to-end test: pipeline with `GitCheckpointMode::Host` emits `GitCheckpoint`
|
||||
/// events with valid commit SHAs and writes `diff.patch` per stage.
|
||||
#[tokio::test]
|
||||
async fn git_checkpoint_host_emits_events_and_diff_patch() {
|
||||
// 1. Create a temporary git repo with an initial commit
|
||||
let repo = tempfile::tempdir().unwrap();
|
||||
std::process::Command::new("git")
|
||||
.args(["init"])
|
||||
.current_dir(repo.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
std::process::Command::new("git")
|
||||
.args(["-c", "user.name=test", "-c", "user.email=test@test",
|
||||
"commit", "--allow-empty", "-m", "init"])
|
||||
.current_dir(repo.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// 2. Create a branch and worktree (like cli/run.rs setup_worktree)
|
||||
let base_sha = {
|
||||
let out = std::process::Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.current_dir(repo.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
String::from_utf8_lossy(&out.stdout).trim().to_string()
|
||||
};
|
||||
std::process::Command::new("git")
|
||||
.args(["branch", "arc/run/test-docker", "HEAD"])
|
||||
.current_dir(repo.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
let worktree_path = repo.path().join("worktree");
|
||||
std::process::Command::new("git")
|
||||
.args(["worktree", "add"])
|
||||
.arg(&worktree_path)
|
||||
.arg("arc/run/test-docker")
|
||||
.current_dir(repo.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Write a file in the worktree so there's something to commit
|
||||
std::fs::write(worktree_path.join("hello.txt"), "from docker test").unwrap();
|
||||
|
||||
// 3. Build a simple pipeline: start -> work -> exit
|
||||
let mut graph = Graph::new("DockerGitCheckpoint");
|
||||
graph.attrs.insert("goal".to_string(), AttrValue::String("Test Host git checkpoint".to_string()));
|
||||
let mut start = Node::new("start");
|
||||
start.attrs.insert("shape".to_string(), AttrValue::String("Mdiamond".to_string()));
|
||||
graph.nodes.insert("start".to_string(), start);
|
||||
let mut exit = Node::new("exit");
|
||||
exit.attrs.insert("shape".to_string(), AttrValue::String("Msquare".to_string()));
|
||||
graph.nodes.insert("exit".to_string(), exit);
|
||||
let mut work = Node::new("work");
|
||||
work.attrs.insert("label".to_string(), AttrValue::String("Work".to_string()));
|
||||
graph.nodes.insert("work".to_string(), work);
|
||||
graph.edges.push(Edge::new("start", "work"));
|
||||
graph.edges.push(Edge::new("work", "exit"));
|
||||
|
||||
// 4. Set up event collection and engine
|
||||
let logs_dir = tempfile::tempdir().unwrap();
|
||||
let mut emitter = EventEmitter::new();
|
||||
let events = collect_events(&mut emitter);
|
||||
|
||||
let env: Arc<dyn arc_agent::ExecutionEnvironment> = Arc::new(
|
||||
arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()),
|
||||
);
|
||||
let mut registry = HandlerRegistry::new(Box::new(ContextSetterHandler));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
let engine = PipelineEngine::new(registry, Arc::new(emitter), env);
|
||||
|
||||
let config = RunConfig {
|
||||
logs_root: logs_dir.path().to_path_buf(),
|
||||
cancel_token: None,
|
||||
dry_run: false,
|
||||
run_id: "test-docker".into(),
|
||||
git_checkpoint: Some(GitCheckpointMode::Host(worktree_path.clone())),
|
||||
base_sha: Some(base_sha.clone()),
|
||||
run_branch: Some("arc/run/test-docker".to_string()),
|
||||
};
|
||||
|
||||
// 5. Run pipeline
|
||||
let outcome = engine.run(&graph, &config).await.expect("pipeline should succeed");
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
|
||||
// 6. Assert GitCheckpoint events were emitted
|
||||
let events = events.lock().unwrap();
|
||||
let git_events: Vec<_> = events
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let PipelineEvent::GitCheckpoint { node_id, git_commit_sha, .. } = e {
|
||||
Some((node_id.clone(), git_commit_sha.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// start, work, exit = 3 nodes, each gets a checkpoint commit
|
||||
assert!(
|
||||
git_events.len() >= 2,
|
||||
"expected at least 2 GitCheckpoint events, got {}",
|
||||
git_events.len()
|
||||
);
|
||||
// Each SHA should be a valid 40-char hex string
|
||||
assert!(
|
||||
git_events.iter().all(|(_, sha)| sha.len() == 40 && sha.chars().all(|c| c.is_ascii_hexdigit())),
|
||||
"all SHAs should be 40-char hex, got: {git_events:?}"
|
||||
);
|
||||
|
||||
// 7. Assert diff.patch was written for the "work" node
|
||||
let work_diff = logs_dir.path().join("nodes").join("work").join("diff.patch");
|
||||
assert!(work_diff.exists(), "diff.patch should exist for work node");
|
||||
|
||||
// 8. Verify checkpoint.json has git_commit_sha
|
||||
let checkpoint = Checkpoint::load(&logs_dir.path().join("checkpoint.json"))
|
||||
.expect("checkpoint should load");
|
||||
assert!(
|
||||
checkpoint.git_commit_sha.is_some(),
|
||||
"checkpoint should have git_commit_sha"
|
||||
);
|
||||
|
||||
// Cleanup worktree
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["worktree", "remove", "--force"])
|
||||
.arg(&worktree_path)
|
||||
.current_dir(repo.path())
|
||||
.output();
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue