Skip to content

Commit 0e906db

Browse files
Merge pull request #121 from codeflash-ai/codex/slack-pull-recent-files
Fix plain-files pull for Slack recent files
2 parents 4f49d49 + 44f519c commit 0e906db

5 files changed

Lines changed: 923 additions & 56 deletions

File tree

crates/loc-cli/src/commands.rs

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ const EXIT_USAGE: i32 = 2;
129129
const EXIT_VALIDATION: i32 = 3;
130130
const DEFAULT_DAEMON_CONTROL_TIMEOUT: Duration = Duration::from_secs(5);
131131
const DEFAULT_DAEMON_MUTATING_TIMEOUT: Duration = Duration::from_secs(60);
132+
const DEFAULT_DAEMON_PULL_TIMEOUT: Duration = Duration::from_secs(2 * 60 * 60);
132133
const MIN_SLACK_HISTORY_LIMIT: u32 = 1;
133134
const MAX_SLACK_HISTORY_LIMIT: u32 = 15;
134135
const SLACK_CONVERSATION_TYPE_VALUES: &str = "public_channel,private_channel,im,mpim";
@@ -8197,7 +8198,7 @@ fn pull_direct_fallback_error(
81978198
"daemon_timeout",
81988199
format!(
81998200
"localityd did not respond within {}ms after the pull request was submitted; refusing direct fallback to avoid racing daemon hydration",
8200-
daemon_mutating_request_timeout().as_millis()
8201+
daemon_pull_request_timeout().as_millis()
82018202
),
82028203
)
82038204
.with_suggested_command("loc daemon restart"),
@@ -8502,11 +8503,19 @@ fn daemon_mutating_request_timeout() -> Duration {
85028503
.unwrap_or(DEFAULT_DAEMON_MUTATING_TIMEOUT)
85038504
}
85048505

