From 4bb31ee04a359c4c329ee4a8f4105c4510b8b66b Mon Sep 17 00:00:00 2001 From: Chris Raethke Date: Thu, 9 Apr 2026 23:14:02 +1000 Subject: [PATCH] fix: execute config commands in declaration order instead of alphabetical (#39) Replace BTreeMap with IndexMap for Config.commands so commands run in the order they appear in bugatti.config.toml. Bump to v0.4.1. --- CHANGELOG.md | 9 ++++++++- Cargo.lock | 3 ++- Cargo.toml | 3 ++- src/claude_code.rs | 4 ++-- src/command.rs | 29 +++++++++++++++++++++++++---- src/config.rs | 43 +++++++++++++++++++++++++++++++++---------- src/run.rs | 4 ++-- 7 files changed, 74 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db92df1..89eb270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.1] - 2026-04-09 + +### Fixed + +- Config commands now execute in declaration order instead of alphabetical key order (#39) + ## [0.4.0] - 2026-04-08 ### Added @@ -55,7 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Docs deploy workflow triggers and Node version - Result marker parser handling of embedded markers -[Unreleased]: https://github.com/codesoda/bugatti-cli/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/codesoda/bugatti-cli/compare/v0.4.1...HEAD +[0.4.1]: https://github.com/codesoda/bugatti-cli/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/codesoda/bugatti-cli/compare/v0.3.1...v0.4.0 [0.3.1]: https://github.com/codesoda/bugatti-cli/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/codesoda/bugatti-cli/releases/tag/v0.3.0 diff --git a/Cargo.lock b/Cargo.lock index 8b4c09f..5d5e882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,13 +117,14 @@ dependencies = [ [[package]] name = "bugatti" -version = "0.4.0" +version = "0.4.1" dependencies = [ "chrono", "clap", "ctrlc", "flate2", "glob", + "indexmap", "libc", "reqwest", "self-replace", diff --git a/Cargo.toml b/Cargo.toml index cc6532b..75c1f17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bugatti" -version = "0.4.0" +version = "0.4.1" edition = "2021" description = "A CLI for plain-English, agent-assisted local application verification using *.test.toml files" @@ -9,6 +9,7 @@ chrono = { version = "0.4", features = ["serde"] } clap = { version = "4", features = ["derive"] } ctrlc = "3" glob = "0.3.3" +indexmap = { version = "2", features = ["serde"] } libc = "0.2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src/claude_code.rs b/src/claude_code.rs index 20fa7df..884a5df 100644 --- a/src/claude_code.rs +++ b/src/claude_code.rs @@ -535,7 +535,7 @@ impl<'a> Iterator for StreamTurnIterator<'a> { mod tests { use super::*; use crate::config::{Config, ProviderConfig}; - use std::collections::BTreeMap; + use indexmap::IndexMap; fn test_config() -> Config { Config { @@ -547,7 +547,7 @@ mod tests { strict_warnings: None, base_url: None, }, - commands: BTreeMap::new(), + commands: IndexMap::new(), checkpoint: None, } } diff --git a/src/command.rs b/src/command.rs index bab048d..0fd6f18 100644 --- a/src/command.rs +++ b/src/command.rs @@ -126,7 +126,7 @@ pub fn validate_skip_readiness( /// Execute all short-lived commands from the config during the setup phase. /// -/// Commands are executed in BTreeMap order (alphabetical by name). +/// Commands are executed in declaration order (the order they appear in bugatti.config.toml). /// stdout and stderr are captured and stored under the run's logs/ directory. /// If any command exits non-zero, execution stops and an error is returned. /// @@ -615,7 +615,7 @@ mod tests { use super::*; use crate::config::{CommandDef, CommandKind, Config, ProviderConfig}; use crate::run::{ArtifactDir, RunId}; - use std::collections::BTreeMap; + use indexmap::IndexMap; fn make_config(commands: Vec<(&str, CommandKind, &str)>) -> Config { make_config_with_readiness( @@ -629,7 +629,7 @@ mod tests { fn make_config_with_readiness( commands: Vec<(&str, CommandKind, &str, Option<&str>)>, ) -> Config { - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); for (name, kind, cmd, readiness_url) in commands { map.insert( name.to_string(), @@ -758,7 +758,7 @@ mod tests { let artifact_dir = ArtifactDir::from_run_id(tmp.path(), &run_id); artifact_dir.create_all().unwrap(); - // BTreeMap ordering: "a_first" comes before "b_second" + // Insertion ordering: "a_first" was inserted before "b_second" let config = make_config(vec![ ("a_first", CommandKind::ShortLived, "exit 1"), ("b_second", CommandKind::ShortLived, "echo should_not_run"), @@ -991,4 +991,25 @@ mod tests { teardown_processes(&mut tracked); } + + #[test] + fn commands_execute_in_declaration_order() { + let tmp = tempfile::tempdir().unwrap(); + let run_id = RunId("test-run".to_string()); + let artifact_dir = ArtifactDir::from_run_id(tmp.path(), &run_id); + artifact_dir.create_all().unwrap(); + + // Insert in reverse-alpha order: z_last first, a_first second. + // With BTreeMap this would have executed a_first then z_last. + // With IndexMap it must execute z_last then a_first. + let config = make_config(vec![ + ("z_last", CommandKind::ShortLived, "echo z_last"), + ("a_first", CommandKind::ShortLived, "echo a_first"), + ]); + + let results = run_short_lived_commands(&config, &artifact_dir, &[]).unwrap(); + assert_eq!(results.len(), 2); + assert_eq!(results[0].name, "z_last"); + assert_eq!(results[1].name, "a_first"); + } } diff --git a/src/config.rs b/src/config.rs index ba8c73d..421406d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use crate::test_file::ProviderOverrides; +use indexmap::IndexMap; use serde::Deserialize; -use std::collections::BTreeMap; use std::path::Path; /// Top-level project configuration loaded from bugatti.config.toml. @@ -10,7 +10,7 @@ pub struct Config { #[serde(default)] pub provider: ProviderConfig, #[serde(default)] - pub commands: BTreeMap, + pub commands: IndexMap, #[serde(default)] pub checkpoint: Option, } @@ -196,6 +196,7 @@ pub fn load_config(dir: &Path) -> Result { mod tests { use super::*; use crate::test_file::{ProviderOverrides, Step, TestFile, TestOverrides}; + use indexmap::IndexMap; use std::fs; #[test] @@ -243,6 +244,28 @@ readiness_url = "http://localhost:3000/health" ); } + #[test] + fn config_preserves_toml_declaration_order() { + let dir = tempfile::tempdir().unwrap(); + fs::write( + dir.path().join("bugatti.config.toml"), + r#" +[commands.z_server] +kind = "long_lived" +cmd = "sleep 60" + +[commands.a_migrate] +kind = "short_lived" +cmd = "echo migrate" +"#, + ) + .unwrap(); + + let config = load_config(dir.path()).unwrap(); + let names: Vec<&String> = config.commands.keys().collect(); + assert_eq!(names, vec!["z_server", "a_migrate"]); + } + #[test] fn missing_config_returns_defaults() { let dir = tempfile::tempdir().unwrap(); @@ -278,7 +301,7 @@ readiness_url = "http://localhost:3000/health" strict_warnings: None, base_url: None, }, - commands: BTreeMap::new(), + commands: IndexMap::new(), checkpoint: None, }; let test_file = TestFile { @@ -321,7 +344,7 @@ readiness_url = "http://localhost:3000/health" strict_warnings: None, base_url: None, }, - commands: BTreeMap::new(), + commands: IndexMap::new(), checkpoint: None, }; let test_file = TestFile { @@ -360,7 +383,7 @@ readiness_url = "http://localhost:3000/health" strict_warnings: None, base_url: None, }, - commands: BTreeMap::new(), + commands: IndexMap::new(), checkpoint: None, }; let test_file = TestFile { @@ -419,7 +442,7 @@ step_timeout_secs = 600 step_timeout_secs: Some(300), ..ProviderConfig::default() }, - commands: BTreeMap::new(), + commands: IndexMap::new(), checkpoint: None, }; let test_file = TestFile { @@ -445,7 +468,7 @@ step_timeout_secs = 600 step_timeout_secs: Some(300), ..ProviderConfig::default() }, - commands: BTreeMap::new(), + commands: IndexMap::new(), checkpoint: None, }; let test_file = TestFile { @@ -487,7 +510,7 @@ strict_warnings = true strict_warnings: Some(true), ..ProviderConfig::default() }, - commands: BTreeMap::new(), + commands: IndexMap::new(), checkpoint: None, }; let test_file = TestFile { @@ -533,7 +556,7 @@ base_url = "http://localhost:3000" base_url: Some("http://localhost:3000".to_string()), ..ProviderConfig::default() }, - commands: BTreeMap::new(), + commands: IndexMap::new(), checkpoint: None, }; let test_file = TestFile { @@ -562,7 +585,7 @@ base_url = "http://localhost:3000" base_url: Some("http://localhost:3000".to_string()), ..ProviderConfig::default() }, - commands: BTreeMap::new(), + commands: IndexMap::new(), checkpoint: None, }; let test_file = TestFile { diff --git a/src/run.rs b/src/run.rs index f294a3c..4832d46 100644 --- a/src/run.rs +++ b/src/run.rs @@ -213,10 +213,10 @@ pub fn initialize_run( mod tests { use super::*; use crate::config::{CommandDef, CommandKind, Config, ProviderConfig}; - use std::collections::BTreeMap; + use indexmap::IndexMap; fn test_config() -> Config { - let mut commands = BTreeMap::new(); + let mut commands = IndexMap::new(); commands.insert( "migrate".to_string(), CommandDef {