Files
Yanxin Lu 0910bd5d5c contacts: support multiple address book collections
Migadu exposes "family" and "business" address books via CardDAV.
The contacts script now searches all subdirs under contacts/ and
adds new contacts to the "family" collection by default.
2026-03-31 13:33:04 -07:00

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 add prints "Added contact: ..."
  • Second contact add prints "Updated contact: ... — added ..."
  • contact list shows all contacts with email types
  • .vcf files created in ~/.openclaw/workspace/contacts/family/

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 list no longer shows that contact
  • Second delete fails with "No contact matching"
  • .vcf file 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 --to recipients appear as attendees
  • Times and timezone look correct
  • ICS has BEGIN:VALARM with correct TRIGGER duration
# 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)
  • .ics file 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 sync completes without errors
  • .ics file exists in ~/.openclaw/workspace/calendars/home/
  • khal list shows "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 sync ran
  • khal 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 PRIORITY value (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
  • .ics file created in ~/.openclaw/workspace/calendars/tasks/
  • todo list (todoman directly) shows "Test Todo"
  • vdirsyncer sync ran

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
  • --all flag 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 list shows the new due date
  • vdirsyncer sync ran
# 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 sync ran

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 sync ran

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 check again — 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 sync ran 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)"
  • .ics file still exists in calendar dir
  • khal list no 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: --date or --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/