- Auto-connect WebSocket on page load in service console - Settings modal for editing sgclaw_config.json (API key, base URL, model, skills dir, etc.) - UpdateConfig/ConfigUpdated protocol messages for remote config save - save_to_path() for SgClawSettings serialization - ConfigUpdated handler in sg_claw_client binary - Protocol serialization tests for new message types - HTML test assertions for auto-connect and settings UI - Additional pending changes: deterministic submit, org units, lineloss xlsx export, browser script tool, and docs 🤖 Generated with [Qoder][https://qoder.com]
106 lines
3.8 KiB
Rust
106 lines
3.8 KiB
Rust
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
use serde_json::json;
|
|
use sgclaw::compat::lineloss_xlsx_export::{export_lineloss_xlsx, LinelossExportRequest};
|
|
|
|
fn temp_output_path(name: &str) -> PathBuf {
|
|
let dir = std::env::temp_dir().join("sgclaw-test-xlsx");
|
|
fs::create_dir_all(&dir).unwrap();
|
|
dir.join(name)
|
|
}
|
|
|
|
#[test]
|
|
fn export_month_lineloss_produces_valid_xlsx() {
|
|
let output_path = temp_output_path("month-test.xlsx");
|
|
if output_path.exists() {
|
|
fs::remove_file(&output_path).unwrap();
|
|
}
|
|
|
|
let request = LinelossExportRequest {
|
|
sheet_name: "国网兰州供电公司月度线损分析报表(2026-03)".to_string(),
|
|
column_defs: vec![
|
|
("ORG_NAME".to_string(), "供电单位".to_string()),
|
|
("YGDL".to_string(), "累计供电量".to_string()),
|
|
("YYDL".to_string(), "累计售电量".to_string()),
|
|
("YXSL".to_string(), "线损完成率(%)".to_string()),
|
|
("RAT_SCOPE".to_string(), "线损率累计目标值".to_string()),
|
|
("BLANK3".to_string(), "目标完成率".to_string()),
|
|
("BLANK2".to_string(), "排行".to_string()),
|
|
],
|
|
rows: vec![
|
|
serde_json::from_value(json!({
|
|
"ORG_NAME": "城关供电",
|
|
"YGDL": "12345.67",
|
|
"YYDL": "11234.56",
|
|
"YXSL": "9.00",
|
|
"RAT_SCOPE": "9.50",
|
|
"BLANK3": "94.74",
|
|
"BLANK2": "1"
|
|
}))
|
|
.unwrap(),
|
|
serde_json::from_value(json!({
|
|
"ORG_NAME": "七里河供电",
|
|
"YGDL": "9876.54",
|
|
"YYDL": "8765.43",
|
|
"YXSL": "11.24",
|
|
"RAT_SCOPE": "10.00",
|
|
"BLANK3": "112.40",
|
|
"BLANK2": "2"
|
|
}))
|
|
.unwrap(),
|
|
],
|
|
output_path: output_path.clone(),
|
|
};
|
|
|
|
let result_path = export_lineloss_xlsx(&request).unwrap();
|
|
assert_eq!(result_path, output_path);
|
|
assert!(output_path.exists());
|
|
|
|
// Verify it's a valid ZIP (xlsx is a zip archive)
|
|
let file = fs::File::open(&output_path).unwrap();
|
|
let mut archive = zip::ZipArchive::new(file).unwrap();
|
|
|
|
// Must contain the standard OpenXML entries
|
|
let entry_names: Vec<String> = (0..archive.len())
|
|
.map(|i| archive.by_index(i).unwrap().name().to_string())
|
|
.collect();
|
|
|
|
assert!(entry_names.contains(&"[Content_Types].xml".to_string()));
|
|
assert!(entry_names.contains(&"xl/worksheets/sheet1.xml".to_string()));
|
|
assert!(entry_names.contains(&"xl/workbook.xml".to_string()));
|
|
|
|
// Read sheet1.xml and verify it contains our data
|
|
let mut sheet = archive.by_name("xl/worksheets/sheet1.xml").unwrap();
|
|
let mut xml = String::new();
|
|
std::io::Read::read_to_string(&mut sheet, &mut xml).unwrap();
|
|
|
|
assert!(xml.contains("供电单位"), "header row should contain 供电单位");
|
|
assert!(xml.contains("累计供电量"), "header row should contain 累计供电量");
|
|
assert!(xml.contains("城关供电"), "data should contain 城关供电");
|
|
assert!(xml.contains("12345.67"), "data should contain 12345.67");
|
|
assert!(xml.contains("七里河供电"), "data should contain second row");
|
|
|
|
// Cleanup
|
|
fs::remove_file(&output_path).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn export_empty_rows_returns_error() {
|
|
let output_path = temp_output_path("empty-test.xlsx");
|
|
|
|
let request = LinelossExportRequest {
|
|
sheet_name: "test".to_string(),
|
|
column_defs: vec![("A".to_string(), "ColA".to_string())],
|
|
rows: vec![],
|
|
output_path: output_path.clone(),
|
|
};
|
|
|
|
let result = export_lineloss_xlsx(&request);
|
|
assert!(result.is_err());
|
|
assert!(
|
|
result.unwrap_err().to_string().contains("rows must not be empty"),
|
|
"should reject empty rows"
|
|
);
|
|
}
|