8506+
fn daemon_pull_request_timeout() -> Duration {
8507+
std::env::var("LOCALITY_DAEMON_REQUEST_TIMEOUT_MS")
8508+
.ok()
8509+
.and_then(|value| value.parse::<u64>().ok())
8510+
.filter(|value| *value > 0)
8511+
.map(Duration::from_millis)
8512+
.unwrap_or(DEFAULT_DAEMON_PULL_TIMEOUT)
8513+
}
8514+
85058515
fn daemon_request_timeout_for(request: &DaemonRequest) -> Duration {
85068516
match request {
8507-
DaemonRequest::Pull { .. } | DaemonRequest::Push { .. } => {
8508-
daemon_mutating_request_timeout()
8509-
}
8517+
DaemonRequest::Pull { .. } => daemon_pull_request_timeout(),
8518+
DaemonRequest::Push { .. } => daemon_mutating_request_timeout(),
85108519
_ => daemon_request_timeout(),
85118520
}
85128521
}
@@ -9457,14 +9466,15 @@ mod tests {
94579466
#[cfg(target_os = "windows")]
94589467
use super::resolve_mount_target;
94599468
use super::{
9460-
Cli, ConnectReport, DaemonUnavailableReason, EXIT_SUCCESS, EXIT_USAGE, EXIT_VALIDATION,
9461-
FileProviderCommandReport, PushConfirmationPromptError, SLACK_CONNECTOR_ID,
9462-
VirtualProjectionRegistration, absolute_command_path,
9463-
auto_registration_for_mounted_projection, default_mount_id_for_source,
9464-
diff_report_exit_code, exact_located_entity_record, file_provider_list_lines,
9465-
google_calendar_oauth_broker_config, google_docs_oauth_broker_config,
9466-
guard_linux_fuse_shared_root_unregister, guard_unresolved_linux_fuse_unregister,
9467-
guard_unresolved_windows_cloud_files_unregister,
9469+
Cli, ConnectReport, DEFAULT_DAEMON_CONTROL_TIMEOUT, DEFAULT_DAEMON_MUTATING_TIMEOUT,
9470+
DEFAULT_DAEMON_PULL_TIMEOUT, DaemonRequest, DaemonUnavailableReason, EXIT_SUCCESS,
9471+
EXIT_USAGE, EXIT_VALIDATION, FileProviderCommandReport, PushConfirmationPromptError,
9472+
SLACK_CONNECTOR_ID, VirtualProjectionRegistration, absolute_command_path,
9473+
auto_registration_for_mounted_projection, daemon_request_timeout_for,
9474+
default_mount_id_for_source, diff_report_exit_code, exact_located_entity_record,
9475+
file_provider_list_lines, google_calendar_oauth_broker_config,
9476+
google_docs_oauth_broker_config, guard_linux_fuse_shared_root_unregister,
9477+
guard_unresolved_linux_fuse_unregister, guard_unresolved_windows_cloud_files_unregister,
94689478
guard_windows_cloud_files_shared_root_unregister, legacy_args_for_command,
94699479
locality_error_code, locate_result_from_report, mount_slack, mount_usage,
94709480
mounted_projection_preflight_error, notion_authorize_url, notion_oauth_broker_config,
@@ -11328,6 +11338,40 @@ mod tests {
1132811338
let _ = fs::remove_dir_all(&other_root);
1132911339
}
1133011340

11341+
#[test]
11342+
fn daemon_request_timeout_for_uses_pull_specific_default() {
11343+
let _lock = state_root_env_lock()
11344+
.lock()
11345+
.expect("daemon timeout env lock");
11346+
let previous = std::env::var_os("LOCALITY_DAEMON_REQUEST_TIMEOUT_MS");
11347+
unsafe {
11348+
std::env::remove_var("LOCALITY_DAEMON_REQUEST_TIMEOUT_MS");
11349+
}
11350+
11351+
let pull_timeout = daemon_request_timeout_for(&DaemonRequest::Pull {
11352+
path: PathBuf::from("/tmp/locality-pull"),
11353+
});
11354+
let push_timeout = daemon_request_timeout_for(&DaemonRequest::Push {
11355+
path: PathBuf::from("/tmp/locality-push"),
11356+
assume_yes: false,
11357+
confirm_dangerous: false,
11358+
});
11359+
let status_timeout = daemon_request_timeout_for(&DaemonRequest::Status);
11360+
11361+
match previous {
11362+
Some(value) => unsafe {
11363+
std::env::set_var("LOCALITY_DAEMON_REQUEST_TIMEOUT_MS", value);
11364+
},
11365+
None => unsafe {
11366+
std::env::remove_var("LOCALITY_DAEMON_REQUEST_TIMEOUT_MS");
11367+
},
11368+
}
11369+
11370+
assert_eq!(pull_timeout, DEFAULT_DAEMON_PULL_TIMEOUT);
11371+
assert_eq!(push_timeout, DEFAULT_DAEMON_MUTATING_TIMEOUT);
11372+
assert_eq!(status_timeout, DEFAULT_DAEMON_CONTROL_TIMEOUT);
11373+
}
11374+
1133111375
#[test]
1133211376
fn pull_direct_fallback_refuses_timeout_and_virtual_mount_without_daemon() {
1133311377
let virtual_mount =

crates/loc-cli/tests/pull.rs

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ use locality_notion::dto::{
2323
TitleBlockDto,
2424
};
2525
use locality_notion::{NotionConfig, NotionConnector};
26+
use locality_slack::{
27+
SlackApi, SlackAuthTestResponse, SlackConfig, SlackConnector, SlackConversation,
28+
SlackConversationsListResponse, SlackHistoryResponse, SlackJoinResponse, SlackMessage,
29+
SlackResponseMetadata, SlackUser, SlackUserProfile, SlackUsersListResponse,
30+
};
2631
use locality_store::{
2732
EntityRecord, EntityRepository, FreshnessStateRecord, FreshnessStateRepository,
2833
InMemoryStateStore, MountConfig, MountRepository, ProjectionMode, RemoteObservationRecord,
@@ -195,6 +200,78 @@ fn pull_virtual_file_target_does_not_stat_projection_path_as_directory() {
195200
let _ = fs::remove_dir_all(&fixture.root);
196201
}
197202

203+
#[test]
204+
fn pull_slack_channel_bucket_hydrates_recent_messages() {
205+
let root = unique_temp_path("loc-cli-pull-slack");
206+
fs::create_dir_all(&root).expect("create slack mount root");
207+
let mount_id = MountId::new("slack-main");
208+
let mut store = InMemoryStateStore::new();
209+
store
210+
.save_mount(MountConfig::new(mount_id.clone(), "slack", &root).read_only(true))
211+
.expect("save Slack mount");
212+
let connector = SlackConnector::with_api(
213+
SlackConfig::new("xoxb-test"),
214+
Arc::new(PullSlackApi::default()),
215+
);
216+
217+
run_pull(&mut store, &connector, &root).expect("initial Slack root pull");
218+
assert!(root.join("channels/general-C123").is_dir());
219+
220+
let report =
221+
run_pull(&mut store, &connector, root.join("channels")).expect("pull Slack channel bucket");
222+
223+
assert!(report.ok);
224+
assert_eq!(report.hydrated, 1);
225+
let recent_path = root.join("channels/general-C123/recent.md");
226+
let recent = fs::read_to_string(&recent_path).expect("read Slack recent.md");
227+
assert!(recent.contains("id: \"slack-recent:C123\""));
228+
assert!(recent.contains("Hello from Slack, @Grace Hopper"));
229+
let recent_entity = store
230+
.find_entity_by_path(
231+
&mount_id,
232+
std::path::Path::new("channels/general-C123/recent.md"),
233+
)
234+
.expect("find Slack recent entity")
235+
.expect("Slack recent entity");
236+
assert_eq!(recent_entity.hydration, HydrationState::Hydrated);
237+
238+
let _ = fs::remove_dir_all(root);
239+
}
240+
241+
#[test]
242+
fn pull_slack_channel_bucket_hydrates_all_recent_messages() {
243+
let root = unique_temp_path("loc-cli-pull-slack-large");
244+
fs::create_dir_all(&root).expect("create slack mount root");
245+
let mount_id = MountId::new("slack-main");
246+
let mut store = InMemoryStateStore::new();
247+
store
248+
.save_mount(MountConfig::new(mount_id, "slack", &root).read_only(true))
249+
.expect("save Slack mount");
250+
let history_calls = Arc::new(AtomicU64::new(0));
251+
let connector = SlackConnector::with_api(
252+
SlackConfig::new("xoxb-test"),
253+
Arc::new(PullSlackApi::with_channel_count(2, history_calls.clone())),
254+
);
255+
256+
run_pull(&mut store, &connector, &root).expect("initial Slack root pull");
257+
let report =
258+
run_pull(&mut store, &connector, root.join("channels")).expect("pull Slack channel bucket");
259+
260+
assert!(report.ok);
261+
assert_eq!(report.hydrated, 2);
262+
assert_eq!(history_calls.load(Ordering::Relaxed), 2);
263+
for recent_path in [
264+
root.join("channels/general-C123/recent.md"),
265+
root.join("channels/alerts-C124/recent.md"),
266+
] {
267+
let recent = fs::read_to_string(&recent_path).expect("read Slack recent.md");
268+
assert!(!recent.contains(CanonicalDocument::STUB_MARKER), "{recent}");
269+
assert!(recent.contains("Hello from Slack, @Grace Hopper"));
270+
}
271+
272+
let _ = fs::remove_dir_all(root);
273+
}
274+
198275
#[cfg(target_os = "macos")]
199276
#[test]
200277
fn pull_macos_file_provider_accepts_system_assigned_connector_roots() {
@@ -2843,3 +2920,167 @@ fn rich_text(text: &str) -> RichTextDto {
28432920
annotations: Default::default(),
28442921
}
28452922
}
2923+
2924+
#[derive(Debug)]
2925+
struct PullSlackApi {
2926+
conversations: Vec<SlackConversation>,
2927+
history_calls: Arc<AtomicU64>,
2928+
}
2929+
2930+
impl Default for PullSlackApi {
2931+
fn default() -> Self {
2932+
Self::with_channel_count(1, Arc::new(AtomicU64::new(0)))
2933+
}
2934+
}
2935+
2936+
impl PullSlackApi {
2937+
fn with_channel_count(count: usize, history_calls: Arc<AtomicU64>) -> Self {
2938+
Self {
2939+
conversations: (0..count).map(Self::conversation).collect(),
2940+
history_calls,
2941+
}
2942+
}
2943+
2944+
fn conversation(index: usize) -> SlackConversation {
2945+
let id = match index {
2946+
0 => "C123".to_string(),
2947+
1 => "C124".to_string(),
2948+
_ => format!("C{}", 123 + index),
2949+
};
2950+
let name = match index {
2951+
0 => "general".to_string(),
2952+
1 => "alerts".to_string(),
2953+
_ => format!("channel-{index}"),
2954+
};
2955+
SlackConversation {
2956+
id,
2957+
name: Some(name),
2958+
is_channel: true,
2959+
is_member: Some(true),
2960+
updated: Some(1780000000000000),
2961+
topic: None,
2962+
purpose: None,
2963+
..Default::default()
2964+
}
2965+
}
2966+
2967+
fn users() -> Vec<SlackUser> {
2968+
vec![
2969+
SlackUser {
2970+
id: "U123".to_string(),
2971+
name: Some("ada".to_string()),
2972+
real_name: Some("Ada Lovelace".to_string()),
2973+
profile: Some(SlackUserProfile {
2974+
real_name: Some("Ada Lovelace".to_string()),
2975+
display_name: Some("Ada".to_string()),
2976+
..Default::default()
2977+
}),
2978+
..Default::default()
2979+
},
2980+
SlackUser {
2981+
id: "U456".to_string(),
2982+
name: Some("grace".to_string()),
2983+
real_name: Some("Grace Hopper".to_string()),
2984+
profile: Some(SlackUserProfile {
2985+
real_name: Some("Grace Hopper".to_string()),
2986+
display_name: Some("Grace Hopper".to_string()),
2987+
..Default::default()
2988+
}),
2989+
..Default::default()
2990+
},
2991+
]
2992+
}
2993+
}
2994+
2995+
impl SlackApi for PullSlackApi {
2996+
fn auth_test(&self) -> locality_core::LocalityResult<SlackAuthTestResponse> {
2997+
Ok(SlackAuthTestResponse {
2998+
ok: true,
2999+
team: Some("Test".to_string()),
3000+
..Default::default()
3001+
})
3002+
}
3003+
3004+
fn conversations_list(
3005+
&self,
3006+
types: &str,
3007+
_cursor: Option<&str>,
3008+
_limit: u32,
3009+
) -> locality_core::LocalityResult<SlackConversationsListResponse> {
3010+
let channels = if types.split(',').any(|value| value == "public_channel") {
3011+
self.conversations.clone()
3012+
} else {
3013+
Vec::new()
3014+
};
3015+
Ok(SlackConversationsListResponse {
3016+
ok: true,
3017+
channels,
3018+
response_metadata: SlackResponseMetadata::default(),
3019+
..Default::default()
3020+
})
3021+
}
3022+
3023+
fn conversations_history(
3024+
&self,
3025+
channel: &str,
3026+
_cursor: Option<&str>,
3027+
_limit: u32,
3028+
) -> locality_core::LocalityResult<SlackHistoryResponse> {
3029+
assert!(
3030+
self.conversations
3031+
.iter()
3032+
.any(|conversation| conversation.id == channel)
3033+
);
3034+
self.history_calls.fetch_add(1, Ordering::Relaxed);
3035+
Ok(SlackHistoryResponse {
3036+
ok: true,
3037+
messages: vec![SlackMessage {
3038+
r#type: Some("message".to_string()),
3039+
user: Some("U123".to_string()),
3040+
text: "Hello from Slack, <@U456>".to_string(),
3041+
ts: "1780000000.000100".to_string(),
3042+
..Default::default()
3043+
}],
3044+
response_metadata: SlackResponseMetadata::default(),
3045+
..Default::default()
3046+
})
3047+
}
3048+
3049+
fn conversations_replies(
3050+
&self,
3051+
_channel: &str,
3052+
_thread_ts: &str,
3053+
_cursor: Option<&str>,
3054+
_limit: u32,
3055+
) -> locality_core::LocalityResult<SlackHistoryResponse> {
3056+
Ok(SlackHistoryResponse {
3057+
ok: true,
3058+
response_metadata: SlackResponseMetadata::default(),
3059+
..Default::default()
3060+
})
3061+
}
3062+
3063+
fn conversations_join(
3064+
&self,
3065+
_channel: &str,
3066+
) -> locality_core::LocalityResult<SlackJoinResponse> {
3067+
Ok(SlackJoinResponse {
3068+
ok: true,
3069+
channel: self.conversations.first().cloned(),
3070+
..Default::default()
3071+
})
3072+
}
3073+
3074+
fn users_list(
3075+
&self,
3076+
_cursor: Option<&str>,
3077+
_limit: u32,
3078+
) -> locality_core::LocalityResult<SlackUsersListResponse> {
3079+
Ok(SlackUsersListResponse {
3080+
ok: true,
3081+
members: Self::users(),
3082+
response_metadata: SlackResponseMetadata::default(),
3083+
..Default::default()
3084+
})
3085+
}
3086+
}

0 commit comments

Comments
 (0)