Skip to content

Commit f101bc7

Browse files
authored
Merge pull request #40 from codesoda/fix/declaration-order-commands
fix: execute config commands in declaration order (#39)
2 parents 2ec71fa + 4bb31ee commit f101bc7

File tree

7 files changed

+74
-21
lines changed

7 files changed

+74
-21
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.4.1] - 2026-04-09
11+
12+
### Fixed
13+
14+
- Config commands now execute in declaration order instead of alphabetical key order (#39)
15+
1016
## [0.4.0] - 2026-04-08
1117

1218
### Added
@@ -55,7 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5561
- Docs deploy workflow triggers and Node version
5662
- Result marker parser handling of embedded markers
5763

58-
[Unreleased]: https://github.com/codesoda/bugatti-cli/compare/v0.4.0...HEAD
64+
[Unreleased]: https://github.com/codesoda/bugatti-cli/compare/v0.4.1...HEAD
65+
[0.4.1]: https://github.com/codesoda/bugatti-cli/compare/v0.4.0...v0.4.1
5966
[0.4.0]: https://github.com/codesoda/bugatti-cli/compare/v0.3.1...v0.4.0
6067
[0.3.1]: https://github.com/codesoda/bugatti-cli/compare/v0.3.0...v0.3.1
6168
[0.3.0]: https://github.com/codesoda/bugatti-cli/releases/tag/v0.3.0

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "bugatti"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
edition = "2021"
55
description = "A CLI for plain-English, agent-assisted local application verification using *.test.toml files"
66

@@ -9,6 +9,7 @@ chrono = { version = "0.4", features = ["serde"] }
99
clap = { version = "4", features = ["derive"] }
1010
ctrlc = "3"
1111
glob = "0.3.3"
12+
indexmap = { version = "2", features = ["serde"] }
1213
libc = "0.2"
1314
serde = { version = "1", features = ["derive"] }
1415
serde_json = "1"

src/claude_code.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ impl<'a> Iterator for StreamTurnIterator<'a> {
535535
mod tests {
536536
use super::*;
537537
use crate::config::{Config, ProviderConfig};
538-
use std::collections::BTreeMap;
538+
use indexmap::IndexMap;
539539

540540
fn test_config() -> Config {
541541
Config {
@@ -547,7 +547,7 @@ mod tests {
547547
strict_warnings: None,
548548
base_url: None,
549549
},
550-
commands: BTreeMap::new(),
550+
commands: IndexMap::new(),
551551
checkpoint: None,
552552
}
553553
}

src/command.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ pub fn validate_skip_readiness(
126126

127127
/// Execute all short-lived commands from the config during the setup phase.
128128
///
129-
/// Commands are executed in BTreeMap order (alphabetical by name).
129+
/// Commands are executed in declaration order (the order they appear in bugatti.config.toml).
130130
/// stdout and stderr are captured and stored under the run's logs/ directory.
131131
/// If any command exits non-zero, execution stops and an error is returned.
132132
///
@@ -615,7 +615,7 @@ mod tests {
615615
use super::*;
616616
use crate::config::{CommandDef, CommandKind, Config, ProviderConfig};
617617
use crate::run::{ArtifactDir, RunId};
618-
use std::collections::BTreeMap;
618+
use indexmap::IndexMap;
619619

620620
fn make_config(commands: Vec<(&str, CommandKind, &str)>) -> Config {
621621
make_config_with_readiness(
@@ -629,7 +629,7 @@ mod tests {
629629
fn make_config_with_readiness(
630630
commands: Vec<(&str, CommandKind, &str, Option<&str>)>,
631631
) -> Config {
632-
let mut map = BTreeMap::new();
632+
let mut map = IndexMap::new();
633633
for (name, kind, cmd, readiness_url) in commands {
634634
map.insert(
635635
name.to_string(),
@@ -758,7 +758,7 @@ mod tests {
758758
let artifact_dir = ArtifactDir::from_run_id(tmp.path(), &run_id);
759759
artifact_dir.create_all().unwrap();
760760

761-
// BTreeMap ordering: "a_first" comes before "b_second"
761+
// Insertion ordering: "a_first" was inserted before "b_second"
762762
let config = make_config(vec![
763763
("a_first", CommandKind::ShortLived, "exit 1"),
764764
("b_second", CommandKind::ShortLived, "echo should_not_run"),
@@ -991,4 +991,25 @@ mod tests {
991991

992992
teardown_processes(&mut tracked);
993993
}
994+
995+
#[test]
996+
fn commands_execute_in_declaration_order() {
997+
let tmp = tempfile::tempdir().unwrap();
998+
let run_id = RunId("test-run".to_string());
999+
let artifact_dir = ArtifactDir::from_run_id(tmp.path(), &run_id);
1000+
artifact_dir.create_all().unwrap();
1001+
1002+
// Insert in reverse-alpha order: z_last first, a_first second.
1003+
// With BTreeMap this would have executed a_first then z_last.
1004+
// With IndexMap it must execute z_last then a_first.
1005+
let config = make_config(vec![
1006+
("z_last", CommandKind::ShortLived, "echo z_last"),
1007+
("a_first", CommandKind::ShortLived, "echo a_first"),
1008+
]);
1009+
1010+
let results = run_short_lived_commands(&config, &artifact_dir, &[]).unwrap();
1011+
assert_eq!(results.len(), 2);
1012+
assert_eq!(results[0].name, "z_last");
1013+
assert_eq!(results[1].name, "a_first");
1014+
}
9941015
}

src/config.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::test_file::ProviderOverrides;
2+
use indexmap::IndexMap;
23
use serde::Deserialize;
3-
use std::collections::BTreeMap;
44
use std::path::Path;
55

66
/// Top-level project configuration loaded from bugatti.config.toml.
@@ -10,7 +10,7 @@ pub struct Config {
1010
#[serde(default)]
1111
pub provider: ProviderConfig,
1212
#[serde(default)]
13-
pub commands: BTreeMap<String, CommandDef>,
13+
pub commands: IndexMap<String, CommandDef>,
1414
#[serde(default)]
1515
pub checkpoint: Option<CheckpointConfig>,
1616
}
@@ -196,6 +196,7 @@ pub fn load_config(dir: &Path) -> Result<Config, ConfigError> {
196196
mod tests {
197197
use super::*;
198198
use crate::test_file::{ProviderOverrides, Step, TestFile, TestOverrides};
199+
use indexmap::IndexMap;
199200
use std::fs;
200201

201202
#[test]
@@ -243,6 +244,28 @@ readiness_url = "http://localhost:3000/health"
243244
);
244245
}
245246

247+
#[test]
248+
fn config_preserves_toml_declaration_order() {
249+
let dir = tempfile::tempdir().unwrap();
250+
fs::write(
251+
dir.path().join("bugatti.config.toml"),
252+
r#"
253+
[commands.z_server]
254+
kind = "long_lived"
255+
cmd = "sleep 60"
256+
257+
[commands.a_migrate]
258+
kind = "short_lived"
259+
cmd = "echo migrate"
260+
"#,
261+
)
262+
.unwrap();
263+
264+
let config = load_config(dir.path()).unwrap();
265+
let names: Vec<&String> = config.commands.keys().collect();
266+
assert_eq!(names, vec!["z_server", "a_migrate"]);
267+
}
268+
246269
#[test]
247270
fn missing_config_returns_defaults() {
248271
let dir = tempfile::tempdir().unwrap();
@@ -278,7 +301,7 @@ readiness_url = "http://localhost:3000/health"
278301
strict_warnings: None,
279302
base_url: None,
280303
},
281-
commands: BTreeMap::new(),
304+
commands: IndexMap::new(),
282305
checkpoint: None,
283306
};
284307
let test_file = TestFile {
@@ -321,7 +344,7 @@ readiness_url = "http://localhost:3000/health"
321344
strict_warnings: None,
322345
base_url: None,
323346
},
324-
commands: BTreeMap::new(),
347+
commands: IndexMap::new(),
325348
checkpoint: None,
326349
};
327350
let test_file = TestFile {
@@ -360,7 +383,7 @@ readiness_url = "http://localhost:3000/health"
360383
strict_warnings: None,
361384
base_url: None,
362385
},
363-
commands: BTreeMap::new(),
386+
commands: IndexMap::new(),
364387
checkpoint: None,
365388
};
366389
let test_file = TestFile {
@@ -419,7 +442,7 @@ step_timeout_secs = 600
419442
step_timeout_secs: Some(300),
420443
..ProviderConfig::default()
421444
},
422-
commands: BTreeMap::new(),
445+
commands: IndexMap::new(),
423446
checkpoint: None,
424447
};
425448
let test_file = TestFile {
@@ -445,7 +468,7 @@ step_timeout_secs = 600
445468
step_timeout_secs: Some(300),
446469
..ProviderConfig::default()
447470
},
448-
commands: BTreeMap::new(),
471+
commands: IndexMap::new(),
449472
checkpoint: None,
450473
};
451474
let test_file = TestFile {
@@ -487,7 +510,7 @@ strict_warnings = true
487510
strict_warnings: Some(true),
488511
..ProviderConfig::default()
489512
},
490-
commands: BTreeMap::new(),
513+
commands: IndexMap::new(),
491514
checkpoint: None,
492515
};
493516
let test_file = TestFile {
@@ -533,7 +556,7 @@ base_url = "http://localhost:3000"
533556
base_url: Some("http://localhost:3000".to_string()),
534557
..ProviderConfig::default()
535558
},
536-
commands: BTreeMap::new(),
559+
commands: IndexMap::new(),
537560
checkpoint: None,
538561
};
539562
let test_file = TestFile {
@@ -562,7 +585,7 @@ base_url = "http://localhost:3000"
562585
base_url: Some("http://localhost:3000".to_string()),
563586
..ProviderConfig::default()
564587
},
565-
commands: BTreeMap::new(),
588+
commands: IndexMap::new(),
566589
checkpoint: None,
567590
};
568591
let test_file = TestFile {

src/run.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,10 @@ pub fn initialize_run(
213213
mod tests {
214214
use super::*;
215215
use crate::config::{CommandDef, CommandKind, Config, ProviderConfig};
216-
use std::collections::BTreeMap;
216+
use indexmap::IndexMap;
217217

218218
fn test_config() -> Config {
219-
let mut commands = BTreeMap::new();
219+
let mut commands = IndexMap::new();
220220
commands.insert(
221221
"migrate".to_string(),
222222
CommandDef {

0 commit comments

Comments
 (0)