Skip to content

Commit d14fbfb

Browse files
authored
Merge pull request #103 from imbue-ai/vitest/framework-support
Add vitest framework with discovery and execution support
2 parents 5f0302f + 1bb6e94 commit d14fbfb

File tree

7 files changed

+890
-34
lines changed

7 files changed

+890
-34
lines changed

src/config/schema.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ pub enum FrameworkConfig {
319319

320320
/// Discover and run tests with custom shell commands.
321321
Default(DefaultFrameworkConfig),
322+
323+
/// Discover and run JavaScript/TypeScript tests with vitest.
324+
Vitest(VitestFrameworkConfig),
322325
}
323326

324327
impl FrameworkConfig {
@@ -331,6 +334,7 @@ impl FrameworkConfig {
331334
FrameworkConfig::Pytest(config) => &config.test_id_format,
332335
FrameworkConfig::Cargo(config) => &config.test_id_format,
333336
FrameworkConfig::Default(config) => &config.test_id_format,
337+
FrameworkConfig::Vitest(config) => &config.test_id_format,
334338
}
335339
}
336340
}
@@ -394,6 +398,44 @@ fn default_cargo_test_id_format() -> String {
394398
"{classname} {name}".to_string()
395399
}
396400

401+
fn default_vitest_command() -> String {
402+
"npx vitest".to_string()
403+
}
404+
405+
fn default_vitest_test_id_format() -> String {
406+
"{classname} > {name}".to_string()
407+
}
408+
409+
/// Configuration for vitest test framework.
410+
#[derive(Debug, Clone, Deserialize, Serialize)]
411+
pub struct VitestFrameworkConfig {
412+
/// Full command prefix for invoking vitest (e.g. `"npx vitest"`).
413+
///
414+
/// Default: `"npx vitest"`
415+
#[serde(default = "default_vitest_command")]
416+
pub command: String,
417+
418+
/// Extra arguments appended only during test execution (not discovery).
419+
#[serde(default, skip_serializing_if = "Option::is_none")]
420+
pub run_args: Option<String>,
421+
422+
/// Format string for constructing test IDs from JUnit XML attributes.
423+
///
424+
/// Default: `"{classname} > {name}"`
425+
#[serde(default = "default_vitest_test_id_format")]
426+
pub test_id_format: String,
427+
}
428+
429+
impl Default for VitestFrameworkConfig {
430+
fn default() -> Self {
431+
Self {
432+
command: default_vitest_command(),
433+
run_args: None,
434+
test_id_format: default_vitest_test_id_format(),
435+
}
436+
}
437+
}
438+
397439
/// Configuration for Rust/Cargo test framework.
398440
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
399441
pub struct CargoFrameworkConfig {
@@ -854,4 +896,70 @@ mod tests {
854896

855897
Ok(())
856898
}
899+
900+
fn vitest_local_config() -> Config {
901+
Config {
902+
offload: OffloadConfig {
903+
max_parallel: 10,
904+
test_timeout_secs: 900,
905+
working_dir: None,
906+
sandbox_project_root: "/app".to_string(),
907+
sandbox_init_cmd: None,
908+
},
909+
provider: ProviderConfig::Local(LocalProviderConfig {
910+
working_dir: Some(PathBuf::from(".")),
911+
..Default::default()
912+
}),
913+
framework: FrameworkConfig::Vitest(VitestFrameworkConfig {
914+
command: "npx vitest".into(),
915+
test_id_format: "{classname} > {name}".into(),
916+
..Default::default()
917+
}),
918+
groups: HashMap::from([(
919+
"default".to_string(),
920+
GroupConfig {
921+
retry_count: 0,
922+
filters: String::new(),
923+
},
924+
)]),
925+
report: ReportConfig::default(),
926+
}
927+
}
928+
929+
#[test]
930+
fn test_init_config_vitest_deserializes() -> Result<(), Box<dyn std::error::Error>> {
931+
let config = vitest_local_config();
932+
let toml_str = toml::to_string_pretty(&config)?;
933+
let deserialized: Config = toml::from_str(&toml_str)?;
934+
assert_eq!(
935+
deserialized.framework.test_id_format(),
936+
"{classname} > {name}"
937+
);
938+
Ok(())
939+
}
940+
941+
#[test]
942+
fn test_vitest_default_command() -> Result<(), Box<dyn std::error::Error>> {
943+
let toml_str = r#"
944+
[offload]
945+
sandbox_project_root = "/app"
946+
947+
[provider]
948+
type = "local"
949+
950+
[framework]
951+
type = "vitest"
952+
953+
[groups.all]
954+
retry_count = 0
955+
"#;
956+
let config: Config = toml::from_str(toml_str)?;
957+
if let FrameworkConfig::Vitest(ref vitest) = config.framework {
958+
assert_eq!(vitest.command, "npx vitest");
959+
assert!(vitest.run_args.is_none());
960+
} else {
961+
return Err("Expected Vitest framework".into());
962+
}
963+
Ok(())
964+
}
857965
}

src/framework.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pub mod cargo;
33
pub mod default;
44
pub mod pytest;
5+
pub mod vitest;
56

67
use std::path::PathBuf;
78

@@ -270,6 +271,26 @@ pub trait TestFramework: Send + Sync {
270271
///
271272
/// * `tests` - Tests to execute (borrowed from TestRecords)
272273
fn produce_test_execution_command(&self, tests: &[TestInstance], result_path: &str) -> Command;
274+
275+
/// File format for the test result file produced by the framework.
276+
///
277+
/// Used as the file extension for the result file path.
278+
/// Default: `"xml"` (JUnit XML). Frameworks that produce other formats
279+
/// (e.g., JSON) should override this.
280+
fn report_format(&self) -> &str {
281+
"xml"
282+
}
283+
284+
/// Processes raw test result output into JUnit XML.
285+
///
286+
/// Frameworks can override this to convert non-JUnit output formats
287+
/// (e.g., vitest JSON) into JUnit XML, or to filter artifacts from
288+
/// their JUnit output.
289+
///
290+
/// Default implementation returns the input unchanged (assumes JUnit XML).
291+
fn xml_from_report(&self, raw_output: &str) -> FrameworkResult<String> {
292+
Ok(raw_output.to_string())
293+
}
273294
}
274295

275296
#[cfg(test)]

0 commit comments

Comments
 (0)