Refactors the contacts system from being embedded in cal_tool.py into a standalone contacts skill that serves as the single source of truth for recipient validation across all outbound email paths. - New skills/contacts/ skill: list, add, delete, resolve commands - New skills/himalaya/scripts/himalaya.sh wrapper: validates To/Cc/Bcc recipients against contacts for message send, template send, and message write commands; passes everything else through unchanged - cal_tool.py now delegates to contacts.py resolve instead of inline logic - TOOLS.md updated: agent should use himalaya wrapper, not raw himalaya
18 KiB
Testing the Calendar Skill
End-to-end tests for contacts, send, reply, todo, calendar sync, and local calendar. All commands use --dry-run first, then live.
Important: Tests 1-3 (contacts) must run first — send requires recipients to be in the contacts list.
SKILL_DIR=~/.openclaw/workspace/skills/calendar
CONTACTS_DIR=~/.openclaw/workspace/skills/contacts
# Use a date 3 days from now for test events
TEST_DATE=$(date -d "+3 days" +%Y-%m-%d)
1. Contact Add and List
Set up test contacts needed for send tests.
# Add a contact with a single email
$CONTACTS_DIR/scripts/contacts.sh add --name "测试用户" --email "mail@luyx.org"
# List contacts
$CONTACTS_DIR/scripts/contacts.sh list
# Add a contact with typed email and nickname
$CONTACTS_DIR/scripts/contacts.sh add --name "测试多邮箱" --email "work@example.com" --type work --nickname "多邮箱"
# Add a second email to the same contact
$CONTACTS_DIR/scripts/contacts.sh add --name "测试多邮箱" --email "home@example.com" --type home
# List again — should show both emails
$CONTACTS_DIR/scripts/contacts.sh list
Verify:
contact addprints "Added contact: ..."- Second
contact addprints "Updated contact: ... — added ..." contact listshows all contacts with email types.vcffiles created in~/.openclaw/workspace/contacts/default/
2. Recipient Resolution (Send Validation)
Test that send --to correctly resolves contacts and rejects unknown addresses.
# Name resolves (single email contact) — should work
$SKILL_DIR/scripts/calendar.sh send \
--to "测试用户" \
--subject "Resolve Test" --summary "Resolve Test" \
--start "${TEST_DATE}T15:00:00" --end "${TEST_DATE}T16:00:00" \
--dry-run
# Name:type resolves — should work
$SKILL_DIR/scripts/calendar.sh send \
--to "测试多邮箱:work" \
--subject "Resolve Test" --summary "Resolve Test" \
--start "${TEST_DATE}T15:00:00" --end "${TEST_DATE}T16:00:00" \
--dry-run
# Nickname resolves — should work
$SKILL_DIR/scripts/calendar.sh send \
--to "多邮箱:home" \
--subject "Resolve Test" --summary "Resolve Test" \
--start "${TEST_DATE}T15:00:00" --end "${TEST_DATE}T16:00:00" \
--dry-run
# Known raw email resolves — should work
$SKILL_DIR/scripts/calendar.sh send \
--to "mail@luyx.org" \
--subject "Resolve Test" --summary "Resolve Test" \
--start "${TEST_DATE}T15:00:00" --end "${TEST_DATE}T16:00:00" \
--dry-run
# Unknown email REJECTED — should FAIL
$SKILL_DIR/scripts/calendar.sh send \
--to "xiaojuzi@meta.com" \
--subject "Resolve Test" --summary "Resolve Test" \
--start "${TEST_DATE}T15:00:00" --end "${TEST_DATE}T16:00:00" \
--dry-run
# Multi-email without type REJECTED — should FAIL (ambiguous)
$SKILL_DIR/scripts/calendar.sh send \
--to "测试多邮箱" \
--subject "Resolve Test" --summary "Resolve Test" \
--start "${TEST_DATE}T15:00:00" --end "${TEST_DATE}T16:00:00" \
--dry-run
# Unknown name REJECTED — should FAIL
$SKILL_DIR/scripts/calendar.sh send \
--to "不存在的人" \
--subject "Resolve Test" --summary "Resolve Test" \
--start "${TEST_DATE}T15:00:00" --end "${TEST_DATE}T16:00:00" \
--dry-run
Verify:
- First 4 commands succeed (show ICS output)
- Unknown email fails with "not found in contacts" + available contacts list
- Multi-email without type fails with "has multiple emails. Specify type"
- Unknown name fails with "not found" + available contacts list
3. Contact Delete
# Delete the multi-email test contact
$CONTACTS_DIR/scripts/contacts.sh delete --name "测试多邮箱"
# Verify it's gone
$CONTACTS_DIR/scripts/contacts.sh list
# Delete by nickname — should fail (contact already deleted)
$CONTACTS_DIR/scripts/contacts.sh delete --name "多邮箱"
Verify:
- Delete prints "Deleted contact: 测试多邮箱"
contact listno longer shows that contact- Second delete fails with "No contact matching"
.vcffile removed from contacts dir
4. Dry Run: Send Invite
Prerequisite: "测试用户" contact from test 1 must exist.
Generates the ICS and MIME email without sending. Check that:
- ICS has
METHOD:REQUEST - MIME has
Content-Type: text/calendar; method=REQUEST - Only
--torecipients appear as attendees - Times and timezone look correct
- ICS has
BEGIN:VALARMwith correctTRIGGERduration
# Default alarm (1 day before)
$SKILL_DIR/scripts/calendar.sh send \
--to "测试用户" \
--subject "Test Invite" \
--summary "Test Event" \
--start "${TEST_DATE}T15:00:00" \
--end "${TEST_DATE}T16:00:00" \
--dry-run
# Custom alarm (1 hour before)
$SKILL_DIR/scripts/calendar.sh send \
--to "测试用户" \
--subject "Test Invite (1h alarm)" \
--summary "Test Event (1h alarm)" \
--start "${TEST_DATE}T15:00:00" \
--end "${TEST_DATE}T16:00:00" \
--alarm 1h \
--dry-run
5. Live Send: Self-Invite
Send a real invite to mail@luyx.org only (no confirmation needed per email rules).
$SKILL_DIR/scripts/calendar.sh send \
--to "测试用户" \
--subject "Calendar Skill Test" \
--summary "Calendar Skill Test" \
--start "${TEST_DATE}T15:00:00" \
--end "${TEST_DATE}T16:00:00" \
--location "Test Location"
Verify:
- Script exits without error
- Email arrives at
mail@luyx.org - Email shows Accept/Decline/Tentative buttons (not just an attachment)
.icsfile saved to~/.openclaw/workspace/calendars/home/
6. Verify Calendar Sync and Local Calendar
After sending in step 2, check that the event synced and appears locally.
# Check vdirsyncer sync ran (should have printed "Synced to CalDAV server" in step 2)
# If not, run manually:
vdirsyncer sync
# List .ics files in local calendar
ls ~/.openclaw/workspace/calendars/home/
# Check the event shows up in khal
khal list "$TEST_DATE"
Verify:
vdirsyncer synccompletes without errors.icsfile exists in~/.openclaw/workspace/calendars/home/khal listshows "Calendar Skill Test" on the test date
7. Reply: Accept the Self-Invite
The invite sent in step 2 should be in the inbox. Find it, then accept it. This tests the full reply flow without needing an external sender.
# Find the test invite in inbox
himalaya envelope list
# Confirm it's the calendar invite
himalaya message read <envelope-id>
# Accept it
$SKILL_DIR/scripts/calendar.sh reply \
--envelope-id <envelope-id> \
--action accept
Verify:
- Reply sent to organizer (youlu@luyanxin.com, i.e. ourselves)
- Event saved to
~/.openclaw/workspace/calendars/home/ vdirsyncer syncrankhal list "$TEST_DATE"still shows the event
8. Reply: Decline an Invite
Send another self-invite, then decline it. This verifies decline removes the event from local calendar.
# Send a second test invite
$SKILL_DIR/scripts/calendar.sh send \
--to "测试用户" \
--subject "Decline Test" \
--summary "Decline Test Event" \
--start "${TEST_DATE}T17:00:00" \
--end "${TEST_DATE}T18:00:00"
# Find it in inbox
himalaya envelope list
# Decline it
$SKILL_DIR/scripts/calendar.sh reply \
--envelope-id <envelope-id> \
--action decline \
--comment "Testing decline flow."
Verify:
- Reply sent to organizer with comment
- Event removed from local calendar
khal list "$TEST_DATE"does NOT show "Decline Test Event"
9. Verify Final Calendar State
After all tests, confirm the calendar is in a clean state.
# Sync one more time
vdirsyncer sync
# Only the accepted event should remain
khal list "$TEST_DATE"
# List all upcoming events
khal list today 7d
10. Dry Run: Add Todo
Generates the VTODO ICS without saving. Check that:
- ICS has
BEGIN:VTODO - ICS has correct
PRIORITYvalue (1 for high) - ICS has
STATUS:NEEDS-ACTION - ICS has
BEGIN:VALARM
$SKILL_DIR/scripts/calendar.sh todo add \
--summary "Test Todo" \
--due "$TEST_DATE" \
--priority high \
--dry-run
11. Live Add: Create a Todo
$SKILL_DIR/scripts/calendar.sh todo add \
--summary "Test Todo" \
--due "$TEST_DATE" \
--priority medium \
--description "Test description"
Verify:
- Script exits without error
.icsfile created in~/.openclaw/workspace/calendars/tasks/todo list(todoman directly) shows "Test Todo"vdirsyncer syncran
12. List Todos
# Via our wrapper (formatted Chinese output)
$SKILL_DIR/scripts/calendar.sh todo list
# Via todoman directly (should show the same items)
todo list
# Include completed
$SKILL_DIR/scripts/calendar.sh todo list --all
Verify:
- "Test Todo" appears in both outputs
- Priority grouping is correct in wrapper output
--allflag works (same output when none are completed)
13. Edit a Todo
Change the due date and priority of the test todo from step 8.
# Edit due date
$SKILL_DIR/scripts/calendar.sh todo edit --match "Test Todo" --due "$(date -d '+5 days' +%Y-%m-%d)"
# Verify change
$SKILL_DIR/scripts/calendar.sh todo list
Verify:
- Script exits without error
- Output shows "Updated todo: Test Todo" with the change
todo listshows the new due datevdirsyncer syncran
# Edit priority
$SKILL_DIR/scripts/calendar.sh todo edit --match "Test Todo" --priority high
# Verify change
$SKILL_DIR/scripts/calendar.sh todo list
Verify:
- Priority changed to high
- Todo appears under the high priority group in formatted output
# Edit multiple fields at once
$SKILL_DIR/scripts/calendar.sh todo edit --match "Test Todo" --due "$TEST_DATE" --priority low
Verify:
- Both due date and priority updated in one command
# Error: no matching todo
$SKILL_DIR/scripts/calendar.sh todo edit --match "nonexistent" --due "$TEST_DATE"
# Should print error and exit non-zero
# Error: no fields to edit
$SKILL_DIR/scripts/calendar.sh todo edit --match "Test Todo"
# Should print "Nothing to change" message
14. Complete a Todo
$SKILL_DIR/scripts/calendar.sh todo complete --match "Test Todo"
Verify:
todo list(todoman) — "Test Todo" no longer appears$SKILL_DIR/scripts/calendar.sh todo list— also gone$SKILL_DIR/scripts/calendar.sh todo list --all— appears as completed (with checkmark)vdirsyncer syncran
15. Delete a Todo
Create a second test todo, then delete it.
# Create
$SKILL_DIR/scripts/calendar.sh todo add \
--summary "Delete Me Todo" \
--due "$TEST_DATE" \
--priority low
# Confirm it appears
todo list
# Delete
$SKILL_DIR/scripts/calendar.sh todo delete --match "Delete Me"
Verify:
- .ics file removed from tasks dir
todo list(todoman) does not show "Delete Me Todo"vdirsyncer syncran
16. Todo Check (Cron Output)
# Create a test todo
$SKILL_DIR/scripts/calendar.sh todo add \
--summary "Check Test Todo" \
--due "$TEST_DATE"
# Run check
$SKILL_DIR/scripts/calendar.sh todo check
Verify:
- Output matches daily digest format (priority groups, urgency labels)
- Complete the todo, run
todo checkagain — silent exit (no output)
$SKILL_DIR/scripts/calendar.sh todo complete --match "Check Test"
$SKILL_DIR/scripts/calendar.sh todo check
# Should produce no output
17. Dry Run: Recurring Event (--rrule)
Test recurring event generation. Use a date that falls on a Tuesday.
# Find next Tuesday
NEXT_TUE=$(python3 -c "from datetime import date,timedelta; d=date.today(); d+=timedelta((1-d.weekday())%7 or 7); print(d)")
$SKILL_DIR/scripts/calendar.sh send \
--to "测试用户" \
--subject "Recurring Test (Tue)" \
--summary "Recurring Test (Tue)" \
--start "${NEXT_TUE}T14:30:00" \
--end "${NEXT_TUE}T15:00:00" \
--rrule "FREQ=WEEKLY;COUNT=4;BYDAY=TU" \
--dry-run
Verify:
- ICS has
RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=4 - DTSTART falls on a Tuesday
- No validation errors
18. Validation: DTSTART/BYDAY Mismatch
Verify the tool rejects mismatched DTSTART and BYDAY.
# This should FAIL — start is on a Tuesday but BYDAY=TH
$SKILL_DIR/scripts/calendar.sh send \
--to "测试用户" \
--subject "Mismatch Test" \
--summary "Mismatch Test" \
--start "${NEXT_TUE}T09:00:00" \
--end "${NEXT_TUE}T09:30:00" \
--rrule "FREQ=WEEKLY;COUNT=4;BYDAY=TH" \
--dry-run
Verify:
- Script exits with error
- Error message says DTSTART falls on TU but RRULE says BYDAY=TH
- Suggests changing --start to a date that falls on TH
19. Event List
# List upcoming events
$SKILL_DIR/scripts/calendar.sh event list
# Search by text
$SKILL_DIR/scripts/calendar.sh event list --search "Calendar Skill Test"
# List with UIDs
$SKILL_DIR/scripts/calendar.sh event list --format "{uid} {title}"
Verify:
- Events from earlier tests appear
- Search narrows results correctly
- UIDs are displayed with --format
20. Event Delete
# Send a throwaway event first
$SKILL_DIR/scripts/calendar.sh send \
--to "测试用户" \
--subject "Delete Me Event" \
--summary "Delete Me Event" \
--start "${TEST_DATE}T20:00:00" \
--end "${TEST_DATE}T21:00:00"
# Verify it exists
$SKILL_DIR/scripts/calendar.sh event list --search "Delete Me"
# Delete it
$SKILL_DIR/scripts/calendar.sh event delete --match "Delete Me Event"
# Verify it's gone
$SKILL_DIR/scripts/calendar.sh event list --search "Delete Me"
Verify:
- Event is created and visible
- Delete removes exactly one event
- Other events are untouched
vdirsyncer syncran after delete
21. Event Delete: Cancel Single Occurrence (EXDATE)
Test that --date cancels one occurrence of a recurring event without deleting the series.
# Create a recurring event (weekly on Saturday, 4 weeks)
NEXT_SAT=$(python3 -c "from datetime import date,timedelta; d=date.today(); d+=timedelta((5-d.weekday())%7 or 7); print(d)")
$SKILL_DIR/scripts/calendar.sh send \
--to "测试用户" \
--subject "EXDATE Test (Sat)" \
--summary "EXDATE Test (Sat)" \
--start "${NEXT_SAT}T10:00:00" \
--end "${NEXT_SAT}T11:00:00" \
--rrule "FREQ=WEEKLY;COUNT=4;BYDAY=SA"
# Verify it exists
$SKILL_DIR/scripts/calendar.sh event list --search "EXDATE Test"
# Cancel just the first occurrence
$SKILL_DIR/scripts/calendar.sh event delete --match "EXDATE Test" --date "$NEXT_SAT"
# Verify: .ics file still exists (not deleted)
ls ~/.openclaw/workspace/calendars/home/ | grep -i exdate
Verify:
event delete --match ... --date ...prints "Cancelled ... (added EXDATE, series continues)".icsfile still exists in calendar dirkhal listno longer shows the cancelled date but shows subsequent Saturdays
22. Event Delete: Recurring Without --date or --all (Safety Guard)
# Try to delete the recurring event without --date or --all — should FAIL
$SKILL_DIR/scripts/calendar.sh event delete --match "EXDATE Test"
Verify:
- Script exits with error
- Error message explains the two options:
--dateor--all
23. Event Delete: Recurring With --all
# Delete the entire series
$SKILL_DIR/scripts/calendar.sh event delete --match "EXDATE Test" --all
Verify:
- .ics file is removed
event list --search "EXDATE Test"shows nothing
24. Regression: Send Rejects Unknown Addresses
Verify that send no longer accepts arbitrary email addresses.
# This MUST fail — raw unknown email should be rejected
$SKILL_DIR/scripts/calendar.sh send \
--to "test@example.com" \
--subject "Regression Test" \
--summary "Regression Test Event" \
--start "${TEST_DATE}T10:00:00" \
--end "${TEST_DATE}T11:00:00" \
--dry-run
Verify:
- Command exits with error
- Error shows "not found in contacts" with available contacts list
- No ICS generated
Quick Health Checks
Run these first if any step fails.
# icalendar library is available
uv run --project $SKILL_DIR python -c "import icalendar; print('ok')"
# himalaya can list emails
himalaya envelope list --page-size 5
# vdirsyncer can sync
vdirsyncer sync
# khal can read local calendar
khal list today 7d
# todoman can list todos
todo list
Common Failures
| Symptom | Likely Cause |
|---|---|
himalaya message send errors |
SMTP config issue, check ~/.config/himalaya/config.toml |
No .ics attachment found |
Email doesn't have a calendar invite, or himalaya can't download attachments |
vdirsyncer sync fails |
Check credentials in ~/.config/vdirsyncer/config, or server is unreachable |
ModuleNotFoundError: icalendar |
Run uv sync --project $SKILL_DIR to install dependencies |
| Invite shows as attachment (no Accept/Decline) | Check MIME Content-Type includes method=REQUEST |
Event not in khal list after sync |
Check .ics file exists in ~/.openclaw/workspace/calendars/home/ |
todo command not found |
Install with uv tool install todoman |
todo list errors |
Check ~/.config/todoman/config.py exists and path points to tasks dir |
| Todo not syncing | Check ~/.openclaw/workspace/calendars/tasks/ exists, verify vdirsyncer cal/tasks pair |
| DTSTART/BYDAY mismatch error | --start date doesn't fall on the BYDAY day. Change the start date to match |
| Recurring events on wrong day | DTSTART was not aligned with BYDAY. Delete the event and resend with correct --start |
| SMTP rate limit / EOF error | Too many sends too fast. Wait 10+ seconds between sends (Migadu limit) |
| Events disappeared after cleanup | Never use rm *.ics on calendar dirs. Use event delete --match instead |
| Recurring series deleted when cancelling one date | Use --date YYYY-MM-DD to add EXDATE, not bare event delete (which requires --all for recurring) |
send rejects email address |
Address not in contacts. Add with contacts.sh add first (separate from send) |
send says "has multiple emails" |
Contact has work+home emails. Use name:type syntax (e.g. 小橘子:work) |
| Contacts dir empty after sync | Check vdirsyncer CardDAV pair is configured for contacts/default/ |