Add a shared RunSpec test fixture and adopt it

`RunSpec` has 13 fields and no `Default`, so every test that needed one
spelled out all 13 even when it cared about one or two. That put 64
hand-rolled `RunSpec { .. }` literals in `lib/`, and made a single
additive field cost a mechanical edit at roughly 30 sites.

Add `test_run_spec()` to `fabro-types`' feature-gated `test_support`
module: fixed `fixtures::RUN_1`, default settings, a minimal `test`
graph, `test_run_provenance()`, and every optional field unset. Tests
now spread it and only spell out what they assert on.

Adopt it at the 13 literals where the spread removes real duplication,
including the crate-local `test_run_spec` helpers in `fabro-store` and
`fabro-workflow`, which are now defined in terms of the shared fixture.
Tests that populate every field on purpose — the exhaustive `RunSpec`
serde round-trip in particular — keep spelling it out.

No production code and no behavior changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-08-19 16:50:37 -04:00
parent d3825fb2b2
commit 19aa5940ea
10 changed files with 76 additions and 190 deletions

View file

@ -486,8 +486,7 @@ mod tests {
use fabro_types::{
Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunSandboxInstance,
RunSandboxPlan, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage,
StageOutcome, StartRecord, SuccessReason, WorkflowSettings, first_event_seq, fixtures,
test_support,
StageOutcome, StartRecord, SuccessReason, first_event_seq, fixtures, test_support,
};
use futures::executor;
@ -495,24 +494,18 @@ mod tests {
fn sample_run_spec() -> RunSpec {
RunSpec {
run_id: fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph: Graph::new("ship"),
graph_source: Some("digraph Ship {}".to_string()),
workflow_slug: Some("demo".to_string()),
automation: None,
graph: Graph::new("ship"),
graph_source: Some("digraph Ship {}".to_string()),
workflow_slug: Some("demo".to_string()),
source_directory: Some("/tmp/project".to_string()),
git: Some(fabro_types::GitContext {
git: Some(fabro_types::GitContext {
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
}),
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
..test_support::test_run_spec()
}
}

View file

@ -2281,19 +2281,8 @@ mod tests {
fn test_run_spec() -> RunSpec {
RunSpec {
run_id: fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph: Graph::new("test"),
graph_source: Some("digraph test {}".to_string()),
workflow_slug: None,
automation: None,
source_directory: None,
labels: HashMap::new(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: None,
fork_source_ref: None,
graph_source: Some("digraph test {}".to_string()),
..test_support::test_run_spec()
}
}
@ -4086,19 +4075,9 @@ mod tests {
fn summary_synthesizes_submitted_when_run_exists_without_status() {
let mut state = initialized_projection();
state.spec = fabro_types::RunSpec {
run_id: fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph: fabro_types::Graph::new("test"),
graph_source: None,
workflow_slug: Some("test".to_string()),
automation: None,
workflow_slug: Some("test".to_string()),
source_directory: Some("/tmp/repo".to_string()),
git: None,
labels: HashMap::new(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
..test_support::test_run_spec()
};
let summary_json = serde_json::to_value(build_summary(&state, &fixtures::RUN_1)).unwrap();
@ -4112,19 +4091,10 @@ mod tests {
fn summary_preserves_absent_workflow_name_and_reports_graph_name() {
let mut state = initialized_projection();
state.spec = fabro_types::RunSpec {
run_id: fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph: fabro_types::Graph::new("GraphName"),
graph_source: None,
workflow_slug: Some("release-flow".to_string()),
automation: None,
graph: fabro_types::Graph::new("GraphName"),
workflow_slug: Some("release-flow".to_string()),
source_directory: Some("/tmp/repo".to_string()),
git: None,
labels: HashMap::new(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
..test_support::test_run_spec()
};
let summary = build_summary(&state, &fixtures::RUN_1);

View file

@ -8,30 +8,23 @@ use fabro_types::{
BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, InterviewQuestionRecord,
ParallelBranchResult, QuestionType, RunDiff, RunSandbox, RunSandboxInstance, RunSandboxPlan,
RunSandboxRuntime, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage,
StageOutcome, StartRecord, WorkflowSettings, first_event_seq, fixtures, test_support,
StageOutcome, StartRecord, first_event_seq, fixtures, test_support,
};
use serde_json::json;
fn sample_run_spec() -> RunSpec {
RunSpec {
run_id: fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph: Graph::new("ship"),
graph_source: None,
workflow_slug: Some("demo".to_string()),
automation: None,
graph: Graph::new("ship"),
workflow_slug: Some("demo".to_string()),
source_directory: Some("/tmp/project".to_string()),
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: Some(fabro_types::GitContext {
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
git: Some(fabro_types::GitContext {
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
}),
fork_source_ref: None,
..test_support::test_run_spec()
}
}

View file

@ -126,12 +126,10 @@ pub fn billing_rollup_from_projection(
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use fabro_model::{Catalog, ModelRef, ProviderId};
use fabro_types::{
AttrValue, BilledTokenCounts, Graph, Node, RunProjection, RunSpec, StageCompletion,
StageOutcome, WorkflowSettings, first_event_seq, fixtures, test_support,
StageOutcome, first_event_seq, test_support,
};
use super::billing_rollup_from_projection;
@ -311,19 +309,8 @@ mod tests {
});
RunSpec {
run_id: fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph,
graph_source: None,
workflow_slug: None,
automation: None,
source_directory: None,
labels: HashMap::new(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: None,
fork_source_ref: None,
..test_support::test_run_spec()
}
}
}

View file

@ -445,13 +445,11 @@ fn run_id_matches(run_id: RunId, prefix: &str) -> bool {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use fabro_graphviz::graph::Graph;
use fabro_store::Database;
use fabro_types::{RunStatus, WorkflowSettings, fixtures, test_support};
use fabro_types::{RunStatus, fixtures, test_support};
use object_store::memory::InMemory;
use super::scan_runs_combined;
@ -470,24 +468,15 @@ mod tests {
fn sample_run_spec() -> RunSpec {
RunSpec {
run_id: fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph: Graph::new("test"),
graph_source: None,
workflow_slug: Some("test".to_string()),
automation: None,
workflow_slug: Some("test".to_string()),
source_directory: Some("/tmp/project".to_string()),
git: Some(fabro_types::GitContext {
git: Some(fabro_types::GitContext {
origin_url: String::new(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
}),
labels: HashMap::new(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
..test_support::test_run_spec()
}
}

View file

@ -112,15 +112,13 @@ impl RunStoreBackend for LocalRunStoreBackend {
#[cfg(test)]
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::Database;
use fabro_types::run_event::RunSubmittedProps;
use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures, test_support};
use fabro_types::{EventBody, RunEvent, fixtures, test_support};
use object_store::memory::InMemory;
use super::RunStoreHandle;
@ -139,19 +137,9 @@ mod tests {
fn test_run_spec() -> RunSpec {
RunSpec {
run_id: fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph: Graph::new("test"),
graph_source: None,
workflow_slug: Some("test".to_string()),
automation: None,
workflow_slug: Some("test".to_string()),
source_directory: Some("/tmp/test".to_string()),
git: None,
labels: HashMap::new(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
fork_source_ref: None,
..test_support::test_run_spec()
}
}

View file

@ -1,7 +1,7 @@
use std::any::{TypeId, type_name};
use fabro_api::types::RunProjection as ApiRunProjection;
use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, test_support};
use fabro_types::{RunProjection, RunSpec, test_support};
use serde_json::json;
#[test]
fn run_projection_reuses_canonical_type() {
@ -129,19 +129,8 @@ fn run_projection_round_trips_with_pending_control_unset() {
fn run_spec_json() -> serde_json::Value {
serde_json::to_value(RunSpec {
run_id: fabro_types::fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph: Graph::new("test"),
graph_source: Some("digraph test {}".to_string()),
workflow_slug: None,
automation: None,
source_directory: None,
labels: std::collections::HashMap::new(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: None,
fork_source_ref: None,
graph_source: Some("digraph test {}".to_string()),
..test_support::test_run_spec()
})
.unwrap()
}

View file

@ -1061,11 +1061,9 @@ impl RunProjection {
#[cfg(test)]
mod title_tests {
use std::collections::HashMap;
use chrono::Utc;
use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings, test_support};
use crate::{AttrValue, Graph, RunProjection, RunSpec, test_support};
fn projection_with_goal(goal: Option<&str>) -> RunProjection {
let mut graph = Graph::new("test");
@ -1076,19 +1074,8 @@ mod title_tests {
}
let spec = RunSpec {
run_id: RunId::new(),
settings: WorkflowSettings::default(),
graph,
graph_source: None,
workflow_slug: None,
automation: None,
source_directory: None,
labels: HashMap::new(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: None,
fork_source_ref: None,
..test_support::test_run_spec()
};
RunProjection::new(String::new(), spec, Utc::now())
}
@ -1129,7 +1116,6 @@ mod title_tests {
#[cfg(test)]
mod iter_stages_tests {
use std::collections::HashMap;
use std::num::NonZeroU32;
use chrono::Utc;
@ -1137,10 +1123,7 @@ mod iter_stages_tests {
use serde_json::json;
use super::RunProjection;
use crate::{
AgentControlState, BilledTokenCounts, Graph, RunId, RunSpec, StageProjection,
WorkflowSettings, test_support,
};
use crate::{AgentControlState, BilledTokenCounts, StageProjection, test_support};
fn seq(n: u32) -> NonZeroU32 {
NonZeroU32::new(n).unwrap()
@ -1149,21 +1132,7 @@ mod iter_stages_tests {
fn projection() -> RunProjection {
RunProjection::new(
"Test run".to_string(),
RunSpec {
run_id: RunId::new(),
settings: WorkflowSettings::default(),
graph: Graph::new("test"),
graph_source: None,
workflow_slug: None,
automation: None,
source_directory: None,
labels: HashMap::default(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: None,
fork_source_ref: None,
},
test_support::test_run_spec(),
Utc::now(),
)
}
@ -1336,14 +1305,12 @@ mod iter_stages_tests {
#[cfg(test)]
mod live_timing_tests {
use std::collections::HashMap;
use chrono::{DateTime, TimeZone, Utc};
use super::{RunProjection, StageToolBatchProjection};
use crate::{
Graph, ModelRef, RunId, RunSpec, StageHandler, StageInferenceProjection, StageProjection,
StageState, StageTiming, StartRecord, WorkflowSettings, first_event_seq, test_support,
ModelRef, StageHandler, StageInferenceProjection, StageProjection, StageState, StageTiming,
StartRecord, first_event_seq, test_support,
};
fn at(seconds: i64) -> DateTime<Utc> {
@ -1351,25 +1318,7 @@ mod live_timing_tests {
}
fn projection() -> RunProjection {
RunProjection::new(
"Test run".to_string(),
RunSpec {
run_id: RunId::new(),
settings: WorkflowSettings::default(),
graph: Graph::new("test"),
graph_source: None,
workflow_slug: None,
automation: None,
source_directory: None,
labels: HashMap::default(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: None,
fork_source_ref: None,
},
at(0),
)
RunProjection::new("Test run".to_string(), test_support::test_run_spec(), at(0))
}
/// In-flight stage that started at `at(0)`.

View file

@ -1,4 +1,8 @@
use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance};
use std::collections::HashMap;
use crate::{
AuthMethod, Graph, IdpIdentity, Principal, RunProvenance, RunSpec, WorkflowSettings, fixtures,
};
#[must_use]
pub fn test_principal() -> Principal {
@ -17,3 +21,33 @@ pub fn test_run_provenance() -> RunProvenance {
subject: test_principal(),
}
}
/// Neutral [`RunSpec`] for tests: a fixed run id, default settings, a minimal
/// `test` graph, and every optional field unset.
///
/// Spread it so a test only spells out the fields it actually asserts on:
///
/// ```ignore
/// let spec = RunSpec {
/// workflow_slug: Some("release-flow".to_string()),
/// ..test_run_spec()
/// };
/// ```
#[must_use]
pub fn test_run_spec() -> RunSpec {
RunSpec {
run_id: fixtures::RUN_1,
settings: WorkflowSettings::default(),
graph: Graph::new("test"),
graph_source: None,
workflow_slug: None,
automation: None,
source_directory: None,
labels: HashMap::new(),
provenance: test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: None,
fork_source_ref: None,
}
}

View file

@ -3,7 +3,7 @@ use std::collections::HashMap;
use fabro_types::graph::Graph;
use fabro_types::run::{DirtyStatus, GitContext, RunSpec};
use fabro_types::settings::{ProjectNamespace, WorkflowNamespace};
use fabro_types::test_support::test_run_provenance;
use fabro_types::test_support::test_run_spec;
use fabro_types::{WorkflowSettings, fixtures};
fn sample_run_spec() -> RunSpec {
@ -20,24 +20,18 @@ fn sample_run_spec() -> RunSpec {
};
RunSpec {
run_id: fixtures::RUN_1,
settings,
graph: Graph::new("ship"),
graph_source: None,
workflow_slug: Some("demo".to_string()),
automation: None,
source_directory: Some("/Users/client/project".to_string()),
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
provenance: test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: Some(GitContext {
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
branch: "main".to_string(),
sha: Some("abc123".to_string()),
dirty: DirtyStatus::Dirty,
}),
fork_source_ref: None,
..test_run_spec()
}
}