rojo/tests/tests/two_way_sync.rs
Joe 9bed6dfaa2 Complete todos for stress tests and add syncback_stress fixture
- Marked all todos in the stress test plan as completed, including the implementation of timing helpers, a PRNG, and various fuzz tests for two-way sync operations.
- Introduced a new `syncback_stress` fixture with a project configuration and multiple Luau modules (Alpha, Bravo, Charlie, Delta, Echo) to support stress testing.
- Enhanced the `api.rs` file with improved path handling logic for better integration with the stress testing framework.
2026-02-05 22:36:19 -06:00

3258 lines
116 KiB
Rust

//! Integration tests for the two-way sync pipeline (POST /api/write).
//!
//! These tests validate the core operations that allow the plugin's two-way
//! sync to eventually replace `rojo syncback`:
//!
//! 1. Source property writes to disk
//! 2. Non-Source property persistence to meta files
//! 3. Instance rename → file rename
//! 4. ClassName transitions → file extension rename
//! 5. Instance removal with meta file cleanup
//! 6. End-to-end round-trip lifecycle (modify, rename, remove)
//! 7. ProjectNode guard (no project file corruption)
//! 8. Echo suppression (no redundant VFS patches)
//!
//! NOTE: Tests operate on pre-existing instances already in the Rojo tree
//! (from the initial snapshot). Adding new instances via the API writes files
//! to disk but relies on VFS events for tree updates; with echo suppression,
//! those events are suppressed for the paths the API wrote. This is correct
//! behavior — the plugin reconciles its own tree and doesn't need Rojo to
//! re-snapshot what it just sent. Tests that need to interact with the tree
//! (read back IDs, send updates) must use instances from the initial snapshot.
use std::collections::HashMap;
use std::path::Path;
use std::time::{Duration, Instant};
use std::{fs, thread};
use librojo::web_api::{AddedInstance, InstanceUpdate, WriteRequest};
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{ustr, UstrMap};
use crate::rojo_test::serve_util::run_serve_test;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Find an instance by class name in the read response.
fn find_by_class<'a>(
instances: &'a HashMap<Ref, librojo::web_api::Instance<'a>>,
class_name: &str,
) -> (Ref, &'a librojo::web_api::Instance<'a>) {
instances
.iter()
.find(|(_, inst)| inst.class_name == class_name)
.map(|(id, inst)| (*id, inst))
.unwrap_or_else(|| panic!("Instance with class '{}' not found", class_name))
}
/// Find an instance by name in the read response.
fn find_by_name<'a>(
instances: &'a HashMap<Ref, librojo::web_api::Instance<'a>>,
name: &str,
) -> (Ref, &'a librojo::web_api::Instance<'a>) {
instances
.iter()
.find(|(_, inst)| inst.name == name)
.map(|(id, inst)| (*id, inst))
.unwrap_or_else(|| {
let available: Vec<&str> = instances.values().map(|i| i.name.as_ref()).collect();
panic!(
"Instance with name '{}' not found. Available: {:?}",
name, available
)
})
}
/// Send an InstanceUpdate via the write API and wait for processing.
fn send_update(
session: &crate::rojo_test::serve_util::TestServeSession,
session_id: &librojo::SessionId,
update: InstanceUpdate,
) {
let write_request = WriteRequest {
session_id: *session_id,
removed: vec![],
added: HashMap::new(),
updated: vec![update],
};
session
.post_api_write(&write_request)
.expect("Write request should succeed");
thread::sleep(Duration::from_millis(300));
}
/// Send a removal via the write API and wait for processing.
fn send_removal(
session: &crate::rojo_test::serve_util::TestServeSession,
session_id: &librojo::SessionId,
ids: Vec<Ref>,
) {
let write_request = WriteRequest {
session_id: *session_id,
removed: ids,
added: HashMap::new(),
updated: vec![],
};
session
.post_api_write(&write_request)
.expect("Write request should succeed");
thread::sleep(Duration::from_millis(300));
}
fn assert_file_exists(path: &Path, msg: &str) {
assert!(
path.exists() && path.is_file(),
"{}: expected file at {}",
msg,
path.display()
);
}
fn assert_not_exists(path: &Path, msg: &str) {
assert!(
!path.exists(),
"{}: should NOT exist: {}",
msg,
path.display()
);
}
/// Get ReplicatedStorage ID and the ID of the "existing" ModuleScript.
fn get_rs_and_existing(
session: &crate::rojo_test::serve_util::TestServeSession,
) -> (librojo::SessionId, Ref, Ref) {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let (existing_id, _) = find_by_name(&rs_read.instances, "existing");
(info.session_id, rs_id, existing_id)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
/// Test 1: The most fundamental two-way sync operation — update Source and
/// verify the file content changes on disk.
#[test]
fn update_source_writes_to_disk() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let file_path = session.path().join("src").join("existing.luau");
assert_file_exists(&file_path, "existing.luau before update");
// Send Source update
let mut props = UstrMap::default();
props.insert(
ustr("Source"),
Some(Variant::String(
"-- Updated via two-way sync\nreturn { updated = true }".to_string(),
)),
);
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
let updated = fs::read_to_string(&file_path).unwrap();
assert!(
updated.contains("Updated via two-way sync"),
"File should contain new Source, got: {}",
updated
);
});
}
/// Test 2: Non-Source property updates create an adjacent meta file.
#[test]
fn update_non_source_property_creates_meta_file() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let file_path = session.path().join("src").join("existing.luau");
let meta_path = session.path().join("src").join("existing.meta.json5");
assert_not_exists(&meta_path, "Meta file before update");
let original_content = fs::read_to_string(&file_path).unwrap();
// Send a non-Source property update (Attributes)
let mut attrs = rbx_dom_weak::types::Attributes::new();
attrs.insert("TestAttribute".to_string(), Variant::Float64(42.0));
let mut props = UstrMap::default();
props.insert(ustr("Attributes"), Some(Variant::Attributes(attrs)));
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
// Meta file should now exist with the attribute
assert_file_exists(&meta_path, "Meta file after property update");
let meta_content = fs::read_to_string(&meta_path).unwrap();
assert!(
meta_content.contains("TestAttribute"),
"Meta should contain attribute, got: {}",
meta_content
);
// Script file should be unchanged
let current = fs::read_to_string(&file_path).unwrap();
assert_eq!(original_content, current, "Script file should not change");
});
}
/// Test 3: Non-Source property update on a directory-format instance writes
/// to init.meta.json5 inside the directory.
#[test]
fn update_properties_for_directory_instance_writes_init_meta() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
// DirModelWithChildren is a Configuration directory that IS in the tree
let (dir_model_id, _) = find_by_name(&rs_read.instances, "DirModelWithChildren");
let dir_path = session.path().join("src").join("DirModelWithChildren");
let init_meta = dir_path.join("init.meta.json5");
// Send a non-Source property update
let mut attrs = rbx_dom_weak::types::Attributes::new();
attrs.insert("DirTestAttr".to_string(), Variant::Float64(99.0));
let mut props = UstrMap::default();
props.insert(ustr("Attributes"), Some(Variant::Attributes(attrs)));
send_update(
&session,
&info.session_id,
InstanceUpdate {
id: dir_model_id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
assert_file_exists(&init_meta, "init.meta.json5 after property update");
let meta_content = fs::read_to_string(&init_meta).unwrap();
assert!(
meta_content.contains("DirTestAttr"),
"init.meta.json5 should contain attribute, got: {}",
meta_content
);
});
}
/// Test 4: Renaming an instance renames the file on disk.
#[test]
fn rename_standalone_script() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let old_path = session.path().join("src").join("existing.luau");
let new_path = session.path().join("src").join("renamed_module.luau");
assert_file_exists(&old_path, "existing.luau before rename");
let original_content = fs::read_to_string(&old_path).unwrap();
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: Some("renamed_module".to_string()),
changed_class_name: None,
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
assert_not_exists(&old_path, "Old file after rename");
assert_file_exists(&new_path, "New file after rename");
let new_content = fs::read_to_string(&new_path).unwrap();
assert_eq!(
original_content, new_content,
"Content preserved after rename"
);
});
}
/// Test 5: Renaming an instance also renames its adjacent meta file.
#[test]
fn rename_preserves_adjacent_meta_file() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
// First, create a meta file by sending a non-Source property update
let mut attrs = rbx_dom_weak::types::Attributes::new();
attrs.insert("SomeAttr".to_string(), Variant::Bool(true));
let mut props = UstrMap::default();
props.insert(ustr("Attributes"), Some(Variant::Attributes(attrs)));
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
let old_meta = session.path().join("src").join("existing.meta.json5");
assert_file_exists(&old_meta, "Meta file before rename");
// Re-read tree to get current ID (may have changed after re-snapshot)
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
// Now rename — the meta file name won't match "existing" anymore
// because the instance is now at "renamed_meta"
// Note: we need to find the instance by its CURRENT name
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: Some("renamed_meta".to_string()),
changed_class_name: None,
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
let new_script = session.path().join("src").join("renamed_meta.luau");
let new_meta = session.path().join("src").join("renamed_meta.meta.json5");
assert_not_exists(
&session.path().join("src").join("existing.luau"),
"Old script gone",
);
assert_not_exists(&old_meta, "Old meta gone");
assert_file_exists(&new_script, "Renamed script exists");
assert_file_exists(&new_meta, "Renamed meta exists");
});
}
/// Test 6: Changing ClassName from ModuleScript to Script renames the file extension.
#[test]
fn classname_change_module_to_script() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let module_path = session.path().join("src").join("existing.luau");
let script_path = session.path().join("src").join("existing.server.luau");
assert_file_exists(&module_path, "Module file before class change");
assert_not_exists(&script_path, "Script file before class change");
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: None,
changed_class_name: Some(ustr("Script")),
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
assert_not_exists(&module_path, "Module file after class change");
assert_file_exists(&script_path, "Script file after class change");
});
}
/// Test 7: Changing ClassName from ModuleScript to LocalScript.
#[test]
fn classname_change_module_to_localscript() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let module_path = session.path().join("src").join("existing.luau");
let local_path = session.path().join("src").join("existing.local.luau");
assert_file_exists(&module_path, "Module file before class change");
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: None,
changed_class_name: Some(ustr("LocalScript")),
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
assert_not_exists(&module_path, "Module file after class change");
assert_file_exists(&local_path, "LocalScript file after class change");
});
}
/// Test 8: Removing an instance deletes both its file and adjacent meta file.
#[test]
fn remove_instance_deletes_file_and_meta() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let file_path = session.path().join("src").join("existing.luau");
assert_file_exists(&file_path, "File before removal");
// Create meta file via property update
let mut attrs = rbx_dom_weak::types::Attributes::new();
attrs.insert("Temp".to_string(), Variant::Bool(true));
let mut props = UstrMap::default();
props.insert(ustr("Attributes"), Some(Variant::Attributes(attrs)));
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
let meta_path = session.path().join("src").join("existing.meta.json5");
assert_file_exists(&meta_path, "Meta file before removal");
// Re-read ID in case tree was rebuilt
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
send_removal(&session, &session_id, vec![existing_id]);
assert_not_exists(&file_path, "File after removal");
assert_not_exists(&meta_path, "Meta file after removal");
});
}
/// Test 9: Removing a directory-format instance deletes the entire directory.
#[test]
fn remove_directory_instance() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
// DirModelWithChildren is a Configuration directory in the tree
let (dir_model_id, _) = find_by_name(&rs_read.instances, "DirModelWithChildren");
let dir_path = session.path().join("src").join("DirModelWithChildren");
assert!(dir_path.is_dir(), "Directory should exist before removal");
send_removal(&session, &info.session_id, vec![dir_model_id]);
assert_not_exists(&dir_path, "Directory after removal");
});
}
/// Test 10: End-to-end round-trip — modify Source, rename, then remove.
/// Validates the complete lifecycle on a single instance.
#[test]
fn round_trip_modify_rename_remove() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let src = session.path().join("src");
// Step 1: MODIFY Source
let mut props = UstrMap::default();
props.insert(
ustr("Source"),
Some(Variant::String("-- Round trip v1".to_string())),
);
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
let file = src.join("existing.luau");
let content = fs::read_to_string(&file).unwrap();
assert!(content.contains("Round trip v1"), "Step 1: Source updated");
// Re-read to get current ID
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
// Step 2: RENAME
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: Some("GameService".to_string()),
changed_class_name: None,
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
assert_not_exists(&file, "Step 2: old file gone");
let new_file = src.join("GameService.luau");
assert_file_exists(&new_file, "Step 2: renamed file exists");
let content = fs::read_to_string(&new_file).unwrap();
assert!(
content.contains("Round trip v1"),
"Step 2: content preserved after rename"
);
// Re-read to get current ID after rename
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let (game_service_id, _) = find_by_name(&rs_read.instances, "GameService");
// Step 3: REMOVE
send_removal(&session, &info.session_id, vec![game_service_id]);
assert_not_exists(&new_file, "Step 3: file removed");
});
}
/// Test 11: Property updates on project-defined instances (Services) should
/// NOT corrupt the project file.
#[test]
fn property_update_skips_project_node_instances() {
run_serve_test("syncback_write", |session, _redactions| {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
// Save original project file content
let project_path = session.path().join("default.project.json5");
let original_project = fs::read_to_string(&project_path).unwrap();
// Send a non-Source property update to the service
let mut attrs = rbx_dom_weak::types::Attributes::new();
attrs.insert("ServiceAttr".to_string(), Variant::Float64(1.0));
let mut props = UstrMap::default();
props.insert(ustr("Attributes"), Some(Variant::Attributes(attrs)));
send_update(
&session,
&info.session_id,
InstanceUpdate {
id: rs_id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
// Project file should be UNCHANGED
let current_project = fs::read_to_string(&project_path).unwrap();
assert_eq!(
original_project, current_project,
"Project file must not be modified by property updates to project-defined instances"
);
});
}
/// Test 12: Echo suppression — adding an instance should not cause the server
/// to become unresponsive from echo loops.
#[test]
fn echo_suppression_prevents_redundant_patches() {
run_serve_test("syncback_write", |session, _redactions| {
let info = session.get_api_rojo().unwrap();
let root_id = info.root_instance_id;
let root_read = session.get_api_read(root_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let initial_cursor = root_read.message_cursor;
// Add a new instance (creates file on disk → VFS event → should be suppressed)
let mut properties = HashMap::new();
properties.insert(
"Source".to_string(),
Variant::String("return 'echo test'".to_string()),
);
let added = AddedInstance {
parent: Some(rs_id),
name: "EchoTest".to_string(),
class_name: "ModuleScript".to_string(),
properties,
children: vec![],
};
let mut added_map = HashMap::new();
added_map.insert(Ref::new(), added);
let write_request = WriteRequest {
session_id: info.session_id,
removed: vec![],
added: added_map,
updated: vec![],
};
session.post_api_write(&write_request).unwrap();
// Wait for any echo events to settle
thread::sleep(Duration::from_millis(500));
// File should exist on disk
let file_path = session.path().join("src").join("EchoTest.luau");
assert_file_exists(&file_path, "EchoTest.luau created");
// Server should still be responsive
let read_after = session.get_api_read(root_id).unwrap();
let cursor_delta = read_after.message_cursor - initial_cursor;
// With proper echo suppression, the cursor should not advance excessively
assert!(
cursor_delta < 10,
"Message cursor should not advance excessively (delta={}), \
indicating echo suppression is working",
cursor_delta
);
});
}
// ---------------------------------------------------------------------------
// Helpers (syncback_format_transitions fixture)
// ---------------------------------------------------------------------------
/// Look up an instance by name under ReplicatedStorage in the format_transitions fixture.
fn get_format_transitions_instance(
session: &crate::rojo_test::serve_util::TestServeSession,
instance_name: &str,
) -> (librojo::SessionId, Ref) {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let (instance_id, _) = find_by_name(&rs_read.instances, instance_name);
(info.session_id, instance_id)
}
// ---------------------------------------------------------------------------
// Tests: Directory-format operations
// ---------------------------------------------------------------------------
/// Test 13: Renaming a directory-format script renames the parent directory,
/// not the init file inside it. Children should move with it.
#[test]
fn rename_directory_format_script() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let (session_id, dir_module_id) =
get_format_transitions_instance(&session, "DirModuleWithChildren");
let old_dir = session.path().join("src").join("DirModuleWithChildren");
let new_dir = session.path().join("src").join("RenamedDirModule");
assert!(old_dir.is_dir(), "Old directory should exist before rename");
send_update(
&session,
&session_id,
InstanceUpdate {
id: dir_module_id,
changed_name: Some("RenamedDirModule".to_string()),
changed_class_name: None,
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
assert!(
!old_dir.exists(),
"Old directory should be gone after rename: {}",
old_dir.display()
);
assert!(
new_dir.is_dir(),
"New directory should exist after rename: {}",
new_dir.display()
);
// init.luau should be inside the renamed directory
let init_file = new_dir.join("init.luau");
assert_file_exists(&init_file, "init.luau inside renamed directory");
// Children should also be present in the renamed directory
let child_a = new_dir.join("ChildA.luau");
assert_file_exists(&child_a, "ChildA.luau inside renamed directory");
});
}
/// Test 14: Changing ClassName on a directory-format ModuleScript to Script
/// renames the init file inside the directory (init.luau → init.server.luau).
#[test]
fn classname_change_directory_module_to_script() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let (session_id, dir_module_id) =
get_format_transitions_instance(&session, "DirModuleWithChildren");
let dir = session.path().join("src").join("DirModuleWithChildren");
let old_init = dir.join("init.luau");
let new_init = dir.join("init.server.luau");
assert_file_exists(&old_init, "init.luau before class change");
assert_not_exists(&new_init, "init.server.luau before class change");
send_update(
&session,
&session_id,
InstanceUpdate {
id: dir_module_id,
changed_name: None,
changed_class_name: Some(ustr("Script")),
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
assert_not_exists(&old_init, "init.luau after class change to Script");
assert_file_exists(&new_init, "init.server.luau after class change to Script");
// Children should be unaffected
let child_a = dir.join("ChildA.luau");
assert_file_exists(&child_a, "ChildA.luau unaffected by class change");
});
}
// ---------------------------------------------------------------------------
// Tests: Reverse ClassName transitions (code paths added in these commits)
// ---------------------------------------------------------------------------
/// Test 15: Script → ModuleScript ClassName transition (reverse of test 6).
/// Validates that the .server suffix is correctly stripped.
#[test]
fn classname_change_script_to_module() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let (session_id, script_id) = get_format_transitions_instance(&session, "StandaloneScript");
let old_path = session
.path()
.join("src")
.join("StandaloneScript.server.luau");
let new_path = session.path().join("src").join("StandaloneScript.luau");
assert_file_exists(&old_path, "Script file before class change");
send_update(
&session,
&session_id,
InstanceUpdate {
id: script_id,
changed_name: None,
changed_class_name: Some(ustr("ModuleScript")),
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
assert_not_exists(&old_path, "Script file after class change to ModuleScript");
assert_file_exists(&new_path, "ModuleScript file after class change");
});
}
/// Test 16: Script → LocalScript ClassName transition.
/// Validates that .server is replaced with .local.
#[test]
fn classname_change_script_to_localscript() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let (session_id, script_id) = get_format_transitions_instance(&session, "StandaloneScript");
let old_path = session
.path()
.join("src")
.join("StandaloneScript.server.luau");
let new_path = session
.path()
.join("src")
.join("StandaloneScript.local.luau");
assert_file_exists(&old_path, "Script file before class change");
send_update(
&session,
&session_id,
InstanceUpdate {
id: script_id,
changed_name: None,
changed_class_name: Some(ustr("LocalScript")),
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
assert_not_exists(&old_path, "Script file after class change to LocalScript");
assert_file_exists(&new_path, "LocalScript file after class change");
});
}
/// Test 17: LocalScript → Script ClassName transition.
/// Exercises .client suffix handling (the fixture uses .client.luau for LocalScript).
#[test]
fn classname_change_localscript_to_script() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let (session_id, local_id) =
get_format_transitions_instance(&session, "StandaloneLocalScript");
let old_path = session
.path()
.join("src")
.join("StandaloneLocalScript.client.luau");
let new_path = session
.path()
.join("src")
.join("StandaloneLocalScript.server.luau");
assert_file_exists(&old_path, "LocalScript file before class change");
send_update(
&session,
&session_id,
InstanceUpdate {
id: local_id,
changed_name: None,
changed_class_name: Some(ustr("Script")),
changed_properties: UstrMap::default(),
changed_metadata: None,
},
);
assert_not_exists(&old_path, "LocalScript file after class change to Script");
assert_file_exists(&new_path, "Script file after class change");
});
}
// ---------------------------------------------------------------------------
// Tests: Combined operations (overridden_source_path logic)
// ---------------------------------------------------------------------------
/// Test 18: Combined rename + Source update in a single request.
/// Validates that Source is written to the RENAMED file, not the old location.
/// This exercises the `overridden_source_path` tracking in ChangeProcessor.
#[test]
fn combined_rename_and_source_update() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let old_path = session.path().join("src").join("existing.luau");
let new_path = session.path().join("src").join("CombinedRename.luau");
assert_file_exists(&old_path, "existing.luau before combined update");
let mut props = UstrMap::default();
props.insert(
ustr("Source"),
Some(Variant::String("-- Written after rename".to_string())),
);
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: Some("CombinedRename".to_string()),
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
assert_not_exists(&old_path, "Old file after combined rename+source");
assert_file_exists(&new_path, "Renamed file after combined rename+source");
let content = fs::read_to_string(&new_path).unwrap();
assert!(
content.contains("Written after rename"),
"Source should be written to the NEW file location, got: {}",
content
);
});
}
/// Test 19: Combined ClassName change + Source update in a single request.
/// Validates that Source is written to the file with the NEW extension.
#[test]
fn combined_classname_and_source_update() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let old_path = session.path().join("src").join("existing.luau");
let new_path = session.path().join("src").join("existing.server.luau");
assert_file_exists(&old_path, "existing.luau before combined update");
let mut props = UstrMap::default();
props.insert(
ustr("Source"),
Some(Variant::String("-- Source after class change".to_string())),
);
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: None,
changed_class_name: Some(ustr("Script")),
changed_properties: props,
changed_metadata: None,
},
);
assert_not_exists(&old_path, "ModuleScript file after ClassName+Source");
assert_file_exists(&new_path, "Script file after ClassName+Source");
let content = fs::read_to_string(&new_path).unwrap();
assert!(
content.contains("Source after class change"),
"Source should be in the new file, got: {}",
content
);
});
}
/// Test 19b: Combined rename + ClassName change in a single update.
/// The rename handler moves the file first (existing.luau → Renamed.luau),
/// then the ClassName handler must use the renamed path to apply the extension
/// change (Renamed.luau → Renamed.server.luau). Without the fix, the ClassName
/// handler checks the OLD path which no longer exists, silently skipping the
/// extension rename.
#[test]
fn combined_rename_and_classname_change() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let old_path = session.path().join("src").join("existing.luau");
// After rename + ClassName change: should end up as Renamed.server.luau
let final_path = session.path().join("src").join("Renamed.server.luau");
// Should NOT exist: renamed but without extension change
let renamed_only = session.path().join("src").join("Renamed.luau");
assert_file_exists(&old_path, "existing.luau before combined update");
let mut props = UstrMap::default();
props.insert(
ustr("Source"),
Some(Variant::String(
"-- After rename + class change".to_string(),
)),
);
send_update(
&session,
&session_id,
InstanceUpdate {
id: existing_id,
changed_name: Some("Renamed".to_string()),
changed_class_name: Some(ustr("Script")),
changed_properties: props,
changed_metadata: None,
},
);
assert_not_exists(&old_path, "Original file after combined rename+classname");
assert_not_exists(
&renamed_only,
"Renamed.luau should not exist — ClassName handler should have \
applied the .server extension",
);
assert_file_exists(
&final_path,
"Renamed.server.luau should exist after combined rename+classname",
);
let content = fs::read_to_string(&final_path).unwrap();
assert!(
content.contains("After rename + class change"),
"Source should be in the final file, got: {}",
content
);
});
}
// ---------------------------------------------------------------------------
// Tests: Standalone → directory conversion & suffix-aware removal
// ---------------------------------------------------------------------------
/// Test 20: Adding a child instance to a standalone script converts it to
/// directory format (e.g., StandaloneModule.luau → StandaloneModule/init.luau).
/// This exercises `convert_standalone_script_to_directory` in ApiService.
#[test]
fn add_child_converts_standalone_to_directory() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let (standalone_id, _) = find_by_name(&rs_read.instances, "StandaloneModule");
let standalone_file = session.path().join("src").join("StandaloneModule.luau");
let dir_path = session.path().join("src").join("StandaloneModule");
assert_file_exists(&standalone_file, "Standalone file before child add");
assert!(
!dir_path.exists(),
"Directory should NOT exist before child add"
);
// Add a child ModuleScript to StandaloneModule
let mut properties = HashMap::new();
properties.insert(
"Source".to_string(),
Variant::String("-- New child module".to_string()),
);
let added = AddedInstance {
parent: Some(standalone_id),
name: "NewChild".to_string(),
class_name: "ModuleScript".to_string(),
properties,
children: vec![],
};
let mut added_map = HashMap::new();
added_map.insert(Ref::new(), added);
let write_request = WriteRequest {
session_id: info.session_id,
removed: vec![],
added: added_map,
updated: vec![],
};
session.post_api_write(&write_request).unwrap();
thread::sleep(Duration::from_millis(500));
// Standalone file should be gone, replaced by directory
assert_not_exists(
&standalone_file,
"Standalone file after child add (converted to directory)",
);
assert!(
dir_path.is_dir(),
"Directory should exist after conversion: {}",
dir_path.display()
);
// init.luau should contain the original source
let init_file = dir_path.join("init.luau");
assert_file_exists(
&init_file,
"init.luau after standalone→directory conversion",
);
let init_content = fs::read_to_string(&init_file).unwrap();
assert!(
init_content.contains("Standalone ModuleScript"),
"init.luau should contain original source, got: {}",
init_content
);
// Child should be created inside the directory
let child_file = dir_path.join("NewChild.luau");
assert_file_exists(&child_file, "NewChild.luau inside converted directory");
});
}
/// Test 21: Removing a Script (with .server suffix) properly cleans up
/// the adjacent meta file by stripping the script suffix to find the
/// correct meta file name (e.g., StandaloneScript.meta.json5).
#[test]
fn remove_script_with_suffix_cleans_meta() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let (session_id, script_id) = get_format_transitions_instance(&session, "StandaloneScript");
let script_path = session
.path()
.join("src")
.join("StandaloneScript.server.luau");
let meta_path = session
.path()
.join("src")
.join("StandaloneScript.meta.json5");
assert_file_exists(&script_path, "Script file before operations");
// First, create a meta file by sending a non-Source property update
let mut attrs = rbx_dom_weak::types::Attributes::new();
attrs.insert("ScriptAttr".to_string(), Variant::Float64(7.0));
let mut props = UstrMap::default();
props.insert(ustr("Attributes"), Some(Variant::Attributes(attrs)));
send_update(
&session,
&session_id,
InstanceUpdate {
id: script_id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
assert_file_exists(&meta_path, "Meta file after property update");
// Re-read ID in case tree was rebuilt after the update
let (session_id, script_id) = get_format_transitions_instance(&session, "StandaloneScript");
// Now remove the instance — both script and meta should be deleted
send_removal(&session, &session_id, vec![script_id]);
assert_not_exists(&script_path, "Script file after removal");
assert_not_exists(
&meta_path,
"Meta file after removal (suffix-stripped cleanup)",
);
});
}
// ---------------------------------------------------------------------------
// Helpers (syncback_encoded_names fixture)
// ---------------------------------------------------------------------------
/// Look up an instance by name under ReplicatedStorage in the encoded_names fixture.
/// Instance names are DECODED (e.g., `Encoded?Module`), not the filesystem names.
fn get_encoded_names_instance(
session: &crate::rojo_test::serve_util::TestServeSession,
instance_name: &str,
) -> (librojo::SessionId, Ref) {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let (instance_id, _) = find_by_name(&rs_read.instances, instance_name);
(info.session_id, instance_id)
}
// ---------------------------------------------------------------------------
// Tests: Encoded name directory conversion (path encoding fix)
// ---------------------------------------------------------------------------
/// Test 22: Adding a child to a standalone ModuleScript with Rojo-encoded
/// characters (e.g., `What%QUESTION%Module.luau`) creates the directory using
/// the filesystem-encoded name (`What%QUESTION%Module/`), NOT the decoded
/// instance name (`What?Module/`).
///
/// This is the core regression test for the path encoding fix in
/// `convert_standalone_script_to_directory`.
#[test]
fn add_child_to_encoded_module_creates_encoded_directory() {
run_serve_test("syncback_encoded_names", |session, _redactions| {
let (session_id, encoded_module_id) = get_encoded_names_instance(&session, "What?Module");
let standalone_file = session.path().join("src").join("What%QUESTION%Module.luau");
// The CORRECT directory name uses the encoded filesystem name
let encoded_dir = session.path().join("src").join("What%QUESTION%Module");
// The WRONG directory name would use the decoded instance name
let decoded_dir = session.path().join("src").join("What?Module");
assert_file_exists(&standalone_file, "Encoded standalone file before child add");
// Add a child to trigger standalone → directory conversion
let mut properties = HashMap::new();
properties.insert(
"Source".to_string(),
Variant::String("-- Child of encoded module".to_string()),
);
let added = AddedInstance {
parent: Some(encoded_module_id),
name: "EncodedChild".to_string(),
class_name: "ModuleScript".to_string(),
properties,
children: vec![],
};
let mut added_map = HashMap::new();
added_map.insert(Ref::new(), added);
let write_request = WriteRequest {
session_id,
removed: vec![],
added: added_map,
updated: vec![],
};
session.post_api_write(&write_request).unwrap();
thread::sleep(Duration::from_millis(500));
// Standalone file should be removed
assert_not_exists(
&standalone_file,
"Standalone file after directory conversion",
);
// Directory should use ENCODED name (the fix)
assert!(
encoded_dir.is_dir(),
"Directory should use encoded name (What%QUESTION%Module/): {}",
encoded_dir.display()
);
// Decoded name directory should NOT exist (the bug we fixed)
assert!(
!decoded_dir.exists(),
"Directory with decoded name (What?Module/) must NOT be created: {}",
decoded_dir.display()
);
// init.luau should exist inside the encoded directory
let init_file = encoded_dir.join("init.luau");
assert_file_exists(&init_file, "init.luau inside encoded directory");
// Child should be created inside the encoded directory
let child_file = encoded_dir.join("EncodedChild.luau");
assert_file_exists(&child_file, "Child inside encoded directory");
});
}
/// Test 23: Adding a child to a standalone Script with Rojo-encoded characters
/// AND a script suffix (e.g., `Key%COLON%Script.server.luau`) strips the suffix
/// correctly when creating the directory name.
///
/// The directory should be `Key%COLON%Script/`, not `Key%COLON%Script.server/`
/// or `Key:Script/`.
#[test]
fn add_child_to_encoded_script_strips_suffix_correctly() {
run_serve_test("syncback_encoded_names", |session, _redactions| {
let (session_id, encoded_script_id) = get_encoded_names_instance(&session, "Key:Script");
let standalone_file = session
.path()
.join("src")
.join("Key%COLON%Script.server.luau");
let encoded_dir = session.path().join("src").join("Key%COLON%Script");
// Wrong: decoded name
let decoded_dir = session.path().join("src").join("Key:Script");
// Wrong: suffix not stripped
let unsuffixed_dir = session.path().join("src").join("Key%COLON%Script.server");
assert_file_exists(&standalone_file, "Encoded script before child add");
let mut properties = HashMap::new();
properties.insert(
"Source".to_string(),
Variant::String("-- Child of encoded script".to_string()),
);
let added = AddedInstance {
parent: Some(encoded_script_id),
name: "ScriptChild".to_string(),
class_name: "ModuleScript".to_string(),
properties,
children: vec![],
};
let mut added_map = HashMap::new();
added_map.insert(Ref::new(), added);
let write_request = WriteRequest {
session_id,
removed: vec![],
added: added_map,
updated: vec![],
};
session.post_api_write(&write_request).unwrap();
thread::sleep(Duration::from_millis(500));
assert_not_exists(&standalone_file, "Standalone script after conversion");
assert!(
encoded_dir.is_dir(),
"Directory should be Key%COLON%Script/ (suffix stripped, encoded): {}",
encoded_dir.display()
);
assert!(
!decoded_dir.exists(),
"Directory with decoded name must NOT be created: {}",
decoded_dir.display()
);
assert!(
!unsuffixed_dir.exists(),
"Directory with unsuffixed name must NOT be created: {}",
unsuffixed_dir.display()
);
// Script → init.server.luau (preserves class)
let init_file = encoded_dir.join("init.server.luau");
assert_file_exists(&init_file, "init.server.luau inside encoded directory");
let child_file = encoded_dir.join("ScriptChild.luau");
assert_file_exists(&child_file, "Child inside encoded script directory");
});
}
/// Test 24: Adding a child to a non-script standalone instance with Rojo-encoded
/// characters (e.g., `What%QUESTION%Model.model.json5`) creates the directory
/// using the encoded name with the compound extension stripped.
///
/// This exercises `convert_standalone_instance_to_directory`.
#[test]
fn add_child_to_encoded_model_creates_encoded_directory() {
run_serve_test("syncback_encoded_names", |session, _redactions| {
let (session_id, encoded_model_id) = get_encoded_names_instance(&session, "What?Model");
let standalone_file = session
.path()
.join("src")
.join("What%QUESTION%Model.model.json5");
let encoded_dir = session.path().join("src").join("What%QUESTION%Model");
let decoded_dir = session.path().join("src").join("What?Model");
assert_file_exists(&standalone_file, "Encoded model file before child add");
let mut properties = HashMap::new();
properties.insert(
"Source".to_string(),
Variant::String("-- Child of encoded model".to_string()),
);
let added = AddedInstance {
parent: Some(encoded_model_id),
name: "ModelChild".to_string(),
class_name: "ModuleScript".to_string(),
properties,
children: vec![],
};
let mut added_map = HashMap::new();
added_map.insert(Ref::new(), added);
let write_request = WriteRequest {
session_id,
removed: vec![],
added: added_map,
updated: vec![],
};
session.post_api_write(&write_request).unwrap();
thread::sleep(Duration::from_millis(500));
assert_not_exists(&standalone_file, "Standalone model after conversion");
assert!(
encoded_dir.is_dir(),
"Directory should use encoded name (What%QUESTION%Model/): {}",
encoded_dir.display()
);
assert!(
!decoded_dir.exists(),
"Directory with decoded name must NOT be created: {}",
decoded_dir.display()
);
// init.meta.json5 should exist inside the directory (converted from .model.json5)
let init_meta = encoded_dir.join("init.meta.json5");
assert_file_exists(&init_meta, "init.meta.json5 inside encoded model directory");
let child_file = encoded_dir.join("ModelChild.luau");
assert_file_exists(&child_file, "Child inside encoded model directory");
});
}
/// Test 25: Non-Source property update on a standalone script with Rojo-encoded
/// name creates the adjacent meta file using the encoded filesystem name,
/// not the decoded instance name.
///
/// e.g., `What%QUESTION%Module.meta.json5` NOT `What?Module.meta.json5`
#[test]
fn property_update_on_encoded_script_uses_encoded_meta_path() {
run_serve_test("syncback_encoded_names", |session, _redactions| {
let (session_id, encoded_module_id) = get_encoded_names_instance(&session, "What?Module");
let script_file = session.path().join("src").join("What%QUESTION%Module.luau");
let encoded_meta = session
.path()
.join("src")
.join("What%QUESTION%Module.meta.json5");
let decoded_meta = session.path().join("src").join("What?Module.meta.json5");
assert_file_exists(&script_file, "Encoded script file before property update");
assert_not_exists(&encoded_meta, "Meta file before property update");
let mut attrs = rbx_dom_weak::types::Attributes::new();
attrs.insert("EncodedAttr".to_string(), Variant::Float64(99.0));
let mut props = UstrMap::default();
props.insert(ustr("Attributes"), Some(Variant::Attributes(attrs)));
send_update(
&session,
&session_id,
InstanceUpdate {
id: encoded_module_id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
},
);
// Meta file should use encoded name
assert_file_exists(
&encoded_meta,
"Meta file should use encoded name (What%QUESTION%Module.meta.json5)",
);
assert!(
!decoded_meta.exists(),
"Meta file with decoded name must NOT be created: {}",
decoded_meta.display()
);
// Script file should be unchanged
let content = fs::read_to_string(&script_file).unwrap();
assert!(
content.contains("Module with encoded"),
"Script content should be unchanged, got: {}",
content
);
});
}
// ===========================================================================
// PART 1: API-Driven Stress Tests (Tests 26-41)
// ===========================================================================
// ---------------------------------------------------------------------------
// Stress helpers
// ---------------------------------------------------------------------------
/// Send an InstanceUpdate with a short 50ms wait (races the 200ms recovery).
fn send_update_fast(
session: &crate::rojo_test::serve_util::TestServeSession,
session_id: &librojo::SessionId,
update: InstanceUpdate,
) {
let write_request = WriteRequest {
session_id: *session_id,
removed: vec![],
added: HashMap::new(),
updated: vec![update],
};
session
.post_api_write(&write_request)
.expect("Write request should succeed");
thread::sleep(Duration::from_millis(50));
}
/// Send an InstanceUpdate with zero wait (fire and forget).
fn send_update_no_wait(
session: &crate::rojo_test::serve_util::TestServeSession,
session_id: &librojo::SessionId,
update: InstanceUpdate,
) {
let write_request = WriteRequest {
session_id: *session_id,
removed: vec![],
added: HashMap::new(),
updated: vec![update],
};
session
.post_api_write(&write_request)
.expect("Write request should succeed");
}
/// Send a removal with a short 50ms wait.
fn send_removal_fast(
session: &crate::rojo_test::serve_util::TestServeSession,
session_id: &librojo::SessionId,
ids: Vec<Ref>,
) {
let write_request = WriteRequest {
session_id: *session_id,
removed: ids,
added: HashMap::new(),
updated: vec![],
};
session
.post_api_write(&write_request)
.expect("Write request should succeed");
thread::sleep(Duration::from_millis(50));
}
/// Wait long enough for all VFS events, recovery sweeps, and debouncing.
fn wait_for_settle() {
thread::sleep(Duration::from_millis(800));
}
/// Get session_id and Refs for Alpha-Echo in the syncback_stress fixture.
fn get_stress_instances(
session: &crate::rojo_test::serve_util::TestServeSession,
) -> (librojo::SessionId, Ref, Vec<(Ref, String)>) {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let names = ["Alpha", "Bravo", "Charlie", "Delta", "Echo"];
let mut instances = Vec::new();
for name in &names {
let (id, _) = find_by_name(&rs_read.instances, name);
instances.push((id, name.to_string()));
}
(info.session_id, rs_id, instances)
}
/// Build a Source update for an instance.
fn make_source_update(id: Ref, source: &str) -> InstanceUpdate {
let mut props = UstrMap::default();
props.insert(ustr("Source"), Some(Variant::String(source.to_string())));
InstanceUpdate {
id,
changed_name: None,
changed_class_name: None,
changed_properties: props,
changed_metadata: None,
}
}
/// Build a rename update.
fn make_rename_update(id: Ref, new_name: &str) -> InstanceUpdate {
InstanceUpdate {
id,
changed_name: Some(new_name.to_string()),
changed_class_name: None,
changed_properties: UstrMap::default(),
changed_metadata: None,
}
}
/// Build a ClassName update.
fn make_class_update(id: Ref, class: &str) -> InstanceUpdate {
InstanceUpdate {
id,
changed_name: None,
changed_class_name: Some(ustr(class)),
changed_properties: UstrMap::default(),
changed_metadata: None,
}
}
/// Build a combined rename + class + source update.
fn make_combined_update(
id: Ref,
name: Option<&str>,
class: Option<&str>,
source: Option<&str>,
) -> InstanceUpdate {
let mut props = UstrMap::default();
if let Some(src) = source {
props.insert(ustr("Source"), Some(Variant::String(src.to_string())));
}
InstanceUpdate {
id,
changed_name: name.map(|n| n.to_string()),
changed_class_name: class.map(ustr),
changed_properties: props,
changed_metadata: None,
}
}
/// Get the expected file extension suffix for a class name.
fn class_suffix(class: &str) -> &'static str {
match class {
"Script" => ".server",
"LocalScript" => ".local",
_ => "",
}
}
/// Verify that exactly one file exists for the given name+class, and no stale
/// files remain with other extensions.
fn verify_instance_file(src_dir: &Path, name: &str, class: &str, expected_source: Option<&str>) {
let suffix = class_suffix(class);
let expected_file = src_dir.join(format!("{}{}.luau", name, suffix));
assert!(
expected_file.is_file(),
"Expected file: {}",
expected_file.display()
);
if let Some(source) = expected_source {
let content = fs::read_to_string(&expected_file).unwrap();
assert!(
content.contains(source),
"Expected source '{}' in file {}, got: {}",
source,
expected_file.display(),
content
);
}
// Check no stale files with wrong extension
for stale_suffix in &[".server", ".local", ".client", ".plugin", ""] {
if *stale_suffix == suffix {
continue;
}
let stale = src_dir.join(format!("{}{}.luau", name, stale_suffix));
assert!(
!stale.exists(),
"Stale file should not exist: {}",
stale.display()
);
}
}
// ---------------------------------------------------------------------------
// Tree polling helpers (for file-watcher tests)
// ---------------------------------------------------------------------------
/// Get ReplicatedStorage Ref.
fn get_rs_id(session: &crate::rojo_test::serve_util::TestServeSession) -> Ref {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
rs_id
}
/// Poll tree until an instance named `name` under `parent_id` has Source
/// containing `expected`. Panics after `timeout_ms`.
fn poll_tree_source(
session: &crate::rojo_test::serve_util::TestServeSession,
parent_id: Ref,
instance_name: &str,
expected_content: &str,
timeout_ms: u64,
) {
let start = Instant::now();
loop {
if let Ok(read) = session.get_api_read(parent_id) {
for inst in read.instances.values() {
if inst.name == instance_name {
if let Some(source) = inst.properties.get(&ustr("Source")) {
if let Variant::String(s) = source.as_ref() {
if s.contains(expected_content) {
return;
}
}
}
}
}
}
if start.elapsed() > Duration::from_millis(timeout_ms) {
panic!(
"Timed out after {}ms waiting for instance '{}' Source to contain '{}'",
timeout_ms, instance_name, expected_content
);
}
thread::sleep(Duration::from_millis(50));
}
}
/// Poll tree until an instance named `name` exists under `parent_id`.
fn poll_tree_has_instance(
session: &crate::rojo_test::serve_util::TestServeSession,
parent_id: Ref,
instance_name: &str,
timeout_ms: u64,
) -> Ref {
let start = Instant::now();
loop {
if let Ok(read) = session.get_api_read(parent_id) {
for (&id, inst) in &read.instances {
if inst.name == instance_name {
return id;
}
}
}
if start.elapsed() > Duration::from_millis(timeout_ms) {
panic!(
"Timed out after {}ms waiting for instance '{}' to appear",
timeout_ms, instance_name
);
}
thread::sleep(Duration::from_millis(50));
}
}
/// Poll tree until NO instance named `name` exists under `parent_id`.
fn poll_tree_no_instance(
session: &crate::rojo_test::serve_util::TestServeSession,
parent_id: Ref,
instance_name: &str,
timeout_ms: u64,
) {
let start = Instant::now();
loop {
if let Ok(read) = session.get_api_read(parent_id) {
let found = read
.instances
.values()
.any(|inst| inst.name == instance_name);
if !found {
return;
}
}
if start.elapsed() > Duration::from_millis(timeout_ms) {
panic!(
"Timed out after {}ms waiting for instance '{}' to disappear",
timeout_ms, instance_name
);
}
thread::sleep(Duration::from_millis(50));
}
}
/// Poll tree until instance `name` has the expected ClassName.
fn poll_tree_class(
session: &crate::rojo_test::serve_util::TestServeSession,
parent_id: Ref,
instance_name: &str,
expected_class: &str,
timeout_ms: u64,
) {
let start = Instant::now();
loop {
if let Ok(read) = session.get_api_read(parent_id) {
for inst in read.instances.values() {
if inst.name == instance_name && inst.class_name == expected_class {
return;
}
}
}
if start.elapsed() > Duration::from_millis(timeout_ms) {
panic!(
"Timed out after {}ms waiting for instance '{}' to have class '{}'",
timeout_ms, instance_name, expected_class
);
}
thread::sleep(Duration::from_millis(50));
}
}
/// Poll tree until instance `name` under `parent_id` has at least one child
/// with the given `child_name`. Returns the child's Ref.
fn poll_tree_has_child(
session: &crate::rojo_test::serve_util::TestServeSession,
parent_id: Ref,
instance_name: &str,
child_name: &str,
timeout_ms: u64,
) -> Ref {
let start = Instant::now();
loop {
if let Ok(read) = session.get_api_read(parent_id) {
for (&id, inst) in &read.instances {
if inst.name == instance_name {
// Now read this instance's children
if let Ok(child_read) = session.get_api_read(id) {
for (&cid, cinst) in &child_read.instances {
if cinst.name == child_name {
return cid;
}
}
}
}
}
}
if start.elapsed() > Duration::from_millis(timeout_ms) {
panic!(
"Timed out after {}ms waiting for '{}'.'{}'",
timeout_ms, instance_name, child_name
);
}
thread::sleep(Duration::from_millis(50));
}
}
// ---------------------------------------------------------------------------
// Tests 26-27: Rapid Source writes
// ---------------------------------------------------------------------------
#[test]
fn rapid_source_writes_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let file_path = session.path().join("src").join("existing.luau");
for i in 1..=10 {
send_update_fast(
&session,
&session_id,
make_source_update(existing_id, &format!("-- rapid v{}", i)),
);
}
wait_for_settle();
let content = fs::read_to_string(&file_path).unwrap();
assert!(
content.contains("-- rapid v10"),
"Final source should be v10, got: {}",
content
);
});
}
#[test]
fn rapid_source_writes_no_wait_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let file_path = session.path().join("src").join("existing.luau");
for i in 1..=10 {
send_update_no_wait(
&session,
&session_id,
make_source_update(existing_id, &format!("-- nowait v{}", i)),
);
}
wait_for_settle();
let content = fs::read_to_string(&file_path).unwrap();
assert!(
content.contains("-- nowait v10"),
"Final source should be v10, got: {}",
content
);
});
}
// ---------------------------------------------------------------------------
// Tests 28-29: Rapid rename chain
// ---------------------------------------------------------------------------
#[test]
fn rapid_rename_chain_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let original_content = fs::read_to_string(src.join("existing.luau")).unwrap();
let mut current_name = "existing".to_string();
for i in 1..=10 {
let new_name = format!("R{}", i);
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let (id, _) = find_by_name(&rs_read.instances, &current_name);
send_update_fast(
&session,
&info.session_id,
make_rename_update(id, &new_name),
);
current_name = new_name;
}
wait_for_settle();
// Only R10.luau should exist
assert_file_exists(&src.join("R10.luau"), "R10.luau after chain");
for i in 1..10 {
assert_not_exists(
&src.join(format!("R{}.luau", i)),
&format!("Intermediate R{}.luau", i),
);
}
assert_not_exists(&src.join("existing.luau"), "Original file");
let content = fs::read_to_string(src.join("R10.luau")).unwrap();
assert_eq!(
original_content, content,
"Content preserved through 10 renames"
);
});
}
#[test]
fn rapid_rename_chain_directory_10x() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let mut current_name = "DirModuleWithChildren".to_string();
for i in 1..=10 {
let new_name = format!("DirR{}", i);
let (session_id, id) = get_format_transitions_instance(&session, &current_name);
send_update_fast(&session, &session_id, make_rename_update(id, &new_name));
current_name = new_name;
}
wait_for_settle();
let final_dir = src.join("DirR10");
assert!(final_dir.is_dir(), "DirR10 should exist");
assert_file_exists(&final_dir.join("init.luau"), "init.luau in DirR10");
assert_file_exists(&final_dir.join("ChildA.luau"), "ChildA in DirR10");
assert_file_exists(&final_dir.join("ChildB.luau"), "ChildB in DirR10");
// All intermediates should be gone
assert_not_exists(&src.join("DirModuleWithChildren"), "Original directory");
for i in 1..10 {
assert!(
!src.join(format!("DirR{}", i)).exists(),
"Intermediate DirR{} should not exist",
i
);
}
});
}
// ---------------------------------------------------------------------------
// Tests 30-31: Rapid ClassName cycling
// ---------------------------------------------------------------------------
#[test]
fn rapid_classname_cycle_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let classes = [
"Script",
"LocalScript",
"ModuleScript",
"Script",
"LocalScript",
"ModuleScript",
"Script",
"LocalScript",
"ModuleScript",
"Script",
];
for class in &classes {
let (session_id, _rs_id, id) = get_rs_and_existing(&session);
send_update_fast(&session, &session_id, make_class_update(id, class));
thread::sleep(Duration::from_millis(200)); // Let tree rebuild
}
wait_for_settle();
// Final class is Script -> existing.server.luau
verify_instance_file(&src, "existing", "Script", None);
});
}
#[test]
fn rapid_classname_cycle_directory_init_10x() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let dir = src.join("DirModuleWithChildren");
let classes = [
"Script",
"LocalScript",
"ModuleScript",
"Script",
"LocalScript",
"ModuleScript",
"Script",
"LocalScript",
"ModuleScript",
"Script",
];
for class in &classes {
let (session_id, id) =
get_format_transitions_instance(&session, "DirModuleWithChildren");
send_update_fast(&session, &session_id, make_class_update(id, class));
thread::sleep(Duration::from_millis(200));
}
wait_for_settle();
// Final class is Script -> init.server.luau
assert_file_exists(
&dir.join("init.server.luau"),
"init.server.luau after 10 cycles",
);
// Children survive
assert_file_exists(&dir.join("ChildA.luau"), "ChildA after 10 cycles");
assert_file_exists(&dir.join("ChildB.luau"), "ChildB after 10 cycles");
});
}
// ---------------------------------------------------------------------------
// Tests 32-34: Combined operations blitz
// ---------------------------------------------------------------------------
#[test]
fn combined_rename_classname_source_blitz_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let rounds: Vec<(&str, &str, &str)> = vec![
("Blitz1", "Script", "-- blitz v1"),
("Blitz2", "LocalScript", "-- blitz v2"),
("Blitz3", "ModuleScript", "-- blitz v3"),
("Blitz4", "Script", "-- blitz v4"),
("Blitz5", "LocalScript", "-- blitz v5"),
("Blitz6", "ModuleScript", "-- blitz v6"),
("Blitz7", "Script", "-- blitz v7"),
("Blitz8", "LocalScript", "-- blitz v8"),
("Blitz9", "ModuleScript", "-- blitz v9"),
("Blitz10", "Script", "-- blitz v10"),
];
let mut current_name = "existing".to_string();
for (name, class, source) in &rounds {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let (id, _) = find_by_name(&rs_read.instances, &current_name);
send_update_fast(
&session,
&info.session_id,
make_combined_update(id, Some(name), Some(class), Some(source)),
);
current_name = name.to_string();
thread::sleep(Duration::from_millis(200));
}
wait_for_settle();
// Final: Blitz10.server.luau with "-- blitz v10"
verify_instance_file(&src, "Blitz10", "Script", Some("-- blitz v10"));
// All intermediates gone
for i in 1..10 {
let name = format!("Blitz{}", i);
for suffix in &[".server", ".local", ""] {
assert_not_exists(
&src.join(format!("{}{}.luau", name, suffix)),
&format!("Intermediate {}{}.luau", name, suffix),
);
}
}
assert_not_exists(&src.join("existing.luau"), "Original file");
});
}
#[test]
fn combined_rename_and_source_rapid_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let mut current_name = "existing".to_string();
for i in 1..=10 {
let new_name = format!("RS{}", i);
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let (id, _) = find_by_name(&rs_read.instances, &current_name);
send_update_fast(
&session,
&info.session_id,
make_combined_update(id, Some(&new_name), None, Some(&format!("-- rs v{}", i))),
);
current_name = new_name;
}
wait_for_settle();
verify_instance_file(&src, "RS10", "ModuleScript", Some("-- rs v10"));
});
}
#[test]
fn combined_classname_and_source_rapid_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let classes = [
"Script",
"LocalScript",
"ModuleScript",
"Script",
"LocalScript",
"ModuleScript",
"Script",
"LocalScript",
"ModuleScript",
"Script",
];
for (i, class) in classes.iter().enumerate() {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
send_update_fast(
&session,
&session_id,
make_combined_update(
existing_id,
None,
Some(class),
Some(&format!("-- cs v{}", i + 1)),
),
);
thread::sleep(Duration::from_millis(200));
}
wait_for_settle();
verify_instance_file(&src, "existing", "Script", Some("-- cs v10"));
});
}
// ---------------------------------------------------------------------------
// Tests 35-36: Multi-instance concurrent
// ---------------------------------------------------------------------------
#[test]
fn multi_instance_source_update_single_request() {
run_serve_test("syncback_stress", |session, _redactions| {
let (session_id, _rs_id, instances) = get_stress_instances(&session);
let src = session.path().join("src");
let updates: Vec<InstanceUpdate> = instances
.iter()
.enumerate()
.map(|(i, (id, _name))| make_source_update(*id, &format!("-- multi source v{}", i + 1)))
.collect();
let write_request = WriteRequest {
session_id,
removed: vec![],
added: HashMap::new(),
updated: updates,
};
session.post_api_write(&write_request).unwrap();
wait_for_settle();
let names = ["Alpha", "Bravo", "Charlie", "Delta", "Echo"];
for (i, name) in names.iter().enumerate() {
let file = src.join(format!("{}.luau", name));
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains(&format!("-- multi source v{}", i + 1)),
"{}.luau should contain expected source, got: {}",
name,
content
);
}
});
}
#[test]
fn multi_instance_rename_single_request() {
run_serve_test("syncback_stress", |session, _redactions| {
let (session_id, _rs_id, instances) = get_stress_instances(&session);
let src = session.path().join("src");
let new_names = ["AlphaR", "BravoR", "CharlieR", "DeltaR", "EchoR"];
let updates: Vec<InstanceUpdate> = instances
.iter()
.zip(new_names.iter())
.map(|((id, _), new_name)| make_rename_update(*id, new_name))
.collect();
let write_request = WriteRequest {
session_id,
removed: vec![],
added: HashMap::new(),
updated: updates,
};
session.post_api_write(&write_request).unwrap();
wait_for_settle();
for new_name in &new_names {
assert_file_exists(
&src.join(format!("{}.luau", new_name)),
&format!("{}.luau after rename", new_name),
);
}
for old_name in &["Alpha", "Bravo", "Charlie", "Delta", "Echo"] {
assert_not_exists(
&src.join(format!("{}.luau", old_name)),
&format!("Old {}.luau", old_name),
);
}
});
}
// ---------------------------------------------------------------------------
// Tests 37-38: Delete + recreate race
// ---------------------------------------------------------------------------
#[test]
fn delete_and_recreate_via_filesystem_recovery() {
run_serve_test("syncback_write", |session, _redactions| {
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
let file_path = session.path().join("src").join("existing.luau");
// Delete via API
send_removal_fast(&session, &session_id, vec![existing_id]);
// Immediately recreate the file on disk (simulating editor undo)
fs::write(&file_path, "-- recovered content\nreturn {}").unwrap();
// Wait for the recovery mechanism (200ms delay + 500ms sweep + buffer)
thread::sleep(Duration::from_millis(1500));
// Instance should be back in the tree
let rs_id = get_rs_id(&session);
poll_tree_has_instance(&session, rs_id, "existing", 3000);
});
}
#[test]
fn rapid_delete_recreate_cycle_5x() {
run_serve_test("syncback_write", |session, _redactions| {
let file_path = session.path().join("src").join("existing.luau");
for cycle in 1..=5 {
// Re-read tree to get current instance
let (session_id, _rs_id, existing_id) = get_rs_and_existing(&session);
// Delete via API
send_removal_fast(&session, &session_id, vec![existing_id]);
// Recreate on disk
let content = format!("-- cycle {} recovered\nreturn {{}}", cycle);
fs::write(&file_path, &content).unwrap();
// Wait for recovery
thread::sleep(Duration::from_millis(1500));
// Verify instance is back
let rs_id = get_rs_id(&session);
poll_tree_has_instance(&session, rs_id, "existing", 3000);
}
});
}
// ---------------------------------------------------------------------------
// Tests 39-40: Echo suppression under load
// ---------------------------------------------------------------------------
#[test]
fn echo_suppression_rapid_adds_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let info = session.get_api_rojo().unwrap();
let root_id = info.root_instance_id;
let root_read = session.get_api_read(root_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let initial_cursor = root_read.message_cursor;
for i in 0..10 {
let mut properties = HashMap::new();
properties.insert(
"Source".to_string(),
Variant::String(format!("-- echo add {}", i)),
);
let added = AddedInstance {
parent: Some(rs_id),
name: format!("EchoAdd{}", i),
class_name: "ModuleScript".to_string(),
properties,
children: vec![],
};
let mut added_map = HashMap::new();
added_map.insert(Ref::new(), added);
let write_request = WriteRequest {
session_id: info.session_id,
removed: vec![],
added: added_map,
updated: vec![],
};
session.post_api_write(&write_request).unwrap();
}
wait_for_settle();
// All 10 files should exist
let src = session.path().join("src");
for i in 0..10 {
assert_file_exists(
&src.join(format!("EchoAdd{}.luau", i)),
&format!("EchoAdd{}.luau", i),
);
}
// Server should still be responsive
let read_after = session.get_api_read(root_id).unwrap();
let cursor_delta = read_after.message_cursor - initial_cursor;
assert!(
cursor_delta < 50,
"Cursor delta {} should be reasonable with echo suppression",
cursor_delta
);
});
}
#[test]
fn echo_suppression_mixed_operations() {
run_serve_test("syncback_stress", |session, _redactions| {
let (session_id, rs_id, instances) = get_stress_instances(&session);
let src = session.path().join("src");
// 2 adds
let mut added_map = HashMap::new();
for i in 0..2 {
let mut properties = HashMap::new();
properties.insert(
"Source".to_string(),
Variant::String(format!("-- mixed add {}", i)),
);
added_map.insert(
Ref::new(),
AddedInstance {
parent: Some(rs_id),
name: format!("MixedAdd{}", i),
class_name: "ModuleScript".to_string(),
properties,
children: vec![],
},
);
}
// 2 updates (Alpha and Bravo)
let updates: Vec<InstanceUpdate> = instances
.iter()
.take(2)
.enumerate()
.map(|(i, (id, _))| make_source_update(*id, &format!("-- mixed update {}", i)))
.collect();
// 1 removal (Echo)
let removed = vec![instances[4].0];
let write_request = WriteRequest {
session_id,
removed,
added: added_map,
updated: updates,
};
session.post_api_write(&write_request).unwrap();
wait_for_settle();
// Verify adds
for i in 0..2 {
assert_file_exists(
&src.join(format!("MixedAdd{}.luau", i)),
&format!("MixedAdd{}.luau", i),
);
}
// Verify updates
let alpha_content = fs::read_to_string(src.join("Alpha.luau")).unwrap();
assert!(
alpha_content.contains("-- mixed update 0"),
"Alpha should be updated"
);
// Verify removal
assert_not_exists(&src.join("Echo.luau"), "Echo.luau after removal");
});
}
// ---------------------------------------------------------------------------
// Test 41: Encoded name rapid rename chain
// ---------------------------------------------------------------------------
#[test]
fn encoded_name_rapid_rename_chain() {
run_serve_test("syncback_encoded_names", |session, _redactions| {
let src = session.path().join("src");
let (_session_id, _module_id) = get_encoded_names_instance(&session, "What?Module");
// Rename through names with special characters
let names = ["Foo?Bar", "Key:Value", "Test?End", "Final:Name", "Done?Now"];
let mut current_name = "What?Module".to_string();
for new_name in &names {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let (id, _) = find_by_name(&rs_read.instances, &current_name);
send_update_fast(&session, &info.session_id, make_rename_update(id, new_name));
current_name = new_name.to_string();
}
wait_for_settle();
// Final file should use encoded name: Done%QUESTION%Now.luau
let final_file = src.join("Done%QUESTION%Now.luau");
assert_file_exists(&final_file, "Final encoded file");
// Original should be gone
assert_not_exists(
&src.join("What%QUESTION%Module.luau"),
"Original encoded file",
);
});
}
// ===========================================================================
// PART 2: Randomized Fuzzing (Tests 42-46)
// ===========================================================================
// ---------------------------------------------------------------------------
// PRNG and fuzzing infrastructure
// ---------------------------------------------------------------------------
struct XorShift64(u64);
impl XorShift64 {
fn new(seed: u64) -> Self {
Self(if seed == 0 { 1 } else { seed })
}
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn range(&mut self, min: u64, max: u64) -> u64 {
min + (self.next() % (max - min))
}
fn choose<'a, T>(&mut self, items: &'a [T]) -> &'a T {
&items[self.next() as usize % items.len()]
}
}
/// State tracker for fuzzing — knows expected file name, class, and content.
struct FuzzState {
current_name: String,
current_class: String,
current_source: String,
}
impl FuzzState {
fn new(name: &str, class: &str, source: &str) -> Self {
Self {
current_name: name.to_string(),
current_class: class.to_string(),
current_source: source.to_string(),
}
}
fn expected_file(&self) -> String {
let suffix = class_suffix(&self.current_class);
format!("{}{}.luau", self.current_name, suffix)
}
}
// ---------------------------------------------------------------------------
// Test 42: Fuzz source and rename
// ---------------------------------------------------------------------------
#[test]
fn fuzz_source_and_rename_20_iterations() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let mut rng = XorShift64::new(42);
let mut state = FuzzState::new(
"existing",
"ModuleScript",
"-- Existing module for testing\nreturn {}",
);
// 20 random operations in a single continuous chain (no reset)
for op_idx in 0..20 {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let found = rs_read
.instances
.iter()
.find(|(_, inst)| inst.name == state.current_name);
let inst_id = match found {
Some((&id, _)) => id,
None => {
thread::sleep(Duration::from_millis(500));
continue;
}
};
if rng.next().is_multiple_of(2) {
let new_name = format!("Fz{}", op_idx);
send_update(
&session,
&info.session_id,
make_rename_update(inst_id, &new_name),
);
state.current_name = new_name;
} else {
let new_source = format!("-- fuzz o{}", op_idx);
send_update(
&session,
&info.session_id,
make_source_update(inst_id, &new_source),
);
state.current_source = new_source;
}
}
wait_for_settle();
let expected = src.join(state.expected_file());
assert!(
expected.is_file(),
"Expected file {} to exist after 20 fuzz ops",
expected.display()
);
});
}
// ---------------------------------------------------------------------------
// Test 43: Fuzz ClassName cycling
// ---------------------------------------------------------------------------
#[test]
fn fuzz_classname_cycling_20_iterations() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let class_options = ["ModuleScript", "Script", "LocalScript"];
let mut rng = XorShift64::new(77);
let mut state = FuzzState::new(
"existing",
"ModuleScript",
"-- Existing module for testing\nreturn {}",
);
// 20 random ClassName changes in a single continuous chain
for _ in 0..20 {
let (session_id, _rs_id, id) = get_rs_and_existing(&session);
let new_class = *rng.choose(&class_options);
send_update(&session, &session_id, make_class_update(id, new_class));
state.current_class = new_class.to_string();
}
wait_for_settle();
let expected = src.join(state.expected_file());
assert!(
expected.is_file(),
"Expected file {} to exist after 20 class changes",
expected.display()
);
});
}
// ---------------------------------------------------------------------------
// Test 44: Fuzz combined operations
// ---------------------------------------------------------------------------
#[test]
fn fuzz_combined_operations_20_iterations() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let class_options = ["ModuleScript", "Script", "LocalScript"];
let mut rng = XorShift64::new(99);
let mut state = FuzzState::new(
"existing",
"ModuleScript",
"-- Existing module for testing\nreturn {}",
);
// 20 random combined operations in a single continuous chain
for op_idx in 0..20 {
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let found = rs_read
.instances
.iter()
.find(|(_, inst)| inst.name == state.current_name);
let inst_id = match found {
Some((&id, _)) => id,
None => {
thread::sleep(Duration::from_millis(500));
continue;
}
};
let op_type = rng.range(0, 4);
let new_name = format!("CF{}", op_idx);
let new_source = format!("-- cf o{}", op_idx);
let new_class = *rng.choose(&class_options);
match op_type {
0 => {
send_update(
&session,
&info.session_id,
make_source_update(inst_id, &new_source),
);
state.current_source = new_source;
}
1 => {
send_update(
&session,
&info.session_id,
make_rename_update(inst_id, &new_name),
);
state.current_name = new_name;
}
2 => {
send_update(
&session,
&info.session_id,
make_class_update(inst_id, new_class),
);
state.current_class = new_class.to_string();
}
_ => {
send_update(
&session,
&info.session_id,
make_combined_update(
inst_id,
Some(&new_name),
Some(new_class),
Some(&new_source),
),
);
state.current_name = new_name;
state.current_class = new_class.to_string();
state.current_source = new_source;
}
}
}
wait_for_settle();
let expected = src.join(state.expected_file());
assert!(
expected.is_file(),
"Expected file {} to exist after 20 combined fuzz ops. State: name={}, class={}",
expected.display(),
state.current_name,
state.current_class,
);
});
}
// ---------------------------------------------------------------------------
// Test 45: Fuzz multi-instance
// ---------------------------------------------------------------------------
#[test]
fn fuzz_multi_instance_15_iterations() {
run_serve_test("syncback_stress", |session, _redactions| {
let src = session.path().join("src");
let original_names = ["Alpha", "Bravo", "Charlie", "Delta", "Echo"];
for seed in 1u64..=15 {
let mut rng = XorShift64::new(seed);
// Pick 1-5 instances to update
let count = rng.range(1, 6) as usize;
let (session_id, _rs_id, instances) = get_stress_instances(&session);
let updates: Vec<InstanceUpdate> = instances
.iter()
.take(count)
.enumerate()
.map(|(i, (id, _name))| {
make_source_update(*id, &format!("-- fuzz mi s{} i{}", seed, i))
})
.collect();
let write_request = WriteRequest {
session_id,
removed: vec![],
added: HashMap::new(),
updated: updates,
};
session.post_api_write(&write_request).unwrap();
thread::sleep(Duration::from_millis(100));
}
wait_for_settle();
// All 5 files should still exist (we only did Source updates)
for name in &original_names {
assert_file_exists(
&src.join(format!("{}.luau", name)),
&format!("{}.luau after 100 fuzz iterations", name),
);
}
});
}
// ---------------------------------------------------------------------------
// Test 46: Fuzz directory format operations
// ---------------------------------------------------------------------------
#[test]
fn fuzz_directory_format_operations_15_iterations() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let class_options = ["ModuleScript", "Script", "LocalScript"];
let mut current_name = "DirModuleWithChildren".to_string();
for seed in 1u64..=15 {
let mut rng = XorShift64::new(seed);
let is_rename = rng.next().is_multiple_of(2);
let info = session.get_api_rojo().unwrap();
let root_read = session.get_api_read(info.root_instance_id).unwrap();
let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage");
let rs_read = session.get_api_read(rs_id).unwrap();
let found = rs_read
.instances
.iter()
.find(|(_, inst)| inst.name == current_name);
let inst_id = match found {
Some((&id, _)) => id,
None => {
thread::sleep(Duration::from_millis(500));
continue;
}
};
if is_rename {
let new_name = format!("DirFz{}", seed);
send_update_fast(
&session,
&info.session_id,
make_rename_update(inst_id, &new_name),
);
current_name = new_name;
} else {
let new_class = *rng.choose(&class_options);
send_update_fast(
&session,
&info.session_id,
make_class_update(inst_id, new_class),
);
}
thread::sleep(Duration::from_millis(200));
}
wait_for_settle();
// Directory should exist with children intact
let final_dir = src.join(&current_name);
assert!(
final_dir.is_dir(),
"Final directory {} should exist",
final_dir.display()
);
assert_file_exists(
&final_dir.join("ChildA.luau"),
"ChildA.luau after 100 iterations",
);
assert_file_exists(
&final_dir.join("ChildB.luau"),
"ChildB.luau after 100 iterations",
);
});
}
// ===========================================================================
// PART 3: File Watcher Stress Tests (Tests 47-66)
// ===========================================================================
// ---------------------------------------------------------------------------
// Tests 47-48: Rapid source edits on disk
// ---------------------------------------------------------------------------
#[test]
fn watcher_rapid_source_edits_on_disk_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let file_path = session.path().join("src").join("existing.luau");
let rs_id = get_rs_id(&session);
for i in 1..=10 {
fs::write(&file_path, format!("-- disk v{}\nreturn {{}}", i)).unwrap();
thread::sleep(Duration::from_millis(30));
}
poll_tree_source(&session, rs_id, "existing", "-- disk v10", 3000);
});
}
#[test]
fn watcher_burst_writes_100x() {
run_serve_test("syncback_write", |session, _redactions| {
let file_path = session.path().join("src").join("existing.luau");
let rs_id = get_rs_id(&session);
for i in 0..100 {
fs::write(&file_path, format!("-- burst {}\nreturn {{}}", i)).unwrap();
}
fs::write(&file_path, "-- burst final\nreturn {}").unwrap();
poll_tree_source(&session, rs_id, "existing", "-- burst final", 3000);
});
}
// ---------------------------------------------------------------------------
// Tests 49-50: Filesystem rename
// ---------------------------------------------------------------------------
#[test]
fn watcher_filesystem_rename_chain_10x() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let original_content = fs::read_to_string(src.join("existing.luau")).unwrap();
let mut current_file = src.join("existing.luau");
for i in 1..=10 {
let new_file = src.join(format!("R{}.luau", i));
fs::rename(&current_file, &new_file).unwrap();
current_file = new_file;
thread::sleep(Duration::from_millis(30));
}
poll_tree_has_instance(&session, rs_id, "R10", 3000);
let content = fs::read_to_string(&current_file).unwrap();
assert_eq!(
original_content, content,
"Content preserved through renames"
);
});
}
#[test]
fn watcher_rename_with_content_change() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
// Atomic overwrite: write temp file, rename over existing
let temp = src.join("existing.luau.tmp");
fs::write(&temp, "-- atomic overwrite content\nreturn {}").unwrap();
fs::rename(&temp, src.join("existing.luau")).unwrap();
poll_tree_source(
&session,
rs_id,
"existing",
"-- atomic overwrite content",
3000,
);
});
}
// ---------------------------------------------------------------------------
// Tests 51-53: Delete + recreate via filesystem
// ---------------------------------------------------------------------------
#[test]
fn watcher_delete_recreate_immediate() {
run_serve_test("syncback_write", |session, _redactions| {
let file_path = session.path().join("src").join("existing.luau");
let rs_id = get_rs_id(&session);
fs::remove_file(&file_path).unwrap();
fs::write(&file_path, "-- recreated immediately\nreturn {}").unwrap();
poll_tree_source(
&session,
rs_id,
"existing",
"-- recreated immediately",
3000,
);
});
}
#[test]
fn watcher_delete_recreate_cycle_5x() {
run_serve_test("syncback_write", |session, _redactions| {
let file_path = session.path().join("src").join("existing.luau");
let rs_id = get_rs_id(&session);
for cycle in 1..=5 {
fs::remove_file(&file_path).unwrap();
thread::sleep(Duration::from_millis(50));
let content = format!("-- cycle {} content\nreturn {{}}", cycle);
fs::write(&file_path, &content).unwrap();
poll_tree_source(
&session,
rs_id,
"existing",
&format!("-- cycle {} content", cycle),
5000,
);
}
});
}
#[test]
fn watcher_delete_recreate_different_content() {
run_serve_test("syncback_write", |session, _redactions| {
let file_path = session.path().join("src").join("existing.luau");
let rs_id = get_rs_id(&session);
fs::remove_file(&file_path).unwrap();
// Wait for removal to propagate
thread::sleep(Duration::from_millis(300));
// Recreate with completely different content
fs::write(
&file_path,
"-- completely different\nlocal x = 42\nreturn x",
)
.unwrap();
poll_tree_source(&session, rs_id, "existing", "-- completely different", 5000);
});
}
// ---------------------------------------------------------------------------
// Tests 54-57: Init file shenanigans
// ---------------------------------------------------------------------------
#[test]
fn watcher_edit_init_file() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let init_path = src.join("DirModuleWithChildren").join("init.luau");
fs::write(&init_path, "-- edited init content\nreturn {}").unwrap();
poll_tree_source(
&session,
rs_id,
"DirModuleWithChildren",
"-- edited init content",
3000,
);
// Children should still exist
let dir_id = poll_tree_has_instance(&session, rs_id, "DirModuleWithChildren", 1000);
let child_read = session.get_api_read(dir_id).unwrap();
assert!(
child_read.instances.values().any(|i| i.name == "ChildA"),
"ChildA should still exist after init edit"
);
});
}
#[test]
fn watcher_init_type_cycling_10x() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let dir = src.join("DirModuleWithChildren");
// Cycle: init.luau -> init.server.luau -> init.local.luau -> init.luau -> ...
let transitions = [
("init.luau", "init.server.luau", "Script"),
("init.server.luau", "init.local.luau", "LocalScript"),
("init.local.luau", "init.luau", "ModuleScript"),
("init.luau", "init.server.luau", "Script"),
("init.server.luau", "init.local.luau", "LocalScript"),
("init.local.luau", "init.luau", "ModuleScript"),
("init.luau", "init.server.luau", "Script"),
("init.server.luau", "init.local.luau", "LocalScript"),
("init.local.luau", "init.luau", "ModuleScript"),
("init.luau", "init.server.luau", "Script"),
];
for (from, to, expected_class) in &transitions {
fs::rename(dir.join(from), dir.join(to)).unwrap();
poll_tree_class(
&session,
rs_id,
"DirModuleWithChildren",
expected_class,
3000,
);
}
// Children should survive all transitions
let dir_id = poll_tree_has_instance(&session, rs_id, "DirModuleWithChildren", 1000);
let child_read = session.get_api_read(dir_id).unwrap();
assert!(
child_read.instances.values().any(|i| i.name == "ChildA"),
"ChildA should survive init type cycling"
);
assert!(
child_read.instances.values().any(|i| i.name == "ChildB"),
"ChildB should survive init type cycling"
);
});
}
#[test]
fn watcher_delete_init_file() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let init_path = src.join("DirModuleWithChildren").join("init.luau");
fs::remove_file(&init_path).unwrap();
// Without init file, directory becomes a Folder
poll_tree_class(&session, rs_id, "DirModuleWithChildren", "Folder", 3000);
});
}
#[test]
fn watcher_replace_init_file_type() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let dir = src.join("DirModuleWithChildren");
let old_init = dir.join("init.luau");
let new_init = dir.join("init.server.luau");
let content = fs::read_to_string(&old_init).unwrap();
fs::remove_file(&old_init).unwrap();
fs::write(&new_init, &content).unwrap();
poll_tree_class(&session, rs_id, "DirModuleWithChildren", "Script", 3000);
});
}
// ---------------------------------------------------------------------------
// Tests 58-60: Singular <-> directory format conversion
// ---------------------------------------------------------------------------
#[test]
fn watcher_standalone_to_directory_conversion() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let standalone = src.join("StandaloneModule.luau");
let original = fs::read_to_string(&standalone).unwrap();
// Convert: remove file, create directory with init + child
fs::remove_file(&standalone).unwrap();
let dir = src.join("StandaloneModule");
fs::create_dir(&dir).unwrap();
fs::write(dir.join("init.luau"), &original).unwrap();
fs::write(dir.join("ChildNew.luau"), "-- new child\nreturn {}").unwrap();
poll_tree_has_child(&session, rs_id, "StandaloneModule", "ChildNew", 5000);
});
}
#[test]
fn watcher_directory_to_standalone_conversion() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let dir = src.join("DirModuleWithChildren");
let init_content = fs::read_to_string(dir.join("init.luau")).unwrap();
// Convert: remove directory, create standalone file
fs::remove_dir_all(&dir).unwrap();
fs::write(src.join("DirModuleWithChildren.luau"), &init_content).unwrap();
// Wait and verify no children remain
thread::sleep(Duration::from_millis(500));
let id = poll_tree_has_instance(&session, rs_id, "DirModuleWithChildren", 5000);
// Read children
let child_read = session.get_api_read(id).unwrap();
let child_count = child_read
.instances
.values()
.filter(|i| i.name != "DirModuleWithChildren")
.count();
assert!(
child_count == 0,
"Standalone should have no children, found {}",
child_count
);
});
}
#[test]
fn watcher_format_flip_flop_5x() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let standalone_path = src.join("StandaloneModule.luau");
let dir_path = src.join("StandaloneModule");
for cycle in 1..=5 {
// Standalone -> Directory
if standalone_path.exists() {
fs::remove_file(&standalone_path).unwrap();
}
if !dir_path.exists() {
fs::create_dir(&dir_path).unwrap();
}
fs::write(
dir_path.join("init.luau"),
format!("-- dir cycle {}\nreturn {{}}", cycle),
)
.unwrap();
fs::write(
dir_path.join("Child.luau"),
format!("-- child cycle {}", cycle),
)
.unwrap();
poll_tree_has_child(&session, rs_id, "StandaloneModule", "Child", 5000);
// Directory -> Standalone
fs::remove_dir_all(&dir_path).unwrap();
fs::write(
&standalone_path,
format!("-- standalone cycle {}\nreturn {{}}", cycle),
)
.unwrap();
poll_tree_source(
&session,
rs_id,
"StandaloneModule",
&format!("-- standalone cycle {}", cycle),
5000,
);
}
});
}
// ---------------------------------------------------------------------------
// Tests 61-62: Editor save patterns
// ---------------------------------------------------------------------------
#[test]
fn watcher_atomic_save_pattern() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let file = src.join("existing.luau");
let temp = src.join("existing.luau.tmp");
for i in 1..=5 {
let content = format!("-- atomic save v{}\nreturn {{}}", i);
fs::write(&temp, &content).unwrap();
fs::rename(&temp, &file).unwrap();
poll_tree_source(
&session,
rs_id,
"existing",
&format!("-- atomic save v{}", i),
3000,
);
}
});
}
#[test]
fn watcher_backup_rename_write_pattern() {
run_serve_test("syncback_write", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let file = src.join("existing.luau");
let backup = src.join("existing.luau.bak");
// Backup-rename-write: rename existing to .bak, write new content
fs::rename(&file, &backup).unwrap();
fs::write(&file, "-- backup-rename-write content\nreturn {}").unwrap();
poll_tree_source(
&session,
rs_id,
"existing",
"-- backup-rename-write content",
3000,
);
// Clean up backup
if backup.exists() {
fs::remove_file(&backup).unwrap();
}
});
}
// ---------------------------------------------------------------------------
// Tests 63-64: Parent directory operations
// ---------------------------------------------------------------------------
#[test]
fn watcher_parent_directory_rename() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let old_dir = src.join("DirModuleWithChildren");
let new_dir = src.join("RenamedDir");
fs::rename(&old_dir, &new_dir).unwrap();
poll_tree_has_instance(&session, rs_id, "RenamedDir", 5000);
poll_tree_no_instance(&session, rs_id, "DirModuleWithChildren", 3000);
// Children should be intact
let dir_id = poll_tree_has_instance(&session, rs_id, "RenamedDir", 1000);
let child_read = session.get_api_read(dir_id).unwrap();
assert!(
child_read.instances.values().any(|i| i.name == "ChildA"),
"ChildA should be in renamed directory"
);
});
}
#[test]
fn watcher_parent_directory_delete_all() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
fs::remove_dir_all(src.join("DirModuleWithChildren")).unwrap();
poll_tree_no_instance(&session, rs_id, "DirModuleWithChildren", 5000);
});
}
// ---------------------------------------------------------------------------
// Tests 65-66: Concurrent filesystem + API
// ---------------------------------------------------------------------------
#[test]
fn watcher_filesystem_and_api_concurrent() {
run_serve_test("syncback_format_transitions", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
// Filesystem edit on StandaloneModule
let standalone_file = src.join("StandaloneModule.luau");
fs::write(&standalone_file, "-- fs edited module\nreturn {}").unwrap();
// API edit on StandaloneScript
let (session_id, script_id) = get_format_transitions_instance(&session, "StandaloneScript");
send_update_no_wait(
&session,
&session_id,
make_source_update(script_id, "-- api edited script"),
);
// Both should propagate
poll_tree_source(
&session,
rs_id,
"StandaloneModule",
"-- fs edited module",
3000,
);
let script_file = src.join("StandaloneScript.server.luau");
// Wait for API write to complete
thread::sleep(Duration::from_millis(500));
let script_content = fs::read_to_string(&script_file).unwrap();
assert!(
script_content.contains("-- api edited script"),
"Script should be updated via API, got: {}",
script_content
);
});
}
#[test]
fn watcher_multi_file_simultaneous_edits() {
run_serve_test("syncback_stress", |session, _redactions| {
let src = session.path().join("src");
let rs_id = get_rs_id(&session);
let names = ["Alpha", "Bravo", "Charlie", "Delta", "Echo"];
// Write all 5 files as fast as possible
for (i, name) in names.iter().enumerate() {
fs::write(
src.join(format!("{}.luau", name)),
format!("-- simultaneous edit {}\nreturn {{}}", i),
)
.unwrap();
}
// Poll for all 5 to update
for (i, name) in names.iter().enumerate() {
poll_tree_source(
&session,
rs_id,
name,
&format!("-- simultaneous edit {}", i),
5000,
);
}
});
}