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