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