Enhance two-way sync functionality and integration test coverage

- Improved the two-way sync pipeline by refining instance renaming logic, ensuring correct handling of directory-format scripts and adjacent meta file renaming.
- Enhanced property persistence for non-Source properties, ensuring proper creation of meta files during updates.
- Introduced comprehensive integration tests for critical two-way sync operations, including instance renames, ClassName transitions, and end-to-end workflows, addressing previously uncovered scenarios.
- Updated logging for better insights during sync operations and file handling, aiding in debugging and user awareness of changes.
This commit is contained in:
Joe 2026-02-05 19:19:34 -06:00
parent dc1a912982
commit 06d296ffba
4 changed files with 351 additions and 421 deletions

View file

@ -495,15 +495,64 @@ impl JobThreadContext {
InstigatingSource::Path(path) => {
if path.exists() {
let old_name = instance.name();
// Compute new path by replacing the old name
// in the filename/directory name
if let Some(parent) = path.parent() {
let old_file_name = path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("");
let file_name = path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("");
// For directory-format scripts, the instigating_source
// is the init file (e.g., src/MyModule/init.luau).
// The instance name corresponds to the parent directory,
// not the init file, so we must rename the directory.
let is_init_file = file_name.starts_with("init.");
if is_init_file {
let dir_path = path.parent().unwrap();
if let Some(grandparent) = dir_path.parent() {
let dir_name = dir_path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("");
let new_dir_name =
dir_name.replacen(old_name, new_name, 1);
let new_dir_path =
grandparent.join(&new_dir_name);
if new_dir_path != dir_path {
log::info!(
"Two-way sync: Renaming directory {} -> {}",
dir_path.display(),
new_dir_path.display()
);
if let Err(err) =
fs::rename(dir_path, &new_dir_path)
{
log::error!(
"Failed to rename directory {:?} to {:?}: {}",
dir_path,
new_dir_path,
err
);
} else {
let old_meta = grandparent.join(format!(
"{}.meta.json5",
old_name
));
if old_meta.exists() {
let new_meta =
grandparent.join(format!(
"{}.meta.json5",
new_name
));
let _ =
fs::rename(&old_meta, &new_meta);
}
}
}
}
} else if let Some(parent) = path.parent() {
let new_file_name =
old_file_name.replacen(old_name, new_name, 1);
file_name.replacen(old_name, new_name, 1);
let new_path = parent.join(&new_file_name);
if new_path != *path {
@ -520,7 +569,6 @@ impl JobThreadContext {
err
);
} else {
// Also rename adjacent meta file if it exists
let old_meta = parent
.join(format!("{}.meta.json5", old_name));
if old_meta.exists() {

View file

@ -1035,9 +1035,12 @@ impl ApiService {
let raw = fs::read(standalone_path).unwrap_or_default();
let init_meta_path = new_dir.join("init.meta.json5");
// Parse the model JSON to extract only meta-compatible fields
// Parse the model JSON5 to extract only meta-compatible fields.
// Use json5::from_str (not serde_json) because .model.json5 files
// can contain JSON5 features (comments, trailing commas, unquoted keys).
let raw_str = String::from_utf8_lossy(&raw);
let meta_content =
if let Ok(model) = serde_json::from_slice::<serde_json::Value>(&raw) {
if let Ok(model) = json5::from_str::<serde_json::Value>(&raw_str) {
// Warn about inline children that will be lost
if let Some(children) = model.get("children").or(model.get("Children")) {
if children.is_array()