test: add unit tests for multi-mode generation path
Covers: single-mode auto-wrap, multi-mode routing, form-urlencoded body format, responsePath extraction, and processData flag. 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
318
tests/scene_generator_modes_test.rs
Normal file
318
tests/scene_generator_modes_test.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use sgclaw::generated_scene::analyzer::SceneKind;
|
||||
use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest};
|
||||
use sgclaw::generated_scene::ir::{
|
||||
ApiEndpointIr, ModeConditionIr, ModeIr, NormalizeRulesIr, SceneIdDiagnosticsIr, SceneIr,
|
||||
WorkflowArchetype,
|
||||
};
|
||||
|
||||
fn make_test_mode(
|
||||
name: &str,
|
||||
url: &str,
|
||||
content_type: Option<&str>,
|
||||
response_path: &str,
|
||||
) -> ModeIr {
|
||||
ModeIr {
|
||||
name: name.to_string(),
|
||||
label: Some(name.to_string()),
|
||||
condition: Some(ModeConditionIr {
|
||||
field: "period_mode".to_string(),
|
||||
operator: "equals".to_string(),
|
||||
value: serde_json::Value::String(name.to_string()),
|
||||
}),
|
||||
api_endpoint: Some(ApiEndpointIr {
|
||||
name: format!("{}_endpoint", name),
|
||||
url: url.to_string(),
|
||||
method: "POST".to_string(),
|
||||
content_type: content_type.map(|s| s.to_string()),
|
||||
description: None,
|
||||
}),
|
||||
column_defs: vec![("id".to_string(), "ID".to_string())],
|
||||
request_template: serde_json::json!({}),
|
||||
normalize_rules: Some(NormalizeRulesIr {
|
||||
rules_type: "validate_required".to_string(),
|
||||
required_fields: vec!["id".to_string()],
|
||||
filter_null: true,
|
||||
}),
|
||||
response_path: response_path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_test_scene_ir(modes: Vec<ModeIr>) -> SceneIr {
|
||||
let is_multi = modes.len() > 1;
|
||||
SceneIr {
|
||||
scene_id: "test-scene".to_string(),
|
||||
scene_id_diagnostics: SceneIdDiagnosticsIr::default(),
|
||||
scene_name: "Test Scene".to_string(),
|
||||
scene_kind: "report_collection".to_string(),
|
||||
workflow_archetype: Some(if is_multi {
|
||||
WorkflowArchetype::MultiModeRequest
|
||||
} else {
|
||||
WorkflowArchetype::SingleRequestTable
|
||||
}),
|
||||
bootstrap: Default::default(),
|
||||
params: Vec::new(),
|
||||
modes,
|
||||
default_mode: Some("month".to_string()),
|
||||
mode_switch_field: Some("period_mode".to_string()),
|
||||
workflow_steps: vec![
|
||||
sgclaw::generated_scene::ir::WorkflowStepIr {
|
||||
step_type: "request".to_string(),
|
||||
description: Some("select mode and query corresponding endpoint".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
sgclaw::generated_scene::ir::WorkflowStepIr {
|
||||
step_type: "transform".to_string(),
|
||||
description: Some("normalize mode-specific table rows".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
workflow_evidence: Default::default(),
|
||||
request_template: serde_json::Value::Null,
|
||||
response_path: "".to_string(),
|
||||
normalize_rules: None,
|
||||
artifact_contract: Default::default(),
|
||||
validation_hints: Default::default(),
|
||||
evidence: Vec::new(),
|
||||
readiness: Default::default(),
|
||||
api_endpoints: Vec::new(),
|
||||
static_params: Default::default(),
|
||||
column_defs: Vec::new(),
|
||||
confidence: 0.0,
|
||||
uncertainties: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_workspace(prefix: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("{prefix}-{nanos}"));
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
/// Test 1: Single mode generates MODES array (routes through compile_multi_mode_request)
|
||||
#[test]
|
||||
fn test_single_mode_generates_modes_array() {
|
||||
let output_root = temp_workspace("sgclaw-single-mode-test");
|
||||
let modes = vec![make_test_mode(
|
||||
"month",
|
||||
"http://example.com/api/month",
|
||||
None,
|
||||
"data",
|
||||
)];
|
||||
let scene_ir = make_test_scene_ir(modes);
|
||||
|
||||
// Use SingleRequestTable archetype - the compile path should auto-wrap into multi-mode
|
||||
let mut scene_ir = scene_ir;
|
||||
scene_ir.workflow_archetype = Some(WorkflowArchetype::SingleRequestTable);
|
||||
// Provide one api_endpoint so ensure_modes_populated works
|
||||
scene_ir.api_endpoints = vec![ApiEndpointIr {
|
||||
name: "default_endpoint".to_string(),
|
||||
url: "http://example.com/api/data".to_string(),
|
||||
method: "POST".to_string(),
|
||||
content_type: None,
|
||||
description: None,
|
||||
}];
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "single-mode-scene".to_string(),
|
||||
scene_name: "Single Mode Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/single-mode-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_single_mode_scene.js")).unwrap();
|
||||
|
||||
assert!(
|
||||
generated_script.contains("const MODES ="),
|
||||
"Generated JS should contain 'const MODES =' since SingleRequestTable routes through compile_multi_mode_request"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 2: Multi-mode generates mode routing (detectMode and MODES.find)
|
||||
#[test]
|
||||
fn test_multi_mode_generates_mode_routing() {
|
||||
let output_root = temp_workspace("sgclaw-multi-mode-test");
|
||||
let modes = vec![
|
||||
make_test_mode(
|
||||
"month",
|
||||
"http://example.com/api/month",
|
||||
None,
|
||||
"data",
|
||||
),
|
||||
make_test_mode(
|
||||
"week",
|
||||
"http://example.com/api/week",
|
||||
None,
|
||||
"data",
|
||||
),
|
||||
];
|
||||
let scene_ir = make_test_scene_ir(modes);
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "multi-mode-scene".to_string(),
|
||||
scene_name: "Multi Mode Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/multi-mode-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_multi_mode_scene.js")).unwrap();
|
||||
|
||||
assert!(
|
||||
generated_script.contains("function detectMode"),
|
||||
"Generated JS should contain 'detectMode' function for multi-mode routing"
|
||||
);
|
||||
assert!(
|
||||
generated_script.contains("MODES.find"),
|
||||
"Generated JS should contain 'MODES.find' for mode selection"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 3: Form-urlencoded request body uses Object.entries().join('&') not JSON.stringify
|
||||
#[test]
|
||||
fn test_form_urlencoded_request_body() {
|
||||
let output_root = temp_workspace("sgclaw-form-urlencoded-test");
|
||||
let modes = vec![make_test_mode(
|
||||
"month",
|
||||
"http://example.com/api/month",
|
||||
Some("application/x-www-form-urlencoded"),
|
||||
"data",
|
||||
)];
|
||||
let scene_ir = make_test_scene_ir(modes);
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "form-urlencoded-scene".to_string(),
|
||||
scene_name: "Form URL Encoded Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/form-urlencoded-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_form_urlencoded_scene.js")).unwrap();
|
||||
|
||||
// The buildModeRequest function should use Object.entries for form-urlencoded
|
||||
assert!(
|
||||
generated_script.contains("Object.entries(requestBody)"),
|
||||
"Generated JS should use Object.entries for form-urlencoded body encoding"
|
||||
);
|
||||
assert!(
|
||||
generated_script.contains(".join('&')"),
|
||||
"Generated JS should join form-urlencoded entries with '&'"
|
||||
);
|
||||
// Verify the conditional exists in buildModeRequest
|
||||
assert!(
|
||||
generated_script.contains("application/x-www-form-urlencoded"),
|
||||
"Generated JS should reference form-urlencoded content type"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 4: Response path extraction uses mode.responsePath in the template
|
||||
#[test]
|
||||
fn test_response_path_extraction_in_template() {
|
||||
let output_root = temp_workspace("sgclaw-response-path-test");
|
||||
let modes = vec![make_test_mode(
|
||||
"month",
|
||||
"http://example.com/api/month",
|
||||
None,
|
||||
"data.list",
|
||||
)];
|
||||
let scene_ir = make_test_scene_ir(modes);
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "response-path-scene".to_string(),
|
||||
scene_name: "Response Path Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/response-path-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_response_path_scene.js")).unwrap();
|
||||
|
||||
// The multi-mode template uses mode.responsePath for response extraction
|
||||
assert!(
|
||||
generated_script.contains("mode.responsePath"),
|
||||
"Generated JS should use 'mode.responsePath' for per-mode response extraction"
|
||||
);
|
||||
// The safeGet call should reference the mode's responsePath
|
||||
assert!(
|
||||
generated_script.contains("safeGet(raw, mode.responsePath"),
|
||||
"Generated JS should call safeGet with mode.responsePath"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 5: processData flag in $.ajax call with correct conditional
|
||||
#[test]
|
||||
fn test_process_data_flag_in_ajax() {
|
||||
let output_root = temp_workspace("sgclaw-process-data-test");
|
||||
let modes = vec![make_test_mode(
|
||||
"month",
|
||||
"http://example.com/api/month",
|
||||
Some("application/x-www-form-urlencoded"),
|
||||
"data",
|
||||
)];
|
||||
let scene_ir = make_test_scene_ir(modes);
|
||||
|
||||
generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: PathBuf::from("tests/fixtures/generated_scene/report_collection"),
|
||||
scene_id: "process-data-scene".to_string(),
|
||||
scene_name: "Process Data Scene".to_string(),
|
||||
scene_kind: Some(SceneKind::ReportCollection),
|
||||
target_url: None,
|
||||
output_root: output_root.clone(),
|
||||
lessons_path: None,
|
||||
scene_info_json: None,
|
||||
scene_ir_json: Some(scene_ir),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let skill_root = output_root.join("skills/process-data-scene");
|
||||
let generated_script =
|
||||
fs::read_to_string(skill_root.join("scripts/collect_process_data_scene.js")).unwrap();
|
||||
|
||||
// The $.ajax call should contain processData flag
|
||||
assert!(
|
||||
generated_script.contains("processData:"),
|
||||
"Generated JS $.ajax call should contain 'processData:' flag"
|
||||
);
|
||||
// processData should be false for form-urlencoded (negated condition)
|
||||
assert!(
|
||||
generated_script.contains("processData: request.headers['Content-Type'] !== 'application/x-www-form-urlencoded'"),
|
||||
"Generated JS should set processData to false for form-urlencoded content type"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user