Files
claw/tests/scene_generator_modes_test.rs
木炎 6591a0d849 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]
2026-04-17 18:49:43 +08:00

319 lines
11 KiB
Rust

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"
);
}