mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-08 22:21:45 +00:00
Deduplicate timeline resolution and test helpers in operations
Move resolve_target into RunTimeline::resolve() method and extract shared test helpers (temp_repo, test_sig, make_checkpoint_json) into a test_support module used by both fork.rs and rewind.rs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
46737b7728
commit
127e0211cc
4 changed files with 93 additions and 162 deletions
|
|
@ -7,7 +7,7 @@ use crate::git::MetadataStore;
|
|||
use crate::records::RunRecord;
|
||||
use crate::records::StartRecord;
|
||||
|
||||
use super::rewind::{build_timeline, RewindTarget, RunTimeline, TimelineEntry};
|
||||
use super::rewind::{build_timeline, RewindTarget, TimelineEntry};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ForkRunInput {
|
||||
|
|
@ -22,7 +22,7 @@ pub struct ForkRunInput {
|
|||
pub fn fork(store: &Store, input: ForkRunInput) -> Result<String> {
|
||||
let timeline = build_timeline(store, &input.source_run_id)?;
|
||||
let entry = match input.target.as_ref() {
|
||||
Some(target) => resolve_timeline_entry(&timeline, target)?,
|
||||
Some(target) => timeline.resolve(target)?,
|
||||
None => timeline.entries.last().ok_or_else(|| {
|
||||
anyhow::anyhow!("no checkpoints found for run {}", input.source_run_id)
|
||||
})?,
|
||||
|
|
@ -30,59 +30,6 @@ pub fn fork(store: &Store, input: ForkRunInput) -> Result<String> {
|
|||
fork_from_entry(store, &input.source_run_id, entry, input.push)
|
||||
}
|
||||
|
||||
fn resolve_timeline_entry<'a>(
|
||||
timeline: &'a RunTimeline,
|
||||
target: &RewindTarget,
|
||||
) -> Result<&'a TimelineEntry> {
|
||||
match target {
|
||||
RewindTarget::Ordinal(n) => timeline
|
||||
.entries
|
||||
.iter()
|
||||
.find(|e| e.ordinal == *n)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"ordinal @{n} out of range (max @{})",
|
||||
timeline.entries.len()
|
||||
)
|
||||
}),
|
||||
RewindTarget::LatestVisit(name) => {
|
||||
let effective_name = timeline.parallel_map.get(name).unwrap_or(name);
|
||||
timeline
|
||||
.entries
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|e| e.node_name == *effective_name)
|
||||
.ok_or_else(|| {
|
||||
if effective_name != name {
|
||||
anyhow::anyhow!(
|
||||
"node '{name}' is inside parallel '{effective_name}'; \
|
||||
no checkpoint found for '{effective_name}'"
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!("no checkpoint found for node '{name}'")
|
||||
}
|
||||
})
|
||||
}
|
||||
RewindTarget::SpecificVisit(name, visit) => {
|
||||
let effective_name = timeline.parallel_map.get(name).unwrap_or(name);
|
||||
timeline
|
||||
.entries
|
||||
.iter()
|
||||
.find(|e| e.node_name == *effective_name && e.visit == *visit)
|
||||
.ok_or_else(|| {
|
||||
if effective_name != name {
|
||||
anyhow::anyhow!(
|
||||
"node '{name}' is inside parallel '{effective_name}'; \
|
||||
no visit {visit} found for '{effective_name}'"
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!("no visit {visit} found for node '{name}'")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fork_from_entry(
|
||||
store: &Store,
|
||||
source_run_id: &str,
|
||||
|
|
@ -217,40 +164,14 @@ fn fork_from_entry(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::super::test_support::*;
|
||||
use super::*;
|
||||
use git2::Repository;
|
||||
use git2::Oid;
|
||||
|
||||
use crate::operations::find_run_id_by_prefix;
|
||||
|
||||
fn temp_repo() -> (tempfile::TempDir, Store) {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let repo = Repository::init(dir.path()).unwrap();
|
||||
(dir, Store::new(repo))
|
||||
}
|
||||
|
||||
fn test_sig() -> Signature<'static> {
|
||||
Signature::now("Test", "test@example.com").unwrap()
|
||||
}
|
||||
|
||||
fn make_checkpoint_json(current_node: &str, visit: usize, git_sha: Option<&str>) -> Vec<u8> {
|
||||
let mut node_visits = HashMap::new();
|
||||
node_visits.insert(current_node.to_string(), visit);
|
||||
let cp = serde_json::json!({
|
||||
"timestamp": "2025-01-01T00:00:00Z",
|
||||
"current_node": current_node,
|
||||
"completed_nodes": [current_node],
|
||||
"node_retries": {},
|
||||
"context_values": {},
|
||||
"logs": [],
|
||||
"node_visits": node_visits,
|
||||
"git_commit_sha": git_sha,
|
||||
});
|
||||
serde_json::to_vec(&cp).unwrap()
|
||||
}
|
||||
|
||||
fn make_run_record_json(run_id: &str) -> Vec<u8> {
|
||||
let record = serde_json::json!({
|
||||
"run_id": run_id,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ mod resume;
|
|||
mod rewind;
|
||||
mod source;
|
||||
mod start;
|
||||
#[cfg(test)]
|
||||
mod test_support;
|
||||
mod validate;
|
||||
|
||||
pub use crate::pipeline::{DevcontainerSpec, LlmSpec, SandboxEnvSpec, SandboxSpec};
|
||||
|
|
|
|||
|
|
@ -61,6 +61,54 @@ pub struct RunTimeline {
|
|||
pub parallel_map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl RunTimeline {
|
||||
pub fn resolve(&self, target: &RewindTarget) -> Result<&TimelineEntry> {
|
||||
match target {
|
||||
RewindTarget::Ordinal(n) => {
|
||||
self.entries
|
||||
.iter()
|
||||
.find(|e| e.ordinal == *n)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("ordinal @{n} out of range (max @{})", self.entries.len())
|
||||
})
|
||||
}
|
||||
RewindTarget::LatestVisit(name) => {
|
||||
let effective_name = self.parallel_map.get(name).unwrap_or(name);
|
||||
self.entries
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|e| e.node_name == *effective_name)
|
||||
.ok_or_else(|| {
|
||||
if effective_name != name {
|
||||
anyhow::anyhow!(
|
||||
"node '{name}' is inside parallel '{effective_name}'; \
|
||||
no checkpoint found for '{effective_name}'"
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!("no checkpoint found for node '{name}'")
|
||||
}
|
||||
})
|
||||
}
|
||||
RewindTarget::SpecificVisit(name, visit) => {
|
||||
let effective_name = self.parallel_map.get(name).unwrap_or(name);
|
||||
self.entries
|
||||
.iter()
|
||||
.find(|e| e.node_name == *effective_name && e.visit == *visit)
|
||||
.ok_or_else(|| {
|
||||
if effective_name != name {
|
||||
anyhow::anyhow!(
|
||||
"node '{name}' is inside parallel '{effective_name}'; \
|
||||
no visit {visit} found for '{effective_name}'"
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!("no visit {visit} found for node '{name}'")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RewindInput {
|
||||
pub run_id: String,
|
||||
|
|
@ -193,55 +241,9 @@ fn detect_parallel_interior(graph: &Graph) -> HashMap<String, String> {
|
|||
interior_map
|
||||
}
|
||||
|
||||
fn resolve_target<'a>(
|
||||
timeline: &'a [TimelineEntry],
|
||||
target: &RewindTarget,
|
||||
parallel_map: &HashMap<String, String>,
|
||||
) -> Result<&'a TimelineEntry> {
|
||||
match target {
|
||||
RewindTarget::Ordinal(n) => timeline
|
||||
.iter()
|
||||
.find(|e| e.ordinal == *n)
|
||||
.ok_or_else(|| anyhow::anyhow!("ordinal @{n} out of range (max @{})", timeline.len())),
|
||||
RewindTarget::LatestVisit(name) => {
|
||||
let effective_name = parallel_map.get(name).unwrap_or(name);
|
||||
timeline
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|e| e.node_name == *effective_name)
|
||||
.ok_or_else(|| {
|
||||
if effective_name != name {
|
||||
anyhow::anyhow!(
|
||||
"node '{name}' is inside parallel '{effective_name}'; \
|
||||
no checkpoint found for '{effective_name}'"
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!("no checkpoint found for node '{name}'")
|
||||
}
|
||||
})
|
||||
}
|
||||
RewindTarget::SpecificVisit(name, visit) => {
|
||||
let effective_name = parallel_map.get(name).unwrap_or(name);
|
||||
timeline
|
||||
.iter()
|
||||
.find(|e| e.node_name == *effective_name && e.visit == *visit)
|
||||
.ok_or_else(|| {
|
||||
if effective_name != name {
|
||||
anyhow::anyhow!(
|
||||
"node '{name}' is inside parallel '{effective_name}'; \
|
||||
no visit {visit} found for '{effective_name}'"
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!("no visit {visit} found for node '{name}'")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewind(store: &Store, input: RewindInput) -> Result<()> {
|
||||
let timeline = build_timeline(store, &input.run_id)?;
|
||||
let entry = resolve_target(&timeline.entries, &input.target, &timeline.parallel_map)?;
|
||||
let entry = timeline.resolve(&input.target)?;
|
||||
rewind_to_entry(store, &input.run_id, entry, input.push)
|
||||
}
|
||||
|
||||
|
|
@ -371,34 +373,9 @@ fn load_parallel_map(store: &Store, run_id: &str) -> HashMap<String, String> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::test_support::*;
|
||||
use super::*;
|
||||
|
||||
fn temp_repo() -> (tempfile::TempDir, Store) {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let repo = Repository::init(dir.path()).unwrap();
|
||||
(dir, Store::new(repo))
|
||||
}
|
||||
|
||||
fn test_sig() -> Signature<'static> {
|
||||
Signature::now("Test", "test@example.com").unwrap()
|
||||
}
|
||||
|
||||
fn make_checkpoint_json(current_node: &str, visit: usize, git_sha: Option<&str>) -> Vec<u8> {
|
||||
let mut node_visits = HashMap::new();
|
||||
node_visits.insert(current_node.to_string(), visit);
|
||||
let cp = serde_json::json!({
|
||||
"timestamp": "2025-01-01T00:00:00Z",
|
||||
"current_node": current_node,
|
||||
"completed_nodes": [current_node],
|
||||
"node_retries": {},
|
||||
"context_values": {},
|
||||
"logs": [],
|
||||
"node_visits": node_visits,
|
||||
"git_commit_sha": git_sha,
|
||||
});
|
||||
serde_json::to_vec(&cp).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_target_ordinal() {
|
||||
assert_eq!(
|
||||
|
|
@ -466,12 +443,9 @@ mod tests {
|
|||
parallel_map: HashMap::new(),
|
||||
};
|
||||
|
||||
let entry = resolve_target(
|
||||
&timeline.entries,
|
||||
&RewindTarget::LatestVisit("build".to_string()),
|
||||
&timeline.parallel_map,
|
||||
)
|
||||
.unwrap();
|
||||
let entry = timeline
|
||||
.resolve(&RewindTarget::LatestVisit("build".to_string()))
|
||||
.unwrap();
|
||||
assert_eq!(entry.ordinal, 3);
|
||||
}
|
||||
|
||||
|
|
|
|||
34
lib/crates/fabro-workflows/src/operations/test_support.rs
Normal file
34
lib/crates/fabro-workflows/src/operations/test_support.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use fabro_git_storage::gitobj::Store;
|
||||
use git2::{Repository, Signature};
|
||||
|
||||
pub(super) fn temp_repo() -> (tempfile::TempDir, Store) {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let repo = Repository::init(dir.path()).unwrap();
|
||||
(dir, Store::new(repo))
|
||||
}
|
||||
|
||||
pub(super) fn test_sig() -> Signature<'static> {
|
||||
Signature::now("Test", "test@example.com").unwrap()
|
||||
}
|
||||
|
||||
pub(super) fn make_checkpoint_json(
|
||||
current_node: &str,
|
||||
visit: usize,
|
||||
git_sha: Option<&str>,
|
||||
) -> Vec<u8> {
|
||||
let mut node_visits = HashMap::new();
|
||||
node_visits.insert(current_node.to_string(), visit);
|
||||
let cp = serde_json::json!({
|
||||
"timestamp": "2025-01-01T00:00:00Z",
|
||||
"current_node": current_node,
|
||||
"completed_nodes": [current_node],
|
||||
"node_retries": {},
|
||||
"context_values": {},
|
||||
"logs": [],
|
||||
"node_visits": node_visits,
|
||||
"git_commit_sha": git_sha,
|
||||
});
|
||||
serde_json::to_vec(&cp).unwrap()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue