mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
Steven Enamakel
parent
e1a8f16778
commit
d666ce61d7
@@ -34,12 +34,6 @@ describe('OpenhumanLinkModal notifications test flow', () => {
|
||||
});
|
||||
}
|
||||
|
||||
async function flushAsyncWork() {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
it('shows success after permission is granted and native notification send succeeds', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(true);
|
||||
vi.mocked(ensureNotificationPermission).mockResolvedValue(true);
|
||||
@@ -69,9 +63,10 @@ describe('OpenhumanLinkModal notifications test flow', () => {
|
||||
openNotificationsModal();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send test notification' }));
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(screen.getByText(/Notification permission is off\./i)).toBeInTheDocument();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/Notification permission is off\./i)).toBeInTheDocument()
|
||||
);
|
||||
expect(showNativeNotification).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('button', { name: 'Retry test notification' })).toBeInTheDocument();
|
||||
});
|
||||
@@ -89,11 +84,12 @@ describe('OpenhumanLinkModal notifications test flow', () => {
|
||||
openNotificationsModal();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send test notification' }));
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(
|
||||
screen.getByText(/Couldn't send: notification show failed: test error/i)
|
||||
).toBeInTheDocument();
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText(/Couldn't send: notification show failed: test error/i)
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('retries successfully after user grants permission on a second attempt', async () => {
|
||||
@@ -111,9 +107,10 @@ describe('OpenhumanLinkModal notifications test flow', () => {
|
||||
openNotificationsModal();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send test notification' }));
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(screen.getByText(/Notification permission is off\./i)).toBeInTheDocument();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/Notification permission is off\./i)).toBeInTheDocument()
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Retry test notification' }));
|
||||
|
||||
|
||||
@@ -315,6 +315,54 @@ fn reshape_fetch_emails(data: &mut Value) {
|
||||
*container = Value::Object(envelope);
|
||||
}
|
||||
|
||||
/// Parse an RFC 3339 or RFC 2822 date string into a UTC `DateTime`.
|
||||
pub(crate) fn parse_email_date(date_str: &str) -> Option<chrono::DateTime<chrono::Utc>> {
|
||||
date_str
|
||||
.parse::<chrono::DateTime<chrono::Utc>>()
|
||||
.or_else(|_| {
|
||||
chrono::DateTime::parse_from_rfc2822(date_str).map(|d| d.with_timezone(&chrono::Utc))
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
const EMAIL_LOCAL_TIME_FMT: &str = "%Y-%m-%d %I:%M %p %:z";
|
||||
|
||||
/// Format a UTC `DateTime` in the given timezone. Returns `None` when the
|
||||
/// formatted result is identical to the UTC rendering (no-op for UTC hosts).
|
||||
pub(crate) fn format_at_tz<Tz: chrono::TimeZone>(
|
||||
utc: chrono::DateTime<chrono::Utc>,
|
||||
tz: &Tz,
|
||||
) -> Option<String>
|
||||
where
|
||||
Tz::Offset: std::fmt::Display,
|
||||
{
|
||||
let local_dt = utc.with_timezone(tz);
|
||||
let formatted = local_dt.format(EMAIL_LOCAL_TIME_FMT).to_string();
|
||||
|
||||
let utc_formatted = utc.format(EMAIL_LOCAL_TIME_FMT).to_string();
|
||||
if formatted == utc_formatted {
|
||||
return None;
|
||||
}
|
||||
Some(formatted)
|
||||
}
|
||||
|
||||
/// Convert a UTC email timestamp string to a human-readable local-time string.
|
||||
///
|
||||
/// Accepts RFC 3339 (`"2026-05-31T10:33:00Z"`) or RFC 2822
|
||||
/// (`"Sat, 31 May 2026 10:33:00 +0000"`) input. Returns a formatted string
|
||||
/// in the host's local timezone, e.g. `"2026-05-31 05:33 AM -05:00"`,
|
||||
/// so the agent can present local times without UTC arithmetic.
|
||||
///
|
||||
/// The raw `date` field is always preserved alongside this field so
|
||||
/// internal sorting, deduplication, and debugging remain UTC-based.
|
||||
///
|
||||
/// Returns `None` when the input cannot be parsed or the output format
|
||||
/// would be identical to the UTC input (no-op for UTC hosts).
|
||||
pub(crate) fn format_email_local_time(date_str: &str) -> Option<String> {
|
||||
let utc = parse_email_date(date_str)?;
|
||||
format_at_tz(utc, &chrono::Local)
|
||||
}
|
||||
|
||||
/// Map one raw Composio message object to its slim counterpart.
|
||||
///
|
||||
/// Body source picked by [`extract_markdown_body`]:
|
||||
@@ -346,6 +394,10 @@ fn reshape_message(raw: Value) -> Value {
|
||||
let markdown = extract_markdown_body(&obj);
|
||||
let attachments = extract_attachments(&obj);
|
||||
|
||||
// Compute a local-time representation of the UTC `date` so the agent
|
||||
// presents times in the user's timezone rather than quoting raw UTC.
|
||||
let date_local = date.as_str().and_then(format_email_local_time);
|
||||
|
||||
let mut out = Map::new();
|
||||
out.insert("id".into(), id);
|
||||
out.insert("threadId".into(), thread_id);
|
||||
@@ -353,6 +405,9 @@ fn reshape_message(raw: Value) -> Value {
|
||||
out.insert("from".into(), sender);
|
||||
out.insert("to".into(), to);
|
||||
out.insert("date".into(), date);
|
||||
if let Some(local) = date_local {
|
||||
out.insert("date_local".into(), Value::String(local));
|
||||
}
|
||||
out.insert("labels".into(), labels);
|
||||
if !list_unsubscribe.is_null() {
|
||||
out.insert("list_unsubscribe".into(), list_unsubscribe);
|
||||
|
||||
@@ -275,6 +275,63 @@ fn split_with_hint_skips_messages_with_blank_subject() {
|
||||
assert_eq!(slices.len(), 2);
|
||||
}
|
||||
|
||||
// ── format_email_local_time ──────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn format_email_local_time_returns_none_for_unparseable_date() {
|
||||
assert!(super::format_email_local_time("not-a-date").is_none());
|
||||
assert!(super::format_email_local_time("").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_email_local_time_preserves_utc_raw_date_in_reshape() {
|
||||
let mut v = json!({
|
||||
"messages": [{
|
||||
"messageId": "m1",
|
||||
"threadId": "t1",
|
||||
"subject": "Test",
|
||||
"sender": "a@example.com",
|
||||
"to": "b@example.com",
|
||||
"messageTimestamp": "2026-05-31T10:33:00Z",
|
||||
"labelIds": [],
|
||||
"messageText": "body",
|
||||
"payload": {}
|
||||
}]
|
||||
});
|
||||
post_process("GMAIL_FETCH_EMAILS", None, &mut v);
|
||||
let msg = &v["messages"][0];
|
||||
assert_eq!(msg["date"], "2026-05-31T10:33:00Z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_email_date_accepts_rfc3339_and_rfc2822() {
|
||||
assert!(super::parse_email_date("2026-05-31T10:33:00Z").is_some());
|
||||
assert!(super::parse_email_date("Sun, 31 May 2026 10:33:00 +0000").is_some());
|
||||
assert!(super::parse_email_date("not-a-date").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_at_tz_deterministic_with_fixed_offset() {
|
||||
use chrono::FixedOffset;
|
||||
|
||||
let utc = super::parse_email_date("2026-05-31T10:33:00Z").unwrap();
|
||||
|
||||
let est = FixedOffset::west_opt(5 * 3600).unwrap();
|
||||
let result = super::format_at_tz(utc, &est).unwrap();
|
||||
assert_eq!(result, "2026-05-31 05:33 AM -05:00");
|
||||
|
||||
let ist = FixedOffset::east_opt(5 * 3600 + 1800).unwrap();
|
||||
let result = super::format_at_tz(utc, &ist).unwrap();
|
||||
assert_eq!(result, "2026-05-31 04:03 PM +05:30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_at_tz_returns_none_for_utc() {
|
||||
let utc = super::parse_email_date("2026-05-31T10:33:00Z").unwrap();
|
||||
let utc_tz = chrono::FixedOffset::east_opt(0).unwrap();
|
||||
assert!(super::format_at_tz(utc, &utc_tz).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_response_level_markdown_stashes_per_message_field() {
|
||||
let mut data = json!({
|
||||
|
||||
Reference in New Issue
Block a user