mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-09 22:33:37 +00:00
Expand store-backed run metadata and detached startup
Add the missing run-store records for node metadata, final patches, and pull request state, and extend the store snapshots/backends to round-trip them. Also cut the detached startup path over to explicit run IDs and store-backed status loading so start and detached execution no longer require run.json for bootstrap.
This commit is contained in:
parent
96359f95cb
commit
c83cf3a577
18 changed files with 755 additions and 62 deletions
|
|
@ -646,9 +646,12 @@ pub(crate) enum RunCommands {
|
|||
/// Run ID prefix or workflow name
|
||||
run: String,
|
||||
},
|
||||
/// Internal: run the engine process (reads run.json from run dir)
|
||||
/// Internal: run the engine process
|
||||
#[command(name = "__detached", hide = true)]
|
||||
Detached {
|
||||
/// Run ID
|
||||
#[arg(long)]
|
||||
run_id: fabro_types::RunId,
|
||||
/// Run directory
|
||||
#[arg(long)]
|
||||
run_dir: PathBuf,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::Result;
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::{GlobalArgs, RunArgs};
|
||||
|
|
@ -21,7 +22,8 @@ pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<(
|
|||
#[cfg(not(feature = "sleep_inhibitor"))]
|
||||
let _ = prevent_idle_sleep;
|
||||
|
||||
let child = super::start::start_run(&run_dir, false)?;
|
||||
let child =
|
||||
super::start::start_run(&run_dir, &run_id, &cli_settings.storage_dir(), false).await?;
|
||||
|
||||
if args.detach {
|
||||
if globals.json {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,43 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, anyhow};
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_interview::FileInterviewer;
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_store::{RuntimeState, Store};
|
||||
use fabro_types::RunId;
|
||||
use fabro_workflow::event::EventEmitter;
|
||||
use fabro_workflow::operations::{
|
||||
StartServices, open_or_hydrate_run, resume as resume_run, start as start_run,
|
||||
};
|
||||
use fabro_workflow::records::{RunRecord, RunRecordExt};
|
||||
use fabro_workflow::operations::{StartServices, resume as resume_run, start as start_run};
|
||||
|
||||
use crate::shared;
|
||||
use crate::store;
|
||||
pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) -> Result<()> {
|
||||
use crate::user_config::load_user_settings;
|
||||
pub(crate) async fn execute(
|
||||
run_id: RunId,
|
||||
run_dir: PathBuf,
|
||||
storage_dir: Option<PathBuf>,
|
||||
launcher_path: PathBuf,
|
||||
resume: bool,
|
||||
) -> Result<()> {
|
||||
let _ = fabro_proc::title_init();
|
||||
|
||||
let _launcher_guard = scopeguard::guard(launcher_path.clone(), |path| {
|
||||
super::launcher::remove_launcher_record(&path);
|
||||
});
|
||||
|
||||
let run_record = RunRecord::load(&run_dir)?;
|
||||
let storage_dir = match storage_dir {
|
||||
Some(storage_dir) => storage_dir,
|
||||
None => load_user_settings()?.storage_dir(),
|
||||
};
|
||||
let store = store::build_store(&storage_dir)?;
|
||||
let run_store = store
|
||||
.open_run(&run_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Run {run_id} not found in store"))?;
|
||||
let run_record = run_store
|
||||
.get_run()
|
||||
.await?
|
||||
.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();
|
||||
let short_id = super::short_run_id(&run_id).to_string();
|
||||
|
|
@ -29,8 +46,6 @@ pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bo
|
|||
fabro_proc::title_set(&format!("fabro: {short_id} {node_id}"));
|
||||
}) as Arc<dyn Fn(&str) + Send + Sync>
|
||||
});
|
||||
let store = store::build_store(&run_record.settings.storage_dir())?;
|
||||
let run_store = open_or_hydrate_run(store.as_ref(), &run_dir).await?;
|
||||
|
||||
let github_app = shared::github::build_github_app_credentials(run_record.settings.app_id())?;
|
||||
let runtime_state = RuntimeState::new(&run_dir);
|
||||
|
|
|
|||
|
|
@ -51,7 +51,11 @@ pub(crate) fn remove_launcher_record(path: &Path) {
|
|||
|
||||
pub(crate) fn active_launcher_record_for_run(run_dir: &Path) -> Option<LauncherRecord> {
|
||||
let run_record = RunRecord::load(run_dir).ok()?;
|
||||
let path = launcher_record_path(&run_record.settings.storage_dir(), &run_record.run_id);
|
||||
active_launcher_record(&run_record.settings.storage_dir(), &run_record.run_id)
|
||||
}
|
||||
|
||||
pub(crate) fn active_launcher_record(storage_dir: &Path, run_id: &RunId) -> Option<LauncherRecord> {
|
||||
let path = launcher_record_path(storage_dir, run_id);
|
||||
let launcher = read_launcher_record(&path)?;
|
||||
if launcher_record_is_running(&launcher) {
|
||||
Some(launcher)
|
||||
|
|
@ -141,6 +145,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
assert!(active_launcher_record_for_run(&run_dir).is_none());
|
||||
assert!(active_launcher_record(&storage_dir, &fixtures::RUN_1).is_none());
|
||||
assert!(!launcher_path.exists());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,13 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
|
|||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = store::build_store(&cli_settings.storage_dir())?;
|
||||
let run_info = resolve_run_combined(store.as_ref(), &base, &run).await?;
|
||||
let child = start::start_run(&run_info.path, false)?;
|
||||
let child = start::start_run(
|
||||
&run_info.path,
|
||||
&run_info.run_id,
|
||||
&cli_settings.storage_dir(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
if globals.json {
|
||||
print_json_pretty(&serde_json::json!({ "run_id": run_info.run_id }))?;
|
||||
} else {
|
||||
|
|
@ -89,10 +95,20 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
|
|||
Ok(())
|
||||
}
|
||||
RunCommands::Detached {
|
||||
run_id,
|
||||
run_dir,
|
||||
launcher_path,
|
||||
resume,
|
||||
} => detached::execute(run_dir, launcher_path, resume).await,
|
||||
} => {
|
||||
detached::execute(
|
||||
run_id,
|
||||
run_dir,
|
||||
globals.storage_dir.clone(),
|
||||
launcher_path,
|
||||
resume,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RunCommands::Diff(args) => diff::run(args, globals).await,
|
||||
RunCommands::Logs(args) => {
|
||||
let styles = Styles::detect_stdout();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use anyhow::bail;
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflow::records::{RunRecord, RunRecordExt};
|
||||
use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
|
||||
|
||||
use crate::args::{GlobalArgs, ResumeArgs};
|
||||
|
|
@ -29,13 +28,14 @@ pub(crate) async fn resume_command(
|
|||
if !run_dir.join("run.json").exists() {
|
||||
bail!("run directory exists but has no run.json — cannot resume");
|
||||
}
|
||||
let run_id = RunRecord::load(&run_dir)?.run_id;
|
||||
let run_id = run.run_id;
|
||||
|
||||
if launcher_pid_alive(&run_dir) {
|
||||
bail!("an engine process is still running for this run — cannot resume");
|
||||
}
|
||||
|
||||
let child = super::start::start_run(&run_dir, true)?;
|
||||
let child =
|
||||
super::start::start_run(&run_dir, &run_id, &cli_settings.storage_dir(), true).await?;
|
||||
|
||||
if args.detach {
|
||||
if globals.json {
|
||||
|
|
|
|||
|
|
@ -2,30 +2,29 @@ use std::path::Path;
|
|||
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use chrono::Utc;
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_workflow::records::{RunRecord, RunRecordExt};
|
||||
use fabro_workflow::run_status::{RunStatus, RunStatusRecord, RunStatusRecordExt};
|
||||
use fabro_types::RunId;
|
||||
use fabro_workflow::run_status::RunStatus;
|
||||
|
||||
use super::launcher::{
|
||||
LauncherRecord, active_launcher_record_for_run, launcher_log_path, launcher_record_path,
|
||||
LauncherRecord, active_launcher_record, launcher_log_path, launcher_record_path,
|
||||
remove_launcher_record, write_launcher_record,
|
||||
};
|
||||
use crate::store;
|
||||
|
||||
/// Spawn a detached engine process for the given run directory.
|
||||
/// Spawn a detached engine process for the given run.
|
||||
///
|
||||
/// The engine process reads `run.json` from the run directory and executes the
|
||||
/// workflow. Returns the child process handle (use `.id()` for the PID).
|
||||
pub(crate) fn start_run(run_dir: &Path, resume: bool) -> Result<std::process::Child> {
|
||||
/// Returns the child process handle (use `.id()` for the PID).
|
||||
pub(crate) async fn start_run(
|
||||
run_dir: &Path,
|
||||
run_id: &RunId,
|
||||
storage_dir: &Path,
|
||||
resume: bool,
|
||||
) -> Result<std::process::Child> {
|
||||
if !resume {
|
||||
ensure_startable_run(run_dir)?;
|
||||
ensure_startable_run(storage_dir, run_id).await?;
|
||||
}
|
||||
|
||||
let record = RunRecord::load(run_dir)
|
||||
.map_err(|e| anyhow!("Cannot start run: failed to load run.json: {e}"))?;
|
||||
|
||||
let storage_dir = record.settings.storage_dir();
|
||||
let launcher_path = launcher_record_path(&storage_dir, &record.run_id);
|
||||
let log_path = launcher_log_path(&storage_dir, &record.run_id);
|
||||
let launcher_path = launcher_record_path(storage_dir, run_id);
|
||||
let log_path = launcher_log_path(storage_dir, run_id);
|
||||
|
||||
if let Some(parent) = log_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
|
|
@ -36,8 +35,12 @@ pub(crate) fn start_run(run_dir: &Path, resume: bool) -> Result<std::process::Ch
|
|||
let exe = std::env::current_exe()?;
|
||||
|
||||
let mut cmd = std::process::Command::new(&exe);
|
||||
cmd.args(["__detached", "--run-dir"])
|
||||
cmd.args(["__detached", "--run-id"])
|
||||
.arg(run_id.to_string())
|
||||
.args(["--run-dir"])
|
||||
.arg(run_dir)
|
||||
.args(["--storage-dir"])
|
||||
.arg(storage_dir)
|
||||
.args(["--launcher-path"])
|
||||
.arg(&launcher_path);
|
||||
if resume {
|
||||
|
|
@ -56,7 +59,7 @@ pub(crate) fn start_run(run_dir: &Path, resume: bool) -> Result<std::process::Ch
|
|||
if let Err(err) = write_launcher_record(
|
||||
&launcher_path,
|
||||
&LauncherRecord {
|
||||
run_id: record.run_id,
|
||||
run_id: *run_id,
|
||||
run_dir: run_dir.to_path_buf(),
|
||||
pid: child.id(),
|
||||
resume,
|
||||
|
|
@ -75,13 +78,15 @@ pub(crate) fn start_run(run_dir: &Path, resume: bool) -> Result<std::process::Ch
|
|||
Ok(child)
|
||||
}
|
||||
|
||||
fn ensure_startable_run(run_dir: &Path) -> Result<()> {
|
||||
if active_launcher_record_for_run(run_dir).is_some() {
|
||||
async fn ensure_startable_run(storage_dir: &Path, run_id: &RunId) -> Result<()> {
|
||||
if active_launcher_record(storage_dir, run_id).is_some() {
|
||||
bail!("an engine process is still running for this run — cannot start");
|
||||
}
|
||||
|
||||
let status_path = run_dir.join("status.json");
|
||||
if let Ok(record) = RunStatusRecord::load(&status_path) {
|
||||
let run_store = store::open_run_reader(storage_dir, run_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Cannot start run: run {run_id} not found in store"))?;
|
||||
if let Some(record) = run_store.get_status().await? {
|
||||
if !matches!(record.status, RunStatus::Submitted | RunStatus::Starting) {
|
||||
bail!(
|
||||
"cannot start run: status is {:?}, expected submitted",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs)
|
|||
|
||||
info!(run_id = %run_info.run_id, "Waiting for run to complete");
|
||||
|
||||
let run_store = store::open_run_reader(&cli_settings.storage_dir(), &run_info.run_id).await?;
|
||||
let status_path = run_info.path.join("status.json");
|
||||
let deadline = args
|
||||
.timeout
|
||||
|
|
@ -37,6 +36,8 @@ pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs)
|
|||
|
||||
let final_status = loop {
|
||||
let load_file_status = || RunStatusRecord::load(&status_path).ok().map(|r| r.status);
|
||||
let run_store =
|
||||
store::open_run_reader(&cli_settings.storage_dir(), &run_info.run_id).await?;
|
||||
let status = match run_store.as_ref() {
|
||||
Some(run_store) => match run_store.get_status().await {
|
||||
Ok(Some(record)) => Some(record.status),
|
||||
|
|
@ -72,6 +73,7 @@ pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs)
|
|||
};
|
||||
|
||||
let conclusion_path = run_info.path.join("conclusion.json");
|
||||
let run_store = store::open_run_reader(&cli_settings.storage_dir(), &run_info.run_id).await?;
|
||||
let conclusion = match run_store.as_ref() {
|
||||
Some(run_store) => run_store
|
||||
.get_conclusion()
|
||||
|
|
|
|||
|
|
@ -11,18 +11,19 @@ fn help() {
|
|||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Internal: run the engine process (reads run.json from run dir)
|
||||
Internal: run the engine process
|
||||
|
||||
Usage: fabro __detached [OPTIONS] --run-dir <RUN_DIR> --launcher-path <LAUNCHER_PATH>
|
||||
Usage: fabro __detached [OPTIONS] --run-id <RUN_ID> --run-dir <RUN_DIR> --launcher-path <LAUNCHER_PATH>
|
||||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--run-dir <RUN_DIR> Run directory
|
||||
--run-id <RUN_ID> Run ID
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--run-dir <RUN_DIR> Run directory
|
||||
--launcher-path <LAUNCHER_PATH> Launcher metadata path
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
--resume Resume from checkpoint instead of fresh start
|
||||
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
||||
--resume Resume from checkpoint instead of fresh start
|
||||
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
||||
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
-h, --help Print help
|
||||
|
|
@ -74,6 +75,8 @@ digraph CachedGraph {
|
|||
.command()
|
||||
.args([
|
||||
"__detached",
|
||||
"--run-id",
|
||||
run_id,
|
||||
"--run-dir",
|
||||
run_dir.to_str().unwrap(),
|
||||
"--launcher-path",
|
||||
|
|
@ -156,6 +159,8 @@ digraph GitHubApp {
|
|||
cmd.env("GITHUB_APP_PRIVATE_KEY", "%%%not-base64%%%");
|
||||
cmd.args([
|
||||
"__detached",
|
||||
"--run-id",
|
||||
run_id,
|
||||
"--run-dir",
|
||||
run_dir.to_str().unwrap(),
|
||||
"--launcher-path",
|
||||
|
|
@ -171,6 +176,68 @@ digraph GitHubApp {
|
|||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_runs_without_run_json_when_run_id_is_explicit() {
|
||||
let context = test_context!();
|
||||
let run_id = "01ARZ3NDEKTSV4RRFFQ69G5FAJ";
|
||||
let workflow_path = context.temp_dir.join("workflow.fabro");
|
||||
|
||||
context.write_temp(
|
||||
"workflow.fabro",
|
||||
"\
|
||||
digraph DetachedStoreOnly {
|
||||
start [shape=Mdiamond, label=\"Start\"]
|
||||
exit [shape=Msquare, label=\"Exit\"]
|
||||
start -> exit
|
||||
}
|
||||
",
|
||||
);
|
||||
|
||||
context
|
||||
.command()
|
||||
.args([
|
||||
"create",
|
||||
"--dry-run",
|
||||
"--auto-approve",
|
||||
"--run-id",
|
||||
run_id,
|
||||
workflow_path.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let run_dir = context.find_run_dir(run_id);
|
||||
std::fs::remove_file(run_dir.join("run.json")).unwrap();
|
||||
|
||||
context
|
||||
.command()
|
||||
.args([
|
||||
"__detached",
|
||||
"--run-id",
|
||||
run_id,
|
||||
"--run-dir",
|
||||
run_dir.to_str().unwrap(),
|
||||
"--launcher-path",
|
||||
launcher_path(&context, run_id).to_str().unwrap(),
|
||||
])
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let conclusion = read_json(run_dir.join("conclusion.json"));
|
||||
fabro_json_snapshot!(
|
||||
context,
|
||||
serde_json::json!({
|
||||
"status": conclusion["status"],
|
||||
}),
|
||||
@r#"
|
||||
{
|
||||
"status": "success"
|
||||
}
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_resume_rejects_completed_run_without_mutating_it() {
|
||||
let context = test_context!();
|
||||
|
|
@ -235,6 +302,8 @@ digraph Test {
|
|||
let mut cmd = context.command();
|
||||
cmd.args([
|
||||
"__detached",
|
||||
"--run-id",
|
||||
&run_id,
|
||||
"--run-dir",
|
||||
&run_dir,
|
||||
"--launcher-path",
|
||||
|
|
|
|||
|
|
@ -78,6 +78,50 @@ fn start_by_run_id_starts_created_run() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_by_run_id_starts_created_run_without_run_json_or_status_json() {
|
||||
let context = test_context!();
|
||||
let run_id = "01ARZ3NDEKTSV4RRFFQ69G5FAH";
|
||||
|
||||
context
|
||||
.command()
|
||||
.args([
|
||||
"create",
|
||||
"--dry-run",
|
||||
"--auto-approve",
|
||||
"--run-id",
|
||||
run_id,
|
||||
example_fixture("simple.fabro").to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let run_dir = context.find_run_dir(run_id);
|
||||
std::fs::remove_file(run_dir.join("run.json")).unwrap();
|
||||
std::fs::remove_file(run_dir.join("status.json")).unwrap();
|
||||
|
||||
context.command().args(["start", run_id]).assert().success();
|
||||
context
|
||||
.command()
|
||||
.args(["wait", run_id])
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let conclusion = read_json(run_dir.join("conclusion.json"));
|
||||
fabro_json_snapshot!(
|
||||
context,
|
||||
serde_json::json!({
|
||||
"conclusion_status": conclusion["status"],
|
||||
}),
|
||||
@r#"
|
||||
{
|
||||
"conclusion_status": "success"
|
||||
}
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_by_workflow_name_prefers_newly_created_submitted_run() {
|
||||
let context = test_context!();
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ use futures::Stream;
|
|||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
EventEnvelope, EventPayload, NodeSnapshot, NodeVisitRef, Result, RunSnapshot, RunStore,
|
||||
EventEnvelope, EventPayload, NodeOutcomeRecord, NodeSnapshot, NodeVisitRef, Result,
|
||||
RunSnapshot, RunStore,
|
||||
};
|
||||
use fabro_types::{
|
||||
Checkpoint, Conclusion, NodeStatusRecord, Retro, RunRecord, RunStatusRecord, SandboxRecord,
|
||||
StartRecord,
|
||||
Checkpoint, Conclusion, NodeStatusRecord, PullRequestRecord, Retro, RunRecord, RunStatusRecord,
|
||||
SandboxRecord, StartRecord,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -265,6 +266,84 @@ impl RunStore for DiskProjectingRunStore {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_node_outcome(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
outcome: &NodeOutcomeRecord,
|
||||
) -> Result<()> {
|
||||
self.inner.put_node_outcome(node, outcome).await?;
|
||||
self.write_json_best_effort(
|
||||
&disk_node_dir(&self.run_dir, node.node_id, node.visit).join("outcome.json"),
|
||||
outcome,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_node_provider_used(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
provider_used: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.put_node_provider_used(node, provider_used)
|
||||
.await?;
|
||||
self.write_json_best_effort(
|
||||
&disk_node_dir(&self.run_dir, node.node_id, node.visit).join("provider_used.json"),
|
||||
provider_used,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_node_diff(&self, node: &NodeVisitRef<'_>, diff: &str) -> Result<()> {
|
||||
self.inner.put_node_diff(node, diff).await?;
|
||||
self.write_text_best_effort(
|
||||
&disk_node_dir(&self.run_dir, node.node_id, node.visit).join("diff.patch"),
|
||||
diff,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_node_script_invocation(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
invocation: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.put_node_script_invocation(node, invocation)
|
||||
.await?;
|
||||
self.write_json_best_effort(
|
||||
&disk_node_dir(&self.run_dir, node.node_id, node.visit).join("script_invocation.json"),
|
||||
invocation,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_node_script_timing(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
timing: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.inner.put_node_script_timing(node, timing).await?;
|
||||
self.write_json_best_effort(
|
||||
&disk_node_dir(&self.run_dir, node.node_id, node.visit).join("script_timing.json"),
|
||||
timing,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_node_parallel_results(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
results: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.inner.put_node_parallel_results(node, results).await?;
|
||||
self.write_json_best_effort(
|
||||
&disk_node_dir(&self.run_dir, node.node_id, node.visit).join("parallel_results.json"),
|
||||
results,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_node_stdout(&self, node: &NodeVisitRef<'_>, log: &str) -> Result<()> {
|
||||
self.inner.put_node_stdout(node, log).await?;
|
||||
self.write_text_best_effort(
|
||||
|
|
@ -291,6 +370,30 @@ impl RunStore for DiskProjectingRunStore {
|
|||
self.inner.list_node_visits(node_id).await
|
||||
}
|
||||
|
||||
async fn list_node_ids(&self) -> Result<Vec<String>> {
|
||||
self.inner.list_node_ids().await
|
||||
}
|
||||
|
||||
async fn put_final_patch(&self, patch: &str) -> Result<()> {
|
||||
self.inner.put_final_patch(patch).await?;
|
||||
self.write_text_best_effort(&self.run_dir.join("final.patch"), patch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_final_patch(&self) -> Result<Option<String>> {
|
||||
self.inner.get_final_patch().await
|
||||
}
|
||||
|
||||
async fn put_pull_request(&self, record: &PullRequestRecord) -> Result<()> {
|
||||
self.inner.put_pull_request(record).await?;
|
||||
self.write_json_best_effort(&self.run_dir.join("pull_request.json"), record);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_pull_request(&self) -> Result<Option<PullRequestRecord>> {
|
||||
self.inner.get_pull_request().await
|
||||
}
|
||||
|
||||
async fn append_event(&self, payload: &EventPayload) -> Result<u32> {
|
||||
self.append_jsonl_critical(payload);
|
||||
self.inner.append_event(payload).await
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ 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/";
|
||||
|
|
@ -52,6 +54,14 @@ 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)
|
||||
}
|
||||
|
|
@ -68,6 +78,30 @@ pub(crate) fn node_status(node: &NodeVisitRef<'_>) -> String {
|
|||
format!("{}/status.json", node_visit_prefix(node))
|
||||
}
|
||||
|
||||
pub(crate) fn node_outcome(node: &NodeVisitRef<'_>) -> String {
|
||||
format!("{}/outcome.json", node_visit_prefix(node))
|
||||
}
|
||||
|
||||
pub(crate) fn node_provider_used(node: &NodeVisitRef<'_>) -> String {
|
||||
format!("{}/provider_used.json", node_visit_prefix(node))
|
||||
}
|
||||
|
||||
pub(crate) fn node_diff(node: &NodeVisitRef<'_>) -> String {
|
||||
format!("{}/diff.patch", node_visit_prefix(node))
|
||||
}
|
||||
|
||||
pub(crate) fn node_script_invocation(node: &NodeVisitRef<'_>) -> String {
|
||||
format!("{}/script_invocation.json", node_visit_prefix(node))
|
||||
}
|
||||
|
||||
pub(crate) fn node_script_timing(node: &NodeVisitRef<'_>) -> String {
|
||||
format!("{}/script_timing.json", node_visit_prefix(node))
|
||||
}
|
||||
|
||||
pub(crate) fn node_parallel_results(node: &NodeVisitRef<'_>) -> String {
|
||||
format!("{}/parallel_results.json", node_visit_prefix(node))
|
||||
}
|
||||
|
||||
pub(crate) fn node_stdout(node: &NodeVisitRef<'_>) -> String {
|
||||
format!("{}/stdout.log", node_visit_prefix(node))
|
||||
}
|
||||
|
|
@ -149,6 +183,8 @@ mod tests {
|
|||
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!(retro_prompt(), "retro/prompt.md");
|
||||
assert_eq!(retro_response(), "retro/response.md");
|
||||
}
|
||||
|
|
@ -163,6 +199,24 @@ mod tests {
|
|||
assert_eq!(node_prompt(&node), "nodes/plan/visit-3/prompt.md");
|
||||
assert_eq!(node_response(&node), "nodes/plan/visit-3/response.md");
|
||||
assert_eq!(node_status(&node), "nodes/plan/visit-3/status.json");
|
||||
assert_eq!(node_outcome(&node), "nodes/plan/visit-3/outcome.json");
|
||||
assert_eq!(
|
||||
node_provider_used(&node),
|
||||
"nodes/plan/visit-3/provider_used.json"
|
||||
);
|
||||
assert_eq!(node_diff(&node), "nodes/plan/visit-3/diff.patch");
|
||||
assert_eq!(
|
||||
node_script_invocation(&node),
|
||||
"nodes/plan/visit-3/script_invocation.json"
|
||||
);
|
||||
assert_eq!(
|
||||
node_script_timing(&node),
|
||||
"nodes/plan/visit-3/script_timing.json"
|
||||
);
|
||||
assert_eq!(
|
||||
node_parallel_results(&node),
|
||||
"nodes/plan/visit-3/parallel_results.json"
|
||||
);
|
||||
assert_eq!(node_stdout(&node), "nodes/plan/visit-3/stdout.log");
|
||||
assert_eq!(node_stderr(&node), "nodes/plan/visit-3/stderr.log");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ pub use types::{
|
|||
};
|
||||
|
||||
use fabro_types::{
|
||||
Checkpoint, Conclusion, NodeStatusRecord, Retro, RunId, RunRecord, RunStatusRecord,
|
||||
SandboxRecord, StartRecord,
|
||||
Checkpoint, Conclusion, NodeStatusRecord, Outcome, PullRequestRecord, Retro, RunId, RunRecord,
|
||||
RunStatusRecord, SandboxRecord, StageUsage, StartRecord,
|
||||
};
|
||||
|
||||
pub type NodeOutcomeRecord = Outcome<Option<StageUsage>>;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct ListRunsQuery {
|
||||
pub start: Option<DateTime<Utc>>,
|
||||
|
|
@ -83,11 +85,44 @@ pub trait RunStore: Send + Sync {
|
|||
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 append_event(&self, payload: &EventPayload) -> Result<u32>;
|
||||
async fn list_events(&self) -> Result<Vec<EventEnvelope>>;
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
|
|||
|
||||
use crate::keys;
|
||||
use crate::{
|
||||
CatalogRecord, EventEnvelope, EventPayload, ListRunsQuery, NodeSnapshot, NodeVisitRef, Result,
|
||||
RunSnapshot, RunStore, RunSummary, Store, StoreError,
|
||||
CatalogRecord, EventEnvelope, EventPayload, ListRunsQuery, NodeOutcomeRecord, NodeSnapshot,
|
||||
NodeVisitRef, Result, RunSnapshot, RunStore, RunSummary, Store, StoreError,
|
||||
};
|
||||
use fabro_types::{
|
||||
Checkpoint, Conclusion, NodeStatusRecord, Retro, RunId, RunRecord, RunStatusRecord,
|
||||
SandboxRecord, StartRecord,
|
||||
Checkpoint, Conclusion, NodeStatusRecord, PullRequestRecord, Retro, RunId, RunRecord,
|
||||
RunStatusRecord, SandboxRecord, StartRecord,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -126,11 +126,32 @@ impl InMemoryRunStore {
|
|||
prompt: read_text(data, &keys::node_prompt(node))?,
|
||||
response: read_text(data, &keys::node_response(node))?,
|
||||
status: read_json(data, &keys::node_status(node))?,
|
||||
outcome: read_json(data, &keys::node_outcome(node))?,
|
||||
provider_used: read_json(data, &keys::node_provider_used(node))?,
|
||||
diff: read_text(data, &keys::node_diff(node))?,
|
||||
script_invocation: read_json(data, &keys::node_script_invocation(node))?,
|
||||
script_timing: read_json(data, &keys::node_script_timing(node))?,
|
||||
parallel_results: read_json(data, &keys::node_parallel_results(node))?,
|
||||
stdout: read_text(data, &keys::node_stdout(node))?,
|
||||
stderr: read_text(data, &keys::node_stderr(node))?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_node_ids_inner(&self) -> Vec<String> {
|
||||
let data = self.snapshot_data().await;
|
||||
let mut node_ids = BTreeSet::new();
|
||||
for key in data.keys() {
|
||||
if let Some((node_id, _, _)) = keys::parse_node_key(key) {
|
||||
node_ids.insert(node_id);
|
||||
continue;
|
||||
}
|
||||
if let Some((node_id, _, _)) = keys::parse_node_asset_key(key) {
|
||||
node_ids.insert(node_id);
|
||||
}
|
||||
}
|
||||
node_ids.into_iter().collect()
|
||||
}
|
||||
|
||||
async fn list_events_from_inner(&self, seq: u32) -> Result<Vec<EventEnvelope>> {
|
||||
let data = self.snapshot_data().await;
|
||||
let mut events = Vec::new();
|
||||
|
|
@ -222,6 +243,8 @@ impl InMemoryRunStore {
|
|||
retro: read_json(data, keys::retro())?,
|
||||
graph: read_text(data, keys::graph())?,
|
||||
sandbox: read_json(data, keys::sandbox())?,
|
||||
final_patch: read_text(data, keys::final_patch())?,
|
||||
pull_request: read_json(data, keys::pull_request())?,
|
||||
nodes,
|
||||
}))
|
||||
}
|
||||
|
|
@ -413,6 +436,54 @@ impl RunStore for InMemoryRunStore {
|
|||
self.put_json(keys::node_status(node), status).await
|
||||
}
|
||||
|
||||
async fn put_node_outcome(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
outcome: &NodeOutcomeRecord,
|
||||
) -> Result<()> {
|
||||
self.put_json(keys::node_outcome(node), outcome).await
|
||||
}
|
||||
|
||||
async fn put_node_provider_used(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
provider_used: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.put_json(keys::node_provider_used(node), provider_used)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn put_node_diff(&self, node: &NodeVisitRef<'_>, diff: &str) -> Result<()> {
|
||||
self.put_text(keys::node_diff(node), diff).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_node_script_invocation(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
invocation: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.put_json(keys::node_script_invocation(node), invocation)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn put_node_script_timing(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
timing: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.put_json(keys::node_script_timing(node), timing).await
|
||||
}
|
||||
|
||||
async fn put_node_parallel_results(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
results: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.put_json(keys::node_parallel_results(node), results)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn put_node_stdout(&self, node: &NodeVisitRef<'_>, log: &str) -> Result<()> {
|
||||
self.put_text(keys::node_stdout(node), log).await;
|
||||
Ok(())
|
||||
|
|
@ -442,6 +513,28 @@ impl RunStore for InMemoryRunStore {
|
|||
Ok(visits.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn list_node_ids(&self) -> Result<Vec<String>> {
|
||||
Ok(self.list_node_ids_inner().await)
|
||||
}
|
||||
|
||||
async fn put_final_patch(&self, patch: &str) -> Result<()> {
|
||||
self.put_text(keys::final_patch().to_string(), patch).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_final_patch(&self) -> Result<Option<String>> {
|
||||
self.get_text(keys::final_patch()).await
|
||||
}
|
||||
|
||||
async fn put_pull_request(&self, record: &PullRequestRecord) -> Result<()> {
|
||||
self.put_json(keys::pull_request().to_string(), record)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pull_request(&self) -> Result<Option<PullRequestRecord>> {
|
||||
self.get_json(keys::pull_request()).await
|
||||
}
|
||||
|
||||
async fn append_event(&self, payload: &EventPayload) -> Result<u32> {
|
||||
payload.validate(&self.run_id)?;
|
||||
|
||||
|
|
@ -650,7 +743,8 @@ mod tests {
|
|||
|
||||
use chrono::Duration as ChronoDuration;
|
||||
use fabro_types::{
|
||||
AttrValue, FabroSettings, Graph, RunId, RunStatus, StageStatus, StatusReason, fixtures,
|
||||
AttrValue, FabroSettings, Graph, PullRequestRecord, RunId, RunStatus, StageStatus,
|
||||
StatusReason, fixtures,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
|
||||
|
|
@ -787,6 +881,27 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn sample_node_outcome() -> NodeOutcomeRecord {
|
||||
fabro_types::Outcome {
|
||||
status: StageStatus::Success,
|
||||
notes: Some("all good".to_string()),
|
||||
files_touched: vec!["src/lib.rs".to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_pull_request() -> PullRequestRecord {
|
||||
PullRequestRecord {
|
||||
html_url: "https://github.com/fabro-sh/fabro/pull/123".to_string(),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_run_put_get_and_snapshot_round_trip() {
|
||||
let store = InMemoryStore::default();
|
||||
|
|
@ -809,6 +924,12 @@ mod tests {
|
|||
visit: 2,
|
||||
};
|
||||
let node_status = sample_node_status();
|
||||
let node_outcome = sample_node_outcome();
|
||||
let provider_used = serde_json::json!({"provider": "openai", "model": "gpt-5.4"});
|
||||
let script_invocation = serde_json::json!({"command": "cargo test"});
|
||||
let script_timing = serde_json::json!({"duration_ms": 3210});
|
||||
let parallel_results = serde_json::json!([{"node_id": "lint", "status": "success"}]);
|
||||
let pull_request = sample_pull_request();
|
||||
|
||||
run.put_run(&run_record).await.unwrap();
|
||||
run.put_start(&start_record).await.unwrap();
|
||||
|
|
@ -821,8 +942,28 @@ mod tests {
|
|||
run.put_node_prompt(&node, "Plan the fix").await.unwrap();
|
||||
run.put_node_response(&node, "Implemented").await.unwrap();
|
||||
run.put_node_status(&node, &node_status).await.unwrap();
|
||||
run.put_node_outcome(&node, &node_outcome).await.unwrap();
|
||||
run.put_node_provider_used(&node, &provider_used)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_node_diff(&node, "diff --git a/src/lib.rs b/src/lib.rs")
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_node_script_invocation(&node, &script_invocation)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_node_script_timing(&node, &script_timing)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_node_parallel_results(&node, ¶llel_results)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_node_stdout(&node, "ok").await.unwrap();
|
||||
run.put_node_stderr(&node, "").await.unwrap();
|
||||
run.put_final_patch("diff --git a/src/lib.rs b/src/lib.rs\n")
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_pull_request(&pull_request).await.unwrap();
|
||||
run.put_retro_prompt("How did it go?").await.unwrap();
|
||||
run.put_retro_response("Smooth enough").await.unwrap();
|
||||
run.put_artifact_value("summary", &serde_json::json!({"done": true}))
|
||||
|
|
@ -881,6 +1022,15 @@ mod tests {
|
|||
run.get_asset(&node, "src/lib.rs").await.unwrap(),
|
||||
Some(Bytes::from_static(b"fn main() {}"))
|
||||
);
|
||||
assert_eq!(
|
||||
run.get_final_patch().await.unwrap().as_deref(),
|
||||
Some("diff --git a/src/lib.rs b/src/lib.rs\n")
|
||||
);
|
||||
assert_eq!(
|
||||
run.get_pull_request().await.unwrap(),
|
||||
Some(pull_request.clone())
|
||||
);
|
||||
assert_eq!(run.list_node_ids().await.unwrap(), vec!["code".to_string()]);
|
||||
assert_eq!(
|
||||
run.list_assets(&node).await.unwrap(),
|
||||
vec!["src/lib.rs".to_string()]
|
||||
|
|
@ -909,6 +1059,35 @@ mod tests {
|
|||
let snapshot_status = snapshot.nodes[0].status.as_ref().unwrap();
|
||||
assert_eq!(snapshot_status.status, node_status.status);
|
||||
assert_eq!(snapshot_status.failure_reason, node_status.failure_reason);
|
||||
assert_eq!(
|
||||
snapshot.nodes[0].outcome.as_ref().unwrap().status,
|
||||
StageStatus::Success
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.nodes[0].provider_used.as_ref(),
|
||||
Some(&provider_used)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.nodes[0].diff.as_deref(),
|
||||
Some("diff --git a/src/lib.rs b/src/lib.rs")
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.nodes[0].script_invocation.as_ref(),
|
||||
Some(&script_invocation)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.nodes[0].script_timing.as_ref(),
|
||||
Some(&script_timing)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.nodes[0].parallel_results.as_ref(),
|
||||
Some(¶llel_results)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.final_patch.as_deref(),
|
||||
Some("diff --git a/src/lib.rs b/src/lib.rs\n")
|
||||
);
|
||||
assert_eq!(snapshot.pull_request, Some(pull_request));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -964,6 +1143,10 @@ mod tests {
|
|||
("code".to_string(), 2, "src/lib.rs".to_string())
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
run.list_node_ids().await.unwrap(),
|
||||
vec!["artifact-only".to_string(), "code".to_string()]
|
||||
);
|
||||
|
||||
let snapshot = run.get_snapshot().await.unwrap().unwrap();
|
||||
assert_eq!(snapshot.nodes.len(), 1);
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
|
|||
|
||||
use crate::keys;
|
||||
use crate::{
|
||||
CatalogRecord, EventEnvelope, EventPayload, NodeSnapshot, NodeVisitRef, Result, RunSnapshot,
|
||||
RunStore, RunSummary, StoreError,
|
||||
CatalogRecord, EventEnvelope, EventPayload, NodeOutcomeRecord, NodeSnapshot, NodeVisitRef,
|
||||
Result, RunSnapshot, RunStore, RunSummary, StoreError,
|
||||
};
|
||||
use fabro_types::{
|
||||
Checkpoint, Conclusion, NodeStatusRecord, Retro, RunId, RunRecord, RunStatusRecord,
|
||||
SandboxRecord, StartRecord,
|
||||
Checkpoint, Conclusion, NodeStatusRecord, PullRequestRecord, Retro, RunId, RunRecord,
|
||||
RunStatusRecord, SandboxRecord, StartRecord,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -204,6 +204,28 @@ impl SlateRunStore {
|
|||
prompt: self.inner.db.get_text(&keys::node_prompt(node)).await?,
|
||||
response: self.inner.db.get_text(&keys::node_response(node)).await?,
|
||||
status: self.inner.db.get_json(&keys::node_status(node)).await?,
|
||||
outcome: self.inner.db.get_json(&keys::node_outcome(node)).await?,
|
||||
provider_used: self
|
||||
.inner
|
||||
.db
|
||||
.get_json(&keys::node_provider_used(node))
|
||||
.await?,
|
||||
diff: self.inner.db.get_text(&keys::node_diff(node)).await?,
|
||||
script_invocation: self
|
||||
.inner
|
||||
.db
|
||||
.get_json(&keys::node_script_invocation(node))
|
||||
.await?,
|
||||
script_timing: self
|
||||
.inner
|
||||
.db
|
||||
.get_json(&keys::node_script_timing(node))
|
||||
.await?,
|
||||
parallel_results: self
|
||||
.inner
|
||||
.db
|
||||
.get_json(&keys::node_parallel_results(node))
|
||||
.await?,
|
||||
stdout: self.inner.db.get_text(&keys::node_stdout(node)).await?,
|
||||
stderr: self.inner.db.get_text(&keys::node_stderr(node)).await?,
|
||||
})
|
||||
|
|
@ -319,6 +341,65 @@ impl RunStore for SlateRunStore {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn put_node_outcome(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
outcome: &NodeOutcomeRecord,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.db
|
||||
.put_json(&keys::node_outcome(node), outcome)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn put_node_provider_used(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
provider_used: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.db
|
||||
.put_json(&keys::node_provider_used(node), provider_used)
|
||||
.await
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
invocation: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.db
|
||||
.put_json(&keys::node_script_invocation(node), invocation)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn put_node_script_timing(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
timing: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.db
|
||||
.put_json(&keys::node_script_timing(node), timing)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn put_node_parallel_results(
|
||||
&self,
|
||||
node: &NodeVisitRef<'_>,
|
||||
results: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.db
|
||||
.put_json(&keys::node_parallel_results(node), results)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn put_node_stdout(&self, node: &NodeVisitRef<'_>, log: &str) -> Result<()> {
|
||||
self.inner.db.put_text(&keys::node_stdout(node), log).await
|
||||
}
|
||||
|
|
@ -346,6 +427,47 @@ impl RunStore for SlateRunStore {
|
|||
Ok(visits.into_iter().collect())
|
||||
}
|
||||
|
||||
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? {
|
||||
let key = key_to_string(&entry.key)?;
|
||||
if let Some((node_id, _, _)) = keys::parse_node_key(&key) {
|
||||
node_ids.insert(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut asset_iter = self
|
||||
.inner
|
||||
.db
|
||||
.scan_prefix(keys::ARTIFACT_NODES_PREFIX.as_bytes())
|
||||
.await?;
|
||||
while let Some(entry) = asset_iter.next().await? {
|
||||
let key = key_to_string(&entry.key)?;
|
||||
if let Some((node_id, _, _)) = keys::parse_node_asset_key(&key) {
|
||||
node_ids.insert(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
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 append_event(&self, payload: &EventPayload) -> Result<u32> {
|
||||
payload.validate(&self.inner.run_id)?;
|
||||
let seq = self.inner.event_seq.fetch_add(1, Ordering::SeqCst);
|
||||
|
|
@ -503,6 +625,8 @@ impl RunStore for SlateRunStore {
|
|||
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,
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ use std::collections::HashMap;
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Result, StoreError};
|
||||
use crate::{NodeOutcomeRecord, Result, StoreError};
|
||||
use fabro_types::{
|
||||
Checkpoint, Conclusion, NodeStatusRecord, Retro, RunId, RunRecord, RunStatus, RunStatusRecord,
|
||||
SandboxRecord, StartRecord, StatusReason,
|
||||
Checkpoint, Conclusion, NodeStatusRecord, PullRequestRecord, Retro, RunId, RunRecord,
|
||||
RunStatus, RunStatusRecord, SandboxRecord, StartRecord, StatusReason,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -51,6 +51,8 @@ pub struct RunSnapshot {
|
|||
pub retro: Option<Retro>,
|
||||
pub graph: Option<String>,
|
||||
pub sandbox: Option<SandboxRecord>,
|
||||
pub final_patch: Option<String>,
|
||||
pub pull_request: Option<PullRequestRecord>,
|
||||
pub nodes: Vec<NodeSnapshot>,
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +63,12 @@ pub struct NodeSnapshot {
|
|||
pub prompt: Option<String>,
|
||||
pub response: Option<String>,
|
||||
pub status: Option<NodeStatusRecord>,
|
||||
pub outcome: Option<NodeOutcomeRecord>,
|
||||
pub provider_used: Option<serde_json::Value>,
|
||||
pub diff: Option<String>,
|
||||
pub script_invocation: Option<serde_json::Value>,
|
||||
pub script_timing: Option<serde_json::Value>,
|
||||
pub parallel_results: Option<serde_json::Value>,
|
||||
pub stdout: Option<String>,
|
||||
pub stderr: Option<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ pub mod failure_signature;
|
|||
pub mod graph;
|
||||
pub mod node_status;
|
||||
pub mod outcome;
|
||||
pub mod pull_request;
|
||||
pub mod retro;
|
||||
pub mod run;
|
||||
pub mod run_id;
|
||||
|
|
@ -22,6 +23,7 @@ pub use failure_signature::FailureSignature;
|
|||
pub use graph::{AttrValue, Edge, Graph, Node, is_llm_handler_type, shape_to_handler_type};
|
||||
pub use node_status::NodeStatusRecord;
|
||||
pub use outcome::{FailureCategory, FailureDetail, NodeResult, Outcome, OutcomeMeta, StageStatus};
|
||||
pub use pull_request::PullRequestRecord;
|
||||
pub use retro::{
|
||||
AggregateStats, FrictionKind, FrictionPoint, Learning, LearningCategory, OpenItem,
|
||||
OpenItemKind, Retro, RetroNarrative, SmoothnessRating, StageRetro,
|
||||
|
|
|
|||
23
lib/crates/fabro-types/src/pull_request.rs
Normal file
23
lib/crates/fabro-types/src/pull_request.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Record of a pull request created for a workflow run.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PullRequestRecord {
|
||||
pub html_url: String,
|
||||
pub number: u64,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
pub base_branch: String,
|
||||
pub head_branch: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
impl PullRequestRecord {
|
||||
pub fn save(&self, path: &Path) -> Result<(), String> {
|
||||
let json = serde_json::to_string_pretty(self)
|
||||
.map_err(|e| format!("Failed to serialize pull_request.json: {e}"))?;
|
||||
std::fs::write(path, json).map_err(|e| format!("Failed to write pull_request.json: {e}"))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue