Skip to content

Support per-test command overrides in .test.toml files #41

@codesoda

Description

@codesoda

Problem

Commands defined in bugatti.config.toml are global — every test file gets the same commands. There's no way to customize a command for a specific test file without workarounds like wrapper scripts or separate config directories.

Example use case: In ai-barometer, admin tests need additional env vars passed to the ultraman start command:

env ADMIN_GITHUB_LOGINS=cadence-test ultraman start

Currently the only options are wrapper scripts, separate config directories, or baking conditional logic into the command itself.

Proposed solution

Extend [overrides] in .test.toml files to support command overrides, following the same pattern as the existing [overrides.provider]:

name = "Admin tests"

[overrides.commands.server]
cmd = "env ADMIN_GITHUB_LOGINS=cadence-test ultraman start"

[[steps]]
instruction = "Verify admin panel loads"

Semantics

  • Any CommandDef field except kind can be overridden (cmd, readiness_url, readiness_urls, readiness_timeout_secs)
  • Uses the same Option-based merge logic as provider overrides: None = keep global, Some = replace
  • Only existing global commands can be overridden — unknown command names are warned and ignored
  • No changes needed to command.rs, cli.rs, executor.rs, or main.rs — they already consume the merged Config

More examples

Override readiness settings:

[overrides.commands.server]
readiness_timeout_secs = 120

Override multiple commands:

[overrides.commands.server]
cmd = "env ADMIN_GITHUB_LOGINS=cadence-test ultraman start"

[overrides.commands.migrate]
cmd = "cargo sqlx migrate run --with-admin-seeds"
Implementation plan

Files to change

1. src/test_file.rs

  • Add CommandOverrides struct (all fields from CommandDef except kind, wrapped in Option)
  • Add commands: Option<BTreeMap<String, CommandOverrides>> to TestOverrides
  • Add use std::collections::BTreeMap
  • Add parse tests for the new TOML syntax
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct CommandOverrides {
    pub cmd: Option<String>,
    pub readiness_url: Option<String>,
    pub readiness_urls: Option<Vec<String>>,
    pub readiness_timeout_secs: Option<u64>,
}

2. src/config.rs

  • Add CommandDef::merge_overrides(&self, &CommandOverrides) -> CommandDef method
  • Update effective_config() to merge command overrides (same pattern as provider merge)
  • Update import to include CommandOverrides
  • Fix all existing test constructors to include commands: None
  • Add merge tests (full override, partial, unknown name ignored, None preserves global)
impl CommandDef {
    pub fn merge_overrides(&self, overrides: &CommandOverrides) -> CommandDef {
        CommandDef {
            kind: self.kind.clone(),  // never overridden
            cmd: overrides.cmd.clone().unwrap_or_else(|| self.cmd.clone()),
            readiness_url: overrides.readiness_url.clone().or_else(|| self.readiness_url.clone()),
            readiness_urls: overrides.readiness_urls.clone().unwrap_or_else(|| self.readiness_urls.clone()),
            readiness_timeout_secs: overrides.readiness_timeout_secs.or(self.readiness_timeout_secs),
        }
    }
}

Update effective_config():

let commands = match test_file.overrides.as_ref().and_then(|o| o.commands.as_ref()) {
    Some(cmd_overrides) => {
        let mut merged = global.commands.clone();
        for (name, overrides) in cmd_overrides {
            if let Some(global_def) = global.commands.get(name) {
                merged.insert(name.clone(), global_def.merge_overrides(overrides));
            }
        }
        merged
    }
    None => global.commands.clone(),
};

3. tests/pipeline_integration.rs

  • Add integration test for command override merging

What does NOT change

  • src/command.rs — consumes &Config which already has merged commands
  • src/cli.rs — no new CLI flags needed
  • src/executor.rs, src/main.rs, src/run.rs — untouched

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions