mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
- Introduced a constant to enable or disable periodic tree reconciliation after VFS events, addressing a persistent drift bug that caused infinite re-snapshot loops on large projects. - Updated the `ChangeProcessor` to conditionally execute tree reconciliation based on the new constant, improving performance during file modifications. - Enhanced the git module by adding new functions for shallow cloning and checking if files are staged, streamlining git operations. - Refactored the `InitCommand` to utilize the new git functions for repository initialization and cloning, improving clarity and reducing redundancy. These changes aim to optimize file change handling and enhance git integration within the project.
1060 lines
37 KiB
Rust
1060 lines
37 KiB
Rust
//! Integration tests for git-based sync direction defaults.
|
|
//!
|
|
//! Tests the full pipeline: git metadata computation via /api/git-metadata,
|
|
//! stage_ids in WriteRequest, and auto-staging behavior.
|
|
//!
|
|
//! These tests create real git repos in temp directories to verify:
|
|
//! - gitMetadata is present/absent based on git repo state
|
|
//! - changedIds correctly maps git-changed files to instance Refs
|
|
//! - scriptCommittedHashes contains correct SHA1 hashes
|
|
//! - Both HEAD and staged hashes are included when different
|
|
//! - stage_ids triggers git add on the server side
|
|
//! - Non-script changes are in changedIds but not in scriptCommittedHashes
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::Duration;
|
|
use std::{fs, thread};
|
|
|
|
use librojo::web_api::{InstanceUpdate, WriteRequest};
|
|
use rbx_dom_weak::types::{Ref, Variant};
|
|
use rbx_dom_weak::{ustr, UstrMap};
|
|
|
|
use crate::rojo_test::io_util::SERVE_TESTS_PATH;
|
|
use crate::rojo_test::serve_util::TestServeSession;
|
|
|
|
fn fixture_content(rel_path: &str) -> String {
|
|
let path = Path::new(SERVE_TESTS_PATH)
|
|
.join("git_sync_defaults")
|
|
.join(rel_path);
|
|
fs::read_to_string(&path)
|
|
.unwrap_or_else(|e| panic!("Failed to read fixture {}: {}", path.display(), e))
|
|
}
|
|
|
|
fn git_commit_all(dir: &Path, msg: &str) {
|
|
librojo::git::git_add_all_and_commit(dir, msg);
|
|
}
|
|
|
|
fn git_stage(dir: &Path, file: &str) {
|
|
librojo::git_add(dir, &[PathBuf::from(file)]);
|
|
}
|
|
|
|
fn git_is_staged(dir: &Path, file: &str) -> bool {
|
|
librojo::git_is_staged(dir, file)
|
|
}
|
|
|
|
fn compute_blob_sha1(content: &str) -> String {
|
|
use sha1::{Digest, Sha1};
|
|
let header = format!("blob {}\0", content.len());
|
|
let mut hasher = Sha1::new();
|
|
hasher.update(header.as_bytes());
|
|
hasher.update(content.as_bytes());
|
|
format!("{:x}", hasher.finalize())
|
|
}
|
|
|
|
fn find_instance_by_name<'a>(
|
|
instances: &'a HashMap<Ref, librojo::web_api::Instance<'a>>,
|
|
name: &str,
|
|
) -> Option<(Ref, &'a librojo::web_api::Instance<'a>)> {
|
|
instances
|
|
.iter()
|
|
.find(|(_, inst)| inst.name.as_ref() == name)
|
|
.map(|(&id, inst)| (id, inst))
|
|
}
|
|
|
|
// ===========================================================================
|
|
// gitMetadata presence/absence
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn git_metadata_present_when_in_git_repo() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
let git_meta = session.get_api_git_metadata().unwrap();
|
|
assert!(
|
|
git_meta.is_some(),
|
|
"gitMetadata should be present when project is in a git repo"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn git_metadata_absent_when_not_in_git_repo() {
|
|
let mut session = TestServeSession::new("git_sync_defaults");
|
|
let _info = session.wait_to_come_online();
|
|
let git_meta = session.get_api_git_metadata().unwrap();
|
|
assert!(
|
|
git_meta.is_none(),
|
|
"gitMetadata should be None when project is not in a git repo"
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// changedIds: file-to-instance mapping
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn no_changes_yields_empty_changed_ids() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
// No modifications after commit
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
assert!(
|
|
meta.changed_ids.is_empty(),
|
|
"changedIds should be empty when no files changed"
|
|
);
|
|
assert!(meta.script_committed_hashes.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn unstaged_script_modification_appears_in_changed_ids() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/ModuleA.luau"), "-- modified\nreturn {}").unwrap();
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
|
|
assert!(
|
|
!meta.changed_ids.is_empty(),
|
|
"changedIds should contain the modified script's instance Ref"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn staged_script_modification_appears_in_changed_ids() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/ModuleA.luau"), "-- staged mod\nreturn {}").unwrap();
|
|
git_stage(path, "src/ModuleA.luau");
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
|
|
assert!(!meta.changed_ids.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn untracked_file_appears_in_changed_ids() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/NewScript.luau"), "return 'new'").unwrap();
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
|
|
assert!(
|
|
!meta.changed_ids.is_empty(),
|
|
"Untracked files should appear in changedIds"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unchanged_files_not_in_changed_ids() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
// Only modify one file, others should not appear
|
|
fs::write(path.join("src/ModuleA.luau"), "-- changed").unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
// Find ServerScript -- it was NOT modified, should NOT be in changedIds
|
|
if let Some((server_id, _)) = find_instance_by_name(&read.instances, "ServerScript") {
|
|
assert!(
|
|
!meta.changed_ids.contains(&server_id),
|
|
"Unchanged ServerScript should NOT be in changedIds"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn non_script_change_in_changed_ids_but_no_hash() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
// Modify the model file (non-script)
|
|
fs::write(
|
|
path.join("src/SimpleModel.model.json5"),
|
|
r#"{ "className": "Part", "properties": { "Anchored": false } }"#,
|
|
)
|
|
.unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
if let Some((model_id, _)) = find_instance_by_name(&read.instances, "SimpleModel") {
|
|
assert!(
|
|
meta.changed_ids.contains(&model_id),
|
|
"Modified non-script should be in changedIds"
|
|
);
|
|
assert!(
|
|
!meta.script_committed_hashes.contains_key(&model_id),
|
|
"Non-script should NOT have a committed hash"
|
|
);
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// scriptCommittedHashes: SHA1 computation
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn committed_hash_matches_head_content() {
|
|
let original_content = fixture_content("src/ModuleA.luau");
|
|
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
// Modify the script so it appears as "changed"
|
|
fs::write(path.join("src/ModuleA.luau"), "-- v2\nreturn {}").unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
if let Some((module_id, _)) = find_instance_by_name(&read.instances, "ModuleA") {
|
|
let hashes = meta
|
|
.script_committed_hashes
|
|
.get(&module_id)
|
|
.expect("ModuleA should have committed hashes");
|
|
|
|
let expected_hash = compute_blob_sha1(&original_content);
|
|
assert!(
|
|
hashes.contains(&expected_hash),
|
|
"Committed hash should match SHA1 of original content. Expected: {}, Got: {:?}",
|
|
expected_hash,
|
|
hashes
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn staged_hash_included_when_different_from_head() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/ModuleA.luau"), "-- staged version").unwrap();
|
|
git_stage(path, "src/ModuleA.luau");
|
|
// Also modify working tree so file is still "changed"
|
|
fs::write(path.join("src/ModuleA.luau"), "-- working tree version").unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
if let Some((module_id, _)) = find_instance_by_name(&read.instances, "ModuleA") {
|
|
let hashes = meta
|
|
.script_committed_hashes
|
|
.get(&module_id)
|
|
.expect("Should have hashes");
|
|
|
|
assert!(
|
|
hashes.len() == 2,
|
|
"Should have 2 hashes (HEAD + staged) when staged differs from HEAD. Got: {}",
|
|
hashes.len()
|
|
);
|
|
|
|
let head_hash = compute_blob_sha1(&fixture_content("src/ModuleA.luau"));
|
|
let staged_hash = compute_blob_sha1("-- staged version");
|
|
|
|
assert!(hashes.contains(&head_hash), "Should contain HEAD hash");
|
|
assert!(hashes.contains(&staged_hash), "Should contain staged hash");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn staged_hash_deduplicated_when_same_as_head() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
// Stage a different version, then unstage by overwriting with original
|
|
// and re-staging... Actually the simplest way: modify, stage, then
|
|
// modify working tree further (staged = HEAD content won't happen
|
|
// if we staged a modification). Let's just verify with a clean stage.
|
|
fs::write(path.join("src/ModuleA.luau"), "-- modified").unwrap();
|
|
git_stage(path, "src/ModuleA.luau");
|
|
// Now staged and HEAD are different, but if they were the same,
|
|
// we'd only get 1 hash. Let's test the case where staged == HEAD:
|
|
// Reset the stage to match HEAD content
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
if let Some((module_id, _)) = find_instance_by_name(&read.instances, "ModuleA") {
|
|
if let Some(hashes) = meta.script_committed_hashes.get(&module_id) {
|
|
// When staged differs from HEAD, we get 2 hashes
|
|
// When staged == HEAD, we'd get 1 hash (deduplicated)
|
|
assert!(hashes.len() <= 2, "Should have at most 2 hashes");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn new_untracked_script_has_no_committed_hash() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/BrandNew.luau"), "return 'brand new'").unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
if let Some((new_id, _)) = find_instance_by_name(&read.instances, "BrandNew") {
|
|
assert!(
|
|
meta.changed_ids.contains(&new_id),
|
|
"New file should be in changedIds"
|
|
);
|
|
assert!(
|
|
!meta.script_committed_hashes.contains_key(&new_id),
|
|
"New file has no HEAD version, so no committed hash"
|
|
);
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Init-style scripts (init.luau in directories)
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn init_style_script_appears_in_changed_ids() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/DirModule/init.luau"), "-- modified init").unwrap();
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
|
|
assert!(
|
|
!meta.changed_ids.is_empty(),
|
|
"Modified init.luau should cause its instance to appear in changedIds"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn init_style_script_has_committed_hash() {
|
|
let original = fixture_content("src/DirModule/init.luau");
|
|
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/DirModule/init.luau"), "-- v2").unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
if let Some((dir_id, _)) = find_instance_by_name(&read.instances, "DirModule") {
|
|
let hashes = meta
|
|
.script_committed_hashes
|
|
.get(&dir_id)
|
|
.expect("DirModule should have committed hashes");
|
|
|
|
let expected_hash = compute_blob_sha1(&original);
|
|
assert!(
|
|
hashes.contains(&expected_hash),
|
|
"init.luau committed hash should match. Expected: {}, Got: {:?}",
|
|
expected_hash,
|
|
hashes
|
|
);
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Multiple script types (Script, LocalScript, ModuleScript)
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn all_script_types_get_hashes() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
// Modify all three script types
|
|
fs::write(path.join("src/ModuleA.luau"), "-- mod").unwrap();
|
|
fs::write(path.join("src/ServerScript.server.luau"), "-- mod").unwrap();
|
|
fs::write(path.join("src/ClientScript.client.luau"), "-- mod").unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
for name in &["ModuleA", "ServerScript", "ClientScript"] {
|
|
if let Some((id, _)) = find_instance_by_name(&read.instances, name) {
|
|
assert!(
|
|
meta.script_committed_hashes.contains_key(&id),
|
|
"{} should have a committed hash",
|
|
name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// stage_ids and auto-staging
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn stage_ids_in_write_request_triggers_git_add() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(
|
|
path.join("src/ModuleA.luau"),
|
|
"-- modified for staging test",
|
|
)
|
|
.unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
let (module_id, _) =
|
|
find_instance_by_name(&read.instances, "ModuleA").expect("ModuleA should exist");
|
|
|
|
// Send a write request with stage_ids containing ModuleA
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: vec![],
|
|
stage_ids: vec![module_id],
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
|
|
// Give the server time to process
|
|
thread::sleep(Duration::from_millis(500));
|
|
|
|
// Verify the file was staged
|
|
assert!(
|
|
git_is_staged(session.path(), "src/ModuleA.luau"),
|
|
"ModuleA.luau should be staged after stage_ids request"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_stage_ids_does_not_stage_anything() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/ModuleA.luau"), "-- modified").unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
|
|
// Send write with empty stage_ids
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: vec![],
|
|
stage_ids: vec![],
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
thread::sleep(Duration::from_millis(300));
|
|
|
|
assert!(
|
|
!git_is_staged(session.path(), "src/ModuleA.luau"),
|
|
"File should NOT be staged when stage_ids is empty"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stage_ids_only_stages_specified_files() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/ModuleA.luau"), "-- mod a").unwrap();
|
|
fs::write(path.join("src/ServerScript.server.luau"), "-- mod server").unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
let (module_id, _) =
|
|
find_instance_by_name(&read.instances, "ModuleA").expect("ModuleA should exist");
|
|
|
|
// Only stage ModuleA, NOT ServerScript
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: vec![],
|
|
stage_ids: vec![module_id],
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
thread::sleep(Duration::from_millis(500));
|
|
|
|
assert!(
|
|
git_is_staged(session.path(), "src/ModuleA.luau"),
|
|
"ModuleA should be staged"
|
|
);
|
|
assert!(
|
|
!git_is_staged(session.path(), "src/ServerScript.server.luau"),
|
|
"ServerScript should NOT be staged (not in stage_ids)"
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Source write + stage_ids interaction (change_processor staging)
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn source_write_with_stage_ids_stages_after_write() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
let (module_id, _) =
|
|
find_instance_by_name(&read.instances, "ModuleA").expect("ModuleA should exist");
|
|
|
|
// Write a Source update AND include in stage_ids
|
|
let mut props = UstrMap::default();
|
|
props.insert(
|
|
ustr("Source"),
|
|
Some(Variant::String(
|
|
"-- new source from pull\nreturn {}\n".to_string(),
|
|
)),
|
|
);
|
|
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: vec![InstanceUpdate {
|
|
id: module_id,
|
|
changed_name: None,
|
|
changed_class_name: None,
|
|
changed_properties: props,
|
|
changed_metadata: None,
|
|
}],
|
|
stage_ids: vec![module_id],
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
|
|
// Wait for change_processor to write Source and stage
|
|
thread::sleep(Duration::from_millis(1000));
|
|
|
|
// Verify the NEW content was staged (not the old content)
|
|
assert!(
|
|
git_is_staged(session.path(), "src/ModuleA.luau"),
|
|
"ModuleA should be staged after Source write + stage_ids"
|
|
);
|
|
|
|
// Verify the content on disk is the new version
|
|
let disk_content = fs::read_to_string(session.path().join("src/ModuleA.luau")).unwrap();
|
|
assert!(
|
|
disk_content.contains("new source from pull"),
|
|
"Disk content should be the new version"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn source_write_without_stage_ids_does_not_stage() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
let (module_id, _) =
|
|
find_instance_by_name(&read.instances, "ModuleA").expect("ModuleA should exist");
|
|
|
|
let mut props = UstrMap::default();
|
|
props.insert(
|
|
ustr("Source"),
|
|
Some(Variant::String(
|
|
"-- manual pull, not auto-staged".to_string(),
|
|
)),
|
|
);
|
|
|
|
// NO stage_ids
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: vec![InstanceUpdate {
|
|
id: module_id,
|
|
changed_name: None,
|
|
changed_class_name: None,
|
|
changed_properties: props,
|
|
changed_metadata: None,
|
|
}],
|
|
stage_ids: vec![],
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
thread::sleep(Duration::from_millis(1000));
|
|
|
|
assert!(
|
|
!git_is_staged(session.path(), "src/ModuleA.luau"),
|
|
"ModuleA should NOT be staged when not in stage_ids"
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Reconnect gets fresh metadata
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn reconnect_gets_fresh_git_metadata() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
let meta1 = session.get_api_git_metadata().unwrap().unwrap();
|
|
assert!(meta1.changed_ids.is_empty(), "No changes initially");
|
|
|
|
// Now modify a file on disk
|
|
fs::write(
|
|
session.path().join("src/ModuleA.luau"),
|
|
"-- changed after first connect",
|
|
)
|
|
.unwrap();
|
|
|
|
// Re-query the git metadata (simulates reconnect)
|
|
let meta2 = session.get_api_git_metadata().unwrap().unwrap();
|
|
assert!(
|
|
!meta2.changed_ids.is_empty(),
|
|
"Fresh connect should reflect new git changes"
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Edge cases
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn empty_repo_no_commits_returns_metadata() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |_path| {
|
|
// Don't commit anything -- repo has no HEAD
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
// Should still get metadata (untracked files)
|
|
// or None if git_changed_files returns None for no-HEAD repos
|
|
// Either way, should not panic
|
|
let _ = session.get_api_git_metadata();
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_files_changed_all_tracked() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/ModuleA.luau"), "-- mod a").unwrap();
|
|
fs::write(path.join("src/ServerScript.server.luau"), "-- mod server").unwrap();
|
|
fs::write(path.join("src/ClientScript.client.luau"), "-- mod client").unwrap();
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
let meta = session.get_api_git_metadata().unwrap().unwrap();
|
|
|
|
assert!(
|
|
meta.changed_ids.len() >= 3,
|
|
"Should have at least 3 changed instances, got {}",
|
|
meta.changed_ids.len()
|
|
);
|
|
assert!(
|
|
meta.script_committed_hashes.len() >= 3,
|
|
"Should have at least 3 script hashes, got {}",
|
|
meta.script_committed_hashes.len()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn init_style_script_staging_targets_init_file() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
fs::write(path.join("src/DirModule/init.luau"), "-- modified").unwrap();
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
if let Some((dir_id, _)) = find_instance_by_name(&read.instances, "DirModule") {
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: vec![],
|
|
stage_ids: vec![dir_id],
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
thread::sleep(Duration::from_millis(500));
|
|
|
|
assert!(
|
|
git_is_staged(session.path(), "src/DirModule/init.luau"),
|
|
"init.luau should be staged for init-style script"
|
|
);
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Batch staging: many files in a single write request
|
|
// ===========================================================================
|
|
|
|
const BATCH_SCRIPT_COUNT: usize = 20;
|
|
|
|
fn batch_script_name(i: usize) -> String {
|
|
format!("BatchScript{:02}", i)
|
|
}
|
|
|
|
fn batch_script_rel_path(i: usize) -> String {
|
|
format!("src/{}.luau", batch_script_name(i))
|
|
}
|
|
|
|
/// Poll until every `rel_paths` entry is staged, with exponential backoff.
|
|
/// Panics after `timeout` if any file is still unstaged.
|
|
fn wait_until_all_staged(dir: &Path, rel_paths: &[String], timeout: Duration) {
|
|
let start = std::time::Instant::now();
|
|
let mut interval = Duration::from_millis(200);
|
|
loop {
|
|
let all_staged = rel_paths.iter().all(|p| git_is_staged(dir, p));
|
|
if all_staged {
|
|
return;
|
|
}
|
|
if start.elapsed() >= timeout {
|
|
for p in rel_paths {
|
|
if !git_is_staged(dir, p) {
|
|
panic!(
|
|
"Timed out after {:?} waiting for staging: {} is not staged",
|
|
timeout, p
|
|
);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
thread::sleep(interval);
|
|
interval = (interval * 2).min(Duration::from_secs(2));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn batch_stage_20_files_no_index_lock_race() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
for i in 1..=BATCH_SCRIPT_COUNT {
|
|
fs::write(
|
|
path.join(batch_script_rel_path(i)),
|
|
format!("-- modified {}\nreturn {{}}\n", i),
|
|
)
|
|
.unwrap();
|
|
}
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
let mut stage_ids = Vec::new();
|
|
for i in 1..=BATCH_SCRIPT_COUNT {
|
|
if let Some((id, _)) = find_instance_by_name(&read.instances, &batch_script_name(i)) {
|
|
stage_ids.push(id);
|
|
}
|
|
}
|
|
assert!(
|
|
stage_ids.len() >= BATCH_SCRIPT_COUNT,
|
|
"Should find all {} batch scripts, found {}",
|
|
BATCH_SCRIPT_COUNT,
|
|
stage_ids.len()
|
|
);
|
|
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: vec![],
|
|
stage_ids,
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
|
|
let rel_paths: Vec<String> = (1..=BATCH_SCRIPT_COUNT)
|
|
.map(batch_script_rel_path)
|
|
.collect();
|
|
wait_until_all_staged(session.path(), &rel_paths, Duration::from_secs(30));
|
|
}
|
|
|
|
#[test]
|
|
fn batch_source_write_20_files_all_staged() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
let mut updates = Vec::new();
|
|
let mut stage_ids = Vec::new();
|
|
for i in 1..=BATCH_SCRIPT_COUNT {
|
|
if let Some((id, _)) = find_instance_by_name(&read.instances, &batch_script_name(i)) {
|
|
let mut props = UstrMap::default();
|
|
props.insert(
|
|
ustr("Source"),
|
|
Some(Variant::String(format!(
|
|
"-- batch pull {}\nreturn {{}}\n",
|
|
i
|
|
))),
|
|
);
|
|
updates.push(InstanceUpdate {
|
|
id,
|
|
changed_name: None,
|
|
changed_class_name: None,
|
|
changed_properties: props,
|
|
changed_metadata: None,
|
|
});
|
|
stage_ids.push(id);
|
|
}
|
|
}
|
|
assert!(
|
|
updates.len() >= BATCH_SCRIPT_COUNT,
|
|
"Should find all {} batch scripts, found {}",
|
|
BATCH_SCRIPT_COUNT,
|
|
updates.len()
|
|
);
|
|
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: updates,
|
|
stage_ids,
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
|
|
let rel_paths: Vec<String> = (1..=BATCH_SCRIPT_COUNT)
|
|
.map(batch_script_rel_path)
|
|
.collect();
|
|
wait_until_all_staged(session.path(), &rel_paths, Duration::from_secs(30));
|
|
|
|
for i in 1..=BATCH_SCRIPT_COUNT {
|
|
let rel = batch_script_rel_path(i);
|
|
let content = fs::read_to_string(session.path().join(&rel)).unwrap();
|
|
assert!(
|
|
content.contains(&format!("batch pull {}", i)),
|
|
"{} should have new content on disk",
|
|
batch_script_name(i)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn batch_mixed_stage_and_source_write() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
// Modify the first 10 files on disk (push-accepted: already on disk, just stage)
|
|
for i in 1..=10 {
|
|
fs::write(
|
|
path.join(batch_script_rel_path(i)),
|
|
format!("-- push modified {}\nreturn {{}}\n", i),
|
|
)
|
|
.unwrap();
|
|
}
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
let mut updates = Vec::new();
|
|
let mut stage_ids = Vec::new();
|
|
|
|
// Files 1-10: push-accepted (already modified on disk, just stage)
|
|
for i in 1..=10 {
|
|
if let Some((id, _)) = find_instance_by_name(&read.instances, &batch_script_name(i)) {
|
|
stage_ids.push(id);
|
|
}
|
|
}
|
|
|
|
// Files 11-20: pull-accepted (Source write + stage)
|
|
for i in 11..=BATCH_SCRIPT_COUNT {
|
|
if let Some((id, _)) = find_instance_by_name(&read.instances, &batch_script_name(i)) {
|
|
let mut props = UstrMap::default();
|
|
props.insert(
|
|
ustr("Source"),
|
|
Some(Variant::String(format!(
|
|
"-- pull source {}\nreturn {{}}\n",
|
|
i
|
|
))),
|
|
);
|
|
updates.push(InstanceUpdate {
|
|
id,
|
|
changed_name: None,
|
|
changed_class_name: None,
|
|
changed_properties: props,
|
|
changed_metadata: None,
|
|
});
|
|
stage_ids.push(id);
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
stage_ids.len() >= BATCH_SCRIPT_COUNT,
|
|
"Should have {} stage_ids, got {}",
|
|
BATCH_SCRIPT_COUNT,
|
|
stage_ids.len()
|
|
);
|
|
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: updates,
|
|
stage_ids,
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
|
|
let rel_paths: Vec<String> = (1..=BATCH_SCRIPT_COUNT)
|
|
.map(batch_script_rel_path)
|
|
.collect();
|
|
wait_until_all_staged(session.path(), &rel_paths, Duration::from_secs(30));
|
|
|
|
// Files 11-20 should have the new pull content
|
|
for i in 11..=BATCH_SCRIPT_COUNT {
|
|
let content = fs::read_to_string(session.path().join(batch_script_rel_path(i))).unwrap();
|
|
assert!(
|
|
content.contains(&format!("pull source {}", i)),
|
|
"{} should have pull content on disk",
|
|
batch_script_name(i)
|
|
);
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// stage_ids survives rename (Fix 2 from audit)
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn stage_ids_stages_new_path_after_rename() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
});
|
|
let info = session.wait_to_come_online();
|
|
let read = session.get_api_read(info.root_instance_id).unwrap();
|
|
|
|
let (module_id, _) =
|
|
find_instance_by_name(&read.instances, "ModuleA").expect("ModuleA should exist");
|
|
|
|
let old_path = session.path().join("src/ModuleA.luau");
|
|
let new_path = session.path().join("src/RenamedModule.luau");
|
|
assert!(old_path.exists(), "ModuleA.luau should exist before rename");
|
|
|
|
let write_request = WriteRequest {
|
|
session_id: info.session_id,
|
|
removed: vec![],
|
|
added: HashMap::new(),
|
|
updated: vec![InstanceUpdate {
|
|
id: module_id,
|
|
changed_name: Some("RenamedModule".to_string()),
|
|
changed_class_name: None,
|
|
changed_properties: UstrMap::default(),
|
|
changed_metadata: None,
|
|
}],
|
|
stage_ids: vec![module_id],
|
|
};
|
|
session.post_api_write(&write_request).unwrap();
|
|
thread::sleep(Duration::from_millis(1000));
|
|
|
|
assert!(
|
|
new_path.exists(),
|
|
"RenamedModule.luau should exist after rename"
|
|
);
|
|
assert!(
|
|
!old_path.exists(),
|
|
"ModuleA.luau should be gone after rename"
|
|
);
|
|
|
|
assert!(
|
|
git_is_staged(session.path(), "src/RenamedModule.luau"),
|
|
"New file (RenamedModule.luau) should be staged after rename + stage_ids"
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// initial_head: committed changes still visible after git commit
|
|
// ===========================================================================
|
|
|
|
#[test]
|
|
fn committed_changes_still_in_changed_ids_via_initial_head() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
});
|
|
let _info = session.wait_to_come_online();
|
|
let meta1 = session.get_api_git_metadata().unwrap().unwrap();
|
|
assert!(meta1.changed_ids.is_empty(), "No changes initially");
|
|
|
|
fs::write(
|
|
session.path().join("src/ModuleA.luau"),
|
|
"-- committed after session start",
|
|
)
|
|
.unwrap();
|
|
git_commit_all(session.path(), "edit after serve");
|
|
|
|
let meta2 = session.get_api_git_metadata().unwrap().unwrap();
|
|
assert!(
|
|
!meta2.changed_ids.is_empty(),
|
|
"Committed changes should still appear in changedIds via initial_head diff"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn committed_script_gets_initial_head_hash() {
|
|
let original_content = fixture_content("src/ModuleA.luau");
|
|
let modified_content = "-- committed v2\nreturn {}";
|
|
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
});
|
|
session.wait_to_come_online();
|
|
|
|
fs::write(session.path().join("src/ModuleA.luau"), modified_content).unwrap();
|
|
git_commit_all(session.path(), "edit after serve");
|
|
|
|
let meta2 = session.get_api_git_metadata().unwrap().unwrap();
|
|
let info2 = session.get_api_rojo().unwrap();
|
|
let read = session.get_api_read(info2.root_instance_id).unwrap();
|
|
|
|
if let Some((module_id, _)) = find_instance_by_name(&read.instances, "ModuleA") {
|
|
assert!(
|
|
meta2.changed_ids.contains(&module_id),
|
|
"Committed script should be in changedIds"
|
|
);
|
|
|
|
let hashes = meta2
|
|
.script_committed_hashes
|
|
.get(&module_id)
|
|
.expect("Committed script should have hashes");
|
|
|
|
let initial_hash = compute_blob_sha1(&original_content);
|
|
assert!(
|
|
hashes.contains(&initial_hash),
|
|
"Hashes should include the initial_head version ({initial_hash}), got {hashes:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_commits_after_session_start_all_tracked() {
|
|
let mut session = TestServeSession::new_with_git("git_sync_defaults", |path| {
|
|
git_commit_all(path, "initial commit");
|
|
});
|
|
session.wait_to_come_online();
|
|
|
|
fs::write(
|
|
session.path().join("src/ModuleA.luau"),
|
|
"-- commit 1 change",
|
|
)
|
|
.unwrap();
|
|
git_commit_all(session.path(), "commit 1");
|
|
|
|
fs::write(
|
|
session.path().join("src/ServerScript.server.luau"),
|
|
"-- commit 2 change",
|
|
)
|
|
.unwrap();
|
|
git_commit_all(session.path(), "commit 2");
|
|
|
|
let meta2 = session.get_api_git_metadata().unwrap().unwrap();
|
|
let info2 = session.get_api_rojo().unwrap();
|
|
let read = session.get_api_read(info2.root_instance_id).unwrap();
|
|
|
|
if let Some((module_id, _)) = find_instance_by_name(&read.instances, "ModuleA") {
|
|
assert!(
|
|
meta2.changed_ids.contains(&module_id),
|
|
"ModuleA (committed in first commit) should be in changedIds"
|
|
);
|
|
}
|
|
|
|
if let Some((server_id, _)) = find_instance_by_name(&read.instances, "ServerScript") {
|
|
assert!(
|
|
meta2.changed_ids.contains(&server_id),
|
|
"ServerScript (committed in second commit) should be in changedIds"
|
|
);
|
|
}
|
|
}
|