330 lines
12 KiB
Rust
330 lines
12 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!({ "mode": name }),
|
|
request_field_mappings: Vec::new(),
|
|
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;
|
|
let api_endpoints = modes
|
|
.iter()
|
|
.filter_map(|mode| mode.api_endpoint.clone())
|
|
.collect::<Vec<_>>();
|
|
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(),
|
|
main_request: None,
|
|
pagination_plan: None,
|
|
enrichment_requests: Vec::new(),
|
|
join_keys: Vec::new(),
|
|
merge_or_dedupe_rules: Vec::new(),
|
|
export_plan: None,
|
|
merge_plan: None,
|
|
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,
|
|
runtime_dependencies: Vec::new(),
|
|
static_params: Default::default(),
|
|
column_defs: Vec::new(),
|
|
confidence: 0.0,
|
|
uncertainties: Vec::new(),
|
|
monitoring_action_workflow: None,
|
|
}
|
|
}
|
|
|
|
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 request table uses dedicated simple-request path instead of MODES fallback.
|
|
#[test]
|
|
fn test_single_request_table_uses_dedicated_path() {
|
|
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 stay on the dedicated single-request route.
|
|
let mut scene_ir = scene_ir;
|
|
scene_ir.workflow_archetype = Some(WorkflowArchetype::SingleRequestTable);
|
|
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 REQUEST_TEMPLATE ="),
|
|
"Generated JS should contain REQUEST_TEMPLATE on the dedicated single-request path"
|
|
);
|
|
assert!(
|
|
!generated_script.contains("const MODES ="),
|
|
"Generated JS should no longer route SingleRequestTable through MODES fallback"
|
|
);
|
|
}
|
|
|
|
/// 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 mut scene_ir = make_test_scene_ir(modes);
|
|
scene_ir.workflow_archetype = Some(WorkflowArchetype::MultiModeRequest);
|
|
|
|
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 mut scene_ir = make_test_scene_ir(modes);
|
|
scene_ir.workflow_archetype = Some(WorkflowArchetype::MultiModeRequest);
|
|
|
|
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"
|
|
);
|
|
}
|