mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
Collapse store handles onto Slate
This commit is contained in:
parent
79cad86ed4
commit
491f326c12
55 changed files with 2093 additions and 2304 deletions
|
|
@ -37,9 +37,7 @@ async fn create_from(
|
|||
let run_store = store::open_run_reader(storage_dir, &run.run_id).await?;
|
||||
let state = run_store.state().await?;
|
||||
|
||||
let record = state
|
||||
.run
|
||||
.context("Failed to load run record from store")?;
|
||||
let record = state.run.context("Failed to load run record from store")?;
|
||||
|
||||
let start = state
|
||||
.start
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub(super) async fn list_command(
|
|||
}
|
||||
|
||||
async fn list_from(
|
||||
store: &dyn fabro_store::Store,
|
||||
store: &fabro_store::SlateStore,
|
||||
base: &Path,
|
||||
args: PrListArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use fabro_types::RunId;
|
|||
use futures::StreamExt;
|
||||
|
||||
use fabro_interview::{AnswerValue, ConsoleInterviewer};
|
||||
use fabro_store::{EventEnvelope, RunStore, RuntimeState};
|
||||
use fabro_store::{EventEnvelope, RuntimeState, SlateRunStore};
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflow::outcome::StageStatus;
|
||||
use fabro_workflow::records::{Conclusion, ConclusionExt};
|
||||
|
|
@ -106,7 +106,7 @@ pub(crate) async fn attach_run(
|
|||
|
||||
async fn attach_run_store(
|
||||
run_dir: &Path,
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
verbose: bool,
|
||||
existing_events: Vec<String>,
|
||||
last_seq: u32,
|
||||
|
|
@ -156,15 +156,12 @@ async fn attach_run_store(
|
|||
}
|
||||
// Wait briefly for a terminal status or conclusion
|
||||
for _ in 0..20 {
|
||||
if run_store
|
||||
.state()
|
||||
.await
|
||||
.ok()
|
||||
.is_some_and(|state| {
|
||||
state.conclusion.is_some()
|
||||
|| state.status.is_some_and(|record| record.status.is_terminal())
|
||||
})
|
||||
{
|
||||
if run_store.state().await.ok().is_some_and(|state| {
|
||||
state.conclusion.is_some()
|
||||
|| state
|
||||
.status
|
||||
.is_some_and(|record| record.status.is_terminal())
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
|
@ -288,7 +285,7 @@ async fn attach_run_store(
|
|||
}
|
||||
|
||||
async fn flush_remaining_store_events(
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
mut next_seq: u32,
|
||||
progress_ui: &mut run_progress::ProgressUI,
|
||||
json_output: bool,
|
||||
|
|
@ -745,7 +742,7 @@ fn determine_exit_code(conclusion_path: &Path, status_record: Option<RunStatusRe
|
|||
}
|
||||
}
|
||||
|
||||
async fn determine_exit_code_with_store(run_store: &dyn RunStore) -> ExitCode {
|
||||
async fn determine_exit_code_with_store(run_store: &SlateRunStore) -> ExitCode {
|
||||
let deadline = Instant::now() + ATTACH_FINAL_STATUS_GRACE;
|
||||
loop {
|
||||
match run_store.state().await {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||
|
||||
use anyhow::{Result, anyhow};
|
||||
use fabro_interview::FileInterviewer;
|
||||
use fabro_store::{RuntimeState, Store};
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_types::RunId;
|
||||
use fabro_workflow::event::EventEmitter;
|
||||
use fabro_workflow::operations::{StartServices, resume as resume_run, start as start_run};
|
||||
|
|
@ -31,8 +31,9 @@ pub(crate) async fn execute(
|
|||
let store = store::build_store(&storage_dir)?;
|
||||
let run_store = store.open_run(&run_id).await?;
|
||||
let run_record = run_store
|
||||
.get_run()
|
||||
.state()
|
||||
.await?
|
||||
.run
|
||||
.ok_or_else(|| anyhow!("Run {run_id} has no run record in store"))?;
|
||||
let on_node: fabro_workflow::OnNodeCallback = Some({
|
||||
let run_id = run_record.run_id.to_string();
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs) -> Result<()> {
|
|||
|
||||
async fn resolve_diff(
|
||||
_run_dir: &Path,
|
||||
run_store: &dyn fabro_store::RunStore,
|
||||
run_store: &fabro_store::SlateRunStore,
|
||||
args: &DiffArgs,
|
||||
) -> Result<String> {
|
||||
let state = run_store.state().await?;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs)
|
|||
let store = Store::new(repo);
|
||||
let run_store = open_run_reader(&cli_settings.storage_dir(), &run_id).await?;
|
||||
|
||||
let timeline = build_timeline_or_rebuild(&store, Some(run_store.as_ref()), &run_id).await?;
|
||||
let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?;
|
||||
|
||||
if args.list {
|
||||
if globals.json {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_util::redact::redact_jsonl_line;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
|
||||
|
|
@ -138,7 +138,7 @@ fn try_parse_relative_duration(s: &str) -> Option<chrono::Duration> {
|
|||
}
|
||||
|
||||
async fn follow_store_logs(
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
run_dir: &Path,
|
||||
seq: u32,
|
||||
pretty: bool,
|
||||
|
|
@ -187,16 +187,19 @@ async fn follow_store_logs(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_concluded(run_store: &dyn RunStore, _run_dir: &Path) -> Result<bool> {
|
||||
async fn run_concluded(run_store: &SlateRunStore, _run_dir: &Path) -> Result<bool> {
|
||||
let state = run_store
|
||||
.state()
|
||||
.await
|
||||
.context("Failed to read run state from store while following logs")?;
|
||||
Ok(state.conclusion.is_some() || state.status.is_some_and(|record| record.status.is_terminal()))
|
||||
Ok(state.conclusion.is_some()
|
||||
|| state
|
||||
.status
|
||||
.is_some_and(|record| record.status.is_terminal()))
|
||||
}
|
||||
|
||||
async fn flush_remaining_store_events(
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
next_seq: u32,
|
||||
pretty: bool,
|
||||
styles: &Styles,
|
||||
|
|
|
|||
|
|
@ -198,12 +198,16 @@ pub(crate) fn print_run_conclusion(
|
|||
}
|
||||
|
||||
pub(crate) async fn print_final_output(
|
||||
run_store: Option<&dyn fabro_store::RunStore>,
|
||||
run_store: Option<&fabro_store::SlateRunStore>,
|
||||
_run_dir: &Path,
|
||||
styles: &Styles,
|
||||
) {
|
||||
let checkpoint = match run_store {
|
||||
Some(run_store) => run_store.state().await.ok().and_then(|state| state.checkpoint),
|
||||
Some(run_store) => run_store
|
||||
.state()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|state| state.checkpoint),
|
||||
None => None,
|
||||
};
|
||||
let Some(checkpoint) = checkpoint else {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@ use fabro_workflow::operations::{
|
|||
RewindInput, RewindTarget, RunTimeline, TimelineEntry, build_timeline_or_rebuild,
|
||||
find_run_id_by_prefix_or_store, rewind,
|
||||
};
|
||||
use fabro_workflow::records::{RunRecord, RunRecordExt, StartRecord, StartRecordExt};
|
||||
use fabro_workflow::records::{RunRecord, RunRecordExt};
|
||||
use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
|
||||
use fabro_workflow::run_status::RunStatus;
|
||||
use git2::Repository;
|
||||
use serde::Serialize;
|
||||
|
||||
|
|
@ -45,7 +44,7 @@ pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs
|
|||
.await
|
||||
.ok();
|
||||
|
||||
let timeline = build_timeline_or_rebuild(&store, Some(run_store.as_ref()), &run_id).await?;
|
||||
let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?;
|
||||
|
||||
if args.list || args.target.is_none() {
|
||||
if globals.json {
|
||||
|
|
@ -68,8 +67,14 @@ pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs
|
|||
)?;
|
||||
if let Some(run_info) = run_info.as_ref() {
|
||||
let entry = timeline.resolve(&target)?;
|
||||
reset_rewound_run_state(&store, durable_store.as_ref(), &run_id, &run_info.path, entry)
|
||||
.await?;
|
||||
reset_rewound_run_state(
|
||||
&store,
|
||||
durable_store.as_ref(),
|
||||
&run_id,
|
||||
&run_info.path,
|
||||
entry,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let run_id_string = run_id.to_string();
|
||||
|
|
@ -104,7 +109,7 @@ pub(crate) fn timeline_entries_json(timeline: &RunTimeline) -> Vec<TimelineEntry
|
|||
|
||||
async fn reset_rewound_run_state(
|
||||
git_store: &Store,
|
||||
durable_store: &dyn fabro_store::Store,
|
||||
durable_store: &fabro_store::SlateStore,
|
||||
run_id: &fabro_types::RunId,
|
||||
run_dir: &std::path::Path,
|
||||
entry: &TimelineEntry,
|
||||
|
|
@ -113,48 +118,27 @@ async fn reset_rewound_run_state(
|
|||
.open_run_reader(run_id)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to open durable store run before rewind: {err}"))?;
|
||||
let store_run_record = existing_run_store.get_run().await.ok().flatten();
|
||||
let store_start_record = existing_run_store.get_start().await.ok().flatten();
|
||||
let store_graph = existing_run_store.get_graph().await.ok().flatten();
|
||||
let state = existing_run_store.state().await.map_err(|err| {
|
||||
anyhow::anyhow!("failed to load durable store state before rewind: {err}")
|
||||
})?;
|
||||
|
||||
let run_record = store_run_record
|
||||
let _run_record = state
|
||||
.run
|
||||
.or_else(|| RunRecord::load(run_dir).ok())
|
||||
.context("failed to restore run record after rewind: missing run metadata")?;
|
||||
let checkpoint = MetadataStore::read_checkpoint(git_store.repo_dir(), &run_id.to_string())?
|
||||
.context("rewound metadata branch is missing checkpoint.json")?;
|
||||
let previous_status = existing_run_store
|
||||
.get_status()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|status| status.status.to_string());
|
||||
let previous_status = state.status.map(|status| status.status.to_string());
|
||||
|
||||
let _ = std::fs::remove_file(run_dir.join("detached_failure.json"));
|
||||
|
||||
let run_store = durable_store
|
||||
.open_run(run_id)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to open durable store run for rewind reset: {err}"))?;
|
||||
let run_store = durable_store.open_run(run_id).await.map_err(|err| {
|
||||
anyhow::anyhow!("failed to open durable store run for rewind reset: {err}")
|
||||
})?;
|
||||
run_store
|
||||
.reset_for_rewind()
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to clear rewound run state: {err}"))?;
|
||||
run_store
|
||||
.put_run(&run_record)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to restore run record after rewind: {err}"))?;
|
||||
if let Some(start_record) = store_start_record.or_else(|| StartRecord::load(run_dir).ok()) {
|
||||
run_store
|
||||
.put_start(&start_record)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to restore start record after rewind: {err}"))?;
|
||||
}
|
||||
if let Some(dot_source) = store_graph {
|
||||
run_store
|
||||
.put_graph(&dot_source)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to restore graph after rewind: {err}"))?;
|
||||
}
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
run_id,
|
||||
|
|
@ -175,13 +159,6 @@ async fn reset_rewound_run_state(
|
|||
)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to append restored checkpoint event: {err}"))?;
|
||||
run_store
|
||||
.put_status(&fabro_types::RunStatusRecord::new(
|
||||
RunStatus::Submitted,
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to restore run status after rewind: {err}"))?;
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
run_id,
|
||||
|
|
@ -189,10 +166,6 @@ async fn reset_rewound_run_state(
|
|||
)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to append restored run status event: {err}"))?;
|
||||
run_store
|
||||
.put_checkpoint(&checkpoint)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to restore checkpoint after rewind: {err}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +173,10 @@ fn restored_checkpoint_event(checkpoint: &fabro_types::Checkpoint) -> WorkflowRu
|
|||
let current_status = checkpoint
|
||||
.node_outcomes
|
||||
.get(&checkpoint.current_node)
|
||||
.map_or_else(|| "success".to_string(), |outcome| outcome.status.to_string());
|
||||
.map_or_else(
|
||||
|| "success".to_string(),
|
||||
|outcome| outcome.status.to_string(),
|
||||
);
|
||||
WorkflowRunEvent::CheckpointCompleted {
|
||||
node_id: checkpoint.current_node.clone(),
|
||||
status: current_status,
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs)
|
|||
let started_waiting_at = std::time::Instant::now();
|
||||
|
||||
let final_status = loop {
|
||||
let run_store = store::open_run_reader(&cli_settings.storage_dir(), &run_info.run_id)
|
||||
.await?;
|
||||
let run_store =
|
||||
store::open_run_reader(&cli_settings.storage_dir(), &run_info.run_id).await?;
|
||||
let status = run_store.state().await?.status.map(|record| record.status);
|
||||
let status = status.unwrap_or_else(|| {
|
||||
if started_waiting_at.elapsed() < WAIT_STARTUP_GRACE {
|
||||
|
|
@ -63,8 +63,7 @@ pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs)
|
|||
}
|
||||
};
|
||||
|
||||
let run_store = store::open_run_reader(&cli_settings.storage_dir(), &run_info.run_id)
|
||||
.await?;
|
||||
let run_store = store::open_run_reader(&cli_settings.storage_dir(), &run_info.run_id).await?;
|
||||
let conclusion = run_store.state().await?.conclusion;
|
||||
|
||||
if globals.json {
|
||||
|
|
|
|||
|
|
@ -39,50 +39,26 @@ async fn inspect_run_store(
|
|||
run_id: &RunId,
|
||||
run_dir: &Path,
|
||||
status: RunStatus,
|
||||
run_store: &dyn fabro_store::RunStore,
|
||||
run_store: &fabro_store::SlateRunStore,
|
||||
) -> InspectOutput {
|
||||
if let Ok(state) = run_store.state().await {
|
||||
if let Some(snapshot) = state.to_snapshot() {
|
||||
return InspectOutput {
|
||||
run_id: run_id.to_string(),
|
||||
run_dir: run_dir.to_path_buf(),
|
||||
status: state.status.as_ref().map_or(status, |record| record.status),
|
||||
run_record: serde_json::to_value(snapshot.run).ok(),
|
||||
start_record: snapshot
|
||||
.start
|
||||
.and_then(|record| serde_json::to_value(record).ok()),
|
||||
conclusion: snapshot
|
||||
.conclusion
|
||||
.and_then(|record| serde_json::to_value(record).ok()),
|
||||
checkpoint: snapshot
|
||||
.checkpoint
|
||||
.and_then(|record| serde_json::to_value(record).ok()),
|
||||
sandbox: snapshot
|
||||
.sandbox
|
||||
.and_then(|record| serde_json::to_value(record).ok()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(Some(snapshot)) = run_store.get_snapshot().await {
|
||||
return InspectOutput {
|
||||
run_id: run_id.to_string(),
|
||||
run_dir: run_dir.to_path_buf(),
|
||||
status: snapshot
|
||||
.status
|
||||
.as_ref()
|
||||
.map_or(status, |record| record.status),
|
||||
run_record: serde_json::to_value(snapshot.run).ok(),
|
||||
start_record: snapshot
|
||||
status: state.status.as_ref().map_or(status, |record| record.status),
|
||||
run_record: state
|
||||
.run
|
||||
.and_then(|record| serde_json::to_value(record).ok()),
|
||||
start_record: state
|
||||
.start
|
||||
.and_then(|record| serde_json::to_value(record).ok()),
|
||||
conclusion: snapshot
|
||||
conclusion: state
|
||||
.conclusion
|
||||
.and_then(|record| serde_json::to_value(record).ok()),
|
||||
checkpoint: snapshot
|
||||
checkpoint: state
|
||||
.checkpoint
|
||||
.and_then(|record| serde_json::to_value(record).ok()),
|
||||
sandbox: snapshot
|
||||
sandbox: state
|
||||
.sandbox
|
||||
.and_then(|record| serde_json::to_value(record).ok()),
|
||||
};
|
||||
|
|
@ -92,35 +68,10 @@ async fn inspect_run_store(
|
|||
run_id: run_id.to_string(),
|
||||
run_dir: run_dir.to_path_buf(),
|
||||
status,
|
||||
run_record: run_store
|
||||
.get_run()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| serde_json::to_value(v).ok()),
|
||||
start_record: run_store
|
||||
.get_start()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| serde_json::to_value(v).ok()),
|
||||
conclusion: run_store
|
||||
.get_conclusion()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| serde_json::to_value(v).ok()),
|
||||
checkpoint: run_store
|
||||
.get_checkpoint()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| serde_json::to_value(v).ok()),
|
||||
sandbox: run_store
|
||||
.get_sandbox()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| serde_json::to_value(v).ok()),
|
||||
run_record: None,
|
||||
start_record: None,
|
||||
conclusion: None,
|
||||
checkpoint: None,
|
||||
sandbox: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_store::Store;
|
||||
use fabro_store::SlateStore;
|
||||
use tracing::warn;
|
||||
|
||||
use fabro_sandbox::reconnect::reconnect as reconnect_sandbox;
|
||||
use fabro_workflow::event::{WorkflowRunEvent, append_workflow_event};
|
||||
use fabro_workflow::run_lookup::RunInfo;
|
||||
use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
|
||||
use fabro_workflow::run_status::{RunStatus, RunStatusRecord};
|
||||
|
||||
use crate::args::{GlobalArgs, RunsRemoveArgs};
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::store;
|
||||
use crate::user_config::load_user_settings_with_globals;
|
||||
use fabro_sandbox::reconnect::reconnect as reconnect_sandbox;
|
||||
use fabro_workflow::event::{WorkflowRunEvent, append_workflow_event};
|
||||
use fabro_workflow::run_lookup::RunInfo;
|
||||
use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
|
||||
|
||||
use super::short_run_id;
|
||||
|
||||
|
|
@ -26,7 +24,7 @@ pub(crate) async fn remove_command(args: &RunsRemoveArgs, globals: &GlobalArgs)
|
|||
|
||||
async fn remove_from(
|
||||
args: &RunsRemoveArgs,
|
||||
store: &dyn Store,
|
||||
store: &SlateStore,
|
||||
base: &Path,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
|
|
@ -109,12 +107,12 @@ async fn remove_from(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_run_with_cleanup(store: &dyn Store, run: &RunInfo) -> Result<()> {
|
||||
pub(crate) async fn remove_run_with_cleanup(store: &SlateStore, run: &RunInfo) -> Result<()> {
|
||||
remove_run_dir_with_cleanup(store, run).await?;
|
||||
delete_run_store_state(store, run).await
|
||||
}
|
||||
|
||||
async fn remove_run_dir_with_cleanup(store: &dyn Store, run: &RunInfo) -> Result<()> {
|
||||
async fn remove_run_dir_with_cleanup(store: &SlateStore, run: &RunInfo) -> Result<()> {
|
||||
let run_store = match store.open_run_reader(&run.run_id).await {
|
||||
Ok(run_store) => Some(run_store),
|
||||
Err(err) => {
|
||||
|
|
@ -127,16 +125,6 @@ async fn remove_run_dir_with_cleanup(store: &dyn Store, run: &RunInfo) -> Result
|
|||
}
|
||||
};
|
||||
if let Some(run_store) = run_store.as_ref() {
|
||||
if let Err(err) = run_store
|
||||
.put_status(&RunStatusRecord::new(RunStatus::Removing, None))
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
run_id = %run.run_id,
|
||||
error = %err,
|
||||
"failed to save removing status to store"
|
||||
);
|
||||
}
|
||||
if let Err(err) = append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&run.run_id,
|
||||
|
|
@ -171,7 +159,7 @@ async fn remove_run_dir_with_cleanup(store: &dyn Store, run: &RunInfo) -> Result
|
|||
.with_context(|| format!("failed to delete {}", run.path.display()))
|
||||
}
|
||||
|
||||
async fn delete_run_store_state(store: &dyn Store, run: &RunInfo) -> Result<()> {
|
||||
async fn delete_run_store_state(store: &SlateStore, run: &RunInfo) -> Result<()> {
|
||||
store
|
||||
.delete_run(&run.run_id)
|
||||
.await
|
||||
|
|
@ -180,7 +168,7 @@ async fn delete_run_store_state(store: &dyn Store, run: &RunInfo) -> Result<()>
|
|||
|
||||
async fn load_sandbox_record(
|
||||
_run_dir: &Path,
|
||||
run_store: Option<&dyn fabro_store::RunStore>,
|
||||
run_store: Option<&fabro_store::SlateRunStore>,
|
||||
) -> Option<fabro_sandbox::SandboxRecord> {
|
||||
if let Some(run_store) = run_store {
|
||||
match run_store.state().await {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::io::{ErrorKind, Write};
|
|||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_store::{NodeVisitRef, RunSnapshot, RunState, RunStore};
|
||||
use fabro_store::{NodeVisitRef, RunSnapshot, RunState, SlateRunStore};
|
||||
use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
|
||||
use serde::Serialize;
|
||||
#[cfg(test)]
|
||||
|
|
@ -37,7 +37,7 @@ pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) ->
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn export_run(run_store: &dyn RunStore, output_dir: &Path) -> Result<usize> {
|
||||
pub(crate) async fn export_run(run_store: &SlateRunStore, output_dir: &Path) -> Result<usize> {
|
||||
let state = run_store.state().await?;
|
||||
let snapshot = state
|
||||
.to_snapshot()
|
||||
|
|
@ -78,7 +78,7 @@ pub(crate) async fn export_run(run_store: &dyn RunStore, output_dir: &Path) -> R
|
|||
}
|
||||
|
||||
async fn export_run_to_dir(
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
state: &RunState,
|
||||
snapshot: &RunSnapshot,
|
||||
output_dir: &Path,
|
||||
|
|
@ -349,14 +349,18 @@ mod tests {
|
|||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_store::{EventEnvelope, EventPayload, InMemoryStore, Store as _};
|
||||
use fabro_store::{EventEnvelope, EventPayload, SlateStore};
|
||||
use fabro_types::{
|
||||
AggregateStats, AttrValue, Checkpoint, Conclusion, Graph, NodeStatusRecord, Retro, RunId,
|
||||
RunRecord, RunStatus, RunStatusRecord, SandboxRecord, Settings, StageStatus, StartRecord,
|
||||
StatusReason, fixtures,
|
||||
};
|
||||
use fabro_workflow::event::{WorkflowRunEvent, append_workflow_event};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
fn dt(rfc3339: &str) -> DateTime<Utc> {
|
||||
DateTime::parse_from_rfc3339(rfc3339)
|
||||
|
|
@ -368,6 +372,14 @@ mod tests {
|
|||
fixtures::RUN_1
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn sample_run_record(run_id: RunId, created_at: DateTime<Utc>) -> RunRecord {
|
||||
let mut graph = Graph::new("night-sky");
|
||||
graph.attrs.insert(
|
||||
|
|
@ -476,82 +488,256 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn sample_node_status() -> NodeStatusRecord {
|
||||
NodeStatusRecord {
|
||||
status: StageStatus::PartialSuccess,
|
||||
notes: Some("captured output".to_string()),
|
||||
failure_reason: Some("minor lint".to_string()),
|
||||
timestamp: dt("2026-03-27T12:12:00Z"),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_payload(run_id: RunId, ts: &str, event: &str) -> EventPayload {
|
||||
EventPayload::new(
|
||||
serde_json::json!({
|
||||
"id": format!("evt-{run_id}-{event}"),
|
||||
"ts": ts,
|
||||
"run_id": run_id.to_string(),
|
||||
"event": event
|
||||
}),
|
||||
&run_id,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn read_json<T: DeserializeOwned>(path: &Path) -> T {
|
||||
serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap()
|
||||
let bytes = std::fs::read(path)
|
||||
.unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display()));
|
||||
serde_json::from_slice(&bytes)
|
||||
.unwrap_or_else(|err| panic!("failed to parse {}: {err}", path.display()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_run_writes_expected_directory_tree() {
|
||||
let store = InMemoryStore::default();
|
||||
let store = test_store();
|
||||
let created_at = dt("2026-03-27T12:00:00Z");
|
||||
let run_id = test_run_id();
|
||||
let run = store.create_run(&run_id, created_at, None).await.unwrap();
|
||||
|
||||
run.put_run(&sample_run_record(run_id, created_at))
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_start(&sample_start_record(run_id, created_at))
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_status(&sample_status()).await.unwrap();
|
||||
run.append_checkpoint(&sample_checkpoint("plan", 1))
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_checkpoint(&sample_checkpoint("code", 2))
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_conclusion(&sample_conclusion()).await.unwrap();
|
||||
run.put_retro(&sample_retro(run_id)).await.unwrap();
|
||||
run.put_graph("digraph night_sky {}").await.unwrap();
|
||||
run.put_sandbox(&sample_sandbox()).await.unwrap();
|
||||
let run_record = sample_run_record(run_id, created_at);
|
||||
let start_record = sample_start_record(run_id, created_at);
|
||||
let status_record = sample_status();
|
||||
let first_checkpoint = sample_checkpoint("plan", 1);
|
||||
let second_checkpoint = sample_checkpoint("code", 2);
|
||||
let conclusion = sample_conclusion();
|
||||
let retro = sample_retro(run_id);
|
||||
let sandbox = sample_sandbox();
|
||||
|
||||
let node = NodeVisitRef {
|
||||
node_id: "code",
|
||||
visit: 2,
|
||||
};
|
||||
run.put_node_prompt(&node, "Plan the fix").await.unwrap();
|
||||
run.put_node_response(&node, "Implemented").await.unwrap();
|
||||
run.put_node_status(&node, &sample_node_status())
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_node_stdout(&node, "stdout line").await.unwrap();
|
||||
run.put_node_stderr(&node, "").await.unwrap();
|
||||
run.put_retro_prompt("How did it go?").await.unwrap();
|
||||
run.put_retro_response("Smooth enough").await.unwrap();
|
||||
run.append_event(&event_payload(
|
||||
run_id,
|
||||
"2026-03-27T12:00:00.000Z",
|
||||
"run.started",
|
||||
))
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
run_id,
|
||||
settings: serde_json::to_value(&run_record.settings).unwrap(),
|
||||
graph: serde_json::to_value(&run_record.graph).unwrap(),
|
||||
workflow_source: Some("digraph night_sky {}".to_string()),
|
||||
workflow_config: None,
|
||||
labels: run_record.labels.clone().into_iter().collect(),
|
||||
run_dir: "/tmp/night-sky-run".to_string(),
|
||||
working_directory: run_record.working_directory.display().to_string(),
|
||||
host_repo_path: run_record.host_repo_path.clone(),
|
||||
base_branch: run_record.base_branch.clone(),
|
||||
workflow_slug: run_record.workflow_slug.clone(),
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event_payload(
|
||||
run_id,
|
||||
"2026-03-27T12:00:01.000Z",
|
||||
"stage.completed",
|
||||
))
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::WorkflowRunStarted {
|
||||
name: "night-sky".to_string(),
|
||||
run_id,
|
||||
base_branch: run_record.base_branch.clone(),
|
||||
base_sha: start_record.base_sha.clone(),
|
||||
run_branch: start_record.run_branch.clone(),
|
||||
worktree_dir: None,
|
||||
goal: Some("map the constellations".to_string()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::RunRunning {
|
||||
reason: status_record.reason,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
for checkpoint in [&first_checkpoint, &second_checkpoint] {
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::CheckpointCompleted {
|
||||
node_id: checkpoint.current_node.clone(),
|
||||
status: "success".to_string(),
|
||||
current_node: checkpoint.current_node.clone(),
|
||||
completed_nodes: checkpoint.completed_nodes.clone(),
|
||||
node_retries: checkpoint.node_retries.clone().into_iter().collect(),
|
||||
context_values: checkpoint.context_values.clone().into_iter().collect(),
|
||||
node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(),
|
||||
next_node_id: checkpoint.next_node_id.clone(),
|
||||
git_commit_sha: checkpoint.git_commit_sha.clone(),
|
||||
loop_failure_signatures: checkpoint
|
||||
.loop_failure_signatures
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(signature, count)| (signature.to_string(), count))
|
||||
.collect(),
|
||||
restart_failure_signatures: checkpoint
|
||||
.restart_failure_signatures
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(signature, count)| (signature.to_string(), count))
|
||||
.collect(),
|
||||
node_visits: checkpoint.node_visits.clone().into_iter().collect(),
|
||||
diff: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::SandboxInitialized {
|
||||
working_directory: sandbox.working_directory.clone(),
|
||||
provider: sandbox.provider.clone(),
|
||||
identifier: sandbox.identifier.clone(),
|
||||
host_working_directory: sandbox.host_working_directory.clone(),
|
||||
container_mount_point: sandbox.container_mount_point.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::Prompt {
|
||||
stage: "code".to_string(),
|
||||
visit: 2,
|
||||
text: "Plan the fix".to_string(),
|
||||
mode: None,
|
||||
provider: None,
|
||||
model: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::PromptCompleted {
|
||||
node_id: "code".to_string(),
|
||||
response: "Implemented".to_string(),
|
||||
model: "gpt-5".to_string(),
|
||||
provider: "openai".to_string(),
|
||||
usage: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::StageCompleted {
|
||||
node_id: "code".to_string(),
|
||||
name: "Code".to_string(),
|
||||
index: 1,
|
||||
duration_ms: 250,
|
||||
status: "partial_success".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
usage: None,
|
||||
failure: None,
|
||||
notes: Some("captured output".to_string()),
|
||||
files_touched: Vec::new(),
|
||||
context_updates: None,
|
||||
jump_to_node: None,
|
||||
context_values: None,
|
||||
node_visits: Some(std::collections::BTreeMap::from([(
|
||||
"code".to_string(),
|
||||
2usize,
|
||||
)])),
|
||||
loop_failure_signatures: None,
|
||||
restart_failure_signatures: None,
|
||||
response: Some("Implemented".to_string()),
|
||||
attempt: 1,
|
||||
max_attempts: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::CommandStarted {
|
||||
node_id: "code".to_string(),
|
||||
script: "echo hi".to_string(),
|
||||
language: "sh".to_string(),
|
||||
timeout_ms: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::CommandCompleted {
|
||||
node_id: "code".to_string(),
|
||||
stdout: "stdout line".to_string(),
|
||||
stderr: String::new(),
|
||||
exit_code: Some(0),
|
||||
duration_ms: 100,
|
||||
timed_out: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::RetroStarted {
|
||||
prompt: Some("How did it go?".to_string()),
|
||||
provider: None,
|
||||
model: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::RetroCompleted {
|
||||
duration_ms: 50,
|
||||
response: Some("Smooth enough".to_string()),
|
||||
retro: Some(serde_json::to_value(&retro).unwrap()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::WorkflowRunCompleted {
|
||||
duration_ms: conclusion.duration_ms,
|
||||
artifact_count: 0,
|
||||
status: "success".to_string(),
|
||||
reason: None,
|
||||
total_cost: conclusion.total_cost,
|
||||
final_git_commit_sha: conclusion.final_git_commit_sha.clone(),
|
||||
final_patch: None,
|
||||
usage: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(
|
||||
&EventPayload::new(
|
||||
serde_json::json!({
|
||||
"id": format!("evt-{run_id}-stage-completed"),
|
||||
"ts": "2026-03-27T12:00:01.000Z",
|
||||
"run_id": run_id.to_string(),
|
||||
"event": "stage.completed"
|
||||
}),
|
||||
&run_id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_artifact_value("summary", &serde_json::json!({"done": true}))
|
||||
|
|
@ -583,7 +769,7 @@ mod tests {
|
|||
assert_eq!(exported_start.run_id, run_id);
|
||||
|
||||
let exported_status: RunStatusRecord = read_json(&output.path().join("status.json"));
|
||||
assert_eq!(exported_status.status, RunStatus::Running);
|
||||
assert_eq!(exported_status.status, RunStatus::Succeeded);
|
||||
|
||||
let exported_checkpoint: Checkpoint = read_json(&output.path().join("checkpoint.json"));
|
||||
assert_eq!(exported_checkpoint.current_node, "code");
|
||||
|
|
@ -626,12 +812,12 @@ mod tests {
|
|||
.lines()
|
||||
.map(|line| serde_json::from_str(line).unwrap())
|
||||
.collect();
|
||||
assert_eq!(events.len(), 2);
|
||||
assert_eq!(events.len(), 15);
|
||||
assert_eq!(events[0].seq, 1);
|
||||
assert_eq!(events[1].seq, 2);
|
||||
assert_eq!(events.last().unwrap().seq, 15);
|
||||
|
||||
let first_checkpoint: Checkpoint = read_json(&output.path().join("checkpoints/0001.json"));
|
||||
let second_checkpoint: Checkpoint = read_json(&output.path().join("checkpoints/0002.json"));
|
||||
let first_checkpoint: Checkpoint = read_json(&output.path().join("checkpoints/0004.json"));
|
||||
let second_checkpoint: Checkpoint = read_json(&output.path().join("checkpoints/0005.json"));
|
||||
assert_eq!(first_checkpoint.current_node, "plan");
|
||||
assert_eq!(second_checkpoint.current_node, "code");
|
||||
|
||||
|
|
@ -665,14 +851,31 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn export_run_rejects_path_traversal_and_leaves_no_partial_output() {
|
||||
let store = InMemoryStore::default();
|
||||
let store = test_store();
|
||||
let created_at = dt("2026-03-27T12:00:00Z");
|
||||
let run_id = test_run_id();
|
||||
let run = store.create_run(&run_id, created_at, None).await.unwrap();
|
||||
|
||||
run.put_run(&sample_run_record(run_id, created_at))
|
||||
.await
|
||||
.unwrap();
|
||||
let run_record = sample_run_record(run_id, created_at);
|
||||
append_workflow_event(
|
||||
run.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
run_id,
|
||||
settings: serde_json::to_value(&run_record.settings).unwrap(),
|
||||
graph: serde_json::to_value(&run_record.graph).unwrap(),
|
||||
workflow_source: Some("digraph night_sky {}".to_string()),
|
||||
workflow_config: None,
|
||||
labels: run_record.labels.clone().into_iter().collect(),
|
||||
run_dir: "/tmp/night-sky-run".to_string(),
|
||||
working_directory: run_record.working_directory.display().to_string(),
|
||||
host_repo_path: run_record.host_repo_path.clone(),
|
||||
base_branch: run_record.base_branch.clone(),
|
||||
workflow_slug: run_record.workflow_slug.clone(),
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_asset(
|
||||
&NodeVisitRef {
|
||||
node_id: "code",
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ pub(super) async fn df_command(args: &DfArgs, globals: &GlobalArgs) -> Result<()
|
|||
#[allow(clippy::print_stdout)]
|
||||
async fn df_from(
|
||||
args: &DfArgs,
|
||||
store: &dyn fabro_store::Store,
|
||||
store: &fabro_store::SlateStore,
|
||||
data_dir: &Path,
|
||||
runs_base: &Path,
|
||||
logs_base: &Path,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::path::Path;
|
|||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use chrono::Utc;
|
||||
use fabro_store::Store;
|
||||
use fabro_store::SlateStore;
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, info};
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {
|
|||
|
||||
async fn prune_from(
|
||||
args: &RunsPruneArgs,
|
||||
store: &dyn Store,
|
||||
store: &SlateStore,
|
||||
base: &Path,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use fabro_store::{RunStore, SlateStore, Store};
|
||||
use fabro_store::{RunStoreHandle, SlateStore};
|
||||
use fabro_types::RunId;
|
||||
use object_store::local::LocalFileSystem;
|
||||
|
||||
|
|
@ -18,10 +18,7 @@ pub(crate) fn build_store(storage_dir: &Path) -> Result<Arc<SlateStore>> {
|
|||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn open_run_reader(
|
||||
storage_dir: &Path,
|
||||
run_id: &RunId,
|
||||
) -> Result<Arc<dyn RunStore>> {
|
||||
pub(crate) async fn open_run_reader(storage_dir: &Path, run_id: &RunId) -> Result<RunStoreHandle> {
|
||||
build_store(storage_dir)?
|
||||
.open_run_reader(run_id)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1,28 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use fabro_store::Store;
|
||||
use fabro_test::{fabro_snapshot, test_context};
|
||||
use fabro_types::RunId;
|
||||
use object_store::local::LocalFileSystem;
|
||||
|
||||
use super::support::{git_filters, setup_git_backed_changed_run, setup_git_backed_noop_run};
|
||||
|
||||
fn with_runtime<T>(f: impl FnOnce(&tokio::runtime::Runtime) -> T) -> T {
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
f(&runtime)
|
||||
}
|
||||
|
||||
fn build_store(storage_dir: &std::path::Path) -> Arc<fabro_store::SlateStore> {
|
||||
let store_path = storage_dir.join("store");
|
||||
std::fs::create_dir_all(&store_path).unwrap();
|
||||
let object_store = Arc::new(LocalFileSystem::new_with_prefix(&store_path).unwrap());
|
||||
Arc::new(fabro_store::SlateStore::new(
|
||||
object_store,
|
||||
"",
|
||||
std::time::Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
let context = test_context!();
|
||||
|
|
@ -113,24 +92,8 @@ fn diff_completed_run_with_changes_prints_patch() {
|
|||
fn diff_completed_run_reads_store_final_patch_without_disk_file() {
|
||||
let context = test_context!();
|
||||
let setup = setup_git_backed_changed_run(&context);
|
||||
let run_id: RunId = setup.run.run_id.parse().unwrap();
|
||||
let patch = with_runtime(|runtime| {
|
||||
runtime.block_on(async {
|
||||
let store = build_store(&context.storage_dir);
|
||||
let run_store = store.open_run(&run_id).await.unwrap();
|
||||
run_store.get_final_patch().await.unwrap().unwrap()
|
||||
})
|
||||
});
|
||||
let _ = std::fs::remove_file(setup.run.run_dir.join("final.patch"));
|
||||
|
||||
with_runtime(|runtime| {
|
||||
runtime.block_on(async {
|
||||
let store = build_store(&context.storage_dir);
|
||||
let run_store = store.open_run(&run_id).await.unwrap();
|
||||
run_store.put_final_patch(&patch).await.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
let mut cmd = context.command();
|
||||
cmd.args(["diff", &setup.run.run_id]);
|
||||
|
||||
|
|
@ -176,41 +139,8 @@ fn diff_node_outputs_specific_patch() {
|
|||
fn diff_node_reads_store_patch_without_disk_file() {
|
||||
let context = test_context!();
|
||||
let setup = setup_git_backed_changed_run(&context);
|
||||
let run_id: RunId = setup.run.run_id.parse().unwrap();
|
||||
let patch = with_runtime(|runtime| {
|
||||
runtime.block_on(async {
|
||||
let store = build_store(&context.storage_dir);
|
||||
let run_store = store.open_run(&run_id).await.unwrap();
|
||||
run_store
|
||||
.get_node(&fabro_store::NodeVisitRef {
|
||||
node_id: "step_one",
|
||||
visit: 1,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.diff
|
||||
.unwrap()
|
||||
})
|
||||
});
|
||||
let _ = std::fs::remove_file(setup.run.run_dir.join("nodes/step_one/diff.patch"));
|
||||
|
||||
with_runtime(|runtime| {
|
||||
runtime.block_on(async {
|
||||
let store = build_store(&context.storage_dir);
|
||||
let run_store = store.open_run(&run_id).await.unwrap();
|
||||
run_store
|
||||
.put_node_diff(
|
||||
&fabro_store::NodeVisitRef {
|
||||
node_id: "step_one",
|
||||
visit: 1,
|
||||
},
|
||||
&patch,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
let mut cmd = context.command();
|
||||
cmd.args(["diff", &setup.run.run_id, "--node", "step_one"]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use fabro_store::Store;
|
||||
use fabro_test::{fabro_snapshot, test_context};
|
||||
use fabro_types::{PullRequestRecord, RunId};
|
||||
use fabro_types::RunId;
|
||||
use fabro_workflow::event::{WorkflowRunEvent, append_workflow_event};
|
||||
use object_store::local::LocalFileSystem;
|
||||
|
||||
use super::support::setup_completed_dry_run;
|
||||
|
|
@ -77,18 +77,22 @@ fn pr_view_reads_pull_request_from_store_without_pull_request_json() {
|
|||
runtime.block_on(async {
|
||||
let store = build_store(&context.storage_dir);
|
||||
let run_store = store.open_run(&run_id).await.unwrap();
|
||||
run_store
|
||||
.put_pull_request(&PullRequestRecord {
|
||||
html_url: "https://github.com/fabro-sh/fabro/pull/123".to_string(),
|
||||
number: 123,
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::PullRequestCreated {
|
||||
pr_url: "https://github.com/fabro-sh/fabro/pull/123".to_string(),
|
||||
pr_number: 123,
|
||||
owner: "fabro-sh".to_string(),
|
||||
repo: "fabro".to_string(),
|
||||
base_branch: "main".to_string(),
|
||||
head_branch: "fabro/run/demo".to_string(),
|
||||
title: "Map the constellations".to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
draft: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,9 @@ fn rewind_preserves_event_history_and_clears_terminal_snapshot_state() {
|
|||
let setup = setup_git_backed_changed_run(&context);
|
||||
let before_events = run_events(&setup.run.run_dir);
|
||||
assert!(
|
||||
before_events.iter().any(|event| event.payload.as_value()["event"] == "run.completed"),
|
||||
before_events
|
||||
.iter()
|
||||
.any(|event| event.payload.as_value()["event"] == "run.completed"),
|
||||
"setup run should be completed before rewind"
|
||||
);
|
||||
|
||||
|
|
@ -179,9 +181,18 @@ fn rewind_preserves_event_history_and_clears_terminal_snapshot_state() {
|
|||
snapshot.status.as_ref().map(|status| &status.status),
|
||||
Some(&fabro_types::RunStatus::Submitted)
|
||||
);
|
||||
assert!(snapshot.conclusion.is_none(), "rewind should clear conclusion");
|
||||
assert!(snapshot.final_patch.is_none(), "rewind should clear final patch");
|
||||
assert!(snapshot.pull_request.is_none(), "rewind should clear pull request");
|
||||
assert!(
|
||||
snapshot.conclusion.is_none(),
|
||||
"rewind should clear conclusion"
|
||||
);
|
||||
assert!(
|
||||
snapshot.final_patch.is_none(),
|
||||
"rewind should clear final patch"
|
||||
);
|
||||
assert!(
|
||||
snapshot.pull_request.is_none(),
|
||||
"rewind should clear pull request"
|
||||
);
|
||||
assert!(
|
||||
snapshot.nodes.is_empty(),
|
||||
"rewind should clear node snapshots that belonged to the prior execution"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::process::Output;
|
|||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use fabro_store::{EventEnvelope, RunSnapshot, RunStore, SlateStore, Store};
|
||||
use fabro_store::{EventEnvelope, RunSnapshot, RunStoreHandle, SlateStore};
|
||||
use fabro_test::TestContext;
|
||||
use fabro_types::RunId;
|
||||
use object_store::local::LocalFileSystem;
|
||||
|
|
@ -284,9 +284,9 @@ worktree_mode = "never"
|
|||
let run = run_local_workflow(context, &workspace_dir, "run.toml");
|
||||
let store = run_store(&run.run_dir);
|
||||
assert!(
|
||||
block_on(store.get_sandbox())
|
||||
block_on(store.state())
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|state| state.sandbox)
|
||||
.is_some()
|
||||
);
|
||||
|
||||
|
|
@ -374,10 +374,9 @@ pub(crate) fn write_gated_workflow(path: &Path, name: &str, goal: &str) -> Workf
|
|||
pub(crate) fn wait_for_status(run_dir: &Path, expected: &[&str]) -> String {
|
||||
let deadline = Instant::now() + COMMAND_TIMEOUT;
|
||||
loop {
|
||||
if let Some(status) = block_on(run_store(run_dir).get_status())
|
||||
if let Some(status) = block_on(run_store(run_dir).state())
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|record| record.status.to_string())
|
||||
.and_then(|state| state.status.map(|record| record.status.to_string()))
|
||||
{
|
||||
if expected.iter().any(|candidate| *candidate == status) {
|
||||
return status;
|
||||
|
|
@ -481,7 +480,7 @@ fn block_on<T>(future: impl std::future::Future<Output = T>) -> T {
|
|||
.block_on(future)
|
||||
}
|
||||
|
||||
fn run_store(run_dir: &Path) -> Arc<dyn RunStore> {
|
||||
fn run_store(run_dir: &Path) -> RunStoreHandle {
|
||||
let runs_dir = run_dir.parent().expect("run dir should have parent");
|
||||
let storage_dir = runs_dir.parent().expect("runs dir should have parent");
|
||||
let run_id: RunId = infer_run_id(run_dir).parse().expect("run id should parse");
|
||||
|
|
@ -495,9 +494,9 @@ fn run_store(run_dir: &Path) -> Arc<dyn RunStore> {
|
|||
|
||||
pub(crate) fn run_snapshot(run_dir: &Path) -> RunSnapshot {
|
||||
let store = run_store(run_dir);
|
||||
block_on(store.get_snapshot())
|
||||
block_on(store.state())
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|state| state.to_snapshot())
|
||||
.expect("run store snapshot should exist")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_store::{RunSnapshot, RunStore, SlateStore, Store};
|
||||
use fabro_store::{RunSnapshot, RunStoreHandle, SlateStore};
|
||||
use fabro_types::RunId;
|
||||
use object_store::local::LocalFileSystem;
|
||||
pub(super) fn fixture(name: &str) -> PathBuf {
|
||||
|
|
@ -23,7 +23,7 @@ fn block_on<T>(future: impl std::future::Future<Output = T>) -> T {
|
|||
.block_on(future)
|
||||
}
|
||||
|
||||
fn run_store(run_dir: &Path) -> Arc<dyn RunStore> {
|
||||
fn run_store(run_dir: &Path) -> RunStoreHandle {
|
||||
let runs_dir = run_dir.parent().expect("run dir should have parent");
|
||||
let storage_dir = runs_dir.parent().expect("runs dir should have parent");
|
||||
let run_id: RunId = std::fs::read_to_string(run_dir.join("id.txt"))
|
||||
|
|
@ -48,9 +48,9 @@ fn run_store(run_dir: &Path) -> Arc<dyn RunStore> {
|
|||
|
||||
pub(super) fn run_snapshot(run_dir: &Path) -> RunSnapshot {
|
||||
let store = run_store(run_dir);
|
||||
block_on(store.get_snapshot())
|
||||
block_on(store.state())
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|state| state.to_snapshot())
|
||||
.expect("run store snapshot should exist")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use fabro_agent::{
|
|||
use fabro_llm::client::Client;
|
||||
use fabro_llm::provider::Provider;
|
||||
use fabro_llm::types::ToolDefinition;
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_util::redact::redact_jsonl_line;
|
||||
use tokio::sync::broadcast::Receiver;
|
||||
use tokio::task::JoinHandle;
|
||||
|
|
@ -137,7 +137,7 @@ pub fn build_retro_prompt(retro_data_dir: &str) -> String {
|
|||
/// files via tool access, then calls `submit_retro` with its analysis.
|
||||
pub async fn run_retro_agent(
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
run_dir: &Path,
|
||||
llm_client: &Client,
|
||||
provider: Provider,
|
||||
|
|
@ -286,26 +286,20 @@ pub fn dry_run_narrative() -> RetroNarrative {
|
|||
}
|
||||
|
||||
async fn write_retro_prompt(
|
||||
run_store: &dyn RunStore,
|
||||
_run_store: &SlateRunStore,
|
||||
retro_dir: &Path,
|
||||
prompt: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Err(err) = run_store.put_retro_prompt(prompt).await {
|
||||
tracing::warn!(error = %err, "Failed to save retro prompt to store");
|
||||
std::fs::write(retro_dir.join("prompt.md"), prompt)?;
|
||||
}
|
||||
std::fs::write(retro_dir.join("prompt.md"), prompt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_retro_response(
|
||||
run_store: &dyn RunStore,
|
||||
_run_store: &SlateRunStore,
|
||||
retro_dir: &Path,
|
||||
response: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Err(err) = run_store.put_retro_response(response).await {
|
||||
tracing::warn!(error = %err, "Failed to save retro response to store");
|
||||
std::fs::write(retro_dir.join("response.md"), response)?;
|
||||
}
|
||||
std::fs::write(retro_dir.join("response.md"), response)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -385,7 +379,7 @@ fn build_profile(provider: Provider, model: &str) -> Box<dyn AgentProfile> {
|
|||
|
||||
async fn upload_data_files(
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
_run_dir: &Path,
|
||||
target_dir: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
|
|
@ -416,26 +410,24 @@ async fn upload_data_files(
|
|||
.map_err(|e| anyhow::anyhow!("Failed to upload progress.jsonl: {e}"))?;
|
||||
}
|
||||
|
||||
let checkpoint_content = run_store
|
||||
.get_checkpoint()
|
||||
let state = run_store
|
||||
.state()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load checkpoint from store: {e}"))?
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load run state from store: {e}"))?;
|
||||
let checkpoint_content = state
|
||||
.checkpoint
|
||||
.map(|cp| serde_json::to_string_pretty(&cp))
|
||||
.transpose()?;
|
||||
upload_file(sandbox, target_dir, "checkpoint.json", checkpoint_content).await?;
|
||||
|
||||
let run_content = run_store
|
||||
.get_run()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load run metadata from store: {e}"))?
|
||||
let run_content = state
|
||||
.run
|
||||
.map(|run| serde_json::to_string_pretty(&run))
|
||||
.transpose()?;
|
||||
upload_file(sandbox, target_dir, "run.json", run_content).await?;
|
||||
|
||||
let start_content = run_store
|
||||
.get_start()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load start metadata from store: {e}"))?
|
||||
let start_content = state
|
||||
.start
|
||||
.map(|start| serde_json::to_string_pretty(&start))
|
||||
.transpose()?;
|
||||
upload_file(sandbox, target_dir, "start.json", start_content).await?;
|
||||
|
|
|
|||
|
|
@ -19,12 +19,13 @@ use fabro_llm::types::{
|
|||
ContentPart, FinishReason, Message as LlmMessage, Request as LlmRequest,
|
||||
Response as LlmResponse, Role, StreamEvent, ToolChoice, ToolDefinition, Usage,
|
||||
};
|
||||
use fabro_store::{InMemoryStore, Store};
|
||||
use fabro_store::StoreHandle;
|
||||
use fabro_types::{RunId, Settings};
|
||||
use fabro_util::redact::redact_jsonl_line;
|
||||
use fabro_workflow::error::FabroError;
|
||||
use fabro_workflow::handler::HandlerRegistry;
|
||||
use futures_util::stream;
|
||||
use object_store::memory::InMemory as MemoryObjectStore;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::{Notify, OnceCell};
|
||||
|
|
@ -124,7 +125,7 @@ type RegistryFactoryOverride = dyn Fn(Arc<dyn Interviewer>) -> HandlerRegistry +
|
|||
pub struct AppState {
|
||||
runs: Mutex<HashMap<RunId, ManagedRun>>,
|
||||
aggregate_usage: Mutex<AggregateUsageTotals>,
|
||||
store: Arc<dyn Store>,
|
||||
store: StoreHandle,
|
||||
pub db: sqlx::SqlitePool,
|
||||
max_concurrent_runs: usize,
|
||||
scheduler_notify: Notify,
|
||||
|
|
@ -417,7 +418,7 @@ pub fn create_app_state_with_registry_factory(
|
|||
Arc::new(RwLock::new(Settings::default())),
|
||||
Some(Box::new(registry_factory_override)),
|
||||
5,
|
||||
Arc::new(InMemoryStore::default()),
|
||||
test_store(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -431,15 +432,23 @@ pub fn create_app_state_with_options(
|
|||
db,
|
||||
Arc::new(RwLock::new(settings)),
|
||||
max_concurrent_runs,
|
||||
Arc::new(InMemoryStore::default()),
|
||||
test_store(),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_store() -> StoreHandle {
|
||||
Arc::new(fabro_store::SlateStore::new(
|
||||
Arc::new(MemoryObjectStore::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn create_app_state_with_store(
|
||||
db: sqlx::SqlitePool,
|
||||
settings: Arc<RwLock<Settings>>,
|
||||
max_concurrent_runs: usize,
|
||||
store: Arc<dyn Store>,
|
||||
store: StoreHandle,
|
||||
) -> Arc<AppState> {
|
||||
build_app_state(db, settings, None, max_concurrent_runs, store)
|
||||
}
|
||||
|
|
@ -449,7 +458,7 @@ fn build_app_state(
|
|||
settings: Arc<RwLock<Settings>>,
|
||||
registry_factory_override: Option<Box<RegistryFactoryOverride>>,
|
||||
max_concurrent_runs: usize,
|
||||
store: Arc<dyn Store>,
|
||||
store: StoreHandle,
|
||||
) -> Arc<AppState> {
|
||||
Arc::new(AppState {
|
||||
runs: Mutex::new(HashMap::new()),
|
||||
|
|
@ -728,7 +737,7 @@ async fn execute_run(state: Arc<AppState>, run_id: RunId) {
|
|||
cancel_token: Some(Arc::clone(&cancel_token)),
|
||||
emitter: Arc::clone(&emitter),
|
||||
interviewer: Arc::clone(&interviewer) as Arc<dyn Interviewer>,
|
||||
run_store: Arc::clone(&run_store),
|
||||
run_store: run_store.clone(),
|
||||
github_app,
|
||||
on_node: None,
|
||||
registry_override,
|
||||
|
|
@ -2521,10 +2530,10 @@ mod tests {
|
|||
.open_run_reader(&run_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("run store should exist")
|
||||
.get_run()
|
||||
.state()
|
||||
.await
|
||||
.unwrap()
|
||||
.run
|
||||
.expect("run record should exist");
|
||||
let mut expected_settings = settings;
|
||||
expected_settings.goal = Some("Test".to_string());
|
||||
|
|
@ -2656,16 +2665,11 @@ mod tests {
|
|||
assert_eq!(managed_run.status, RunStatus::Cancelled);
|
||||
drop(runs);
|
||||
|
||||
let run_store = state
|
||||
.store
|
||||
.open_run_reader(&run_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("run store should exist");
|
||||
let run_store = state.store.open_run_reader(&run_id).await.unwrap();
|
||||
|
||||
let mut status_record = None;
|
||||
for _ in 0..50 {
|
||||
if let Some(record) = run_store.get_status().await.unwrap() {
|
||||
if let Some(record) = run_store.state().await.unwrap().status {
|
||||
if record.status == fabro_workflow::run_status::RunStatus::Failed
|
||||
&& record.reason == Some(fabro_workflow::run_status::StatusReason::Cancelled)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,20 +1,9 @@
|
|||
use crate::NodeVisitRef;
|
||||
|
||||
pub(crate) const INIT_KEY: &str = "_init.json";
|
||||
pub(crate) const RUN_KEY: &str = "run.json";
|
||||
pub(crate) const START_KEY: &str = "start.json";
|
||||
pub(crate) const STATUS_KEY: &str = "status.json";
|
||||
pub(crate) const CHECKPOINT_KEY: &str = "checkpoint.json";
|
||||
pub(crate) const CONCLUSION_KEY: &str = "conclusion.json";
|
||||
pub(crate) const RETRO_KEY: &str = "retro.json";
|
||||
pub(crate) const GRAPH_KEY: &str = "graph.fabro";
|
||||
pub(crate) const SANDBOX_KEY: &str = "sandbox.json";
|
||||
pub(crate) const FINAL_PATCH_KEY: &str = "final.patch";
|
||||
pub(crate) const PULL_REQUEST_KEY: &str = "pull_request.json";
|
||||
pub(crate) const RETRO_PROMPT_KEY: &str = "retro/prompt.md";
|
||||
pub(crate) const RETRO_RESPONSE_KEY: &str = "retro/response.md";
|
||||
pub(crate) const EVENTS_PREFIX: &str = "events/";
|
||||
pub(crate) const CHECKPOINTS_PREFIX: &str = "checkpoints/";
|
||||
pub(crate) const ARTIFACT_VALUES_PREFIX: &str = "artifacts/values/";
|
||||
pub(crate) const ARTIFACT_NODES_PREFIX: &str = "artifacts/nodes/";
|
||||
|
||||
|
|
@ -22,46 +11,6 @@ pub(crate) fn init() -> &'static str {
|
|||
INIT_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn run() -> &'static str {
|
||||
RUN_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn start() -> &'static str {
|
||||
START_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn status() -> &'static str {
|
||||
STATUS_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn checkpoint() -> &'static str {
|
||||
CHECKPOINT_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn conclusion() -> &'static str {
|
||||
CONCLUSION_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn retro() -> &'static str {
|
||||
RETRO_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn graph() -> &'static str {
|
||||
GRAPH_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn sandbox() -> &'static str {
|
||||
SANDBOX_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn final_patch() -> &'static str {
|
||||
FINAL_PATCH_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn pull_request() -> &'static str {
|
||||
PULL_REQUEST_KEY
|
||||
}
|
||||
|
||||
pub(crate) fn node_visit_prefix(node: &NodeVisitRef<'_>) -> String {
|
||||
format!("nodes/{}/visit-{}", node.node_id, node.visit)
|
||||
}
|
||||
|
|
@ -122,10 +71,6 @@ pub(crate) fn event_key(seq: u32, epoch_ms: i64) -> String {
|
|||
format!("{EVENTS_PREFIX}{seq:06}-{epoch_ms}.json")
|
||||
}
|
||||
|
||||
pub(crate) fn checkpoint_history_key(seq: u32, epoch_ms: i64) -> String {
|
||||
format!("{CHECKPOINTS_PREFIX}{seq:04}-{epoch_ms}.json")
|
||||
}
|
||||
|
||||
pub(crate) fn artifact_value(artifact_id: &str) -> String {
|
||||
format!("{ARTIFACT_VALUES_PREFIX}{artifact_id}.json")
|
||||
}
|
||||
|
|
@ -145,10 +90,6 @@ pub(crate) fn parse_event_seq(key: &str) -> Option<u32> {
|
|||
parse_seq(key, EVENTS_PREFIX)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_checkpoint_seq(key: &str) -> Option<u32> {
|
||||
parse_seq(key, CHECKPOINTS_PREFIX)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_artifact_value_id(key: &str) -> Option<String> {
|
||||
key.strip_prefix(ARTIFACT_VALUES_PREFIX)
|
||||
.and_then(|s| s.strip_suffix(".json"))
|
||||
|
|
@ -181,10 +122,7 @@ mod tests {
|
|||
#[test]
|
||||
fn top_level_keys_match_spec() {
|
||||
assert_eq!(init(), "_init.json");
|
||||
assert_eq!(run(), "run.json");
|
||||
assert_eq!(graph(), "graph.fabro");
|
||||
assert_eq!(final_patch(), "final.patch");
|
||||
assert_eq!(pull_request(), "pull_request.json");
|
||||
assert_eq!(event_key(7, 123), "events/000007-123.json");
|
||||
assert_eq!(retro_prompt(), "retro/prompt.md");
|
||||
assert_eq!(retro_response(), "retro/response.md");
|
||||
}
|
||||
|
|
@ -224,7 +162,6 @@ mod tests {
|
|||
#[test]
|
||||
fn sequence_keys_are_zero_padded() {
|
||||
assert_eq!(event_key(7, 123), "events/000007-123.json");
|
||||
assert_eq!(checkpoint_history_key(42, 456), "checkpoints/0042-456.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -243,7 +180,6 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_helpers_extract_sequences_and_node_visits() {
|
||||
assert_eq!(parse_event_seq("events/000007-123.json"), Some(7));
|
||||
assert_eq!(parse_checkpoint_seq("checkpoints/0042-456.json"), Some(42));
|
||||
assert_eq!(
|
||||
parse_artifact_value_id("artifacts/values/summary.json"),
|
||||
Some("summary".to_string())
|
||||
|
|
@ -261,7 +197,6 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_helpers_reject_invalid_keys() {
|
||||
assert_eq!(parse_event_seq("events/not-a-seq.json"), None);
|
||||
assert_eq!(parse_checkpoint_seq("checkpoints/oops.json"), None);
|
||||
assert_eq!(
|
||||
parse_artifact_value_id("artifacts/values/summary.txt"),
|
||||
None
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::Stream;
|
||||
|
||||
mod error;
|
||||
mod keys;
|
||||
|
|
@ -15,139 +11,22 @@ mod slate;
|
|||
mod types;
|
||||
|
||||
pub use error::{Result, StoreError};
|
||||
pub use memory::InMemoryStore;
|
||||
pub use memory::{InMemoryRunStore, InMemoryStore};
|
||||
pub use run_state::{NodeState, RunState};
|
||||
pub use runtime::RuntimeState;
|
||||
pub use slate::SlateStore;
|
||||
pub use slate::{SlateRunStore, SlateStore};
|
||||
pub use types::{
|
||||
CatalogRecord, EventEnvelope, EventPayload, NodeSnapshot, NodeVisitRef, RunSnapshot, RunSummary,
|
||||
};
|
||||
|
||||
use fabro_types::{
|
||||
Checkpoint, Conclusion, NodeStatusRecord, Outcome, PullRequestRecord, Retro, RunId, RunRecord,
|
||||
RunStatusRecord, SandboxRecord, StageUsage, StartRecord,
|
||||
};
|
||||
use fabro_types::{Outcome, StageUsage};
|
||||
|
||||
pub type NodeOutcomeRecord = Outcome<Option<StageUsage>>;
|
||||
pub type StoreHandle = Arc<SlateStore>;
|
||||
pub type RunStoreHandle = Arc<SlateRunStore>;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct ListRunsQuery {
|
||||
pub start: Option<DateTime<Utc>>,
|
||||
pub end: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Store: Send + Sync {
|
||||
async fn create_run(
|
||||
&self,
|
||||
run_id: &RunId,
|
||||
created_at: DateTime<Utc>,
|
||||
run_dir: Option<&str>,
|
||||
) -> Result<Arc<dyn RunStore>>;
|
||||
async fn open_run(&self, run_id: &RunId) -> Result<Arc<dyn RunStore>>;
|
||||
async fn open_run_reader(&self, run_id: &RunId) -> Result<Arc<dyn RunStore>>;
|
||||
async fn list_runs(&self, query: &ListRunsQuery) -> Result<Vec<RunSummary>>;
|
||||
async fn delete_run(&self, run_id: &RunId) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait RunStore: Send + Sync {
|
||||
async fn put_run(&self, record: &RunRecord) -> Result<()>;
|
||||
async fn get_run(&self) -> Result<Option<RunRecord>>;
|
||||
|
||||
async fn put_start(&self, record: &StartRecord) -> Result<()>;
|
||||
async fn get_start(&self) -> Result<Option<StartRecord>>;
|
||||
|
||||
async fn put_status(&self, record: &RunStatusRecord) -> Result<()>;
|
||||
async fn get_status(&self) -> Result<Option<RunStatusRecord>>;
|
||||
|
||||
async fn put_checkpoint(&self, record: &Checkpoint) -> Result<()>;
|
||||
async fn get_checkpoint(&self) -> Result<Option<Checkpoint>>;
|
||||
async fn append_checkpoint(&self, record: &Checkpoint) -> Result<u32>;
|
||||
async fn list_checkpoints(&self) -> Result<Vec<(u32, Checkpoint)>>;
|
||||
|
||||
async fn put_conclusion(&self, record: &Conclusion) -> Result<()>;
|
||||
async fn get_conclusion(&self) -> Result<Option<Conclusion>>;
|
||||
|
||||
async fn put_retro(&self, retro: &Retro) -> Result<()>;
|
||||
async fn get_retro(&self) -> Result<Option<Retro>>;
|
||||
|
||||
async fn put_graph(&self, dot_source: &str) -> Result<()>;
|
||||
async fn get_graph(&self) -> Result<Option<String>>;
|
||||
|
||||
async fn put_sandbox(&self, record: &SandboxRecord) -> Result<()>;
|
||||
async fn get_sandbox(&self) -> Result<Option<SandboxRecord>>;
|
||||
|
||||
async fn put_node_prompt(&self, node: &NodeVisitRef<'_>, prompt: &str) -> Result<()>;
|
||||
async fn put_node_response(&self, node: &NodeVisitRef<'_>, response: &str) -> Result<()>;
|
||||
async fn put_node_status(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
status: &NodeStatusRecord,
|
||||
) -> Result<()>;
|
||||
async fn put_node_outcome(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
outcome: &NodeOutcomeRecord,
|
||||
) -> Result<()>;
|
||||
async fn put_node_provider_used(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
provider_used: &serde_json::Value,
|
||||
) -> Result<()>;
|
||||
async fn put_node_diff(&self, node: &NodeVisitRef<'_>, diff: &str) -> Result<()>;
|
||||
async fn put_node_script_invocation(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
invocation: &serde_json::Value,
|
||||
) -> Result<()>;
|
||||
async fn put_node_script_timing(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
timing: &serde_json::Value,
|
||||
) -> Result<()>;
|
||||
async fn put_node_parallel_results(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
results: &serde_json::Value,
|
||||
) -> Result<()>;
|
||||
async fn put_node_stdout(&self, node: &NodeVisitRef<'_>, log: &str) -> Result<()>;
|
||||
async fn put_node_stderr(&self, node: &NodeVisitRef<'_>, log: &str) -> Result<()>;
|
||||
|
||||
async fn get_node(&self, node: &NodeVisitRef<'_>) -> Result<NodeSnapshot>;
|
||||
async fn list_node_visits(&self, node_id: &str) -> Result<Vec<u32>>;
|
||||
async fn list_node_ids(&self) -> Result<Vec<String>>;
|
||||
|
||||
async fn put_final_patch(&self, patch: &str) -> Result<()>;
|
||||
async fn get_final_patch(&self) -> Result<Option<String>>;
|
||||
|
||||
async fn put_pull_request(&self, record: &PullRequestRecord) -> Result<()>;
|
||||
async fn get_pull_request(&self) -> Result<Option<PullRequestRecord>>;
|
||||
|
||||
async fn reset_for_rewind(&self) -> Result<()>;
|
||||
|
||||
async fn append_event(&self, payload: &EventPayload) -> Result<u32>;
|
||||
async fn list_events(&self) -> Result<Vec<EventEnvelope>>;
|
||||
async fn list_events_from(&self, seq: u32) -> Result<Vec<EventEnvelope>>;
|
||||
async fn watch_events_from(
|
||||
&self,
|
||||
seq: u32,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<EventEnvelope>> + Send>>>;
|
||||
|
||||
async fn put_retro_prompt(&self, text: &str) -> Result<()>;
|
||||
async fn get_retro_prompt(&self) -> Result<Option<String>>;
|
||||
async fn put_retro_response(&self, text: &str) -> Result<()>;
|
||||
async fn get_retro_response(&self) -> Result<Option<String>>;
|
||||
|
||||
async fn put_artifact_value(&self, artifact_id: &str, value: &serde_json::Value) -> Result<()>;
|
||||
async fn get_artifact_value(&self, artifact_id: &str) -> Result<Option<serde_json::Value>>;
|
||||
async fn list_artifact_values(&self) -> Result<Vec<String>>;
|
||||
|
||||
async fn put_asset(&self, node: &NodeVisitRef<'_>, filename: &str, data: &[u8]) -> Result<()>;
|
||||
async fn get_asset(&self, node: &NodeVisitRef<'_>, filename: &str) -> Result<Option<Bytes>>;
|
||||
async fn list_assets(&self, node: &NodeVisitRef<'_>) -> Result<Vec<String>>;
|
||||
async fn list_all_assets(&self) -> Result<Vec<(String, u32, String)>>;
|
||||
|
||||
async fn state(&self) -> Result<RunState>;
|
||||
async fn get_snapshot(&self) -> Result<Option<RunSnapshot>>;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -131,17 +131,20 @@ impl RunState {
|
|||
"run.failed" => {
|
||||
self.status = Some(run_status_record(RunStatus::Failed, &properties, ts)?);
|
||||
self.conclusion = Some(conclusion_from_failed(&properties, ts));
|
||||
self.last_git_sha =
|
||||
optional_string(&properties, "git_commit_sha").or_else(|| self.last_git_sha.clone());
|
||||
self.last_git_sha = optional_string(&properties, "git_commit_sha")
|
||||
.or_else(|| self.last_git_sha.clone());
|
||||
}
|
||||
"run.rewound" => {
|
||||
self.reset_for_rewind();
|
||||
self.last_git_sha =
|
||||
optional_string(&properties, "run_commit_sha").or_else(|| self.last_git_sha.clone());
|
||||
self.last_git_sha = optional_string(&properties, "run_commit_sha")
|
||||
.or_else(|| self.last_git_sha.clone());
|
||||
}
|
||||
"checkpoint.completed" => {
|
||||
let checkpoint = checkpoint_from_properties(&properties, ts)?;
|
||||
self.last_git_sha = checkpoint.git_commit_sha.clone().or_else(|| self.last_git_sha.clone());
|
||||
self.last_git_sha = checkpoint
|
||||
.git_commit_sha
|
||||
.clone()
|
||||
.or_else(|| self.last_git_sha.clone());
|
||||
if let Some(node_id) = value.get("node_id").and_then(Value::as_str) {
|
||||
let visit = checkpoint
|
||||
.node_visits
|
||||
|
|
@ -270,57 +273,6 @@ impl RunState {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn merge_legacy(
|
||||
&mut self,
|
||||
snapshot: Option<RunSnapshot>,
|
||||
graph_source: Option<String>,
|
||||
retro_prompt: Option<String>,
|
||||
retro_response: Option<String>,
|
||||
checkpoints: Vec<(u32, Checkpoint)>,
|
||||
) {
|
||||
let Some(snapshot) = snapshot else {
|
||||
self.graph_source = self.graph_source.clone().or(graph_source);
|
||||
self.retro_prompt = self.retro_prompt.clone().or(retro_prompt);
|
||||
self.retro_response = self.retro_response.clone().or(retro_response);
|
||||
if self.checkpoints.is_empty() {
|
||||
self.checkpoints = checkpoints;
|
||||
}
|
||||
if self.checkpoint.is_none() {
|
||||
self.checkpoint = self.checkpoints.last().map(|(_, checkpoint)| checkpoint.clone());
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if self.run.is_none() {
|
||||
self.run = Some(snapshot.run);
|
||||
}
|
||||
self.start = self.start.clone().or(snapshot.start);
|
||||
self.status = self.status.clone().or(snapshot.status);
|
||||
self.checkpoint = self.checkpoint.clone().or(snapshot.checkpoint);
|
||||
self.conclusion = self.conclusion.clone().or(snapshot.conclusion);
|
||||
self.retro = self.retro.clone().or(snapshot.retro);
|
||||
self.graph_source = self.graph_source.clone().or(snapshot.graph).or(graph_source);
|
||||
self.sandbox = self.sandbox.clone().or(snapshot.sandbox);
|
||||
self.final_patch = self.final_patch.clone().or(snapshot.final_patch);
|
||||
self.pull_request = self.pull_request.clone().or(snapshot.pull_request);
|
||||
self.retro_prompt = self.retro_prompt.clone().or(retro_prompt);
|
||||
self.retro_response = self.retro_response.clone().or(retro_response);
|
||||
if self.checkpoints.is_empty() {
|
||||
self.checkpoints = checkpoints;
|
||||
}
|
||||
if self.checkpoint.is_none() {
|
||||
self.checkpoint = self.checkpoints.last().map(|(_, checkpoint)| checkpoint.clone());
|
||||
}
|
||||
|
||||
for node in snapshot.nodes {
|
||||
let entry = self
|
||||
.nodes
|
||||
.entry((node.node_id.clone(), node.visit))
|
||||
.or_default();
|
||||
merge_node_snapshot(entry, node);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node(&self, node: &NodeVisitRef<'_>) -> Option<&NodeState> {
|
||||
self.nodes.get(&(node.node_id.to_string(), node.visit))
|
||||
}
|
||||
|
|
@ -355,21 +307,23 @@ impl RunState {
|
|||
let nodes = node_keys
|
||||
.into_iter()
|
||||
.filter_map(|(node_id, visit)| {
|
||||
self.nodes.get(&(node_id.clone(), visit)).map(|node| NodeSnapshot {
|
||||
node_id,
|
||||
visit,
|
||||
prompt: node.prompt.clone(),
|
||||
response: node.response.clone(),
|
||||
status: node.status.clone(),
|
||||
outcome: node.outcome.clone(),
|
||||
provider_used: node.provider_used.clone(),
|
||||
diff: node.diff.clone(),
|
||||
script_invocation: node.script_invocation.clone(),
|
||||
script_timing: node.script_timing.clone(),
|
||||
parallel_results: node.parallel_results.clone(),
|
||||
stdout: node.stdout.clone(),
|
||||
stderr: node.stderr.clone(),
|
||||
})
|
||||
self.nodes
|
||||
.get(&(node_id.clone(), visit))
|
||||
.map(|node| NodeSnapshot {
|
||||
node_id,
|
||||
visit,
|
||||
prompt: node.prompt.clone(),
|
||||
response: node.response.clone(),
|
||||
status: node.status.clone(),
|
||||
outcome: node.outcome.clone(),
|
||||
provider_used: node.provider_used.clone(),
|
||||
diff: node.diff.clone(),
|
||||
script_invocation: node.script_invocation.clone(),
|
||||
script_timing: node.script_timing.clone(),
|
||||
parallel_results: node.parallel_results.clone(),
|
||||
stdout: node.stdout.clone(),
|
||||
stderr: node.stderr.clone(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
@ -417,7 +371,10 @@ impl RunState {
|
|||
start_time: self.start.as_ref().map(|start| start.start_time),
|
||||
status: self.status.as_ref().map(|status| status.status),
|
||||
status_reason: self.status.as_ref().and_then(|status| status.reason),
|
||||
duration_ms: self.conclusion.as_ref().map(|conclusion| conclusion.duration_ms),
|
||||
duration_ms: self
|
||||
.conclusion
|
||||
.as_ref()
|
||||
.map(|conclusion| conclusion.duration_ms),
|
||||
total_cost: self
|
||||
.conclusion
|
||||
.as_ref()
|
||||
|
|
@ -426,9 +383,7 @@ impl RunState {
|
|||
}
|
||||
|
||||
fn node_mut(&mut self, node_id: &str, visit: u32) -> &mut NodeState {
|
||||
self.nodes
|
||||
.entry((node_id.to_string(), visit))
|
||||
.or_default()
|
||||
self.nodes.entry((node_id.to_string(), visit)).or_default()
|
||||
}
|
||||
|
||||
fn current_visit_for(&self, node_id: &str) -> Option<u32> {
|
||||
|
|
@ -454,20 +409,6 @@ impl RunState {
|
|||
}
|
||||
}
|
||||
|
||||
fn merge_node_snapshot(node: &mut NodeState, snapshot: NodeSnapshot) {
|
||||
node.prompt = node.prompt.clone().or(snapshot.prompt);
|
||||
node.response = node.response.clone().or(snapshot.response);
|
||||
node.status = node.status.clone().or(snapshot.status);
|
||||
node.outcome = node.outcome.clone().or(snapshot.outcome);
|
||||
node.provider_used = node.provider_used.clone().or(snapshot.provider_used);
|
||||
node.diff = node.diff.clone().or(snapshot.diff);
|
||||
node.script_invocation = node.script_invocation.clone().or(snapshot.script_invocation);
|
||||
node.script_timing = node.script_timing.clone().or(snapshot.script_timing);
|
||||
node.parallel_results = node.parallel_results.clone().or(snapshot.parallel_results);
|
||||
node.stdout = node.stdout.clone().or(snapshot.stdout);
|
||||
node.stderr = node.stderr.clone().or(snapshot.stderr);
|
||||
}
|
||||
|
||||
fn parse_ts(value: &Value) -> Result<DateTime<Utc>> {
|
||||
let ts = value
|
||||
.get("ts")
|
||||
|
|
@ -493,7 +434,9 @@ fn required_string(properties: &serde_json::Map<String, Value>, key: &str) -> Re
|
|||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.ok_or_else(|| StoreError::InvalidEvent(format!("event missing string property {key}")).into())
|
||||
.ok_or_else(|| {
|
||||
StoreError::InvalidEvent(format!("event missing string property {key}")).into()
|
||||
})
|
||||
}
|
||||
|
||||
fn optional_string(properties: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
|
||||
|
|
@ -504,10 +447,9 @@ fn optional_string(properties: &serde_json::Map<String, Value>, key: &str) -> Op
|
|||
}
|
||||
|
||||
fn required_u64(properties: &serde_json::Map<String, Value>, key: &str) -> Result<u64> {
|
||||
properties
|
||||
.get(key)
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| StoreError::InvalidEvent(format!("event missing integer property {key}")).into())
|
||||
properties.get(key).and_then(Value::as_u64).ok_or_else(|| {
|
||||
StoreError::InvalidEvent(format!("event missing integer property {key}")).into()
|
||||
})
|
||||
}
|
||||
|
||||
fn required_u32(properties: &serde_json::Map<String, Value>, key: &str) -> Result<u32> {
|
||||
|
|
@ -604,9 +546,9 @@ fn conclusion_from_completed(
|
|||
let usage = optional_json::<fabro_types::StageUsage>(properties, "usage")?;
|
||||
Ok(Conclusion {
|
||||
timestamp,
|
||||
status: StageStatus::from_str(&required_string(properties, "status")?).map_err(
|
||||
|err| StoreError::InvalidEvent(format!("invalid completed stage status: {err}")),
|
||||
)?,
|
||||
status: StageStatus::from_str(&required_string(properties, "status")?).map_err(|err| {
|
||||
StoreError::InvalidEvent(format!("invalid completed stage status: {err}"))
|
||||
})?,
|
||||
duration_ms: required_u64(properties, "duration_ms")?,
|
||||
failure_reason: None,
|
||||
final_git_commit_sha: optional_string(properties, "final_git_commit_sha"),
|
||||
|
|
@ -656,7 +598,11 @@ fn conclusion_from_failed(
|
|||
}
|
||||
}
|
||||
|
||||
fn stage_visit(node_id: &str, properties: &serde_json::Map<String, Value>, state: &RunState) -> Option<u32> {
|
||||
fn stage_visit(
|
||||
node_id: &str,
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
state: &RunState,
|
||||
) -> Option<u32> {
|
||||
properties
|
||||
.get("node_visits")
|
||||
.and_then(|value| serde_json::from_value::<HashMap<String, usize>>(value.clone()).ok())
|
||||
|
|
@ -668,9 +614,8 @@ fn stage_visit(node_id: &str, properties: &serde_json::Map<String, Value>, state
|
|||
fn stage_outcome_from_properties(
|
||||
properties: &serde_json::Map<String, Value>,
|
||||
) -> Result<NodeOutcomeRecord> {
|
||||
let status = StageStatus::from_str(&required_string(properties, "status")?).map_err(|err| {
|
||||
StoreError::InvalidEvent(format!("invalid stage status: {err}"))
|
||||
})?;
|
||||
let status = StageStatus::from_str(&required_string(properties, "status")?)
|
||||
.map_err(|err| StoreError::InvalidEvent(format!("invalid stage status: {err}")))?;
|
||||
Ok(Outcome {
|
||||
status,
|
||||
preferred_label: optional_string(properties, "preferred_label"),
|
||||
|
|
@ -685,11 +630,17 @@ fn stage_outcome_from_properties(
|
|||
})
|
||||
}
|
||||
|
||||
fn node_status_from_outcome(outcome: &NodeOutcomeRecord, timestamp: DateTime<Utc>) -> NodeStatusRecord {
|
||||
fn node_status_from_outcome(
|
||||
outcome: &NodeOutcomeRecord,
|
||||
timestamp: DateTime<Utc>,
|
||||
) -> NodeStatusRecord {
|
||||
NodeStatusRecord {
|
||||
status: outcome.status.clone(),
|
||||
notes: outcome.notes.clone(),
|
||||
failure_reason: outcome.failure.as_ref().map(|failure| failure.message.clone()),
|
||||
failure_reason: outcome
|
||||
.failure
|
||||
.as_ref()
|
||||
.map(|failure| failure.message.clone()),
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ use std::collections::HashMap;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::TryStreamExt;
|
||||
use object_store::ObjectStore;
|
||||
|
|
@ -15,9 +14,10 @@ use slatedb::config::{DbReaderOptions, Settings};
|
|||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::keys;
|
||||
use crate::{CatalogRecord, ListRunsQuery, Result, RunStore, RunSummary, Store, StoreError};
|
||||
use crate::{CatalogRecord, ListRunsQuery, Result, RunStoreHandle, RunSummary, StoreError};
|
||||
use fabro_types::RunId;
|
||||
use run_store::{SlateRunStore, SlateRunStoreInner};
|
||||
pub use run_store::SlateRunStore;
|
||||
use run_store::SlateRunStoreInner;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlateStore {
|
||||
|
|
@ -173,14 +173,13 @@ impl SlateStore {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Store for SlateStore {
|
||||
async fn create_run(
|
||||
impl SlateStore {
|
||||
pub async fn create_run(
|
||||
&self,
|
||||
run_id: &RunId,
|
||||
created_at: DateTime<Utc>,
|
||||
run_dir: Option<&str>,
|
||||
) -> Result<Arc<dyn RunStore>> {
|
||||
) -> Result<RunStoreHandle> {
|
||||
let locator =
|
||||
catalog::read_locator(self.object_store.clone(), &self.base_prefix, run_id).await?;
|
||||
if let Some(active) = self.get_active_run(run_id).await {
|
||||
|
|
@ -201,7 +200,7 @@ impl Store for SlateStore {
|
|||
run_dir,
|
||||
)
|
||||
.await?;
|
||||
return Ok(Arc::new(active) as Arc<dyn RunStore>);
|
||||
return Ok(Arc::new(active));
|
||||
}
|
||||
|
||||
let db_prefix = match locator {
|
||||
|
|
@ -233,10 +232,10 @@ impl Store for SlateStore {
|
|||
run_dir,
|
||||
)
|
||||
.await?;
|
||||
Ok(Arc::new(run_store) as Arc<dyn RunStore>)
|
||||
Ok(Arc::new(run_store))
|
||||
}
|
||||
|
||||
async fn open_run(&self, run_id: &RunId) -> Result<Arc<dyn RunStore>> {
|
||||
pub async fn open_run(&self, run_id: &RunId) -> Result<RunStoreHandle> {
|
||||
let locator = catalog::read_locator(self.object_store.clone(), &self.base_prefix, run_id)
|
||||
.await?
|
||||
.ok_or_else(|| StoreError::RunNotFound(run_id.to_string()))?;
|
||||
|
|
@ -245,10 +244,10 @@ impl Store for SlateStore {
|
|||
.open_run_store(&locator)
|
||||
.await?
|
||||
.ok_or_else(|| StoreError::RunNotFound(run_id.to_string()))?;
|
||||
Ok(Arc::new(run_store) as Arc<dyn RunStore>)
|
||||
Ok(Arc::new(run_store))
|
||||
}
|
||||
|
||||
async fn open_run_reader(&self, run_id: &RunId) -> Result<Arc<dyn RunStore>> {
|
||||
pub async fn open_run_reader(&self, run_id: &RunId) -> Result<RunStoreHandle> {
|
||||
let locator = catalog::read_locator(self.object_store.clone(), &self.base_prefix, run_id)
|
||||
.await?
|
||||
.ok_or_else(|| StoreError::RunNotFound(run_id.to_string()))?;
|
||||
|
|
@ -257,10 +256,10 @@ impl Store for SlateStore {
|
|||
.open_run_reader_store(&locator)
|
||||
.await?
|
||||
.ok_or_else(|| StoreError::RunNotFound(run_id.to_string()))?;
|
||||
Ok(Arc::new(run_store) as Arc<dyn RunStore>)
|
||||
Ok(Arc::new(run_store))
|
||||
}
|
||||
|
||||
async fn list_runs(&self, query: &ListRunsQuery) -> Result<Vec<RunSummary>> {
|
||||
pub async fn list_runs(&self, query: &ListRunsQuery) -> Result<Vec<RunSummary>> {
|
||||
let catalogs =
|
||||
catalog::list_catalogs(self.object_store.clone(), &self.base_prefix, query).await?;
|
||||
let mut summaries = Vec::new();
|
||||
|
|
@ -293,7 +292,7 @@ impl Store for SlateStore {
|
|||
Ok(summaries)
|
||||
}
|
||||
|
||||
async fn delete_run(&self, run_id: &RunId) -> Result<()> {
|
||||
pub async fn delete_run(&self, run_id: &RunId) -> Result<()> {
|
||||
let active = self.remove_active_run(run_id).await;
|
||||
let active_record = active.as_ref().map(SlateRunStore::record);
|
||||
if let Some(active) = &active {
|
||||
|
|
@ -384,8 +383,8 @@ mod tests {
|
|||
|
||||
use bytes::Bytes;
|
||||
use fabro_types::{
|
||||
AttrValue, Checkpoint, Conclusion, Graph, NodeStatusRecord, RunId, RunRecord, RunStatus,
|
||||
RunStatusRecord, Settings, StageStatus, StartRecord, StatusReason, fixtures,
|
||||
AttrValue, Graph, NodeStatusRecord, RunId, RunRecord, RunStatus, Settings, StageStatus,
|
||||
StatusReason, fixtures,
|
||||
};
|
||||
use object_store::memory::InMemory;
|
||||
use slatedb::config::Settings as SlateSettings;
|
||||
|
|
@ -433,58 +432,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn sample_start_record(run_id: &str, created_at: DateTime<Utc>) -> StartRecord {
|
||||
StartRecord {
|
||||
run_id: test_run_id(run_id),
|
||||
start_time: created_at + chrono::Duration::seconds(5),
|
||||
run_branch: Some("fabro/run/demo".to_string()),
|
||||
base_sha: Some("abc123".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_status(status: RunStatus, reason: Option<StatusReason>) -> RunStatusRecord {
|
||||
RunStatusRecord {
|
||||
status,
|
||||
reason,
|
||||
updated_at: dt("2026-03-27T12:05:00Z"),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_checkpoint() -> Checkpoint {
|
||||
Checkpoint {
|
||||
timestamp: dt("2026-03-27T12:10:00Z"),
|
||||
current_node: "code".to_string(),
|
||||
completed_nodes: vec!["plan".to_string()],
|
||||
node_retries: std::collections::HashMap::from([("code".to_string(), 1)]),
|
||||
context_values: std::collections::HashMap::new(),
|
||||
node_outcomes: std::collections::HashMap::new(),
|
||||
next_node_id: Some("review".to_string()),
|
||||
git_commit_sha: Some("def456".to_string()),
|
||||
loop_failure_signatures: std::collections::HashMap::new(),
|
||||
restart_failure_signatures: std::collections::HashMap::new(),
|
||||
node_visits: std::collections::HashMap::from([("code".to_string(), 2)]),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_conclusion() -> Conclusion {
|
||||
Conclusion {
|
||||
timestamp: dt("2026-03-27T12:15:00Z"),
|
||||
status: StageStatus::Success,
|
||||
duration_ms: 3210,
|
||||
failure_reason: None,
|
||||
final_git_commit_sha: Some("feedbeef".to_string()),
|
||||
stages: Vec::new(),
|
||||
total_cost: Some(1.25),
|
||||
total_retries: 2,
|
||||
total_input_tokens: 10,
|
||||
total_output_tokens: 20,
|
||||
total_cache_read_tokens: 30,
|
||||
total_cache_write_tokens: 40,
|
||||
total_reasoning_tokens: 50,
|
||||
has_pricing: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_node_status() -> NodeStatusRecord {
|
||||
NodeStatusRecord {
|
||||
status: StageStatus::Success,
|
||||
|
|
@ -494,17 +441,24 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn event_payload(run_id: &str, ts: &str, event: &str) -> EventPayload {
|
||||
EventPayload::new(
|
||||
serde_json::json!({
|
||||
"id": format!("evt-{run_id}-{event}"),
|
||||
"ts": ts,
|
||||
"run_id": test_run_id(run_id).to_string(),
|
||||
"event": event
|
||||
}),
|
||||
&test_run_id(run_id),
|
||||
)
|
||||
.unwrap()
|
||||
fn event_payload(
|
||||
run_id: &str,
|
||||
ts: &str,
|
||||
event: &str,
|
||||
node_id: Option<&str>,
|
||||
properties: serde_json::Value,
|
||||
) -> EventPayload {
|
||||
let mut value = serde_json::json!({
|
||||
"id": format!("evt-{run_id}-{event}"),
|
||||
"ts": ts,
|
||||
"run_id": test_run_id(run_id).to_string(),
|
||||
"event": event,
|
||||
"properties": properties,
|
||||
});
|
||||
if let Some(node_id) = node_id {
|
||||
value["node_id"] = serde_json::Value::String(node_id.to_string());
|
||||
}
|
||||
EventPayload::new(value, &test_run_id(run_id)).unwrap()
|
||||
}
|
||||
|
||||
async fn list_paths(store: Arc<dyn ObjectStore>, prefix: &str) -> Vec<String> {
|
||||
|
|
@ -552,19 +506,51 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
run.put_run(&sample_run_record("run-1", created_at))
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_start(&sample_start_record("run-1", created_at))
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_status(&sample_status(
|
||||
RunStatus::Succeeded,
|
||||
Some(StatusReason::Completed),
|
||||
let run_record = sample_run_record("run-1", created_at);
|
||||
run.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:00Z",
|
||||
"run.created",
|
||||
None,
|
||||
serde_json::json!({
|
||||
"settings": run_record.settings,
|
||||
"graph": run_record.graph,
|
||||
"workflow_slug": run_record.workflow_slug,
|
||||
"working_directory": run_record.working_directory,
|
||||
"host_repo_path": run_record.host_repo_path,
|
||||
"base_branch": run_record.base_branch,
|
||||
"labels": run_record.labels,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:01Z",
|
||||
"run.started",
|
||||
None,
|
||||
serde_json::json!({
|
||||
"run_branch": "fabro/run/demo",
|
||||
"base_sha": "abc123",
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:02Z",
|
||||
"run.completed",
|
||||
None,
|
||||
serde_json::json!({
|
||||
"duration_ms": 3210,
|
||||
"artifact_count": 1,
|
||||
"status": "success",
|
||||
"reason": "completed",
|
||||
"total_cost": 1.25,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_conclusion(&sample_conclusion()).await.unwrap();
|
||||
|
||||
let by_id = catalog::by_id_path("runs/", &test_run_id("run-1"));
|
||||
let by_start = catalog::by_start_path("runs/", created_at, &test_run_id("run-1"));
|
||||
|
|
@ -579,20 +565,12 @@ mod tests {
|
|||
assert_eq!(summary[0].status, Some(RunStatus::Succeeded));
|
||||
assert_eq!(summary[0].status_reason, Some(StatusReason::Completed));
|
||||
|
||||
let reopened = store
|
||||
.open_run(&test_run_id("run-1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let stored = reopened.get_run().await.unwrap().unwrap();
|
||||
let reopened = store.open_run(&test_run_id("run-1")).await.unwrap();
|
||||
let stored = reopened.state().await.unwrap().run.unwrap();
|
||||
assert_eq!(stored.run_id, test_run_id("run-1"));
|
||||
|
||||
store.delete_run(&test_run_id("run-1")).await.unwrap();
|
||||
assert!(
|
||||
store
|
||||
.open_run(&test_run_id("run-1"))
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(store.open_run(&test_run_id("run-1")).await.is_err());
|
||||
assert!(!object_exists(object_store.clone(), &by_id).await);
|
||||
assert!(!object_exists(object_store.clone(), &by_start).await);
|
||||
assert!(list_paths(object_store, "runs/db").await.is_empty());
|
||||
|
|
@ -611,8 +589,23 @@ mod tests {
|
|||
|
||||
let db = seed_db(object_store.clone(), &record, true).await;
|
||||
db.put(
|
||||
keys::run(),
|
||||
serde_json::to_vec(&sample_run_record("run-1", created_at)).unwrap(),
|
||||
keys::event_key(1, created_at.timestamp_millis()),
|
||||
serde_json::to_vec(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:00Z",
|
||||
"run.created",
|
||||
None,
|
||||
serde_json::json!({
|
||||
"settings": sample_run_record("run-1", created_at).settings,
|
||||
"graph": sample_run_record("run-1", created_at).graph,
|
||||
"workflow_slug": sample_run_record("run-1", created_at).workflow_slug,
|
||||
"working_directory": sample_run_record("run-1", created_at).working_directory,
|
||||
"host_repo_path": sample_run_record("run-1", created_at).host_repo_path,
|
||||
"base_branch": sample_run_record("run-1", created_at).base_branch,
|
||||
"labels": sample_run_record("run-1", created_at).labels,
|
||||
}),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -626,12 +619,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
store
|
||||
.open_run(&test_run_id("run-1"))
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
assert!(store.open_run(&test_run_id("run-1")).await.is_ok());
|
||||
assert!(
|
||||
store
|
||||
.list_runs(&ListRunsQuery::default())
|
||||
|
|
@ -653,43 +641,63 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reopen_recovers_event_and_checkpoint_sequences() {
|
||||
async fn reopen_recovers_event_sequences() {
|
||||
let (_object_store, store) = make_store();
|
||||
let created_at = dt("2026-03-27T12:00:00Z");
|
||||
let run = store
|
||||
.create_run(&test_run_id("run-1"), created_at, None)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_run(&sample_run_record("run-1", created_at))
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event_payload("run-1", "2026-03-27T12:00:00Z", "Started"))
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event_payload("run-1", "2026-03-27T12:00:01Z", "Next"))
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_checkpoint(&sample_checkpoint()).await.unwrap();
|
||||
let run_record = sample_run_record("run-1", created_at);
|
||||
run.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:00Z",
|
||||
"run.created",
|
||||
None,
|
||||
serde_json::json!({
|
||||
"settings": run_record.settings,
|
||||
"graph": run_record.graph,
|
||||
"workflow_slug": run_record.workflow_slug,
|
||||
"working_directory": run_record.working_directory,
|
||||
"host_repo_path": run_record.host_repo_path,
|
||||
"base_branch": run_record.base_branch,
|
||||
"labels": run_record.labels,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:01Z",
|
||||
"Started",
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:02Z",
|
||||
"Next",
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
drop(run);
|
||||
|
||||
let reopened = store
|
||||
.open_run(&test_run_id("run-1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let reopened = store.open_run(&test_run_id("run-1")).await.unwrap();
|
||||
let next_event = reopened
|
||||
.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:02Z",
|
||||
"AfterReopen",
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let next_checkpoint = reopened
|
||||
.append_checkpoint(&sample_checkpoint())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(next_event, 3);
|
||||
assert_eq!(next_checkpoint, 2);
|
||||
assert_eq!(next_event, 4);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -720,12 +728,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
store
|
||||
.open_run(&test_run_id("run-1"))
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(store.open_run(&test_run_id("run-1")).await.is_err());
|
||||
assert!(
|
||||
store
|
||||
.list_runs(&ListRunsQuery::default())
|
||||
|
|
@ -766,35 +769,51 @@ mod tests {
|
|||
.create_run(&test_run_id("run-1"), created_at, None)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_run(&sample_run_record("run-1", created_at))
|
||||
.await
|
||||
.unwrap();
|
||||
let run_record = sample_run_record("run-1", created_at);
|
||||
run.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:00Z",
|
||||
"run.created",
|
||||
None,
|
||||
serde_json::json!({
|
||||
"settings": run_record.settings,
|
||||
"graph": run_record.graph,
|
||||
"workflow_slug": run_record.workflow_slug,
|
||||
"working_directory": run_record.working_directory,
|
||||
"host_repo_path": run_record.host_repo_path,
|
||||
"base_branch": run_record.base_branch,
|
||||
"labels": run_record.labels,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let listed = store.list_runs(&ListRunsQuery::default()).await.unwrap();
|
||||
assert_eq!(listed.len(), 1);
|
||||
|
||||
let reopened = store
|
||||
.open_run(&test_run_id("run-1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let reopened = store.open_run(&test_run_id("run-1")).await.unwrap();
|
||||
let first_event = run
|
||||
.append_event(&event_payload("run-1", "2026-03-27T12:00:00Z", "Started"))
|
||||
.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:00Z",
|
||||
"Started",
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let second_event = reopened
|
||||
.append_event(&event_payload("run-1", "2026-03-27T12:00:01Z", "Continued"))
|
||||
.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:01Z",
|
||||
"Continued",
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let first_checkpoint = run.append_checkpoint(&sample_checkpoint()).await.unwrap();
|
||||
let second_checkpoint = reopened
|
||||
.append_checkpoint(&sample_checkpoint())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(first_event, 1);
|
||||
assert_eq!(second_event, 2);
|
||||
assert_eq!(first_checkpoint, 1);
|
||||
assert_eq!(second_checkpoint, 2);
|
||||
assert_eq!(first_event, 2);
|
||||
assert_eq!(second_event, 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -807,9 +826,15 @@ mod tests {
|
|||
.unwrap();
|
||||
let mut stream = run.watch_events_from(1).await.unwrap();
|
||||
|
||||
run.append_event(&event_payload("run-1", "2026-03-27T12:00:00Z", "Started"))
|
||||
.await
|
||||
.unwrap();
|
||||
run.append_event(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:00Z",
|
||||
"Started",
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let event = timeout(
|
||||
Duration::from_secs(2),
|
||||
|
|
@ -834,8 +859,23 @@ mod tests {
|
|||
};
|
||||
let db = seed_db(object_store.clone(), &record, true).await;
|
||||
db.put(
|
||||
keys::run(),
|
||||
serde_json::to_vec(&sample_run_record("run-1", created_at)).unwrap(),
|
||||
keys::event_key(1, created_at.timestamp_millis()),
|
||||
serde_json::to_vec(&event_payload(
|
||||
"run-1",
|
||||
"2026-03-27T12:00:00Z",
|
||||
"run.created",
|
||||
None,
|
||||
serde_json::json!({
|
||||
"settings": sample_run_record("run-1", created_at).settings,
|
||||
"graph": sample_run_record("run-1", created_at).graph,
|
||||
"workflow_slug": sample_run_record("run-1", created_at).workflow_slug,
|
||||
"working_directory": sample_run_record("run-1", created_at).working_directory,
|
||||
"host_repo_path": sample_run_record("run-1", created_at).host_repo_path,
|
||||
"base_branch": sample_run_record("run-1", created_at).base_branch,
|
||||
"labels": sample_run_record("run-1", created_at).labels,
|
||||
}),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -861,23 +901,21 @@ mod tests {
|
|||
.create_run(&test_run_id("run-1"), created_at, None)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_run(&sample_run_record("run-1", created_at))
|
||||
run.put_artifact_value("summary", &serde_json::json!({"done": true}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
store.delete_run(&test_run_id("run-1")).await.unwrap();
|
||||
|
||||
let err = run.put_graph("digraph night_sky {}").await.unwrap_err();
|
||||
let err = run
|
||||
.put_artifact_value("summary", &serde_json::json!({"done": false}))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
StoreError::Slate(err) if matches!(err.kind(), ErrorKind::Closed(CloseReason::Clean))
|
||||
));
|
||||
assert!(
|
||||
store
|
||||
.open_run(&test_run_id("run-1"))
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(store.open_run(&test_run_id("run-1")).await.is_err());
|
||||
assert!(list_paths(object_store, "runs").await.is_empty());
|
||||
}
|
||||
|
||||
|
|
@ -890,7 +928,7 @@ mod tests {
|
|||
.create_run(&test_run_id("run-1"), created_at, None)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_run(&sample_run_record("run-1", created_at))
|
||||
run.put_artifact_value("summary", &serde_json::json!({"done": true}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -927,14 +965,13 @@ mod tests {
|
|||
assert_ne!(orphan.db_prefix, new_prefix);
|
||||
|
||||
let db = seed_db(object_store.clone(), &orphan, true).await;
|
||||
db.put(keys::graph(), b"stale graph").await.unwrap();
|
||||
db.close().await.unwrap();
|
||||
|
||||
let run = store
|
||||
.create_run(&test_run_id("run-1"), new_created_at, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(run.get_graph().await.unwrap(), None);
|
||||
assert!(run.state().await.unwrap().graph_source.is_none());
|
||||
|
||||
let locator = catalog::read_locator(object_store, "runs/", &test_run_id("run-1"))
|
||||
.await
|
||||
|
|
@ -988,9 +1025,6 @@ mod tests {
|
|||
.create_run(&test_run_id("run-1"), created_at, None)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_run(&sample_run_record("run-1", created_at))
|
||||
.await
|
||||
.unwrap();
|
||||
let node = NodeVisitRef {
|
||||
node_id: "code",
|
||||
visit: 2,
|
||||
|
|
@ -1023,10 +1057,6 @@ mod tests {
|
|||
.create_run(&test_run_id("run-1"), created_at, None)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_run(&sample_run_record("run-1", created_at))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
run.put_artifact_value("summary", &serde_json::json!({"done": true}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -1067,8 +1097,7 @@ mod tests {
|
|||
]
|
||||
);
|
||||
|
||||
let snapshot = run.get_snapshot().await.unwrap().unwrap();
|
||||
assert_eq!(snapshot.nodes.len(), 1);
|
||||
assert_eq!(snapshot.nodes[0].node_id, "code");
|
||||
let code_node = run.get_node(&snapshot_node).await.unwrap();
|
||||
assert_eq!(code_node.node_id, "code");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
|||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::Stream;
|
||||
|
|
@ -18,18 +17,26 @@ use crate::keys;
|
|||
use crate::run_state::EventProjectionCache;
|
||||
use crate::{
|
||||
CatalogRecord, EventEnvelope, EventPayload, NodeOutcomeRecord, NodeSnapshot, NodeVisitRef,
|
||||
Result, RunSnapshot, RunState, RunStore, RunSummary, StoreError,
|
||||
};
|
||||
use fabro_types::{
|
||||
Checkpoint, Conclusion, NodeStatusRecord, PullRequestRecord, Retro, RunId, RunRecord,
|
||||
RunStatusRecord, SandboxRecord, StartRecord,
|
||||
Result, RunState, RunSummary, StoreError,
|
||||
};
|
||||
use fabro_types::{NodeStatusRecord, RunId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct SlateRunStore {
|
||||
pub struct SlateRunStore {
|
||||
inner: Arc<SlateRunStoreInner>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SlateRunStore {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SlateRunStore")
|
||||
.field("run_id", &self.inner.run_id)
|
||||
.field("created_at", &self.inner.created_at)
|
||||
.field("db_prefix", &self.inner.db_prefix)
|
||||
.field("run_dir", &self.inner.run_dir)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlateRunStoreInner {
|
||||
run_id: RunId,
|
||||
created_at: DateTime<Utc>,
|
||||
|
|
@ -37,7 +44,6 @@ pub(crate) struct SlateRunStoreInner {
|
|||
run_dir: Option<String>,
|
||||
db: SlateRunDb,
|
||||
event_seq: AtomicU32,
|
||||
checkpoint_seq: AtomicU32,
|
||||
close_lock: Mutex<()>,
|
||||
projection_cache: Mutex<EventProjectionCache>,
|
||||
}
|
||||
|
|
@ -50,8 +56,6 @@ enum SlateRunDb {
|
|||
impl SlateRunStore {
|
||||
pub(crate) async fn open_writer(record: CatalogRecord, db: slatedb::Db) -> Result<Self> {
|
||||
let event_seq = recover_next_seq(&db, keys::EVENTS_PREFIX, keys::parse_event_seq).await?;
|
||||
let checkpoint_seq =
|
||||
recover_next_seq(&db, keys::CHECKPOINTS_PREFIX, keys::parse_checkpoint_seq).await?;
|
||||
Ok(Self {
|
||||
inner: Arc::new(SlateRunStoreInner {
|
||||
run_id: record.run_id,
|
||||
|
|
@ -60,7 +64,6 @@ impl SlateRunStore {
|
|||
run_dir: record.run_dir,
|
||||
db: SlateRunDb::Writer(db),
|
||||
event_seq: AtomicU32::new(event_seq),
|
||||
checkpoint_seq: AtomicU32::new(checkpoint_seq),
|
||||
close_lock: Mutex::new(()),
|
||||
projection_cache: Mutex::new(EventProjectionCache::default()),
|
||||
}),
|
||||
|
|
@ -69,8 +72,6 @@ impl SlateRunStore {
|
|||
|
||||
pub(crate) async fn open_reader(record: CatalogRecord, db: DbReader) -> Result<Self> {
|
||||
let event_seq = recover_next_seq(&db, keys::EVENTS_PREFIX, keys::parse_event_seq).await?;
|
||||
let checkpoint_seq =
|
||||
recover_next_seq(&db, keys::CHECKPOINTS_PREFIX, keys::parse_checkpoint_seq).await?;
|
||||
Ok(Self {
|
||||
inner: Arc::new(SlateRunStoreInner {
|
||||
run_id: record.run_id,
|
||||
|
|
@ -79,7 +80,6 @@ impl SlateRunStore {
|
|||
run_dir: record.run_dir,
|
||||
db: SlateRunDb::Reader(Box::new(db)),
|
||||
event_seq: AtomicU32::new(event_seq),
|
||||
checkpoint_seq: AtomicU32::new(checkpoint_seq),
|
||||
close_lock: Mutex::new(()),
|
||||
projection_cache: Mutex::new(EventProjectionCache::default()),
|
||||
}),
|
||||
|
|
@ -148,91 +148,10 @@ impl SlateRunStore {
|
|||
R: DbRead + Sync,
|
||||
{
|
||||
let events = list_events_from(db, 1).await?;
|
||||
let mut state = RunState::apply_events(&events)?;
|
||||
let mut nodes = BTreeSet::new();
|
||||
let mut iter = db.scan_prefix(b"nodes/").await?;
|
||||
while let Some(entry) = iter.next().await? {
|
||||
let key = key_to_string(&entry.key)?;
|
||||
if let Some((node_id, visit, _)) = keys::parse_node_key(&key) {
|
||||
nodes.insert((node_id, visit));
|
||||
}
|
||||
}
|
||||
let snapshot = if let Some(run) = get_json::<_, RunRecord>(db, keys::run()).await? {
|
||||
let mut snapshot_nodes = Vec::new();
|
||||
for (node_id, visit) in nodes {
|
||||
let node = NodeVisitRef {
|
||||
node_id: &node_id,
|
||||
visit,
|
||||
};
|
||||
let prompt_key = keys::node_prompt(&node);
|
||||
let response_key = keys::node_response(&node);
|
||||
let status_key = keys::node_status(&node);
|
||||
let outcome_key = keys::node_outcome(&node);
|
||||
let provider_key = keys::node_provider_used(&node);
|
||||
let diff_key = keys::node_diff(&node);
|
||||
let invocation_key = keys::node_script_invocation(&node);
|
||||
let timing_key = keys::node_script_timing(&node);
|
||||
let results_key = keys::node_parallel_results(&node);
|
||||
let stdout_key = keys::node_stdout(&node);
|
||||
let stderr_key = keys::node_stderr(&node);
|
||||
snapshot_nodes.push(NodeSnapshot {
|
||||
node_id: node_id.clone(),
|
||||
visit,
|
||||
prompt: get_text(db, &prompt_key).await?,
|
||||
response: get_text(db, &response_key).await?,
|
||||
status: get_json(db, &status_key).await?,
|
||||
outcome: get_json(db, &outcome_key).await?,
|
||||
provider_used: get_json(db, &provider_key).await?,
|
||||
diff: get_text(db, &diff_key).await?,
|
||||
script_invocation: get_json(db, &invocation_key).await?,
|
||||
script_timing: get_json(db, &timing_key).await?,
|
||||
parallel_results: get_json(db, &results_key).await?,
|
||||
stdout: get_text(db, &stdout_key).await?,
|
||||
stderr: get_text(db, &stderr_key).await?,
|
||||
});
|
||||
}
|
||||
Some(RunSnapshot {
|
||||
run,
|
||||
start: get_json::<_, StartRecord>(db, keys::start()).await?,
|
||||
status: get_json::<_, RunStatusRecord>(db, keys::status()).await?,
|
||||
checkpoint: get_json::<_, Checkpoint>(db, keys::checkpoint()).await?,
|
||||
conclusion: get_json::<_, Conclusion>(db, keys::conclusion()).await?,
|
||||
retro: get_json::<_, Retro>(db, keys::retro()).await?,
|
||||
graph: get_text(db, keys::graph()).await?,
|
||||
sandbox: get_json::<_, SandboxRecord>(db, keys::sandbox()).await?,
|
||||
final_patch: get_text(db, keys::final_patch()).await?,
|
||||
pull_request: get_json::<_, PullRequestRecord>(db, keys::pull_request()).await?,
|
||||
nodes: snapshot_nodes,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
state.merge_legacy(
|
||||
snapshot,
|
||||
get_text(db, keys::graph()).await?,
|
||||
get_text(db, keys::retro_prompt()).await?,
|
||||
get_text(db, keys::retro_response()).await?,
|
||||
list_checkpoints(db).await?,
|
||||
);
|
||||
let state = RunState::apply_events(&events)?;
|
||||
Ok(state.build_summary(catalog))
|
||||
}
|
||||
|
||||
fn validate_run_record(&self, record: &RunRecord) -> Result<()> {
|
||||
if record.created_at != self.inner.created_at {
|
||||
return Err(StoreError::Other(format!(
|
||||
"run record created_at {:?} does not match store created_at {:?}",
|
||||
record.created_at, self.inner.created_at
|
||||
)));
|
||||
}
|
||||
if record.run_id != self.inner.run_id {
|
||||
return Err(StoreError::Other(format!(
|
||||
"run record run_id {:?} does not match store run_id {:?}",
|
||||
record.run_id, self.inner.run_id
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn build_node_snapshot(&self, node: &NodeVisitRef<'_>) -> Result<NodeSnapshot> {
|
||||
Ok(NodeSnapshot {
|
||||
node_id: node.node_id.to_string(),
|
||||
|
|
@ -282,105 +201,22 @@ impl SlateRunStore {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RunStore for SlateRunStore {
|
||||
async fn put_run(&self, record: &RunRecord) -> Result<()> {
|
||||
self.validate_run_record(record)?;
|
||||
self.inner.db.put_json(keys::run(), record).await
|
||||
}
|
||||
|
||||
async fn get_run(&self) -> Result<Option<RunRecord>> {
|
||||
self.inner.db.get_json(keys::run()).await
|
||||
}
|
||||
|
||||
async fn put_start(&self, record: &StartRecord) -> Result<()> {
|
||||
self.inner.db.put_json(keys::start(), record).await
|
||||
}
|
||||
|
||||
async fn get_start(&self) -> Result<Option<StartRecord>> {
|
||||
self.inner.db.get_json(keys::start()).await
|
||||
}
|
||||
|
||||
async fn put_status(&self, record: &RunStatusRecord) -> Result<()> {
|
||||
self.inner.db.put_json(keys::status(), record).await
|
||||
}
|
||||
|
||||
async fn get_status(&self) -> Result<Option<RunStatusRecord>> {
|
||||
self.inner.db.get_json(keys::status()).await
|
||||
}
|
||||
|
||||
async fn put_checkpoint(&self, record: &Checkpoint) -> Result<()> {
|
||||
self.inner.db.put_json(keys::checkpoint(), record).await
|
||||
}
|
||||
|
||||
async fn get_checkpoint(&self) -> Result<Option<Checkpoint>> {
|
||||
self.inner.db.get_json(keys::checkpoint()).await
|
||||
}
|
||||
|
||||
async fn append_checkpoint(&self, record: &Checkpoint) -> Result<u32> {
|
||||
let seq = self.inner.checkpoint_seq.fetch_add(1, Ordering::SeqCst);
|
||||
self.put_checkpoint(record).await?;
|
||||
self.inner
|
||||
.db
|
||||
.put_json(
|
||||
&keys::checkpoint_history_key(seq, Utc::now().timestamp_millis()),
|
||||
record,
|
||||
)
|
||||
.await?;
|
||||
Ok(seq)
|
||||
}
|
||||
|
||||
async fn list_checkpoints(&self) -> Result<Vec<(u32, Checkpoint)>> {
|
||||
self.inner.db.list_checkpoints().await
|
||||
}
|
||||
|
||||
async fn put_conclusion(&self, record: &Conclusion) -> Result<()> {
|
||||
self.inner.db.put_json(keys::conclusion(), record).await
|
||||
}
|
||||
|
||||
async fn get_conclusion(&self) -> Result<Option<Conclusion>> {
|
||||
self.inner.db.get_json(keys::conclusion()).await
|
||||
}
|
||||
|
||||
async fn put_retro(&self, retro: &Retro) -> Result<()> {
|
||||
self.inner.db.put_json(keys::retro(), retro).await
|
||||
}
|
||||
|
||||
async fn get_retro(&self) -> Result<Option<Retro>> {
|
||||
self.inner.db.get_json(keys::retro()).await
|
||||
}
|
||||
|
||||
async fn put_graph(&self, dot_source: &str) -> Result<()> {
|
||||
self.inner.db.put_text(keys::graph(), dot_source).await
|
||||
}
|
||||
|
||||
async fn get_graph(&self) -> Result<Option<String>> {
|
||||
self.inner.db.get_text(keys::graph()).await
|
||||
}
|
||||
|
||||
async fn put_sandbox(&self, record: &SandboxRecord) -> Result<()> {
|
||||
self.inner.db.put_json(keys::sandbox(), record).await
|
||||
}
|
||||
|
||||
async fn get_sandbox(&self) -> Result<Option<SandboxRecord>> {
|
||||
self.inner.db.get_json(keys::sandbox()).await
|
||||
}
|
||||
|
||||
async fn put_node_prompt(&self, node: &NodeVisitRef<'_>, prompt: &str) -> Result<()> {
|
||||
impl SlateRunStore {
|
||||
pub async fn put_node_prompt(&self, node: &NodeVisitRef<'_>, prompt: &str) -> Result<()> {
|
||||
self.inner
|
||||
.db
|
||||
.put_text(&keys::node_prompt(node), prompt)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn put_node_response(&self, node: &NodeVisitRef<'_>, response: &str) -> Result<()> {
|
||||
pub async fn put_node_response(&self, node: &NodeVisitRef<'_>, response: &str) -> Result<()> {
|
||||
self.inner
|
||||
.db
|
||||
.put_text(&keys::node_response(node), response)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn put_node_status(
|
||||
pub async fn put_node_status(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
status: &NodeStatusRecord,
|
||||
|
|
@ -391,7 +227,7 @@ impl RunStore for SlateRunStore {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn put_node_outcome(
|
||||
pub async fn put_node_outcome(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
outcome: &NodeOutcomeRecord,
|
||||
|
|
@ -402,7 +238,7 @@ impl RunStore for SlateRunStore {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn put_node_provider_used(
|
||||
pub async fn put_node_provider_used(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
provider_used: &serde_json::Value,
|
||||
|
|
@ -413,11 +249,11 @@ impl RunStore for SlateRunStore {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn put_node_diff(&self, node: &NodeVisitRef<'_>, diff: &str) -> Result<()> {
|
||||
pub async fn put_node_diff(&self, node: &NodeVisitRef<'_>, diff: &str) -> Result<()> {
|
||||
self.inner.db.put_text(&keys::node_diff(node), diff).await
|
||||
}
|
||||
|
||||
async fn put_node_script_invocation(
|
||||
pub async fn put_node_script_invocation(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
invocation: &serde_json::Value,
|
||||
|
|
@ -428,7 +264,7 @@ impl RunStore for SlateRunStore {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn put_node_script_timing(
|
||||
pub async fn put_node_script_timing(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
timing: &serde_json::Value,
|
||||
|
|
@ -439,7 +275,7 @@ impl RunStore for SlateRunStore {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn put_node_parallel_results(
|
||||
pub async fn put_node_parallel_results(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
results: &serde_json::Value,
|
||||
|
|
@ -450,19 +286,19 @@ impl RunStore for SlateRunStore {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn put_node_stdout(&self, node: &NodeVisitRef<'_>, log: &str) -> Result<()> {
|
||||
pub async fn put_node_stdout(&self, node: &NodeVisitRef<'_>, log: &str) -> Result<()> {
|
||||
self.inner.db.put_text(&keys::node_stdout(node), log).await
|
||||
}
|
||||
|
||||
async fn put_node_stderr(&self, node: &NodeVisitRef<'_>, log: &str) -> Result<()> {
|
||||
pub async fn put_node_stderr(&self, node: &NodeVisitRef<'_>, log: &str) -> Result<()> {
|
||||
self.inner.db.put_text(&keys::node_stderr(node), log).await
|
||||
}
|
||||
|
||||
async fn get_node(&self, node: &NodeVisitRef<'_>) -> Result<NodeSnapshot> {
|
||||
pub async fn get_node(&self, node: &NodeVisitRef<'_>) -> Result<NodeSnapshot> {
|
||||
self.build_node_snapshot(node).await
|
||||
}
|
||||
|
||||
async fn list_node_visits(&self, node_id: &str) -> Result<Vec<u32>> {
|
||||
pub async fn list_node_visits(&self, node_id: &str) -> Result<Vec<u32>> {
|
||||
let prefix = format!("nodes/{node_id}/visit-");
|
||||
let mut iter = self.inner.db.scan_prefix(prefix.as_bytes()).await?;
|
||||
let mut visits = BTreeSet::new();
|
||||
|
|
@ -477,7 +313,7 @@ impl RunStore for SlateRunStore {
|
|||
Ok(visits.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn list_node_ids(&self) -> Result<Vec<String>> {
|
||||
pub async fn list_node_ids(&self) -> Result<Vec<String>> {
|
||||
let mut iter = self.inner.db.scan_prefix(b"nodes/").await?;
|
||||
let mut node_ids = BTreeSet::new();
|
||||
while let Some(entry) = iter.next().await? {
|
||||
|
|
@ -502,40 +338,13 @@ impl RunStore for SlateRunStore {
|
|||
Ok(node_ids.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn put_final_patch(&self, patch: &str) -> Result<()> {
|
||||
self.inner.db.put_text(keys::final_patch(), patch).await
|
||||
}
|
||||
|
||||
async fn get_final_patch(&self) -> Result<Option<String>> {
|
||||
self.inner.db.get_text(keys::final_patch()).await
|
||||
}
|
||||
|
||||
async fn put_pull_request(&self, record: &PullRequestRecord) -> Result<()> {
|
||||
self.inner.db.put_json(keys::pull_request(), record).await
|
||||
}
|
||||
|
||||
async fn get_pull_request(&self) -> Result<Option<PullRequestRecord>> {
|
||||
self.inner.db.get_json(keys::pull_request()).await
|
||||
}
|
||||
|
||||
async fn reset_for_rewind(&self) -> Result<()> {
|
||||
pub async fn reset_for_rewind(&self) -> Result<()> {
|
||||
let db = self.inner.db.writer()?;
|
||||
for key in [
|
||||
keys::status(),
|
||||
keys::checkpoint(),
|
||||
keys::conclusion(),
|
||||
keys::retro(),
|
||||
keys::sandbox(),
|
||||
keys::final_patch(),
|
||||
keys::pull_request(),
|
||||
keys::retro_prompt(),
|
||||
keys::retro_response(),
|
||||
] {
|
||||
for key in [keys::retro_prompt(), keys::retro_response()] {
|
||||
db.delete(key).await?;
|
||||
}
|
||||
for prefix in [
|
||||
b"nodes/".as_slice(),
|
||||
keys::CHECKPOINTS_PREFIX.as_bytes(),
|
||||
keys::ARTIFACT_VALUES_PREFIX.as_bytes(),
|
||||
keys::ARTIFACT_NODES_PREFIX.as_bytes(),
|
||||
] {
|
||||
|
|
@ -544,7 +353,7 @@ impl RunStore for SlateRunStore {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn append_event(&self, payload: &EventPayload) -> Result<u32> {
|
||||
pub async fn append_event(&self, payload: &EventPayload) -> Result<u32> {
|
||||
payload.validate(&self.inner.run_id)?;
|
||||
let seq = self.inner.event_seq.fetch_add(1, Ordering::SeqCst);
|
||||
self.inner
|
||||
|
|
@ -557,15 +366,15 @@ impl RunStore for SlateRunStore {
|
|||
Ok(seq)
|
||||
}
|
||||
|
||||
async fn list_events(&self) -> Result<Vec<EventEnvelope>> {
|
||||
pub async fn list_events(&self) -> Result<Vec<EventEnvelope>> {
|
||||
self.inner.db.list_events_from(1).await
|
||||
}
|
||||
|
||||
async fn list_events_from(&self, seq: u32) -> Result<Vec<EventEnvelope>> {
|
||||
pub async fn list_events_from(&self, seq: u32) -> Result<Vec<EventEnvelope>> {
|
||||
self.inner.db.list_events_from(seq).await
|
||||
}
|
||||
|
||||
async fn watch_events_from(
|
||||
pub async fn watch_events_from(
|
||||
&self,
|
||||
seq: u32,
|
||||
) -> Result<std::pin::Pin<Box<dyn Stream<Item = Result<EventEnvelope>> + Send>>> {
|
||||
|
|
@ -603,55 +412,68 @@ impl RunStore for SlateRunStore {
|
|||
Ok(Box::pin(UnboundedReceiverStream::new(receiver)))
|
||||
}
|
||||
|
||||
async fn put_retro_prompt(&self, text: &str) -> Result<()> {
|
||||
pub async fn put_retro_prompt(&self, text: &str) -> Result<()> {
|
||||
self.inner.db.put_text(keys::retro_prompt(), text).await
|
||||
}
|
||||
|
||||
async fn get_retro_prompt(&self) -> Result<Option<String>> {
|
||||
pub async fn get_retro_prompt(&self) -> Result<Option<String>> {
|
||||
self.inner.db.get_text(keys::retro_prompt()).await
|
||||
}
|
||||
|
||||
async fn put_retro_response(&self, text: &str) -> Result<()> {
|
||||
pub async fn put_retro_response(&self, text: &str) -> Result<()> {
|
||||
self.inner.db.put_text(keys::retro_response(), text).await
|
||||
}
|
||||
|
||||
async fn get_retro_response(&self) -> Result<Option<String>> {
|
||||
pub async fn get_retro_response(&self) -> Result<Option<String>> {
|
||||
self.inner.db.get_text(keys::retro_response()).await
|
||||
}
|
||||
|
||||
async fn put_artifact_value(&self, artifact_id: &str, value: &serde_json::Value) -> Result<()> {
|
||||
pub async fn put_artifact_value(
|
||||
&self,
|
||||
artifact_id: &str,
|
||||
value: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.db
|
||||
.put_json(&keys::artifact_value(artifact_id), value)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_artifact_value(&self, artifact_id: &str) -> Result<Option<serde_json::Value>> {
|
||||
pub async fn get_artifact_value(&self, artifact_id: &str) -> Result<Option<serde_json::Value>> {
|
||||
self.inner
|
||||
.db
|
||||
.get_json(&keys::artifact_value(artifact_id))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_artifact_values(&self) -> Result<Vec<String>> {
|
||||
pub async fn list_artifact_values(&self) -> Result<Vec<String>> {
|
||||
self.inner.db.list_artifact_values().await
|
||||
}
|
||||
|
||||
async fn put_asset(&self, node: &NodeVisitRef<'_>, filename: &str, data: &[u8]) -> Result<()> {
|
||||
pub async fn put_asset(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
filename: &str,
|
||||
data: &[u8],
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.db
|
||||
.put_bytes(&keys::node_asset(node, filename), data)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_asset(&self, node: &NodeVisitRef<'_>, filename: &str) -> Result<Option<Bytes>> {
|
||||
pub async fn get_asset(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
filename: &str,
|
||||
) -> Result<Option<Bytes>> {
|
||||
self.inner
|
||||
.db
|
||||
.get_bytes(&keys::node_asset(node, filename))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_assets(&self, node: &NodeVisitRef<'_>) -> Result<Vec<String>> {
|
||||
pub async fn list_assets(&self, node: &NodeVisitRef<'_>) -> Result<Vec<String>> {
|
||||
let prefix = format!("{}/", keys::node_asset_prefix(node));
|
||||
let mut iter = self.inner.db.scan_prefix(prefix.as_bytes()).await?;
|
||||
let mut assets = Vec::new();
|
||||
|
|
@ -665,64 +487,12 @@ impl RunStore for SlateRunStore {
|
|||
Ok(assets)
|
||||
}
|
||||
|
||||
async fn list_all_assets(&self) -> Result<Vec<(String, u32, String)>> {
|
||||
pub async fn list_all_assets(&self) -> Result<Vec<(String, u32, String)>> {
|
||||
self.inner.db.list_all_assets().await
|
||||
}
|
||||
|
||||
async fn state(&self) -> Result<RunState> {
|
||||
let mut state = self.projected_state().await?;
|
||||
state.merge_legacy(
|
||||
self.get_snapshot_legacy().await?,
|
||||
self.get_graph().await?,
|
||||
self.get_retro_prompt().await?,
|
||||
self.get_retro_response().await?,
|
||||
self.list_checkpoints().await?,
|
||||
);
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
async fn get_snapshot(&self) -> Result<Option<RunSnapshot>> {
|
||||
self.state().await.map(|state| state.to_snapshot())
|
||||
}
|
||||
}
|
||||
|
||||
impl SlateRunStore {
|
||||
async fn get_snapshot_legacy(&self) -> Result<Option<RunSnapshot>> {
|
||||
let Some(run) = self.get_run().await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut iter = self.inner.db.scan_prefix(b"nodes/").await?;
|
||||
let mut visits = BTreeSet::new();
|
||||
while let Some(entry) = iter.next().await? {
|
||||
let key = key_to_string(&entry.key)?;
|
||||
if let Some((node_id, visit, _)) = keys::parse_node_key(&key) {
|
||||
visits.insert((node_id, visit));
|
||||
}
|
||||
}
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
for (node_id, visit) in visits {
|
||||
let node = NodeVisitRef {
|
||||
node_id: &node_id,
|
||||
visit,
|
||||
};
|
||||
nodes.push(self.build_node_snapshot(&node).await?);
|
||||
}
|
||||
|
||||
Ok(Some(RunSnapshot {
|
||||
run,
|
||||
start: self.get_start().await?,
|
||||
status: self.get_status().await?,
|
||||
checkpoint: self.get_checkpoint().await?,
|
||||
conclusion: self.get_conclusion().await?,
|
||||
retro: self.get_retro().await?,
|
||||
graph: self.get_graph().await?,
|
||||
sandbox: self.get_sandbox().await?,
|
||||
final_patch: self.get_final_patch().await?,
|
||||
pull_request: self.get_pull_request().await?,
|
||||
nodes,
|
||||
}))
|
||||
pub async fn state(&self) -> Result<RunState> {
|
||||
self.projected_state().await
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -794,13 +564,6 @@ impl SlateRunDb {
|
|||
}
|
||||
}
|
||||
|
||||
async fn list_checkpoints(&self) -> Result<Vec<(u32, Checkpoint)>> {
|
||||
match self {
|
||||
Self::Writer(db) => list_checkpoints(db).await,
|
||||
Self::Reader(db) => list_checkpoints(db.as_ref()).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_artifact_values(&self) -> Result<Vec<String>> {
|
||||
match self {
|
||||
Self::Writer(db) => list_artifact_values(db).await,
|
||||
|
|
@ -910,23 +673,6 @@ where
|
|||
Ok(events)
|
||||
}
|
||||
|
||||
async fn list_checkpoints<R>(db: &R) -> Result<Vec<(u32, Checkpoint)>>
|
||||
where
|
||||
R: DbRead + Sync,
|
||||
{
|
||||
let mut iter = db.scan_prefix(keys::CHECKPOINTS_PREFIX.as_bytes()).await?;
|
||||
let mut checkpoints = Vec::new();
|
||||
while let Some(entry) = iter.next().await? {
|
||||
let key = key_to_string(&entry.key)?;
|
||||
let Some(seq) = keys::parse_checkpoint_seq(&key) else {
|
||||
continue;
|
||||
};
|
||||
checkpoints.push((seq, serde_json::from_slice(&entry.value)?));
|
||||
}
|
||||
checkpoints.sort_by_key(|(seq, _)| *seq);
|
||||
Ok(checkpoints)
|
||||
}
|
||||
|
||||
async fn list_artifact_values<R>(db: &R) -> Result<Vec<String>>
|
||||
where
|
||||
R: DbRead + Sync,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicI64, Ordering};
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use fabro_store::{EventPayload, NodeVisitRef, RunStore};
|
||||
use fabro_store::{EventPayload, NodeVisitRef, RunStoreHandle, SlateRunStore};
|
||||
use fabro_types::RunId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
|
@ -1517,7 +1517,7 @@ pub fn event_payload_from_redacted_json(line: &str, run_id: &RunId) -> Result<Ev
|
|||
}
|
||||
|
||||
pub async fn append_workflow_event(
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
run_id: &RunId,
|
||||
event: &WorkflowRunEvent,
|
||||
) -> Result<()> {
|
||||
|
|
@ -1562,7 +1562,7 @@ pub struct StoreProgressLogger {
|
|||
|
||||
impl StoreProgressLogger {
|
||||
#[must_use]
|
||||
pub fn new(run_store: Arc<dyn RunStore>) -> Self {
|
||||
pub fn new(run_store: RunStoreHandle) -> Self {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
|
@ -1627,7 +1627,7 @@ impl StoreProgressLogger {
|
|||
}
|
||||
|
||||
async fn project_provider_used_from_event_payload(
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
payload: &EventPayload,
|
||||
) -> Result<()> {
|
||||
let value = payload.as_value();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::path::Path;
|
|||
use std::process::Command;
|
||||
|
||||
use fabro_checkpoint::git::Store;
|
||||
use fabro_store::{NodeVisitRef, RunStore};
|
||||
use fabro_store::{NodeVisitRef, SlateRunStore};
|
||||
use fabro_types::Settings;
|
||||
|
||||
use crate::error::{FabroError, Result};
|
||||
|
|
@ -353,7 +353,7 @@ pub fn scan_node_files(run_dir: &Path) -> Vec<(String, Vec<u8>)> {
|
|||
result
|
||||
}
|
||||
|
||||
pub async fn scan_node_files_from_store(run_store: &dyn RunStore) -> Vec<(String, Vec<u8>)> {
|
||||
pub async fn scan_node_files_from_store(run_store: &SlateRunStore) -> Vec<(String, Vec<u8>)> {
|
||||
let mut result = Vec::new();
|
||||
let Ok(node_ids) = run_store.list_node_ids().await else {
|
||||
return result;
|
||||
|
|
@ -441,10 +441,12 @@ fn node_file_path(node_id: &str, visit: u32, filename: &str) -> String {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::{InMemoryStore, Store};
|
||||
use fabro_types::{NodeStatusRecord, RunRecord, StageStatus, fixtures};
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_types::{NodeStatusRecord, StageStatus, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Create a temporary git repo with an initial commit.
|
||||
fn init_repo(dir: &Path) {
|
||||
|
|
@ -469,6 +471,14 @@ mod tests {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_clean_on_clean_repo() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
@ -583,25 +593,11 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn scan_node_files_from_store_reconstructs_allowlisted_entries() {
|
||||
let store = InMemoryStore::default();
|
||||
let created_at = Utc::now();
|
||||
let store = test_store();
|
||||
let run = store
|
||||
.create_run(&fixtures::RUN_1, created_at, None)
|
||||
.create_run(&fixtures::RUN_1, chrono::Utc::now(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_run(&RunRecord {
|
||||
run_id: fixtures::RUN_1,
|
||||
created_at,
|
||||
settings: Settings::default(),
|
||||
graph: Graph::new("test"),
|
||||
workflow_slug: None,
|
||||
working_directory: std::path::PathBuf::from("."),
|
||||
host_repo_path: None,
|
||||
base_branch: None,
|
||||
labels: std::collections::HashMap::new(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let node = NodeVisitRef {
|
||||
node_id: "work",
|
||||
visit: 2,
|
||||
|
|
|
|||
|
|
@ -373,30 +373,40 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::event::EventEmitter;
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_store::{InMemoryStore, NodeVisitRef, RunStore, Store};
|
||||
use fabro_store::{NodeVisitRef, RunStoreHandle, SlateStore};
|
||||
use fabro_types::fixtures;
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn make_services() -> EngineServices {
|
||||
EngineServices::test_default()
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
async fn make_services_with_run_store() -> (
|
||||
EngineServices,
|
||||
Arc<dyn RunStore>,
|
||||
RunStoreHandle,
|
||||
crate::event::StoreProgressLogger,
|
||||
) {
|
||||
let store = InMemoryStore::default();
|
||||
let store = test_store();
|
||||
let run_store = store
|
||||
.create_run(&fixtures::RUN_1, chrono::Utc::now(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let services = EngineServices {
|
||||
run_store: Arc::clone(&run_store),
|
||||
run_store: run_store.clone(),
|
||||
..EngineServices::test_default()
|
||||
};
|
||||
let logger = crate::event::StoreProgressLogger::new(Arc::clone(&run_store));
|
||||
let logger = crate::event::StoreProgressLogger::new(run_store.clone());
|
||||
logger.register(services.emitter.as_ref());
|
||||
(services, run_store, logger)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::context::Context;
|
||||
use crate::context::keys;
|
||||
use crate::error::FabroError;
|
||||
use crate::event::WorkflowRunEvent;
|
||||
use crate::outcome::{Outcome, OutcomeExt};
|
||||
use crate::run_dir::{node_dir, visit_from_context};
|
||||
use async_trait::async_trait;
|
||||
use fabro_graphviz::graph::{Graph, Node};
|
||||
use tokio::fs;
|
||||
|
||||
|
|
@ -199,8 +199,9 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::outcome::StageStatus;
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_store::{InMemoryStore, NodeVisitRef, RunStore, Store};
|
||||
use fabro_store::{NodeVisitRef, RunStoreHandle, SlateStore};
|
||||
use fabro_types::fixtures;
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
@ -208,22 +209,30 @@ mod tests {
|
|||
EngineServices::test_default()
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
async fn make_services_with_run_store() -> (
|
||||
EngineServices,
|
||||
Arc<dyn RunStore>,
|
||||
RunStoreHandle,
|
||||
crate::event::StoreProgressLogger,
|
||||
) {
|
||||
let store = InMemoryStore::default();
|
||||
let store = test_store();
|
||||
let run_store = store
|
||||
.create_run(&fixtures::RUN_1, chrono::Utc::now(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let services = EngineServices {
|
||||
emitter: Arc::new(crate::event::EventEmitter::new(fixtures::RUN_1)),
|
||||
run_store: Arc::clone(&run_store),
|
||||
run_store: run_store.clone(),
|
||||
..EngineServices::test_default()
|
||||
};
|
||||
let logger = crate::event::StoreProgressLogger::new(Arc::clone(&run_store));
|
||||
let logger = crate::event::StoreProgressLogger::new(run_store.clone());
|
||||
logger.register(services.emitter.as_ref());
|
||||
(services, run_store, logger)
|
||||
}
|
||||
|
|
@ -585,10 +594,7 @@ mod tests {
|
|||
.unwrap();
|
||||
logger.flush().await;
|
||||
|
||||
let snapshot = run_store
|
||||
.state()
|
||||
.await
|
||||
.unwrap();
|
||||
let snapshot = run_store.state().await.unwrap();
|
||||
let node = snapshot
|
||||
.node(&NodeVisitRef {
|
||||
node_id: "script_node",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||
|
||||
use async_trait::async_trait;
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_store::{NodeVisitRef, RunStore};
|
||||
use fabro_store::{NodeVisitRef, RunStoreHandle};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::context::keys;
|
||||
|
|
@ -226,7 +226,7 @@ async fn llm_evaluate(
|
|||
node_id: &str,
|
||||
emitter: &Arc<EventEmitter>,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
run_store: Arc<dyn RunStore>,
|
||||
run_store: RunStoreHandle,
|
||||
) -> Result<Candidate, FabroError> {
|
||||
let results_text =
|
||||
serde_json::to_string_pretty(results).unwrap_or_else(|_| results.to_string());
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ use crate::run_options::RunOptions;
|
|||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use fabro_graphviz::graph::{AttrValue, Graph, Node};
|
||||
use fabro_store::{InMemoryStore, Store};
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_types::Settings;
|
||||
use object_store::memory::InMemory;
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
||||
use super::{EngineServices, Handler};
|
||||
|
|
@ -192,7 +193,12 @@ impl Handler for SubWorkflowHandler {
|
|||
let hook_runner = services.hook_runner.clone();
|
||||
let env = services.env.clone();
|
||||
let dry_run = services.dry_run;
|
||||
let run_store = InMemoryStore::default()
|
||||
let store = Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
));
|
||||
let run_store = store
|
||||
.create_run(
|
||||
&child_run_options.run_id,
|
||||
Utc::now(),
|
||||
|
|
|
|||
|
|
@ -15,12 +15,16 @@ use std::any::Any;
|
|||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
#[cfg(test)]
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::RunStoreHandle;
|
||||
#[cfg(test)]
|
||||
use fabro_store::Store;
|
||||
use fabro_store::SlateStore;
|
||||
#[cfg(test)]
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::FabroError;
|
||||
|
|
@ -36,7 +40,7 @@ pub struct EngineServices {
|
|||
pub registry: Arc<HandlerRegistry>,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
pub sandbox: Arc<dyn Sandbox>,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
/// Git state for the current run. Set via `set_git_state` at the start of
|
||||
/// `run_via_core` and read by parallel/fan-in handlers.
|
||||
pub(crate) git_state: std::sync::RwLock<Option<Arc<GitState>>>,
|
||||
|
|
@ -71,6 +75,11 @@ impl EngineServices {
|
|||
/// Test-only default: empty registry, no hooks, local sandbox at cwd.
|
||||
#[cfg(test)]
|
||||
pub fn test_default() -> Self {
|
||||
let store = Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
));
|
||||
Self {
|
||||
registry: Arc::new(HandlerRegistry::new(Box::new(start::StartHandler))),
|
||||
emitter: Arc::new(EventEmitter::default()),
|
||||
|
|
@ -78,10 +87,10 @@ impl EngineServices {
|
|||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
)),
|
||||
run_store: futures::executor::block_on(async {
|
||||
fabro_store::InMemoryStore::default()
|
||||
store
|
||||
.create_run(&fabro_types::RunId::new(), chrono::Utc::now(), None)
|
||||
.await
|
||||
.expect("in-memory test run store should initialize")
|
||||
.expect("slate-backed test run store should initialize")
|
||||
}),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
|
|
|
|||
|
|
@ -594,14 +594,24 @@ fn find_join_node(results: &[BranchResult], graph: &Graph) -> Option<String> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use fabro_graphviz::graph::{AttrValue, Edge};
|
||||
use fabro_store::{InMemoryStore, RunStore, Store};
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_types::fixtures;
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
fn make_services() -> EngineServices {
|
||||
EngineServices::test_default()
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn test_context() -> Context {
|
||||
let context = Context::new();
|
||||
context.set(
|
||||
|
|
@ -680,13 +690,13 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn parallel_handler_stores_results_in_run_store() {
|
||||
let store = Arc::new(InMemoryStore::default());
|
||||
let store = test_store();
|
||||
let run_store = store
|
||||
.create_run(&fixtures::RUN_1, chrono::Utc::now(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let services = EngineServices {
|
||||
run_store: Arc::clone(&run_store) as Arc<dyn RunStore>,
|
||||
run_store: run_store.clone(),
|
||||
..EngineServices::test_default()
|
||||
};
|
||||
let mut node = Node::new("par");
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ use std::path::Path;
|
|||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use fabro_model::Provider;
|
||||
use crate::context::keys;
|
||||
use crate::context::{Context, WorkflowContext};
|
||||
use crate::error::FabroError;
|
||||
|
|
@ -10,6 +9,7 @@ use crate::event::WorkflowRunEvent;
|
|||
use crate::outcome::Outcome;
|
||||
use crate::run_dir::{node_dir, visit_from_context};
|
||||
use fabro_graphviz::graph::{Graph, Node};
|
||||
use fabro_model::Provider;
|
||||
use tokio::fs;
|
||||
|
||||
use super::agent::{
|
||||
|
|
@ -185,30 +185,40 @@ impl Handler for PromptHandler {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_store::{InMemoryStore, NodeVisitRef, RunStore, Store};
|
||||
use fabro_store::{NodeVisitRef, RunStoreHandle, SlateStore};
|
||||
use fabro_types::fixtures;
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn make_services() -> EngineServices {
|
||||
EngineServices::test_default()
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
async fn make_services_with_run_store() -> (
|
||||
EngineServices,
|
||||
Arc<dyn RunStore>,
|
||||
RunStoreHandle,
|
||||
crate::event::StoreProgressLogger,
|
||||
) {
|
||||
let store = InMemoryStore::default();
|
||||
let store = test_store();
|
||||
let run_store = store
|
||||
.create_run(&fixtures::RUN_1, chrono::Utc::now(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let services = EngineServices {
|
||||
run_store: Arc::clone(&run_store),
|
||||
run_store: run_store.clone(),
|
||||
..EngineServices::test_default()
|
||||
};
|
||||
let logger = crate::event::StoreProgressLogger::new(Arc::clone(&run_store));
|
||||
let logger = crate::event::StoreProgressLogger::new(run_store.clone());
|
||||
logger.register(services.emitter.as_ref());
|
||||
(services, run_store, logger)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use std::path::PathBuf;
|
|||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fabro_store::{NodeVisitRef, RunStore};
|
||||
use fabro_types::{NodeStatusRecord, RunId};
|
||||
use fabro_store::RunStoreHandle;
|
||||
use fabro_types::RunId;
|
||||
|
||||
use fabro_core::error::Result as CoreResult;
|
||||
use fabro_core::graph::NodeSpec;
|
||||
|
|
@ -15,20 +15,18 @@ use super::circuit_breaker::CircuitBreakerLifecycle;
|
|||
use crate::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent, append_workflow_event};
|
||||
use crate::graph::WorkflowGraph;
|
||||
use crate::graph::WorkflowNode;
|
||||
use crate::outcome::{OutcomeExt, StageUsage};
|
||||
use crate::records::{Checkpoint, StartRecord};
|
||||
use crate::outcome::StageUsage;
|
||||
use crate::run_options::RunOptions;
|
||||
use crate::run_status::RunStatus;
|
||||
use fabro_graphviz::graph::types::Graph as GvGraph;
|
||||
|
||||
type WfRunState = RunState<Option<StageUsage>>;
|
||||
type WfNodeResult = NodeResult<Option<StageUsage>>;
|
||||
|
||||
/// Sub-lifecycle responsible for writing run state to disk (node status, checkpoints).
|
||||
/// Sub-lifecycle responsible for emitting store-backed run lifecycle events.
|
||||
pub(crate) struct DiskLifecycle {
|
||||
pub run_dir: PathBuf,
|
||||
pub run_id: RunId,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub graph: Arc<GvGraph>,
|
||||
pub run_options: Arc<RunOptions>,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
|
|
@ -39,31 +37,6 @@ pub(crate) struct DiskLifecycle {
|
|||
#[async_trait]
|
||||
impl RunLifecycle<WorkflowGraph> for DiskLifecycle {
|
||||
async fn on_run_start(&self, _graph: &WorkflowGraph, _state: &WfRunState) -> CoreResult<()> {
|
||||
let git_state = self.run_options.git.as_ref();
|
||||
let start_record = StartRecord {
|
||||
run_id: self.run_id,
|
||||
start_time: chrono::Utc::now(),
|
||||
run_branch: git_state.and_then(|g| g.run_branch.clone()),
|
||||
base_sha: git_state.and_then(|g| g.base_sha.clone()),
|
||||
};
|
||||
if let Err(err) = self.run_store.put_start(&start_record).await {
|
||||
self.emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Warn,
|
||||
code: "start_store_save_failed".to_string(),
|
||||
message: format!("failed to save start record to store: {err}"),
|
||||
});
|
||||
}
|
||||
if let Err(err) = self
|
||||
.run_store
|
||||
.put_status(&fabro_types::RunStatusRecord::new(RunStatus::Running, None))
|
||||
.await
|
||||
{
|
||||
self.emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Warn,
|
||||
code: "status_store_save_failed".to_string(),
|
||||
message: format!("failed to save running status to store: {err}"),
|
||||
});
|
||||
}
|
||||
if let Err(err) = append_workflow_event(
|
||||
self.run_store.as_ref(),
|
||||
&self.run_id,
|
||||
|
|
@ -82,35 +55,10 @@ impl RunLifecycle<WorkflowGraph> for DiskLifecycle {
|
|||
|
||||
async fn after_node(
|
||||
&self,
|
||||
node: &WorkflowNode,
|
||||
result: &mut WfNodeResult,
|
||||
state: &WfRunState,
|
||||
_node: &WorkflowNode,
|
||||
_result: &mut WfNodeResult,
|
||||
_state: &WfRunState,
|
||||
) -> CoreResult<()> {
|
||||
let gv = node.inner();
|
||||
let visit = state.node_visits.get(gv.id.as_str()).copied().unwrap_or(1);
|
||||
let node_status = NodeStatusRecord {
|
||||
status: result.outcome.status.clone(),
|
||||
notes: result.outcome.notes.clone(),
|
||||
failure_reason: result.outcome.failure_reason().map(ToOwned::to_owned),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
if let Err(err) = self
|
||||
.run_store
|
||||
.put_node_status(
|
||||
&NodeVisitRef {
|
||||
node_id: &gv.id,
|
||||
visit: u32::try_from(visit).unwrap_or(u32::MAX),
|
||||
},
|
||||
&node_status,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Warn,
|
||||
code: "node_status_store_save_failed".to_string(),
|
||||
message: format!("[node: {}] node status store save failed: {err}", node.id()),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +79,7 @@ impl RunLifecycle<WorkflowGraph> for DiskLifecycle {
|
|||
let mut node_outcomes = state.node_outcomes.clone();
|
||||
node_outcomes.insert(node.id().to_string(), result.outcome.clone());
|
||||
|
||||
let checkpoint = Checkpoint {
|
||||
let _checkpoint = fabro_types::Checkpoint {
|
||||
timestamp: chrono::Utc::now(),
|
||||
current_node: node.id().to_string(),
|
||||
completed_nodes: state.completed_nodes.clone(),
|
||||
|
|
@ -144,21 +92,6 @@ impl RunLifecycle<WorkflowGraph> for DiskLifecycle {
|
|||
loop_failure_signatures: loop_sigs,
|
||||
restart_failure_signatures: restart_sigs,
|
||||
};
|
||||
if let Err(err) = self.run_store.put_checkpoint(&checkpoint).await {
|
||||
self.emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Warn,
|
||||
code: "checkpoint_store_save_failed".to_string(),
|
||||
message: format!("[node: {}] checkpoint store save failed: {err}", node.id()),
|
||||
});
|
||||
}
|
||||
if let Err(err) = self.run_store.append_checkpoint(&checkpoint).await {
|
||||
self.emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Warn,
|
||||
code: "checkpoint_store_append_failed".to_string(),
|
||||
message: format!("[node: {}] checkpoint append failed: {err}", node.id()),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::RunStoreHandle;
|
||||
use fabro_types::RunId;
|
||||
|
||||
use fabro_core::error::{CoreError, Result as CoreResult};
|
||||
|
|
@ -39,7 +39,7 @@ pub(crate) struct GitLifecycle {
|
|||
pub emitter: Arc<EventEmitter>,
|
||||
pub run_dir: PathBuf,
|
||||
pub run_id: RunId,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub run_options: Arc<RunOptions>,
|
||||
pub start_node_id: Option<String>,
|
||||
// Cross-lifecycle data (shared with EventLifecycle)
|
||||
|
|
@ -66,27 +66,19 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
|
|||
) {
|
||||
let git_author = self.run_options.git_author();
|
||||
let store = MetadataStore::new(repo_path, &git_author);
|
||||
let run_json = self
|
||||
.run_store
|
||||
.get_run()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|record| serde_json::to_vec_pretty(&record).ok());
|
||||
let start_json = self
|
||||
.run_store
|
||||
.get_start()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|record| serde_json::to_vec_pretty(&record).ok());
|
||||
let sandbox_json = self
|
||||
.run_store
|
||||
.get_sandbox()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|record| serde_json::to_vec_pretty(&record).ok());
|
||||
let state = self.run_store.state().await.ok();
|
||||
let run_json = state
|
||||
.as_ref()
|
||||
.and_then(|state| state.run.as_ref())
|
||||
.and_then(|record| serde_json::to_vec_pretty(record).ok());
|
||||
let start_json = state
|
||||
.as_ref()
|
||||
.and_then(|state| state.start.as_ref())
|
||||
.and_then(|record| serde_json::to_vec_pretty(record).ok());
|
||||
let sandbox_json = state
|
||||
.as_ref()
|
||||
.and_then(|state| state.sandbox.as_ref())
|
||||
.and_then(|record| serde_json::to_vec_pretty(record).ok());
|
||||
let mut files: Vec<(&str, &[u8])> = Vec::new();
|
||||
if let Some(ref data) = run_json {
|
||||
files.push(("run.json", data));
|
||||
|
|
@ -137,10 +129,10 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
|
|||
// Build checkpoint JSON for shadow branch
|
||||
if let Some(cp_json) = self
|
||||
.run_store
|
||||
.get_checkpoint()
|
||||
.state()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|state| state.checkpoint)
|
||||
.and_then(|checkpoint| serde_json::to_vec_pretty(&checkpoint).ok())
|
||||
{
|
||||
let mut extra_entries: Vec<(String, Vec<u8>)> = {
|
||||
|
|
@ -205,31 +197,6 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
|
|||
diff: None,
|
||||
};
|
||||
|
||||
match self.run_store.get_checkpoint().await {
|
||||
Ok(Some(mut checkpoint)) => {
|
||||
checkpoint.git_commit_sha = Some(sha.clone());
|
||||
if let Err(err) = self.run_store.put_checkpoint(&checkpoint).await {
|
||||
self.emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Warn,
|
||||
code: "checkpoint_store_resave_failed".to_string(),
|
||||
message: format!(
|
||||
"[node: {node_id}] checkpoint store re-save with SHA failed: {err}"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
self.emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Warn,
|
||||
code: "checkpoint_store_load_failed".to_string(),
|
||||
message: format!(
|
||||
"[node: {node_id}] checkpoint store load failed: {err}"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Push run branch (skip in dry-run mode)
|
||||
if !self.run_options.dry_run_enabled() {
|
||||
if let Some(branch) = self
|
||||
|
|
@ -277,7 +244,6 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
|
|||
}
|
||||
|
||||
// Save diff.patch
|
||||
let visit = state.node_visits.get(node_id).copied().unwrap_or(1);
|
||||
let prev = self
|
||||
.last_git_sha
|
||||
.lock()
|
||||
|
|
@ -292,22 +258,6 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
|
|||
.unwrap_or_else(|| sha.clone());
|
||||
match git_diff(&*self.sandbox, &prev).await {
|
||||
Ok(patch) if !patch.is_empty() => {
|
||||
let node_ref = fabro_store::NodeVisitRef {
|
||||
node_id,
|
||||
visit: u32::try_from(visit).unwrap_or(u32::MAX),
|
||||
};
|
||||
if let Err(err) = self.run_store.put_node_diff(&node_ref, &patch).await {
|
||||
self.emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Warn,
|
||||
code: "git_diff_store_failed".to_string(),
|
||||
message: format!(
|
||||
"[node: {node_id}] failed to persist diff in run store: {err}"
|
||||
),
|
||||
});
|
||||
return Err(CoreError::Other(format!(
|
||||
"failed to persist node diff for '{node_id}': {err}"
|
||||
)));
|
||||
}
|
||||
git_result.diff = Some(patch);
|
||||
}
|
||||
Ok(_) => {}
|
||||
|
|
@ -353,15 +303,6 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
|
|||
match git_diff(&*self.sandbox, &base_sha).await {
|
||||
Ok(patch) if !patch.is_empty() => {
|
||||
*self.final_patch.lock().unwrap() = Some(patch.clone());
|
||||
if let Err(err) = self.run_store.put_final_patch(&patch).await {
|
||||
self.emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level: RunNoticeLevel::Warn,
|
||||
code: "final_diff_store_failed".to_string(),
|
||||
message: format!(
|
||||
"failed to persist final diff in run store: {err}"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
*self.final_patch.lock().unwrap() = None;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use std::sync::{Arc, Mutex};
|
|||
use std::time::Instant;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::RunStoreHandle;
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_types::RunId;
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ impl WorkflowLifecycle {
|
|||
sandbox: &Arc<dyn Sandbox>,
|
||||
graph: Arc<GvGraph>,
|
||||
run_dir: &PathBuf,
|
||||
run_store: Arc<dyn RunStore>,
|
||||
run_store: RunStoreHandle,
|
||||
run_options: &Arc<RunOptions>,
|
||||
is_resume: bool,
|
||||
on_node: crate::OnNodeCallback,
|
||||
|
|
@ -122,7 +122,7 @@ impl WorkflowLifecycle {
|
|||
run_start: Mutex::new(Instant::now()),
|
||||
restarted_from: Arc::clone(&restarted_from),
|
||||
base_branch: run_options.base_branch.clone(),
|
||||
base_sha: run_options.display_base_sha.clone(),
|
||||
base_sha: run_options.git.as_ref().and_then(|g| g.base_sha.clone()),
|
||||
run_branch: run_options.git.as_ref().and_then(|g| g.run_branch.clone()),
|
||||
worktree_dir: working_directory.clone(),
|
||||
goal: (!graph.goal().is_empty()).then(|| graph.goal().to_string()),
|
||||
|
|
@ -146,7 +146,7 @@ impl WorkflowLifecycle {
|
|||
let disk = DiskLifecycle {
|
||||
run_dir: run_dir.clone(),
|
||||
run_id: run_options.run_id,
|
||||
run_store: Arc::clone(&run_store),
|
||||
run_store: run_store.clone(),
|
||||
graph: Arc::clone(&graph),
|
||||
run_options: Arc::clone(run_options),
|
||||
emitter: Arc::clone(emitter),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use chrono::{Local, Utc};
|
|||
use fabro_graphviz::graph::{AttrValue, Graph};
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_store::Store;
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_types::{RunId, Settings};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -13,7 +13,6 @@ use crate::pipeline::types::PersistOptions;
|
|||
use crate::pipeline::{self, Persisted, TransformOptions, Validated};
|
||||
use crate::records::RunRecord;
|
||||
use crate::run_lookup::default_runs_base;
|
||||
use crate::run_status::{RunStatus, RunStatusRecord};
|
||||
use crate::transforms::{Transform, expand_vars};
|
||||
use fabro_sandbox::daytona::detect_repo_info;
|
||||
|
||||
|
|
@ -56,7 +55,7 @@ struct PersistCreateOptions {
|
|||
}
|
||||
|
||||
/// Resolve workflow inputs, normalize settings, and persist a run directory.
|
||||
pub async fn create(store: &dyn Store, request: CreateRunInput) -> Result<CreatedRun, FabroError> {
|
||||
pub async fn create(store: &SlateStore, request: CreateRunInput) -> Result<CreatedRun, FabroError> {
|
||||
let resolved = resolve_workflow(ResolveWorkflowInput {
|
||||
workflow: request.workflow,
|
||||
settings: request.settings,
|
||||
|
|
@ -133,7 +132,7 @@ pub async fn create(store: &dyn Store, request: CreateRunInput) -> Result<Create
|
|||
}
|
||||
|
||||
async fn persist_created_run(
|
||||
store: &dyn Store,
|
||||
store: &SlateStore,
|
||||
persisted: &Persisted,
|
||||
workflow_source: &str,
|
||||
workflow_config: Option<String>,
|
||||
|
|
@ -152,18 +151,6 @@ async fn persist_created_run(
|
|||
.or_else(|_| Err(FabroError::engine(err.to_string())))?,
|
||||
};
|
||||
|
||||
run_store.put_run(record).await.map_err(store_error)?;
|
||||
if !workflow_source.is_empty() {
|
||||
run_store
|
||||
.put_graph(workflow_source)
|
||||
.await
|
||||
.map_err(store_error)?;
|
||||
}
|
||||
run_store
|
||||
.put_status(&RunStatusRecord::new(RunStatus::Submitted, None))
|
||||
.await
|
||||
.map_err(store_error)?;
|
||||
|
||||
let envelope = canonicalize_event_at(
|
||||
&record.run_id,
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
|
|
@ -411,15 +398,20 @@ pub(crate) fn make_run_dir(runs_base: &Path, run_id: &str, dry_run: bool) -> Pat
|
|||
mod tests {
|
||||
use super::*;
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_store::{InMemoryStore, SlateStore, Store};
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_types::fixtures;
|
||||
use object_store::local::LocalFileSystem;
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::operations::{ValidateInput, validate};
|
||||
fn memory_store() -> InMemoryStore {
|
||||
InMemoryStore::default()
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn validate_dot(dot_source: &str, settings: Settings) -> Validated {
|
||||
|
|
@ -719,7 +711,7 @@ mod tests {
|
|||
);
|
||||
let run_store = store.open_run(&fixtures::RUN_1).await.unwrap();
|
||||
assert_eq!(
|
||||
run_store.get_status().await.unwrap().unwrap().status,
|
||||
run_store.state().await.unwrap().status.unwrap().status,
|
||||
crate::run_status::RunStatus::Submitted
|
||||
);
|
||||
assert!(!created.run_dir.join("id.txt").exists());
|
||||
|
|
@ -816,7 +808,11 @@ mod tests {
|
|||
std::fs::create_dir_all(storage_dir.join("store")).unwrap();
|
||||
let object_store =
|
||||
Arc::new(LocalFileSystem::new_with_prefix(storage_dir.join("store")).unwrap());
|
||||
let store = Arc::new(SlateStore::new(object_store, "", Duration::from_millis(1)));
|
||||
let store = StoreHandle::from(Arc::new(SlateStore::new(
|
||||
object_store,
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
)));
|
||||
let created = create(
|
||||
store.as_ref(),
|
||||
CreateRunInput {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use anyhow::{Context, Result, bail};
|
|||
use fabro_checkpoint::branch::BranchStore;
|
||||
use fabro_checkpoint::git::Store as GitStore;
|
||||
use fabro_store::{
|
||||
ListRunsQuery, NodeVisitRef, RunStore as DurableRunStore, Store as DurableStore,
|
||||
ListRunsQuery, NodeVisitRef, RunStoreHandle as DurableRunStore, SlateStore as DurableStore,
|
||||
};
|
||||
use fabro_types::RunId;
|
||||
use git2::{Repository, Signature};
|
||||
|
|
@ -18,7 +18,7 @@ use crate::records::Checkpoint;
|
|||
|
||||
pub async fn rebuild_metadata_branch(
|
||||
git_store: &GitStore,
|
||||
run_store: &dyn DurableRunStore,
|
||||
run_store: &DurableRunStore,
|
||||
run_id: &RunId,
|
||||
) -> Result<()> {
|
||||
let branch = MetadataStore::branch_name(&run_id.to_string());
|
||||
|
|
@ -26,10 +26,7 @@ pub async fn rebuild_metadata_branch(
|
|||
bail!("metadata branch already exists for run {run_id}");
|
||||
}
|
||||
|
||||
let state = run_store
|
||||
.state()
|
||||
.await?
|
||||
;
|
||||
let state = run_store.state().await?;
|
||||
let run_record = state
|
||||
.run
|
||||
.clone()
|
||||
|
|
@ -157,7 +154,7 @@ pub async fn rebuild_metadata_branch(
|
|||
|
||||
pub async fn build_timeline_or_rebuild(
|
||||
git_store: &GitStore,
|
||||
run_store: Option<&dyn DurableRunStore>,
|
||||
run_store: Option<&DurableRunStore>,
|
||||
run_id: &RunId,
|
||||
) -> Result<RunTimeline> {
|
||||
let branch = MetadataStore::branch_name(&run_id.to_string());
|
||||
|
|
@ -178,7 +175,7 @@ pub async fn build_timeline_or_rebuild(
|
|||
|
||||
pub async fn find_run_id_by_prefix_or_store(
|
||||
repo: &Repository,
|
||||
fabro_store: &dyn DurableStore,
|
||||
fabro_store: &DurableStore,
|
||||
prefix: &str,
|
||||
) -> Result<RunId> {
|
||||
if let Some(run_id) = find_run_id_by_prefix_in_refs(repo, prefix)? {
|
||||
|
|
@ -334,19 +331,18 @@ fn resolve_prefix_matches(prefix: &str, matches: Vec<RunId>) -> Result<RunId> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{TimeZone, Utc};
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::{NodeVisitRef, SlateStore, StoreHandle};
|
||||
use fabro_types::{RunId, RunRecord, SandboxRecord, Settings, StartRecord, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::{InMemoryStore, NodeVisitRef, Store as _};
|
||||
use fabro_types::{
|
||||
NodeStatusRecord, RunId, RunRecord, SandboxRecord, Settings, StageStatus, StartRecord,
|
||||
fixtures,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::event::{WorkflowRunEvent, append_workflow_event};
|
||||
use crate::operations::test_support::{make_checkpoint_json, temp_repo, test_sig};
|
||||
use crate::records::Checkpoint;
|
||||
|
||||
|
|
@ -362,6 +358,14 @@ mod tests {
|
|||
fixtures::RUN_1
|
||||
}
|
||||
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn sample_run_record(run_id: RunId, host_repo_path: Option<&str>) -> RunRecord {
|
||||
RunRecord {
|
||||
run_id,
|
||||
|
|
@ -422,26 +426,130 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn sample_node_status() -> NodeStatusRecord {
|
||||
NodeStatusRecord {
|
||||
status: StageStatus::Success,
|
||||
notes: Some("done".to_string()),
|
||||
failure_reason: None,
|
||||
timestamp: created_at(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_run_store(
|
||||
store: &InMemoryStore,
|
||||
store: &SlateStore,
|
||||
run_id: RunId,
|
||||
host_repo_path: Option<&str>,
|
||||
) -> Arc<dyn DurableRunStore> {
|
||||
) -> DurableRunStore {
|
||||
let run_store = store.create_run(&run_id, created_at(), None).await.unwrap();
|
||||
let run_record = sample_run_record(run_id, host_repo_path);
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
run_id,
|
||||
settings: serde_json::to_value(&run_record.settings).unwrap(),
|
||||
graph: serde_json::to_value(&run_record.graph).unwrap(),
|
||||
workflow_source: None,
|
||||
workflow_config: None,
|
||||
labels: run_record.labels.clone().into_iter().collect(),
|
||||
run_dir: String::new(),
|
||||
working_directory: run_record.working_directory.display().to_string(),
|
||||
host_repo_path: run_record.host_repo_path.clone(),
|
||||
base_branch: run_record.base_branch.clone(),
|
||||
workflow_slug: run_record.workflow_slug.clone(),
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.put_run(&sample_run_record(run_id, host_repo_path))
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
}
|
||||
|
||||
async fn append_start_event(run_store: &DurableRunStore, run_id: RunId) {
|
||||
let start = sample_start_record(run_id);
|
||||
append_workflow_event(
|
||||
run_store,
|
||||
&run_id,
|
||||
&WorkflowRunEvent::WorkflowRunStarted {
|
||||
name: "test".to_string(),
|
||||
run_id,
|
||||
base_branch: None,
|
||||
base_sha: start.base_sha,
|
||||
run_branch: start.run_branch,
|
||||
worktree_dir: None,
|
||||
goal: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn append_sandbox_event(run_store: &DurableRunStore, run_id: RunId) {
|
||||
let sandbox = sample_sandbox_record();
|
||||
append_workflow_event(
|
||||
run_store,
|
||||
&run_id,
|
||||
&WorkflowRunEvent::SandboxInitialized {
|
||||
provider: sandbox.provider,
|
||||
working_directory: sandbox.working_directory,
|
||||
identifier: sandbox.identifier,
|
||||
host_working_directory: sandbox.host_working_directory,
|
||||
container_mount_point: sandbox.container_mount_point,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn append_checkpoint_event(
|
||||
run_store: &DurableRunStore,
|
||||
run_id: RunId,
|
||||
checkpoint: Checkpoint,
|
||||
) {
|
||||
append_workflow_event(
|
||||
run_store,
|
||||
&run_id,
|
||||
&WorkflowRunEvent::CheckpointCompleted {
|
||||
node_id: checkpoint.current_node.clone(),
|
||||
status: "success".to_string(),
|
||||
current_node: checkpoint.current_node.clone(),
|
||||
completed_nodes: checkpoint.completed_nodes.clone(),
|
||||
node_retries: checkpoint.node_retries.clone().into_iter().collect(),
|
||||
context_values: checkpoint.context_values.clone().into_iter().collect(),
|
||||
node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(),
|
||||
next_node_id: checkpoint.next_node_id.clone(),
|
||||
git_commit_sha: checkpoint.git_commit_sha.clone(),
|
||||
loop_failure_signatures: checkpoint
|
||||
.loop_failure_signatures
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(signature, count)| (signature.to_string(), count))
|
||||
.collect(),
|
||||
restart_failure_signatures: checkpoint
|
||||
.restart_failure_signatures
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(signature, count)| (signature.to_string(), count))
|
||||
.collect(),
|
||||
node_visits: checkpoint.node_visits.clone().into_iter().collect(),
|
||||
diff: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn append_prompt_event(
|
||||
run_store: &DurableRunStore,
|
||||
run_id: RunId,
|
||||
node: &NodeVisitRef<'_>,
|
||||
text: &str,
|
||||
) {
|
||||
append_workflow_event(
|
||||
run_store,
|
||||
&run_id,
|
||||
&WorkflowRunEvent::Prompt {
|
||||
stage: node.node_id.to_string(),
|
||||
visit: node.visit,
|
||||
text: text.to_string(),
|
||||
mode: None,
|
||||
provider: None,
|
||||
model: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn seed_run_branch(git_store: &GitStore, run_id: RunId, nodes: &[&str]) -> Vec<String> {
|
||||
|
|
@ -472,46 +580,41 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn rebuild_metadata_branch_round_trips_timeline() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let run_store = create_run_store(&durable_store, test_run_id(), None).await;
|
||||
run_store
|
||||
.put_start(&sample_start_record(test_run_id()))
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.put_sandbox(&sample_sandbox_record())
|
||||
.await
|
||||
.unwrap();
|
||||
append_start_event(&run_store, test_run_id()).await;
|
||||
append_sandbox_event(&run_store, test_run_id()).await;
|
||||
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
"start",
|
||||
&["start"],
|
||||
&[("start", 1)],
|
||||
Some("aaa"),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint("start", &["start"], &[("start", 1)], Some("aaa")),
|
||||
)
|
||||
.await;
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint(
|
||||
"build",
|
||||
&["start", "build"],
|
||||
&[("start", 1), ("build", 1)],
|
||||
Some("bbb"),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
),
|
||||
)
|
||||
.await;
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint(
|
||||
"build",
|
||||
&["start", "build"],
|
||||
&[("start", 1), ("build", 2)],
|
||||
Some("ccc"),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
rebuild_metadata_branch(&git_store, run_store.as_ref(), &test_run_id())
|
||||
rebuild_metadata_branch(&git_store, &run_store, &test_run_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -531,51 +634,35 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn rebuild_metadata_branch_preserves_historical_node_visits() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let run_store = create_run_store(&durable_store, test_run_id(), None).await;
|
||||
|
||||
let build_v1 = NodeVisitRef {
|
||||
node_id: "build",
|
||||
visit: 1,
|
||||
};
|
||||
run_store
|
||||
.put_node_prompt(&build_v1, "visit one")
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.put_node_status(&build_v1, &sample_node_status())
|
||||
.await
|
||||
.unwrap();
|
||||
append_prompt_event(&run_store, test_run_id(), &build_v1, "visit one").await;
|
||||
|
||||
let build_v2 = NodeVisitRef {
|
||||
node_id: "build",
|
||||
visit: 2,
|
||||
};
|
||||
run_store
|
||||
.put_node_prompt(&build_v2, "visit two")
|
||||
.await
|
||||
.unwrap();
|
||||
append_prompt_event(&run_store, test_run_id(), &build_v2, "visit two").await;
|
||||
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
"build",
|
||||
&["build"],
|
||||
&[("build", 1)],
|
||||
Some("aaa"),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
"build",
|
||||
&["build"],
|
||||
&[("build", 2)],
|
||||
Some("bbb"),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint("build", &["build"], &[("build", 1)], Some("aaa")),
|
||||
)
|
||||
.await;
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint("build", &["build"], &[("build", 2)], Some("bbb")),
|
||||
)
|
||||
.await;
|
||||
|
||||
rebuild_metadata_branch(&git_store, run_store.as_ref(), &test_run_id())
|
||||
rebuild_metadata_branch(&git_store, &run_store, &test_run_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -624,7 +711,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn rebuild_metadata_branch_refuses_to_overwrite_existing_branch() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let run_store = create_run_store(&durable_store, test_run_id(), None).await;
|
||||
|
||||
let sig = test_sig();
|
||||
|
|
@ -633,7 +720,7 @@ mod tests {
|
|||
bs.ensure_branch().unwrap();
|
||||
bs.write_entry("run.json", b"{}", "init run").unwrap();
|
||||
|
||||
let err = rebuild_metadata_branch(&git_store, run_store.as_ref(), &test_run_id())
|
||||
let err = rebuild_metadata_branch(&git_store, &run_store, &test_run_id())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("metadata branch already exists"));
|
||||
|
|
@ -642,23 +729,19 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn build_timeline_or_rebuild_rebuilds_missing_branch() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let run_store = create_run_store(&durable_store, test_run_id(), None).await;
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
"start",
|
||||
&["start"],
|
||||
&[("start", 1)],
|
||||
Some("aaa"),
|
||||
))
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint("start", &["start"], &[("start", 1)], Some("aaa")),
|
||||
)
|
||||
.await;
|
||||
|
||||
let timeline = build_timeline_or_rebuild(&git_store, Some(&run_store), &test_run_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let timeline =
|
||||
build_timeline_or_rebuild(&git_store, Some(run_store.as_ref()), &test_run_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(timeline.entries.len(), 1);
|
||||
assert_eq!(timeline.entries[0].node_name, "start");
|
||||
}
|
||||
|
|
@ -666,35 +749,36 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn build_timeline_or_rebuild_preserves_existing_branch() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let run_store = create_run_store(&durable_store, test_run_id(), None).await;
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
"start",
|
||||
&["start"],
|
||||
&[("start", 1)],
|
||||
Some("aaa"),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint("start", &["start"], &[("start", 1)], Some("aaa")),
|
||||
)
|
||||
.await;
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint(
|
||||
"build",
|
||||
&["start", "build"],
|
||||
&[("start", 1), ("build", 1)],
|
||||
Some("bbb"),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
),
|
||||
)
|
||||
.await;
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint(
|
||||
"test",
|
||||
&["start", "build", "test"],
|
||||
&[("start", 1), ("build", 1), ("test", 1)],
|
||||
Some("ccc"),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let sig = test_sig();
|
||||
let branch = MetadataStore::branch_name(&test_run_id().to_string());
|
||||
|
|
@ -714,10 +798,9 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let timeline =
|
||||
build_timeline_or_rebuild(&git_store, Some(run_store.as_ref()), &test_run_id())
|
||||
.await
|
||||
.unwrap();
|
||||
let timeline = build_timeline_or_rebuild(&git_store, Some(&run_store), &test_run_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(timeline.entries.len(), 2);
|
||||
assert_eq!(timeline.entries[0].node_name, "start");
|
||||
|
|
@ -737,13 +820,13 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn rebuild_metadata_branch_errors_when_run_record_is_missing() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let run_store = durable_store
|
||||
.create_run(&test_run_id(), created_at(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let err = rebuild_metadata_branch(&git_store, run_store.as_ref(), &test_run_id())
|
||||
let err = rebuild_metadata_branch(&git_store, &run_store, &test_run_id())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("run record not found"));
|
||||
|
|
@ -752,7 +835,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn find_run_id_by_prefix_or_store_falls_back_to_store() {
|
||||
let (dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let repo_path = dir.path().to_string_lossy().to_string();
|
||||
let repo_run_id = parse_run_id("01ARZ3NDEKTSV4RRFFQ69G5FAV");
|
||||
let _run_store = create_run_store(&durable_store, repo_run_id, Some(&repo_path)).await;
|
||||
|
|
@ -769,7 +852,7 @@ mod tests {
|
|||
async fn find_run_id_by_prefix_or_store_excludes_other_repos() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let (other_dir, _other_git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let other_repo_path = other_dir.path().to_string_lossy().to_string();
|
||||
let other_run_id = parse_run_id("01ARZ3NDEKTSV4RRFFQ69G5FAV");
|
||||
let _run_store =
|
||||
|
|
@ -786,7 +869,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn find_run_id_by_prefix_or_store_requires_exact_match_without_repo_path() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let repo_run_id = parse_run_id("01ARZ3NDEKTSV4RRFFQ69G5FAV");
|
||||
let _run_store = create_run_store(&durable_store, repo_run_id, None).await;
|
||||
let prefix = &repo_run_id.to_string()[..6];
|
||||
|
|
@ -809,7 +892,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn exact_match_wins_over_prefix_ambiguity() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let repo_path = git_store.repo_dir().to_string_lossy().to_string();
|
||||
let exact_run_id = parse_run_id("01ARZ3NDEKTSV4RRFFQ69G5FAV");
|
||||
let other_run_id = parse_run_id("01ARZ3NDEKTSV4RRFFQ69G5FAW");
|
||||
|
|
@ -853,31 +936,30 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn rebuild_metadata_branch_persists_backfilled_run_shas_in_checkpoint_blobs() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let run_store = create_run_store(&durable_store, test_run_id(), None).await;
|
||||
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
"start",
|
||||
&["start"],
|
||||
&[("start", 1)],
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint("start", &["start"], &[("start", 1)], None),
|
||||
)
|
||||
.await;
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint(
|
||||
"build",
|
||||
&["start", "build"],
|
||||
&[("start", 1), ("build", 1)],
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let expected_shas = seed_run_branch(&git_store, test_run_id(), &["start", "build"]);
|
||||
|
||||
rebuild_metadata_branch(&git_store, run_store.as_ref(), &test_run_id())
|
||||
rebuild_metadata_branch(&git_store, &run_store, &test_run_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -921,7 +1003,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn rebuild_metadata_branch_is_atomic_on_failure() {
|
||||
let (_dir, git_store) = temp_repo();
|
||||
let durable_store = InMemoryStore::default();
|
||||
let durable_store = memory_store();
|
||||
let run_store = create_run_store(&durable_store, test_run_id(), None).await;
|
||||
|
||||
let bad_node = "bad\0node";
|
||||
|
|
@ -929,21 +1011,15 @@ mod tests {
|
|||
node_id: bad_node,
|
||||
visit: 1,
|
||||
};
|
||||
run_store
|
||||
.put_node_prompt(&bad_visit, "prompt")
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.append_checkpoint(&sample_checkpoint(
|
||||
bad_node,
|
||||
&[bad_node],
|
||||
&[(bad_node, 1)],
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
append_prompt_event(&run_store, test_run_id(), &bad_visit, "prompt").await;
|
||||
append_checkpoint_event(
|
||||
&run_store,
|
||||
test_run_id(),
|
||||
sample_checkpoint(bad_node, &[bad_node], &[(bad_node, 1)], None),
|
||||
)
|
||||
.await;
|
||||
|
||||
let err = rebuild_metadata_branch(&git_store, run_store.as_ref(), &test_run_id())
|
||||
let err = rebuild_metadata_branch(&git_store, &run_store, &test_run_id())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("nul") || err.to_string().contains("NUL"));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use fabro_store::RuntimeState;
|
|||
use crate::error::FabroError;
|
||||
use crate::event::{WorkflowRunEvent, append_workflow_event};
|
||||
use crate::outcome::StageStatus;
|
||||
use crate::run_status::{self, RunStatus};
|
||||
use crate::run_status::RunStatus;
|
||||
|
||||
use super::start::{StartServices, Started, execute_persisted_run};
|
||||
|
||||
|
|
@ -40,14 +40,6 @@ pub async fn resume(run_dir: &Path, services: StartServices) -> Result<Started,
|
|||
.ok_or_else(|| FabroError::Precondition("no checkpoint to resume from".to_string()))?;
|
||||
|
||||
cleanup_resume_artifacts(run_dir);
|
||||
services
|
||||
.run_store
|
||||
.put_status(&run_status::RunStatusRecord::new(
|
||||
RunStatus::Submitted,
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.map_err(|err| FabroError::engine(err.to_string()))?;
|
||||
append_workflow_event(
|
||||
services.run_store.as_ref(),
|
||||
&services.run_id,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use fabro_config::{project as project_config, run as run_config, sandbox as sand
|
|||
use fabro_interview::{AutoApproveInterviewer, Interviewer};
|
||||
use fabro_model::{Catalog, FallbackTarget, Provider};
|
||||
use fabro_sandbox::{SandboxProvider, SandboxSpec};
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::{RunStoreHandle, SlateRunStore};
|
||||
use fabro_types::{RunId, Settings};
|
||||
use serde::Serialize;
|
||||
|
||||
|
|
@ -28,9 +28,9 @@ use crate::pipeline::{
|
|||
PullRequestOptions, RetroOptions, SandboxEnvSpec, build_conclusion_from_store,
|
||||
classify_engine_result,
|
||||
};
|
||||
use crate::records::{Checkpoint, Conclusion};
|
||||
use crate::records::Checkpoint;
|
||||
use crate::run_options::{GitCheckpointOptions, LifecycleOptions, RunOptions};
|
||||
use crate::run_status::{self, RunStatus, StatusReason};
|
||||
use crate::run_status::{RunStatus, StatusReason};
|
||||
use fabro_config::run::PullRequestSettings;
|
||||
use fabro_retro::retro::Retro;
|
||||
use fabro_sandbox::daytona::DaytonaConfig;
|
||||
|
|
@ -49,7 +49,7 @@ struct RunSession {
|
|||
sandbox_env: SandboxEnvSpec,
|
||||
devcontainer: Option<DevcontainerSpec>,
|
||||
seed_context: Option<Context>,
|
||||
run_store: Arc<dyn RunStore>,
|
||||
run_store: RunStoreHandle,
|
||||
git: Option<GitCheckpointOptions>,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
worktree_mode: Option<WorktreeMode>,
|
||||
|
|
@ -67,7 +67,7 @@ pub struct StartServices {
|
|||
pub cancel_token: Option<Arc<AtomicBool>>,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
pub interviewer: Arc<dyn Interviewer>,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
pub on_node: crate::OnNodeCallback,
|
||||
pub registry_override: Option<Arc<HandlerRegistry>>,
|
||||
|
|
@ -112,13 +112,28 @@ pub(super) async fn execute_persisted_run(
|
|||
) -> Result<Started, FabroError> {
|
||||
let cancel_token = services.cancel_token.clone();
|
||||
let run_id = services.run_id;
|
||||
let run_store = Arc::clone(&services.run_store);
|
||||
if let Err(err) = run_store
|
||||
.put_status(&run_status::RunStatusRecord::new(
|
||||
RunStatus::Starting,
|
||||
Some(StatusReason::SandboxInitializing),
|
||||
))
|
||||
.await
|
||||
let run_store = services.run_store.clone();
|
||||
if let Err(err) = run_store.state().await {
|
||||
let error = FabroError::engine(err.to_string());
|
||||
let _ = persist_detached_failure(
|
||||
run_id,
|
||||
run_store.as_ref(),
|
||||
run_dir,
|
||||
"bootstrap",
|
||||
StatusReason::BootstrapFailed,
|
||||
&error,
|
||||
)
|
||||
.await;
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(err) = append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::RunStarting {
|
||||
reason: Some(StatusReason::SandboxInitializing),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
let error = FabroError::engine(err.to_string());
|
||||
let _ = persist_detached_failure(
|
||||
|
|
@ -132,22 +147,9 @@ pub(super) async fn execute_persisted_run(
|
|||
.await;
|
||||
return Err(error);
|
||||
}
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&run_id,
|
||||
&WorkflowRunEvent::RunStarting {
|
||||
reason: Some(StatusReason::SandboxInitializing),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|err| FabroError::engine(err.to_string()))?;
|
||||
|
||||
let mut bootstrap_guard = DetachedRunBootstrapGuard::arm(
|
||||
run_id,
|
||||
run_dir,
|
||||
Arc::clone(&run_store),
|
||||
cancel_token.clone(),
|
||||
);
|
||||
let mut bootstrap_guard =
|
||||
DetachedRunBootstrapGuard::arm(run_id, run_dir, run_store.clone(), cancel_token.clone());
|
||||
|
||||
let persisted = match Persisted::load_from_store(services.run_store.as_ref(), run_dir).await {
|
||||
Ok(persisted) => persisted,
|
||||
|
|
@ -185,7 +187,7 @@ pub(super) async fn execute_persisted_run(
|
|||
|
||||
bootstrap_guard.defuse();
|
||||
let mut completion_guard =
|
||||
DetachedRunCompletionGuard::arm(run_id, Arc::clone(&run_store), cancel_token);
|
||||
DetachedRunCompletionGuard::arm(run_id, run_store.clone(), cancel_token);
|
||||
let run_start = Instant::now();
|
||||
let started = Box::pin(session.run(persisted, checkpoint)).await;
|
||||
|
||||
|
|
@ -211,15 +213,15 @@ pub(super) async fn execute_persisted_run(
|
|||
|
||||
async fn persist_terminal_engine_failure(
|
||||
run_id: RunId,
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
_run_dir: &Path,
|
||||
error: &FabroError,
|
||||
duration: Duration,
|
||||
) {
|
||||
let engine_result: Result<Outcome, FabroError> = Err(error.clone());
|
||||
let (final_status, failure_reason, run_status, status_reason) =
|
||||
let (final_status, failure_reason, _run_status, status_reason) =
|
||||
classify_engine_result(&engine_result);
|
||||
let conclusion = build_conclusion_from_store(
|
||||
let _conclusion = build_conclusion_from_store(
|
||||
run_store,
|
||||
final_status,
|
||||
failure_reason,
|
||||
|
|
@ -227,15 +229,6 @@ async fn persist_terminal_engine_failure(
|
|||
None,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = run_store.put_conclusion(&conclusion).await {
|
||||
tracing::warn!(error = %err, "Failed to save terminal engine failure conclusion to store");
|
||||
}
|
||||
if let Err(err) = run_store
|
||||
.put_status(&run_status::RunStatusRecord::new(run_status, status_reason))
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %err, "Failed to save terminal engine failure status to store");
|
||||
}
|
||||
if let Err(err) = append_workflow_event(
|
||||
run_store,
|
||||
&run_id,
|
||||
|
|
@ -257,18 +250,18 @@ impl RunSession {
|
|||
let record = persisted.run_record();
|
||||
let mut settings = record.settings.clone();
|
||||
let working_directory = record.working_directory.clone();
|
||||
let git = services
|
||||
let state = services
|
||||
.run_store
|
||||
.get_start()
|
||||
.state()
|
||||
.await
|
||||
.map_err(|err| FabroError::engine(err.to_string()))?
|
||||
.and_then(|start| {
|
||||
start.run_branch.as_ref().map(|_| GitCheckpointOptions {
|
||||
base_sha: start.base_sha.clone(),
|
||||
run_branch: start.run_branch.clone(),
|
||||
meta_branch: Some(MetadataStore::branch_name(&record.run_id.to_string())),
|
||||
})
|
||||
});
|
||||
.map_err(|err| FabroError::engine(err.to_string()))?;
|
||||
let git = state.start.and_then(|start| {
|
||||
start.run_branch.as_ref().map(|_| GitCheckpointOptions {
|
||||
base_sha: start.base_sha.clone(),
|
||||
run_branch: start.run_branch.clone(),
|
||||
meta_branch: Some(MetadataStore::branch_name(&record.run_id.to_string())),
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(env) = settings
|
||||
.sandbox
|
||||
|
|
@ -498,12 +491,12 @@ impl RunSession {
|
|||
});
|
||||
}
|
||||
|
||||
let store_progress_logger = StoreProgressLogger::new(Arc::clone(&self.run_store));
|
||||
let store_progress_logger = StoreProgressLogger::new(self.run_store.clone());
|
||||
store_progress_logger.register(self.emitter.as_ref());
|
||||
|
||||
let init_options = InitOptions {
|
||||
run_id: record.run_id,
|
||||
run_store: Arc::clone(&self.run_store),
|
||||
run_store: self.run_store.clone(),
|
||||
dry_run: run_options.dry_run_enabled(),
|
||||
emitter: self.emitter,
|
||||
sandbox: self.sandbox,
|
||||
|
|
@ -545,7 +538,7 @@ impl RunSession {
|
|||
|
||||
let retro_opts = RetroOptions {
|
||||
run_id: executed.run_options.run_id,
|
||||
run_store: Arc::clone(&executed.run_store),
|
||||
run_store: executed.run_store.clone(),
|
||||
workflow_name: executed.graph.name.clone(),
|
||||
goal: executed.graph.goal().to_string(),
|
||||
run_dir: executed.run_options.run_dir.clone(),
|
||||
|
|
@ -566,7 +559,7 @@ impl RunSession {
|
|||
let finalize_opts = FinalizeOptions {
|
||||
run_dir: retroed.run_options.run_dir.clone(),
|
||||
run_id: retroed.run_options.run_id,
|
||||
run_store: Arc::clone(&retroed.run_store),
|
||||
run_store: retroed.run_store.clone(),
|
||||
workflow_name: retroed.graph.name.clone(),
|
||||
hook_runner: retroed.hook_runner.clone(),
|
||||
preserve_sandbox: self.preserve_sandbox,
|
||||
|
|
@ -574,7 +567,7 @@ impl RunSession {
|
|||
};
|
||||
let pr_opts = PullRequestOptions {
|
||||
run_dir: retroed.run_options.run_dir.clone(),
|
||||
run_store: Arc::clone(&retroed.run_store),
|
||||
run_store: retroed.run_store.clone(),
|
||||
pr_config: self.pr_config,
|
||||
github_app: self.pr_github_app,
|
||||
origin_url: self.pr_origin_url,
|
||||
|
|
@ -599,7 +592,7 @@ impl RunSession {
|
|||
|
||||
struct DetachedRunBootstrapGuard {
|
||||
run_id: RunId,
|
||||
run_store: Arc<dyn RunStore>,
|
||||
run_store: RunStoreHandle,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
active: bool,
|
||||
}
|
||||
|
|
@ -608,7 +601,7 @@ impl DetachedRunBootstrapGuard {
|
|||
fn arm(
|
||||
run_id: RunId,
|
||||
_run_dir: &Path,
|
||||
run_store: Arc<dyn RunStore>,
|
||||
run_store: RunStoreHandle,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -637,15 +630,9 @@ impl Drop for DetachedRunBootstrapGuard {
|
|||
StatusReason::SandboxInitFailed
|
||||
};
|
||||
let run_id = self.run_id;
|
||||
let run_store = Arc::clone(&self.run_store);
|
||||
let run_store = self.run_store.clone();
|
||||
if let Ok(handle) = Handle::try_current() {
|
||||
handle.spawn(async move {
|
||||
let _ = run_store
|
||||
.put_status(&run_status::RunStatusRecord::new(
|
||||
RunStatus::Failed,
|
||||
Some(reason),
|
||||
))
|
||||
.await;
|
||||
let _ = append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&run_id,
|
||||
|
|
@ -667,7 +654,7 @@ const POSTRUN_ABORTED_MESSAGE: &str = "Run aborted before post-run finalization
|
|||
const POSTRUN_CANCELLED_MESSAGE: &str = "Run cancelled before post-run finalization completed.";
|
||||
|
||||
struct DetachedRunCompletionGuard {
|
||||
run_store: Arc<dyn RunStore>,
|
||||
run_store: RunStoreHandle,
|
||||
run_id: RunId,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
active: bool,
|
||||
|
|
@ -676,7 +663,7 @@ struct DetachedRunCompletionGuard {
|
|||
impl DetachedRunCompletionGuard {
|
||||
fn arm(
|
||||
run_id: RunId,
|
||||
run_store: Arc<dyn RunStore>,
|
||||
run_store: RunStoreHandle,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -740,16 +727,10 @@ impl Drop for DetachedRunCompletionGuard {
|
|||
Some((self.run_id, line))
|
||||
}
|
||||
};
|
||||
let run_store = Arc::clone(&self.run_store);
|
||||
let run_store = self.run_store.clone();
|
||||
let run_id = self.run_id;
|
||||
if let Ok(handle) = Handle::try_current() {
|
||||
handle.spawn(async move {
|
||||
let _ = run_store
|
||||
.put_status(&run_status::RunStatusRecord::new(
|
||||
RunStatus::Failed,
|
||||
Some(reason),
|
||||
))
|
||||
.await;
|
||||
let _ = append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&run_id,
|
||||
|
|
@ -761,15 +742,6 @@ impl Drop for DetachedRunCompletionGuard {
|
|||
},
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = run_store
|
||||
.put_conclusion(&build_failure_conclusion(message))
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"Failed to save post-run abort conclusion to store"
|
||||
);
|
||||
}
|
||||
if let Some((run_id, line)) = serialized_notice.or_else(|| {
|
||||
let envelope = canonicalize_event(
|
||||
&run_id,
|
||||
|
|
@ -802,7 +774,7 @@ impl Drop for DetachedRunCompletionGuard {
|
|||
|
||||
async fn persist_detached_failure(
|
||||
run_id: RunId,
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
run_dir: &Path,
|
||||
phase: &'static str,
|
||||
reason: StatusReason,
|
||||
|
|
@ -830,19 +802,6 @@ async fn persist_detached_failure(
|
|||
)
|
||||
.map_err(|err| FabroError::Io(err.to_string()))?;
|
||||
|
||||
let conclusion = build_failure_conclusion(&message);
|
||||
if let Err(err) = run_store.put_conclusion(&conclusion).await {
|
||||
tracing::warn!(error = %err, "Failed to save detached failure conclusion to store");
|
||||
}
|
||||
if let Err(err) = run_store
|
||||
.put_status(&run_status::RunStatusRecord::new(
|
||||
RunStatus::Failed,
|
||||
Some(reason),
|
||||
))
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %err, "Failed to save detached failure status to store");
|
||||
}
|
||||
if let Err(err) = append_workflow_event(
|
||||
run_store,
|
||||
&run_id,
|
||||
|
|
@ -879,33 +838,17 @@ async fn persist_detached_failure(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn build_failure_conclusion(message: &str) -> Conclusion {
|
||||
Conclusion {
|
||||
timestamp: Utc::now(),
|
||||
status: StageStatus::Fail,
|
||||
duration_ms: 0,
|
||||
failure_reason: Some(message.to_string()),
|
||||
final_git_commit_sha: None,
|
||||
stages: vec![],
|
||||
total_cost: None,
|
||||
total_retries: 0,
|
||||
total_input_tokens: 0,
|
||||
total_output_tokens: 0,
|
||||
total_cache_read_tokens: 0,
|
||||
total_cache_write_tokens: 0,
|
||||
total_reasoning_tokens: 0,
|
||||
has_pricing: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_store::{InMemoryStore, Store};
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_types::{Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
use crate::context::Context;
|
||||
|
|
@ -923,8 +866,16 @@ mod tests {
|
|||
start -> exit
|
||||
}"#;
|
||||
|
||||
async fn persisted_workflow(dot: &str, run_dir: &Path) -> (Persisted, InMemoryStore) {
|
||||
let store = InMemoryStore::default();
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
async fn persisted_workflow(dot: &str, run_dir: &Path) -> (Persisted, StoreHandle) {
|
||||
let store = memory_store();
|
||||
let created = crate::operations::create(
|
||||
&store,
|
||||
crate::operations::CreateRunInput {
|
||||
|
|
@ -960,7 +911,7 @@ mod tests {
|
|||
}
|
||||
|
||||
async fn test_start_services(
|
||||
store: &InMemoryStore,
|
||||
store: &SlateStore,
|
||||
_run_dir: &Path,
|
||||
emitter: Arc<EventEmitter>,
|
||||
registry: Arc<HandlerRegistry>,
|
||||
|
|
@ -1047,7 +998,7 @@ mod tests {
|
|||
|
||||
assert_eq!(started.finalized.conclusion.status, StageStatus::Success);
|
||||
let run_store = store.open_run(&fixtures::RUN_1).await.unwrap();
|
||||
assert!(run_store.get_conclusion().await.unwrap().is_some());
|
||||
assert!(run_store.state().await.unwrap().conclusion.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -1089,7 +1040,7 @@ mod tests {
|
|||
let (_persisted, store) = persisted_workflow(MINIMAL_DOT, &run_dir).await;
|
||||
let services = test_start_services(&store, &run_dir, emitter, registry).await;
|
||||
|
||||
// Write a checkpoint to the store (not disk) so start() sees it
|
||||
// Seed an authoritative checkpoint event so start() sees it
|
||||
let checkpoint = Checkpoint::from_context(
|
||||
&Context::new(),
|
||||
"start",
|
||||
|
|
@ -1101,11 +1052,41 @@ mod tests {
|
|||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
);
|
||||
services
|
||||
.run_store
|
||||
.put_checkpoint(&checkpoint)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
services.run_store.as_ref(),
|
||||
&services.run_id,
|
||||
&WorkflowRunEvent::CheckpointCompleted {
|
||||
node_id: checkpoint.current_node.clone(),
|
||||
status: checkpoint
|
||||
.node_outcomes
|
||||
.get(&checkpoint.current_node)
|
||||
.map_or_else(
|
||||
|| "success".to_string(),
|
||||
|outcome| outcome.status.to_string(),
|
||||
),
|
||||
current_node: checkpoint.current_node.clone(),
|
||||
completed_nodes: checkpoint.completed_nodes.clone(),
|
||||
node_retries: checkpoint.node_retries.clone().into_iter().collect(),
|
||||
context_values: checkpoint.context_values.clone().into_iter().collect(),
|
||||
node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(),
|
||||
next_node_id: checkpoint.next_node_id.clone(),
|
||||
git_commit_sha: checkpoint.git_commit_sha.clone(),
|
||||
loop_failure_signatures: checkpoint
|
||||
.loop_failure_signatures
|
||||
.iter()
|
||||
.map(|(sig, count)| (sig.to_string(), *count))
|
||||
.collect(),
|
||||
restart_failure_signatures: checkpoint
|
||||
.restart_failure_signatures
|
||||
.iter()
|
||||
.map(|(sig, count)| (sig.to_string(), *count))
|
||||
.collect(),
|
||||
node_visits: checkpoint.node_visits.clone().into_iter().collect(),
|
||||
diff: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = start(&run_dir, services).await;
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
registry,
|
||||
emitter: Arc::clone(&emitter),
|
||||
sandbox: Arc::clone(&sandbox),
|
||||
run_store: Arc::clone(&run_store),
|
||||
run_store: run_store.clone(),
|
||||
git_state: std::sync::RwLock::new(git_state),
|
||||
hook_runner: hook_runner.clone(),
|
||||
env,
|
||||
|
|
@ -93,7 +93,7 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
&sandbox,
|
||||
graph_arc,
|
||||
&run_options.run_dir,
|
||||
Arc::clone(&run_store),
|
||||
run_store.clone(),
|
||||
&settings_arc,
|
||||
checkpoint.is_some(),
|
||||
on_node,
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@ use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
|||
use fabro_hooks::HookSettings;
|
||||
use fabro_interview::AutoApproveInterviewer;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_store::InMemoryStore;
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_types::{RunId, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
use crate::context::{self, Context};
|
||||
use crate::error::FabroError;
|
||||
use crate::event::{EventEmitter, RunEventEnvelope};
|
||||
use crate::event::{EventEmitter, RunEventEnvelope, StoreProgressLogger};
|
||||
use crate::handler::start::StartHandler;
|
||||
use crate::handler::{Handler as HandlerTrait, HandlerRegistry};
|
||||
use crate::outcome::{Outcome, OutcomeExt, StageStatus};
|
||||
|
|
@ -156,8 +157,12 @@ fn test_lifecycle(setup_commands: Vec<String>) -> LifecycleOptions {
|
|||
}
|
||||
}
|
||||
|
||||
async fn test_run_store(_run_dir: &Path, run_id: &RunId) -> Arc<dyn fabro_store::RunStore> {
|
||||
let store: &dyn fabro_store::Store = &InMemoryStore::default();
|
||||
async fn test_run_store(_run_dir: &Path, run_id: &RunId) -> fabro_store::RunStoreHandle {
|
||||
let store: StoreHandle = Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
));
|
||||
store
|
||||
.create_run(run_id, chrono::Utc::now(), None)
|
||||
.await
|
||||
|
|
@ -175,13 +180,17 @@ async fn execute_test_run_with_options(
|
|||
) -> Executed {
|
||||
let run_id_value = run_options.run_id;
|
||||
let git_options = run_options.git.clone();
|
||||
let run_store = test_run_store(&run_options.run_dir, &run_id_value).await;
|
||||
let emitter = test_emitter_arc("test-run");
|
||||
let store_logger = StoreProgressLogger::new(run_store.clone());
|
||||
store_logger.register(&emitter);
|
||||
let initialized = initialize(
|
||||
persisted_workflow(graph, String::new(), &run_options.run_dir, run_id_value),
|
||||
InitOptions {
|
||||
run_id: run_id_value,
|
||||
run_store: test_run_store(&run_options.run_dir, &run_id_value).await,
|
||||
run_store,
|
||||
dry_run: false,
|
||||
emitter: test_emitter_arc("test-run"),
|
||||
emitter,
|
||||
sandbox: SandboxSpec::Local {
|
||||
working_directory: std::env::current_dir().unwrap(),
|
||||
},
|
||||
|
|
@ -217,7 +226,9 @@ async fn execute_test_run_with_options(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
execute(initialized).await
|
||||
let executed = execute(initialized).await;
|
||||
store_logger.flush().await;
|
||||
executed
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -503,7 +514,15 @@ async fn execute_runs_simple_workflow() {
|
|||
async fn execute_saves_checkpoint() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let executed = execute_test_run(dir.path(), simple_graph(), "test-run").await;
|
||||
assert!(executed.run_store.get_checkpoint().await.unwrap().is_some());
|
||||
assert!(
|
||||
executed
|
||||
.run_store
|
||||
.state()
|
||||
.await
|
||||
.unwrap()
|
||||
.checkpoint
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -547,7 +566,13 @@ async fn execute_error_when_no_start_node() {
|
|||
async fn execute_mirrors_graph_goal_to_context() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let executed = execute_test_run(dir.path(), simple_graph(), "test-run").await;
|
||||
let cp = executed.run_store.get_checkpoint().await.unwrap().unwrap();
|
||||
let cp = executed
|
||||
.run_store
|
||||
.state()
|
||||
.await
|
||||
.unwrap()
|
||||
.checkpoint
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
cp.context_values.get(context::keys::GRAPH_GOAL),
|
||||
Some(&serde_json::json!("Run tests"))
|
||||
|
|
@ -587,7 +612,13 @@ async fn execute_conditional_routing_uses_unconditional_success_path() {
|
|||
g.edges.push(Edge::new("path_b", "exit"));
|
||||
|
||||
let executed = execute_test_run(dir.path(), g, "test-run").await;
|
||||
let cp = executed.run_store.get_checkpoint().await.unwrap().unwrap();
|
||||
let cp = executed
|
||||
.run_store
|
||||
.state()
|
||||
.await
|
||||
.unwrap()
|
||||
.checkpoint
|
||||
.unwrap();
|
||||
assert!(cp.completed_nodes.contains(&"path_b".to_string()));
|
||||
assert!(!cp.completed_nodes.contains(&"path_a".to_string()));
|
||||
}
|
||||
|
|
@ -603,7 +634,8 @@ async fn execute_writes_start_json_and_node_status() {
|
|||
});
|
||||
|
||||
let executed = execute_test_run_with_options(run_options, simple_graph(), None).await;
|
||||
let start = executed.run_store.get_start().await.unwrap().unwrap();
|
||||
let state = executed.run_store.state().await.unwrap();
|
||||
let start = state.start.as_ref().unwrap();
|
||||
assert_eq!(start.run_id, test_run_id("test-run"));
|
||||
assert_eq!(
|
||||
start.run_branch.as_deref(),
|
||||
|
|
@ -611,15 +643,13 @@ async fn execute_writes_start_json_and_node_status() {
|
|||
);
|
||||
assert_eq!(start.base_sha.as_deref(), Some("abc123"));
|
||||
|
||||
let node = executed
|
||||
.run_store
|
||||
.get_node(&fabro_store::NodeVisitRef {
|
||||
let node = state
|
||||
.node(&fabro_store::NodeVisitRef {
|
||||
node_id: "start",
|
||||
visit: 1,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(node.status.unwrap().status, StageStatus::Success);
|
||||
assert_eq!(node.status.as_ref().unwrap().status, StageStatus::Success);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -668,15 +698,15 @@ async fn timeout_causes_fail_status_record() {
|
|||
Some(Arc::new(registry)),
|
||||
)
|
||||
.await;
|
||||
let status = executed
|
||||
.run_store
|
||||
.get_node(&fabro_store::NodeVisitRef {
|
||||
let state = executed.run_store.state().await.unwrap();
|
||||
let status = state
|
||||
.node(&fabro_store::NodeVisitRef {
|
||||
node_id: "work",
|
||||
visit: 1,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.status
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
assert_eq!(status.status, StageStatus::Fail);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::run_options::RunOptions;
|
|||
use crate::run_status::{RunStatus, StatusReason};
|
||||
use crate::sandbox_git::git_push_host;
|
||||
use fabro_hooks::{HookContext, HookEvent, HookRunner};
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::SlateRunStore;
|
||||
|
||||
use super::types::{Concluded, FinalizeOptions, Retroed};
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ pub fn classify_engine_result(
|
|||
}
|
||||
|
||||
pub(crate) async fn build_conclusion_from_store(
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
status: StageStatus,
|
||||
failure_reason: Option<String>,
|
||||
run_duration_ms: u64,
|
||||
|
|
@ -181,7 +181,7 @@ pub fn persist_terminal_outcome(
|
|||
pub async fn write_finalize_commit(
|
||||
run_options: &RunOptions,
|
||||
_run_dir: &Path,
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
) {
|
||||
let (Some(meta_branch), Some(repo_path)) = (
|
||||
run_options
|
||||
|
|
@ -275,7 +275,7 @@ pub async fn finalize(
|
|||
retro: _,
|
||||
} = retroed;
|
||||
|
||||
let (final_status, failure_reason, run_status, status_reason) =
|
||||
let (final_status, failure_reason, _run_status, _status_reason) =
|
||||
classify_engine_result(&outcome);
|
||||
let conclusion = build_conclusion_from_store(
|
||||
options.run_store.as_ref(),
|
||||
|
|
@ -324,20 +324,6 @@ pub async fn finalize(
|
|||
);
|
||||
}
|
||||
|
||||
if let Err(err) = options.run_store.put_conclusion(&conclusion).await {
|
||||
tracing::warn!(error = %err, "Failed to save conclusion to store");
|
||||
}
|
||||
if let Err(err) = options
|
||||
.run_store
|
||||
.put_status(&fabro_types::RunStatusRecord::new(
|
||||
run_status,
|
||||
status_reason,
|
||||
))
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %err, "Failed to save terminal status to store");
|
||||
}
|
||||
|
||||
Ok(Concluded {
|
||||
run_id: run_options.run_id,
|
||||
outcome,
|
||||
|
|
@ -353,13 +339,16 @@ pub async fn finalize(
|
|||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::{InMemoryStore, Store};
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_types::{RunId, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
use crate::event::StoreProgressLogger;
|
||||
use crate::pipeline::types::Retroed;
|
||||
use crate::run_options::RunOptions;
|
||||
|
||||
|
|
@ -383,12 +372,20 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn finalize_writes_conclusion_json() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let run_dir = temp.path().join("run");
|
||||
std::fs::create_dir_all(&run_dir).unwrap();
|
||||
let inner_store = InMemoryStore::default()
|
||||
let inner_store = test_store()
|
||||
.create_run(
|
||||
&test_run_id(),
|
||||
Utc::now(),
|
||||
|
|
@ -396,14 +393,17 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let run_store: Arc<dyn fabro_store::RunStore> = inner_store;
|
||||
let run_store = inner_store;
|
||||
let emitter = Arc::new(EventEmitter::new(test_run_id()));
|
||||
let store_logger = StoreProgressLogger::new(run_store.clone());
|
||||
store_logger.register(&emitter);
|
||||
let retroed = Retroed {
|
||||
graph: Graph::new("test"),
|
||||
outcome: Ok(Outcome::success()),
|
||||
run_options: test_run_options(&run_dir),
|
||||
run_store: Arc::clone(&run_store),
|
||||
run_store: run_store.clone(),
|
||||
hook_runner: None,
|
||||
emitter: Arc::new(EventEmitter::default()),
|
||||
emitter,
|
||||
sandbox: Arc::new(fabro_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap(),
|
||||
)),
|
||||
|
|
@ -416,7 +416,7 @@ mod tests {
|
|||
&FinalizeOptions {
|
||||
run_dir: run_dir.clone(),
|
||||
run_id: test_run_id(),
|
||||
run_store: Arc::clone(&run_store),
|
||||
run_store: run_store.clone(),
|
||||
workflow_name: "test".to_string(),
|
||||
hook_runner: None,
|
||||
preserve_sandbox: true,
|
||||
|
|
@ -425,8 +425,8 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
store_logger.flush().await;
|
||||
|
||||
assert!(run_store.get_conclusion().await.unwrap().is_some());
|
||||
assert_eq!(concluded.conclusion.status, StageStatus::Success);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -520,9 +520,6 @@ pub async fn initialize(
|
|||
host_working_directory: sandbox_record.host_working_directory.clone(),
|
||||
container_mount_point: sandbox_record.container_mount_point.clone(),
|
||||
});
|
||||
if let Err(err) = options.run_store.put_sandbox(&sandbox_record).await {
|
||||
tracing::warn!(error = %err, "Failed to save sandbox record to store");
|
||||
}
|
||||
|
||||
let env = build_sandbox_env(
|
||||
&options.sandbox_env,
|
||||
|
|
@ -670,15 +667,18 @@ pub async fn initialize(
|
|||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_interview::AutoApproveInterviewer;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_store::InMemoryStore;
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_types::{RunId, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
use crate::event::StoreProgressLogger;
|
||||
use crate::pipeline::types::InitOptions;
|
||||
use crate::records::RunRecord;
|
||||
use crate::run_options::RunOptions;
|
||||
|
|
@ -687,6 +687,14 @@ mod tests {
|
|||
fixtures::RUN_1
|
||||
}
|
||||
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn simple_graph() -> (Graph, String) {
|
||||
let source = r"digraph test {
|
||||
start [shape=Mdiamond];
|
||||
|
|
@ -754,14 +762,14 @@ mod tests {
|
|||
std::fs::create_dir_all(&run_dir).unwrap();
|
||||
let (graph, source) = simple_graph();
|
||||
let persisted = test_persisted(graph, source.clone(), &run_dir);
|
||||
let emitter = Arc::new(crate::event::EventEmitter::default());
|
||||
let emitter = Arc::new(crate::event::EventEmitter::new(test_run_id()));
|
||||
|
||||
let initialized = initialize(
|
||||
persisted,
|
||||
InitOptions {
|
||||
run_id: test_run_id(),
|
||||
run_store: {
|
||||
let store: &dyn fabro_store::Store = &InMemoryStore::default();
|
||||
let store = memory_store();
|
||||
let inner = store
|
||||
.create_run(
|
||||
&test_run_id(),
|
||||
|
|
@ -829,24 +837,29 @@ mod tests {
|
|||
std::fs::create_dir_all(&run_dir).unwrap();
|
||||
let (graph, source) = simple_graph();
|
||||
let persisted = test_persisted(graph, source, &run_dir);
|
||||
let emitter = Arc::new(crate::event::EventEmitter::default());
|
||||
let emitter = Arc::new(crate::event::EventEmitter::new(test_run_id()));
|
||||
let store = memory_store();
|
||||
let run_store = store
|
||||
.create_run(
|
||||
&test_run_id(),
|
||||
chrono::Utc::now(),
|
||||
Some(run_dir.to_string_lossy().as_ref()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let store_logger = StoreProgressLogger::new(run_store.clone());
|
||||
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
emitter.on_event({
|
||||
let seen = Arc::clone(&seen);
|
||||
move |event| seen.lock().unwrap().push(event.event.clone())
|
||||
});
|
||||
store_logger.register(&emitter);
|
||||
|
||||
let initialized = initialize(
|
||||
persisted,
|
||||
InitOptions {
|
||||
run_id: test_run_id(),
|
||||
run_store: {
|
||||
let store: &dyn fabro_store::Store = &InMemoryStore::default();
|
||||
let inner = store
|
||||
.create_run(
|
||||
&test_run_id(),
|
||||
chrono::Utc::now(),
|
||||
Some(run_dir.to_string_lossy().as_ref()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
inner
|
||||
},
|
||||
run_store,
|
||||
dry_run: false,
|
||||
emitter,
|
||||
sandbox: SandboxSpec::Local {
|
||||
|
|
@ -883,8 +896,14 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
store_logger.flush().await;
|
||||
|
||||
assert!(initialized.run_store.get_sandbox().await.unwrap().is_some());
|
||||
assert_eq!(initialized.run_options.run_dir, run_dir);
|
||||
assert!(
|
||||
seen.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|event| event == "sandbox.initialized")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::path::Path;
|
||||
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::SlateRunStore;
|
||||
|
||||
use crate::error::FabroError;
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ pub(crate) fn persist(
|
|||
}
|
||||
|
||||
pub(crate) async fn load_from_store(
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
run_dir: &Path,
|
||||
) -> Result<Persisted, FabroError> {
|
||||
let state = run_store
|
||||
|
|
@ -55,12 +55,24 @@ mod tests {
|
|||
|
||||
use chrono::Utc;
|
||||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_store::{InMemoryStore, Store};
|
||||
use fabro_store::{RunStoreHandle, SlateStore, StoreHandle};
|
||||
use fabro_types::{Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::event::{WorkflowRunEvent, append_workflow_event};
|
||||
use crate::records::RunRecord;
|
||||
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn graph_and_source() -> (Graph, String) {
|
||||
let source = r#"digraph test {
|
||||
graph [goal="Ship feature"];
|
||||
|
|
@ -130,8 +142,8 @@ mod tests {
|
|||
run_dir: &Path,
|
||||
record: &RunRecord,
|
||||
source: Option<&str>,
|
||||
) -> std::sync::Arc<dyn RunStore> {
|
||||
let store = InMemoryStore::default();
|
||||
) -> RunStoreHandle {
|
||||
let store = memory_store();
|
||||
let run_store = store
|
||||
.create_run(
|
||||
&record.run_id,
|
||||
|
|
@ -140,10 +152,26 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run_store.put_run(record).await.unwrap();
|
||||
if let Some(source) = source {
|
||||
run_store.put_graph(source).await.unwrap();
|
||||
}
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&record.run_id,
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
run_id: record.run_id,
|
||||
settings: serde_json::to_value(&record.settings).unwrap(),
|
||||
graph: serde_json::to_value(&record.graph).unwrap(),
|
||||
workflow_source: source.map(ToOwned::to_owned),
|
||||
workflow_config: None,
|
||||
labels: record.labels.clone().into_iter().collect(),
|
||||
run_dir: run_dir.to_string_lossy().to_string(),
|
||||
working_directory: record.working_directory.display().to_string(),
|
||||
host_repo_path: record.host_repo_path.clone(),
|
||||
base_branch: record.base_branch.clone(),
|
||||
workflow_slug: record.workflow_slug.clone(),
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
}
|
||||
|
||||
|
|
@ -214,10 +242,23 @@ mod tests {
|
|||
let run_store = seeded_store(&run_dir, &expected, Some(&source)).await;
|
||||
let loaded = load_from_store(run_store.as_ref(), &run_dir).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(loaded.run_record()).unwrap(),
|
||||
serde_json::to_value(expected).unwrap()
|
||||
let loaded_record = loaded.run_record();
|
||||
assert_eq!(loaded_record.run_id, expected.run_id);
|
||||
assert!(
|
||||
(loaded_record.created_at.timestamp_millis() - expected.created_at.timestamp_millis())
|
||||
.abs()
|
||||
<= 1
|
||||
);
|
||||
assert_eq!(loaded_record.settings, expected.settings);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&loaded_record.graph).unwrap(),
|
||||
serde_json::to_value(&expected.graph).unwrap()
|
||||
);
|
||||
assert_eq!(loaded_record.workflow_slug, expected.workflow_slug);
|
||||
assert_eq!(loaded_record.working_directory, expected.working_directory);
|
||||
assert_eq!(loaded_record.host_repo_path, expected.host_repo_path);
|
||||
assert_eq!(loaded_record.base_branch, expected.base_branch);
|
||||
assert_eq!(loaded_record.labels, expected.labels);
|
||||
assert_eq!(loaded.source(), source);
|
||||
assert!(loaded.diagnostics().is_empty());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::path::Path;
|
||||
|
||||
use fabro_config::run::MergeStrategy;
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_types::PullRequestRecord;
|
||||
use tracing::{debug, info};
|
||||
|
||||
|
|
@ -292,7 +292,7 @@ fn emit_run_notice(
|
|||
});
|
||||
}
|
||||
|
||||
async fn load_pull_request_diff(run_store: &dyn RunStore, run_dir: &Path) -> String {
|
||||
async fn load_pull_request_diff(run_store: &SlateRunStore, run_dir: &Path) -> String {
|
||||
let _ = run_dir;
|
||||
run_store
|
||||
.state()
|
||||
|
|
@ -311,7 +311,7 @@ pub async fn build_pr_body(
|
|||
diff: &str,
|
||||
goal: &str,
|
||||
model: &str,
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
run_dir: &Path,
|
||||
conclusion: Option<&Conclusion>,
|
||||
) -> Result<String, String> {
|
||||
|
|
@ -340,7 +340,9 @@ pub async fn build_pr_body(
|
|||
.ok();
|
||||
let retro = run_state.as_ref().and_then(|state| state.retro.clone());
|
||||
let run_record = run_state.as_ref().and_then(|state| state.run.clone());
|
||||
let dot_source = run_state.as_ref().and_then(|state| state.graph_source.clone());
|
||||
let dot_source = run_state
|
||||
.as_ref()
|
||||
.and_then(|state| state.graph_source.clone());
|
||||
|
||||
// Build LLM prompt
|
||||
let system = if plan_text.is_some() {
|
||||
|
|
@ -418,7 +420,7 @@ pub async fn maybe_open_pull_request(
|
|||
model: &str,
|
||||
draft: bool,
|
||||
auto_merge: Option<AutoMergeOptions>,
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
run_dir: &Path,
|
||||
conclusion: Option<&Conclusion>,
|
||||
) -> Result<Option<PullRequestRecord>, String> {
|
||||
|
|
@ -489,11 +491,6 @@ pub async fn maybe_open_pull_request(
|
|||
title,
|
||||
};
|
||||
|
||||
run_store
|
||||
.put_pull_request(&record)
|
||||
.await
|
||||
.map_err(|err| format!("failed to persist pull request in run store: {err}"))?;
|
||||
|
||||
Ok(Some(record))
|
||||
}
|
||||
|
||||
|
|
@ -522,7 +519,8 @@ pub async fn pull_request(concluded: Concluded, options: &PullRequestOptions) ->
|
|||
result.status,
|
||||
StageStatus::Success | StageStatus::PartialSuccess
|
||||
) {
|
||||
let diff = load_pull_request_diff(options.run_store.as_ref(), &options.run_dir).await;
|
||||
let diff =
|
||||
load_pull_request_diff(options.run_store.as_ref(), &options.run_dir).await;
|
||||
if let (Some(base_branch), Some(run_branch), Some(creds), Some(origin)) = (
|
||||
&run_options.base_branch,
|
||||
pushed_branch.as_deref(),
|
||||
|
|
@ -598,6 +596,7 @@ mod tests {
|
|||
use std::sync::{Arc, Once};
|
||||
|
||||
use super::*;
|
||||
use crate::event::{WorkflowRunEvent, append_workflow_event};
|
||||
use crate::records::StageSummary;
|
||||
use chrono::Utc;
|
||||
use fabro_graphviz::graph::Graph;
|
||||
|
|
@ -609,9 +608,11 @@ mod tests {
|
|||
use fabro_retro::retro::{
|
||||
AggregateStats, FrictionKind, FrictionPoint, OpenItem, OpenItemKind, StageRetro,
|
||||
};
|
||||
use fabro_store::{InMemoryStore, Store};
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_types::{RunRecord, Settings, fixtures};
|
||||
use futures::stream;
|
||||
use object_store::memory::InMemory;
|
||||
use std::time::Duration;
|
||||
|
||||
struct MockProvider {
|
||||
response_text: String,
|
||||
|
|
@ -684,6 +685,14 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn install_mock_llm() {
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
|
|
@ -1034,7 +1043,7 @@ mod tests {
|
|||
install_mock_llm();
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = InMemoryStore::default();
|
||||
let store = test_store();
|
||||
let run_store = store
|
||||
.create_run(
|
||||
&fixtures::RUN_1,
|
||||
|
|
@ -1066,7 +1075,7 @@ mod tests {
|
|||
install_mock_llm();
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = InMemoryStore::default();
|
||||
let store = test_store();
|
||||
let created_at = Utc::now();
|
||||
let run_store = store
|
||||
.create_run(
|
||||
|
|
@ -1077,32 +1086,55 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
run_store
|
||||
.put_run(&RunRecord {
|
||||
let run_record = RunRecord {
|
||||
run_id: fixtures::RUN_1,
|
||||
created_at,
|
||||
settings: Settings::default(),
|
||||
graph: Graph::new("test"),
|
||||
workflow_slug: Some("test".to_string()),
|
||||
working_directory: PathBuf::from("/tmp/project"),
|
||||
host_repo_path: Some("/tmp/project".to_string()),
|
||||
base_branch: Some("main".to_string()),
|
||||
labels: HashMap::new(),
|
||||
};
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
run_id: fixtures::RUN_1,
|
||||
created_at,
|
||||
settings: Settings::default(),
|
||||
graph: Graph::new("test"),
|
||||
workflow_slug: Some("test".to_string()),
|
||||
working_directory: PathBuf::from("/tmp/project"),
|
||||
host_repo_path: Some("/tmp/project".to_string()),
|
||||
base_branch: Some("main".to_string()),
|
||||
labels: HashMap::new(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.put_graph("digraph test { plan -> code }")
|
||||
.await
|
||||
.unwrap();
|
||||
run_store.put_retro(&make_test_retro()).await.unwrap();
|
||||
settings: serde_json::to_value(&run_record.settings).unwrap(),
|
||||
graph: serde_json::to_value(&run_record.graph).unwrap(),
|
||||
workflow_source: Some("digraph test { plan -> code }".to_string()),
|
||||
workflow_config: None,
|
||||
labels: run_record.labels.clone().into_iter().collect(),
|
||||
run_dir: tmp.path().display().to_string(),
|
||||
working_directory: run_record.working_directory.display().to_string(),
|
||||
host_repo_path: run_record.host_repo_path.clone(),
|
||||
base_branch: run_record.base_branch.clone(),
|
||||
workflow_slug: run_record.workflow_slug.clone(),
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::RetroCompleted {
|
||||
duration_ms: 1,
|
||||
response: Some(String::new()),
|
||||
retro: Some(serde_json::to_value(make_test_retro()).unwrap()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let conclusion = make_test_conclusion();
|
||||
let body = build_pr_body(
|
||||
"diff --git a/src/lib.rs b/src/lib.rs\n+fn new_feature() {}\n",
|
||||
"Implement feature",
|
||||
"mock-model",
|
||||
run_store.as_ref(),
|
||||
run_store.as_ref(),
|
||||
tmp.path(),
|
||||
Some(&conclusion),
|
||||
)
|
||||
|
|
@ -1292,7 +1324,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn empty_diff_returns_none() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = InMemoryStore::default();
|
||||
let store = test_store();
|
||||
let run_store = store
|
||||
.create_run(
|
||||
&fixtures::RUN_1,
|
||||
|
|
@ -1327,7 +1359,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn load_pull_request_diff_uses_store_without_disk_patch() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = InMemoryStore::default();
|
||||
let store = test_store();
|
||||
let created_at = Utc::now();
|
||||
let run_store = store
|
||||
.create_run(
|
||||
|
|
@ -1337,24 +1369,55 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.put_run(&RunRecord {
|
||||
let run_record = RunRecord {
|
||||
run_id: fixtures::RUN_1,
|
||||
created_at,
|
||||
settings: Settings::default(),
|
||||
graph: Graph::new("test"),
|
||||
workflow_slug: None,
|
||||
working_directory: tmp.path().to_path_buf(),
|
||||
host_repo_path: None,
|
||||
base_branch: None,
|
||||
labels: std::collections::HashMap::new(),
|
||||
};
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
run_id: fixtures::RUN_1,
|
||||
created_at,
|
||||
settings: Settings::default(),
|
||||
graph: Graph::new("test"),
|
||||
workflow_slug: None,
|
||||
working_directory: tmp.path().to_path_buf(),
|
||||
settings: serde_json::to_value(&run_record.settings).unwrap(),
|
||||
graph: serde_json::to_value(&run_record.graph).unwrap(),
|
||||
workflow_source: None,
|
||||
workflow_config: None,
|
||||
labels: run_record.labels.clone().into_iter().collect(),
|
||||
run_dir: tmp.path().display().to_string(),
|
||||
working_directory: tmp.path().display().to_string(),
|
||||
host_repo_path: None,
|
||||
base_branch: None,
|
||||
labels: std::collections::HashMap::new(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
.put_final_patch("diff --git a/src/lib.rs b/src/lib.rs\n+fn from_store() {}\n")
|
||||
.await
|
||||
.unwrap();
|
||||
workflow_slug: None,
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::WorkflowRunCompleted {
|
||||
duration_ms: 1,
|
||||
artifact_count: 0,
|
||||
status: "success".to_string(),
|
||||
reason: None,
|
||||
total_cost: None,
|
||||
final_git_commit_sha: None,
|
||||
final_patch: Some(
|
||||
"diff --git a/src/lib.rs b/src/lib.rs\n+fn from_store() {}\n".to_string(),
|
||||
),
|
||||
usage: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let diff = load_pull_request_diff(run_store.as_ref(), tmp.path()).await;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ use fabro_retro::retro_agent::{
|
|||
|
||||
use super::types::{Executed, RetroOptions, Retroed};
|
||||
use crate::event::WorkflowRunEvent;
|
||||
#[cfg(test)]
|
||||
use crate::records::RunRecord;
|
||||
|
||||
pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option<Retro> {
|
||||
let state = match options.run_store.state().await {
|
||||
|
|
@ -56,10 +54,6 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option<Retro> {
|
|||
&stage_durations,
|
||||
);
|
||||
|
||||
if let Err(err) = options.run_store.put_retro(&retro).await {
|
||||
tracing::warn!(error = %err, "Failed to save initial retro to store");
|
||||
}
|
||||
|
||||
let retro_start = std::time::Instant::now();
|
||||
let retro_prompt = build_retro_prompt(RETRO_DATA_DIR);
|
||||
if let Some(ref emitter) = options.emitter {
|
||||
|
|
@ -115,9 +109,6 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option<Retro> {
|
|||
retro: serde_json::to_value(&retro).ok(),
|
||||
});
|
||||
}
|
||||
if let Err(err) = options.run_store.put_retro(&retro).await {
|
||||
tracing::warn!(error = %err, "Failed to save retro with narrative to store");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(ref emitter) = options.emitter {
|
||||
|
|
@ -178,17 +169,20 @@ pub async fn retro(executed: Executed, options: &RetroOptions) -> Retroed {
|
|||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::{InMemoryStore, Store};
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_types::{RunId, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
use crate::context::Context;
|
||||
use crate::event::EventEmitter;
|
||||
use crate::event::{StoreProgressLogger, WorkflowRunEvent, append_workflow_event};
|
||||
use crate::pipeline::types::Executed;
|
||||
use crate::records::{Checkpoint, CheckpointExt};
|
||||
use crate::records::{Checkpoint, CheckpointExt, RunRecord};
|
||||
use crate::run_options::RunOptions;
|
||||
|
||||
fn test_run_id() -> RunId {
|
||||
|
|
@ -214,12 +208,20 @@ mod tests {
|
|||
checkpoint
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
async fn test_run_store(
|
||||
run_dir: &std::path::Path,
|
||||
checkpoint: &Checkpoint,
|
||||
) -> Arc<dyn fabro_store::RunStore> {
|
||||
) -> fabro_store::RunStoreHandle {
|
||||
let created_at = Utc::now();
|
||||
let inner = InMemoryStore::default()
|
||||
let inner = test_store()
|
||||
.create_run(
|
||||
&test_run_id(),
|
||||
created_at,
|
||||
|
|
@ -227,22 +229,69 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let run_store: Arc<dyn fabro_store::RunStore> = inner;
|
||||
run_store
|
||||
.put_run(&RunRecord {
|
||||
let run_store = inner;
|
||||
let run_record = RunRecord {
|
||||
run_id: test_run_id(),
|
||||
created_at,
|
||||
settings: Settings::default(),
|
||||
graph: Graph::new("test"),
|
||||
workflow_slug: None,
|
||||
working_directory: run_dir.to_path_buf(),
|
||||
host_repo_path: None,
|
||||
base_branch: None,
|
||||
labels: std::collections::HashMap::new(),
|
||||
};
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&test_run_id(),
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
run_id: test_run_id(),
|
||||
created_at,
|
||||
settings: Settings::default(),
|
||||
graph: Graph::new("test"),
|
||||
workflow_slug: None,
|
||||
working_directory: run_dir.to_path_buf(),
|
||||
settings: serde_json::to_value(&run_record.settings).unwrap(),
|
||||
graph: serde_json::to_value(&run_record.graph).unwrap(),
|
||||
workflow_source: None,
|
||||
workflow_config: None,
|
||||
labels: run_record.labels.clone().into_iter().collect(),
|
||||
run_dir: run_dir.to_string_lossy().to_string(),
|
||||
working_directory: run_dir.to_string_lossy().to_string(),
|
||||
host_repo_path: None,
|
||||
base_branch: None,
|
||||
labels: std::collections::HashMap::new(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
run_store.put_checkpoint(checkpoint).await.unwrap();
|
||||
workflow_slug: None,
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&test_run_id(),
|
||||
&WorkflowRunEvent::CheckpointCompleted {
|
||||
node_id: checkpoint.current_node.clone(),
|
||||
status: "success".to_string(),
|
||||
current_node: checkpoint.current_node.clone(),
|
||||
completed_nodes: checkpoint.completed_nodes.clone(),
|
||||
node_retries: checkpoint.node_retries.clone().into_iter().collect(),
|
||||
context_values: checkpoint.context_values.clone().into_iter().collect(),
|
||||
node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(),
|
||||
next_node_id: checkpoint.next_node_id.clone(),
|
||||
git_commit_sha: checkpoint.git_commit_sha.clone(),
|
||||
loop_failure_signatures: checkpoint
|
||||
.loop_failure_signatures
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(signature, count)| (signature.to_string(), count))
|
||||
.collect(),
|
||||
restart_failure_signatures: checkpoint
|
||||
.restart_failure_signatures
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(signature, count)| (signature.to_string(), count))
|
||||
.collect(),
|
||||
node_visits: checkpoint.node_visits.clone().into_iter().collect(),
|
||||
diff: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run_store
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +319,9 @@ mod tests {
|
|||
let checkpoint = build_checkpoint();
|
||||
let run_store = test_run_store(&run_dir, &checkpoint).await;
|
||||
|
||||
let emitter = Arc::new(EventEmitter::default());
|
||||
let emitter = Arc::new(EventEmitter::new(test_run_id()));
|
||||
let store_logger = StoreProgressLogger::new(run_store.clone());
|
||||
store_logger.register(&emitter);
|
||||
let sandbox: Arc<dyn fabro_agent::Sandbox> = Arc::new(fabro_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap(),
|
||||
));
|
||||
|
|
@ -278,7 +329,7 @@ mod tests {
|
|||
graph: Graph::new("test"),
|
||||
outcome: Ok(crate::outcome::Outcome::success()),
|
||||
run_options: test_run_options(&run_dir),
|
||||
run_store: Arc::clone(&run_store),
|
||||
run_store: run_store.clone(),
|
||||
hook_runner: None,
|
||||
emitter: Arc::clone(&emitter),
|
||||
sandbox: Arc::clone(&sandbox),
|
||||
|
|
@ -308,8 +359,8 @@ mod tests {
|
|||
},
|
||||
)
|
||||
.await;
|
||||
store_logger.flush().await;
|
||||
|
||||
assert!(retroed.run_store.get_retro().await.unwrap().is_some());
|
||||
assert!(retroed.retro.is_some());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use fabro_llm::Provider;
|
|||
use fabro_mcp::config::McpServerSettings;
|
||||
use fabro_model::FallbackTarget;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_store::RunStore;
|
||||
use fabro_store::{RunStoreHandle, SlateRunStore};
|
||||
use fabro_types::RunId;
|
||||
use fabro_validate::Diagnostic;
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ impl Persisted {
|
|||
}
|
||||
|
||||
pub async fn load_from_store(
|
||||
run_store: &dyn RunStore,
|
||||
run_store: &SlateRunStore,
|
||||
run_dir: &Path,
|
||||
) -> Result<Self, FabroError> {
|
||||
super::persist::load_from_store(run_store, run_dir).await
|
||||
|
|
@ -227,7 +227,7 @@ pub struct DevcontainerSpec {
|
|||
|
||||
pub struct InitOptions {
|
||||
pub run_id: RunId,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub dry_run: bool,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
pub sandbox: SandboxSpec,
|
||||
|
|
@ -251,7 +251,7 @@ pub struct Initialized {
|
|||
pub graph: Graph,
|
||||
pub source: String,
|
||||
pub run_options: RunOptions,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub(crate) checkpoint: Option<Checkpoint>,
|
||||
pub(crate) seed_context: Option<Context>,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
|
|
@ -272,7 +272,7 @@ pub struct Executed {
|
|||
pub graph: Graph,
|
||||
pub outcome: Result<Outcome, FabroError>,
|
||||
pub run_options: RunOptions,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub hook_runner: Option<Arc<HookRunner>>,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
pub sandbox: Arc<dyn Sandbox>,
|
||||
|
|
@ -289,7 +289,7 @@ pub struct Retroed {
|
|||
pub graph: Graph,
|
||||
pub outcome: Result<Outcome, FabroError>,
|
||||
pub run_options: RunOptions,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub hook_runner: Option<Arc<HookRunner>>,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
pub sandbox: Arc<dyn Sandbox>,
|
||||
|
|
@ -328,7 +328,7 @@ pub struct TransformOptions {
|
|||
/// Options for the RETRO phase.
|
||||
pub struct RetroOptions {
|
||||
pub run_id: RunId,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub workflow_name: String,
|
||||
pub goal: String,
|
||||
pub run_dir: PathBuf,
|
||||
|
|
@ -346,7 +346,7 @@ pub struct RetroOptions {
|
|||
pub struct FinalizeOptions {
|
||||
pub run_dir: PathBuf,
|
||||
pub run_id: RunId,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub workflow_name: String,
|
||||
pub hook_runner: Option<Arc<HookRunner>>,
|
||||
pub preserve_sandbox: bool,
|
||||
|
|
@ -356,7 +356,7 @@ pub struct FinalizeOptions {
|
|||
/// Options for the PULL_REQUEST phase.
|
||||
pub struct PullRequestOptions {
|
||||
pub run_dir: PathBuf,
|
||||
pub run_store: Arc<dyn RunStore>,
|
||||
pub run_store: RunStoreHandle,
|
||||
pub pr_config: Option<PullRequestSettings>,
|
||||
pub github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
pub origin_url: Option<String>,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_store::{ListRunsQuery, Store};
|
||||
use fabro_store::{ListRunsQuery, SlateStore};
|
||||
use fabro_types::RunId;
|
||||
use serde::Serialize;
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ fn scan_runs_inner(base: &Path, include_status: bool) -> Result<Vec<RunInfo>> {
|
|||
Ok(runs)
|
||||
}
|
||||
|
||||
pub async fn scan_runs_combined(store: &dyn Store, base: &Path) -> Result<Vec<RunInfo>> {
|
||||
pub async fn scan_runs_combined(store: &SlateStore, base: &Path) -> Result<Vec<RunInfo>> {
|
||||
let mut runs_by_id: HashMap<RunId, RunInfo> = HashMap::new();
|
||||
|
||||
if let Ok(store_runs) = store.list_runs(&ListRunsQuery::default()).await {
|
||||
|
|
@ -373,7 +373,7 @@ pub fn resolve_run(base: &Path, identifier: &str) -> Result<RunInfo> {
|
|||
}
|
||||
|
||||
pub async fn resolve_run_combined(
|
||||
store: &dyn Store,
|
||||
store: &SlateStore,
|
||||
base: &Path,
|
||||
identifier: &str,
|
||||
) -> Result<RunInfo> {
|
||||
|
|
@ -438,15 +438,27 @@ fn run_id_matches(run_id: RunId, prefix: &str) -> bool {
|
|||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::{InMemoryStore, Store};
|
||||
use fabro_types::{RunStatus, RunStatusRecord, Settings, fixtures};
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_types::{RunStatus, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::scan_runs_combined;
|
||||
use crate::event::{WorkflowRunEvent, append_workflow_event};
|
||||
use crate::records::{RunRecord, RunRecordExt};
|
||||
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
fn sample_run_record() -> RunRecord {
|
||||
RunRecord {
|
||||
run_id: fixtures::RUN_1,
|
||||
|
|
@ -471,7 +483,7 @@ mod tests {
|
|||
run_record.save(&run_dir).unwrap();
|
||||
std::fs::write(run_dir.join("id.txt"), format!("{}\n", fixtures::RUN_1)).unwrap();
|
||||
|
||||
let store = InMemoryStore::default();
|
||||
let store = memory_store();
|
||||
let run_dir_string = run_dir.to_string_lossy().to_string();
|
||||
let run_store = store
|
||||
.create_run(
|
||||
|
|
@ -481,11 +493,33 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run_store.put_run(&run_record).await.unwrap();
|
||||
run_store
|
||||
.put_status(&RunStatusRecord::new(RunStatus::Submitted, None))
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
run_id: fixtures::RUN_1,
|
||||
settings: serde_json::to_value(&run_record.settings).unwrap(),
|
||||
graph: serde_json::to_value(&run_record.graph).unwrap(),
|
||||
workflow_source: None,
|
||||
workflow_config: None,
|
||||
labels: run_record.labels.clone().into_iter().collect(),
|
||||
run_dir: run_dir_string.clone(),
|
||||
working_directory: run_record.working_directory.display().to_string(),
|
||||
host_repo_path: run_record.host_repo_path.clone(),
|
||||
base_branch: run_record.base_branch.clone(),
|
||||
workflow_slug: run_record.workflow_slug.clone(),
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&fixtures::RUN_1,
|
||||
&WorkflowRunEvent::RunSubmitted { reason: None },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let runs = scan_runs_combined(&store, temp.path()).await.unwrap();
|
||||
let run = runs
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_graphviz::graph::Graph as GvGraph;
|
||||
use fabro_store::{InMemoryStore, RunStore, Store};
|
||||
use fabro_types::run::RunRecord;
|
||||
use fabro_store::{SlateRunStore, SlateStore};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::event::EventEmitter;
|
||||
use crate::event::{EventEmitter, WorkflowRunEvent, append_workflow_event};
|
||||
use crate::git::scan_node_files_from_store;
|
||||
use crate::handler::HandlerRegistry;
|
||||
use crate::outcome::Outcome;
|
||||
|
|
@ -41,32 +42,50 @@ async fn initialized(
|
|||
) -> Initialized {
|
||||
std::fs::create_dir_all(&run_options.run_dir).expect("failed to create run dir");
|
||||
let created_at = Utc::now();
|
||||
let inner_store = InMemoryStore::default()
|
||||
let store = Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
));
|
||||
let inner_store = store
|
||||
.create_run(
|
||||
&run_options.run_id,
|
||||
created_at,
|
||||
Some(run_options.run_dir.to_string_lossy().as_ref()),
|
||||
)
|
||||
.await
|
||||
.expect("failed to create in-memory run store");
|
||||
.expect("failed to create slate-backed test run store");
|
||||
let run_store = inner_store;
|
||||
run_store
|
||||
.put_run(&RunRecord {
|
||||
append_workflow_event(
|
||||
run_store.as_ref(),
|
||||
&run_options.run_id,
|
||||
&WorkflowRunEvent::RunCreated {
|
||||
run_id: run_options.run_id,
|
||||
created_at,
|
||||
settings: run_options.settings.clone(),
|
||||
graph: graph.clone(),
|
||||
workflow_slug: run_options.workflow_slug.clone(),
|
||||
working_directory: PathBuf::from(sandbox.working_directory()),
|
||||
settings: serde_json::to_value(&run_options.settings)
|
||||
.expect("failed to serialize settings"),
|
||||
graph: serde_json::to_value(graph).expect("failed to serialize graph"),
|
||||
workflow_source: None,
|
||||
workflow_config: None,
|
||||
labels: run_options
|
||||
.labels
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
run_dir: run_options.run_dir.display().to_string(),
|
||||
working_directory: PathBuf::from(sandbox.working_directory())
|
||||
.display()
|
||||
.to_string(),
|
||||
host_repo_path: run_options
|
||||
.host_repo_path
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string()),
|
||||
base_branch: run_options.base_branch.clone(),
|
||||
labels: run_options.labels.clone(),
|
||||
})
|
||||
.await
|
||||
.expect("failed to seed run record in run store");
|
||||
workflow_slug: run_options.workflow_slug.clone(),
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("failed to seed run.created event in run store");
|
||||
let emitter = bound_emitter(run_options.run_id, &emitter);
|
||||
Initialized {
|
||||
graph: graph.clone(),
|
||||
|
|
@ -166,12 +185,17 @@ pub async fn run_graph_from_checkpoint(
|
|||
executed.outcome
|
||||
}
|
||||
|
||||
async fn persist_run_artifacts_for_tests(run_store: &dyn RunStore, run_dir: &std::path::Path) {
|
||||
if let Ok(Some(checkpoint)) = run_store.get_checkpoint().await {
|
||||
async fn persist_run_artifacts_for_tests(run_store: &SlateRunStore, run_dir: &std::path::Path) {
|
||||
let state: fabro_store::RunState = match run_store.state().await {
|
||||
Ok(state) => state,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if let Some(checkpoint) = state.checkpoint.as_ref() {
|
||||
let _ = checkpoint.save(&run_dir.join("checkpoint.json"));
|
||||
}
|
||||
|
||||
if let Ok(Some(final_patch)) = run_store.get_final_patch().await {
|
||||
if let Some(final_patch) = state.final_patch.as_ref() {
|
||||
let _ = std::fs::write(run_dir.join("final.patch"), final_patch);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue