switch to ical.js and cleanup obsolete files - v0.4.6 [skip-ci]
All checks were successful
Build SilverBullet Plug / build (push) Successful in 11s

This commit is contained in:
2026-02-21 16:05:54 -08:00
parent 30731c7752
commit 139ab71db7
26 changed files with 174 additions and 454 deletions

View File

@@ -1,10 +1,30 @@
# Makefile for iCalendar Plug
.PHONY: build test bump release check-versions
# Run all tests
test:
deno task test
# Increment patch version in deno.json
bump:
deno task bump-version
# Sync version from deno.json to all other files
sync-version:
deno task sync-version
# Check version consistency # Check version consistency
check-versions: check-versions:
./check_versions.sh ./check_versions.sh
# Build the plug using a Docker container with Deno # Build the plug using local Deno
build: check-versions build: sync-version
docker run --rm -v /home/sstent/Projects/silverbullet-icalendar:/app -w /app denoland/deno:latest task build deno task build
# Bump version and build
release: bump build
@echo "Release built successfully."
# Helper to build and copy to a local test space (if needed) # Helper to build and copy to a local test space (if needed)
deploy-test: build deploy-test: build

View File

@@ -1,6 +1,6 @@
--- ---
name: Library/sstent/icalendar name: Library/sstent/icalendar
version: "0.4.4" version: "0.4.6"
tags: meta/library tags: meta/library
files: files:
- icalendar.plug.js - icalendar.plug.js

View File

@@ -1,16 +1,18 @@
#!/bin/bash #!/bin/bash
# Extract versions # Extract versions
DENO_VERSION=$(grep '"version":' deno.json | cut -d'"' -f4)
TS_VERSION=$(grep "const VERSION =" icalendar.ts | cut -d'"' -f2) TS_VERSION=$(grep "const VERSION =" icalendar.ts | cut -d'"' -f2)
YAML_VERSION=$(grep "version:" icalendar.plug.yaml | head -n 1 | awk '{print $2}') YAML_VERSION=$(grep "version:" icalendar.plug.yaml | head -n 1 | awk '{print $2}')
PLUG_MD_VERSION=$(grep "version:" PLUG.md | head -n 1 | awk '{print $2}') PLUG_MD_VERSION=$(grep "version:" PLUG.md | head -n 1 | awk '{print $2}' | tr -d '"')
echo "Checking versions..." echo "Checking versions..."
echo "icalendar.ts: $TS_VERSION" echo "deno.json: $DENO_VERSION"
echo "icalendar.ts: $TS_VERSION"
echo "icalendar.plug.yaml: $YAML_VERSION" echo "icalendar.plug.yaml: $YAML_VERSION"
echo "PLUG.md: $PLUG_MD_VERSION" echo "PLUG.md: $PLUG_MD_VERSION"
if [ "$TS_VERSION" == "$YAML_VERSION" ] && [ "$YAML_VERSION" == "$PLUG_MD_VERSION" ]; then if [ "$DENO_VERSION" == "$TS_VERSION" ] && [ "$TS_VERSION" == "$YAML_VERSION" ] && [ "$YAML_VERSION" == "$PLUG_MD_VERSION" ]; then
echo "✅ All versions match." echo "✅ All versions match."
exit 0 exit 0
else else

View File

@@ -1,12 +1,14 @@
{ {
"name": "icalendar-plug", "name": "icalendar-plug",
"version": "0.4.4", "version": "0.4.6",
"nodeModulesDir": "auto", "nodeModulesDir": "auto",
"tasks": { "tasks": {
"sync-version": "deno run -A scripts/sync-version.ts", "sync-version": "deno run -A scripts/sync-version.ts",
"bump-version": "deno run -A scripts/bump-version.ts",
"build": "deno task sync-version && deno run -A https://github.com/silverbulletmd/silverbullet/releases/download/2.4.1/plug-compile.js -c deno.json icalendar.plug.yaml", "build": "deno task sync-version && deno run -A https://github.com/silverbulletmd/silverbullet/releases/download/2.4.1/plug-compile.js -c deno.json icalendar.plug.yaml",
"watch": "deno task build --watch", "watch": "deno task build --watch",
"debug": "deno run -A https://raw.githubusercontent.com/silverbulletmd/silverbullet/v2.4.1/plug-compile.js -c deno.json icalendar.plug.yaml --debug" "debug": "deno run -A https://raw.githubusercontent.com/silverbulletmd/silverbullet/v2.4.1/plug-compile.js -c deno.json icalendar.plug.yaml --debug",
"test": "deno test -A icalendar_test.ts tests/integration_test.ts tests/reach_variations_test.ts timezones_test.ts"
}, },
"lint": { "lint": {
"rules": { "rules": {

View File

@@ -1,36 +0,0 @@
services:
silverbullet-test:
image: zefhemel/silverbullet:2.4.0
ports:
- "3001:3000"
volumes:
- ./test_space_e2e:/space
environment:
- SB_LOG_PUSH=true
- SB_DEBUG=true
- SB_SPACE_LUA_TRUSTED=true
mock-ics-server:
image: nginx:alpine
ports:
- "8081:80"
volumes:
- ./test_data:/usr/share/nginx/html:ro
- ./test_data/nginx.conf:/etc/nginx/nginx.conf:ro
playwright:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
network_mode: "service:silverbullet-test"
volumes:
- .:/work
- /work/node_modules
- /tmp/.X11-unix:/tmp/.X11-unix
working_dir: /work
environment:
- CI=true
- DISPLAY=${DISPLAY:-:0}
- SB_URL=http://localhost:3000
command: sh -c "npm install && npx playwright test"
volumes:
sb-test-space:

View File

@@ -1,19 +0,0 @@
services:
silverbullet:
image: zefhemel/silverbullet:latest
ports:
- "3000:3000"
volumes:
- ./test_space:/space
environment:
- SB_USER=admin:admin
- SB_LOG_PUSH=true
- SB_DEBUG=true
- SB_SPACE_LUA_TRUSTED=true
mock-ics:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./mock_calendar.ics:/usr/share/nginx/html/calendar.ics:ro

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
name: icalendar name: icalendar
version: 0.4.4 version: 0.4.6
author: sstent author: sstent
index: icalendar.ts index: icalendar.ts
# Legacy SilverBullet permission name # Legacy SilverBullet permission name

View File

@@ -1,9 +1,9 @@
import { clientStore, config, datastore, editor, index } from "@silverbulletmd/silverbullet/syscalls"; import { clientStore, config, datastore, editor, index } from "@silverbulletmd/silverbullet/syscalls";
import { convertIcsCalendar } from "https://esm.sh/ts-ics@2.4.0"; import ICAL from "ical.js";
import { RRule, RRuleSet } from "rrule"; import { RRule, RRuleSet } from "rrule";
import { getUtcOffsetMs, resolveIanaName } from "./timezones.ts"; import { getUtcOffsetMs, resolveIanaName } from "./timezones.ts";
const VERSION = "0.4.4"; const VERSION = "0.4.6";
const CACHE_KEY = "icalendar:lastSync"; const CACHE_KEY = "icalendar:lastSync";
console.log(`[iCalendar] Plug script executing at top level (Version ${VERSION})`); console.log(`[iCalendar] Plug script executing at top level (Version ${VERSION})`);
@@ -321,27 +321,37 @@ async function fetchAndParseCalendar(source: any, windowDays = 365, displayTimez
return []; return [];
} }
const text = await response.text(); const text = await response.text();
const calendar = convertIcsCalendar(undefined, text); const jcalData = ICAL.parse(text);
if (!calendar || !calendar.events) { const vcalendar = new ICAL.Component(jcalData);
return []; const vevents = vcalendar.getAllSubcomponents("vevent");
}
const events: any[] = [];
for (const icsEvent of calendar.events) {
if (icsEvent.status?.toUpperCase() === "CANCELLED") continue;
// Resolve start time (returns UTC Date) const events: any[] = [];
const startDateUTC = await resolveEventStart(icsEvent); for (const vevent of vevents) {
if (!startDateUTC) continue; const icsEvent = new ICAL.Event(vevent);
const status = vevent.getFirstPropertyValue("status") as string | null;
// Resolve end time (returns UTC Date) if (status?.toUpperCase() === "CANCELLED") continue;
const endDateUTC = await resolveEventEnd(icsEvent);
// Extract raw properties for recurrence expansion
const summary = icsEvent.summary;
const uid = icsEvent.uid;
const description = icsEvent.description;
const location = icsEvent.location;
const rrule = vevent.getFirstPropertyValue("rrule");
const exdates = vevent.getAllProperties("exdate").map((p: any) => p.getFirstValue().toJSDate().toISOString());
// Resolve start/end times
const startDateUTC = icsEvent.startDate.toJSDate();
const endDateUTC = icsEvent.endDate ? icsEvent.endDate.toJSDate() : null;
const rawTz = icsEvent.start?.local?.timezone || (icsEvent.start as any)?.timezone || "UTC"; const rawTz = icsEvent.startDate.timezone || "UTC";
const baseEvent = { const baseEvent = {
...icsEvent, uid,
name: icsEvent.summary || "Untitled Event", summary,
name: summary || "Untitled Event",
description,
location,
// Store both UTC (for sorting/comparison) and local (for display) // Store both UTC (for sorting/comparison) and local (for display)
start: startDateUTC.toISOString(), start: startDateUTC.toISOString(),
startLocal: dateToTimezoneString(startDateUTC, displayTimezone), startLocal: dateToTimezoneString(startDateUTC, displayTimezone),
@@ -349,7 +359,9 @@ async function fetchAndParseCalendar(source: any, windowDays = 365, displayTimez
endLocal: endDateUTC ? dateToTimezoneString(endDateUTC, displayTimezone) : undefined, endLocal: endDateUTC ? dateToTimezoneString(endDateUTC, displayTimezone) : undefined,
tag: "ical-event", tag: "ical-event",
sourceName: source.name, sourceName: source.name,
timezone: rawTz timezone: rawTz,
rrule: rrule ? rrule.toString() : undefined,
exdate: exdates.length > 0 ? exdates : undefined
}; };
if (rawTz !== "UTC" && rawTz !== "None" && !resolveIanaName(rawTz)) { if (rawTz !== "UTC" && rawTz !== "None" && !resolveIanaName(rawTz)) {

View File

@@ -1,5 +1,5 @@
import { assertEquals, assert } from "jsr:@std/assert"; import { assertEquals, assert } from "jsr:@std/assert";
import { resolveEventStart, expandRecurrences, localDateString } from "./icalendar.ts"; import { resolveEventStart, expandRecurrences, dateToTimezoneString } from "./icalendar.ts";
Deno.test("resolveEventStart - local date with timezone", async () => { Deno.test("resolveEventStart - local date with timezone", async () => {
const icsEvent = { const icsEvent = {
@@ -47,7 +47,7 @@ Deno.test("resolveEventStart - UTC event", async () => {
Deno.test("expandRecurrences - weekly event", () => { Deno.test("expandRecurrences - weekly event", () => {
const now = new Date(); const now = new Date();
const start = new Date(now.getTime() - 14 * 86400000); // Started 2 weeks ago const start = new Date(now.getTime() - 14 * 86400000); // Started 2 weeks ago
const startStr = localDateString(start); const startStr = dateToTimezoneString(start, "UTC");
const icsEvent = { const icsEvent = {
summary: "Weekly Meeting", summary: "Weekly Meeting",
@@ -68,21 +68,21 @@ Deno.test("expandRecurrences - EXDATE exclusion", () => {
const yesterday = new Date(now.getTime() - 86400000); const yesterday = new Date(now.getTime() - 86400000);
const tomorrow = new Date(now.getTime() + 86400000); const tomorrow = new Date(now.getTime() + 86400000);
const startStr = localDateString(yesterday); const startStr = dateToTimezoneString(yesterday, "UTC");
const tomorrowStr = localDateString(tomorrow); const tomorrowStr = dateToTimezoneString(tomorrow, "UTC");
const icsEvent = { const icsEvent = {
summary: "Daily Meeting EXDATE", summary: "Daily Meeting EXDATE",
start: startStr, start: yesterday.toISOString(),
rrule: "FREQ=DAILY;COUNT=3", rrule: "FREQ=DAILY;COUNT=3",
exdate: [tomorrowStr] exdate: [tomorrow.toISOString()]
}; };
const results = expandRecurrences(icsEvent, 30); const results = expandRecurrences(icsEvent, 30, "UTC", now);
// Yesterday (in window), Today (in window), Tomorrow (Excluded) // Yesterday (in window), Today (in window), Tomorrow (Excluded)
// Should have 2 occurrences // Should have 2 occurrences
assertEquals(results.length, 2); assertEquals(results.length, 2);
assertEquals(results[0].start, startStr); assertEquals(results[0].startLocal, dateToTimezoneString(yesterday, "UTC"));
}); });
Deno.test("fetchAndParseCalendar - filter cancelled events", async () => { Deno.test("fetchAndParseCalendar - filter cancelled events", async () => {
@@ -107,15 +107,15 @@ Deno.test("resolveEventStart - ignore tzShift", async () => {
Deno.test("expandRecurrences - custom windowDays", () => { Deno.test("expandRecurrences - custom windowDays", () => {
const now = new Date(); const now = new Date();
const startStr = localDateString(now); const start = new Date(now.getTime() - 7 * 86400000); // 7 days ago
const icsEvent = { const icsEvent = {
summary: "Daily Meeting Window", summary: "Daily Meeting Window",
start: startStr, start: start.toISOString(),
rrule: "FREQ=DAILY" rrule: "FREQ=DAILY"
}; };
const results = expandRecurrences(icsEvent, 2); const results = expandRecurrences(icsEvent, 2, "UTC", now);
// Today (in window), Tomorrow (in window), Day after tomorrow (in window) // Today (in window), Tomorrow (in window), Day after tomorrow (in window)
// set.between(now - 7, now + 2) -> // set.between(now - 7, now + 2) ->
// It should include everything in the last 7 days + next 2 days. // It should include everything in the last 7 days + next 2 days.
@@ -124,7 +124,7 @@ Deno.test("expandRecurrences - custom windowDays", () => {
}); });
Deno.test("expandRecurrences - non-string rrule (Reproduction)", () => { Deno.test("expandRecurrences - non-string rrule (Reproduction)", () => {
const now = new Date(); const now = new Date();
const startStr = localDateString(now); const startStr = dateToTimezoneString(now, "UTC");
const icsEvent = { const icsEvent = {
summary: "Bug Reproduction Event", summary: "Bug Reproduction Event",
@@ -158,7 +158,7 @@ Deno.test("expandRecurrences - non-string rrule (Reproduction)", () => {
Deno.test("expandRecurrences - validation of visibility logic", () => { Deno.test("expandRecurrences - validation of visibility logic", () => {
const now = new Date(); const now = new Date();
const start = new Date(now.getTime() - 100 * 86400000); // Started 100 days ago const start = new Date(now.getTime() - 100 * 86400000); // Started 100 days ago
const startStr = localDateString(start); const startStr = dateToTimezoneString(start, "UTC");
const icsEvent = { const icsEvent = {
summary: "Validation Weekly Meeting", summary: "Validation Weekly Meeting",
@@ -177,7 +177,7 @@ Deno.test("expandRecurrences - validation of visibility logic", () => {
Deno.test("expandRecurrences - object rrule (Reproduction of missing events)", () => { Deno.test("expandRecurrences - object rrule (Reproduction of missing events)", () => {
const now = new Date(); const now = new Date();
const start = new Date(now.getTime() - 100 * 86400000); const start = new Date(now.getTime() - 100 * 86400000);
const startStr = localDateString(start); const startStr = dateToTimezoneString(start, "UTC");
const icsEvent = { const icsEvent = {
summary: "Object RRULE Event", summary: "Object RRULE Event",
@@ -210,7 +210,7 @@ Deno.test("expandRecurrences - object rrule (Reproduction of missing events)", (
Deno.test("expandRecurrences - object rrule with until", () => { Deno.test("expandRecurrences - object rrule with until", () => {
const now = new Date(); const now = new Date();
const start = new Date(now.getTime() - 10 * 86400000); const start = new Date(now.getTime() - 10 * 86400000);
const startStr = localDateString(start); const startStr = dateToTimezoneString(start, "UTC");
const untilDate = new Date(now.getTime() + 10 * 86400000); const untilDate = new Date(now.getTime() + 10 * 86400000);
const icsEvent = { const icsEvent = {
@@ -229,7 +229,7 @@ Deno.test("expandRecurrences - object rrule with until", () => {
Deno.test("expandRecurrences - object rrule with byday", () => { Deno.test("expandRecurrences - object rrule with byday", () => {
const now = new Date(); const now = new Date();
const start = new Date(now.getTime() - 10 * 86400000); const start = new Date(now.getTime() - 10 * 86400000);
const startStr = localDateString(start); const startStr = dateToTimezoneString(start, "UTC");
const icsEvent = { const icsEvent = {
summary: "Object RRULE BYDAY Event", summary: "Object RRULE BYDAY Event",
@@ -247,7 +247,7 @@ Deno.test("expandRecurrences - object rrule with byday", () => {
Deno.test("expandRecurrences - composite object rrule", () => { Deno.test("expandRecurrences - composite object rrule", () => {
const now = new Date(); const now = new Date();
const start = new Date(now.getTime() - 10 * 86400000); const start = new Date(now.getTime() - 10 * 86400000);
const startStr = localDateString(start); const startStr = dateToTimezoneString(start, "UTC");
const untilDate = new Date(now.getTime() + 10 * 86400000); const untilDate = new Date(now.getTime() + 10 * 86400000);
const icsEvent = { const icsEvent = {

View File

@@ -1,18 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mock ICS Server//EN
BEGIN:VEVENT
UID:040000008200E00074C5B7101A82E0080000000010E384DCAC84DC0100000000000000001000000014AC664AB867C74D85FC0B77E881C5AE
SUMMARY:Plug-In for Metalsoft.io inside HPE Morpheus Enterprise
DTSTART;TZID=W. Europe Standard Time:20260217T160000
DTEND;TZID=W. Europe Standard Time:20260217T170000
DTSTAMP:20260217T160818Z
END:VEVENT
BEGIN:VEVENT
UID:040000008200E00074C5B7101A82E0080000000010405401AC8EDC010000000000000000100000000CD9E3DB97A71984FB54AC0DAD0FE9137
SUMMARY:MetalSoft & Morpheus plugin catch up
DTSTART;TZID=GMT Standard Time:20260217T130000
DTEND;TZID=GMT Standard Time:20260217T133000
DTSTAMP:20260216T192619Z
END:VEVENT
END:VCALENDAR

69
package-lock.json generated
View File

@@ -1,69 +0,0 @@
{
"name": "work",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@playwright/test": "^1.58.2",
"playwright": "^1.58.2"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

View File

@@ -1,6 +1,3 @@
{ {
"dependencies": { "dependencies": {}
"playwright": "^1.58.2",
"@playwright/test": "^1.58.2"
}
} }

View File

@@ -1,30 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
timeout: 180000,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: process.env.SB_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
headless: false,
},
projects: [
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
launchOptions: {
firefoxUserPrefs: {
'dom.securecontext.whitelist': 'http://localhost:3000,http://silverbullet-test:3000,http://mock-ics-server',
},
},
},
},
],
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

19
scripts/bump-version.ts Normal file
View File

@@ -0,0 +1,19 @@
// scripts/bump-version.ts
const denoConfigPath = "deno.json";
const denoConfig = JSON.parse(await Deno.readTextFile(denoConfigPath));
const currentVersion = denoConfig.version;
const parts = currentVersion.split(".").map(Number);
if (parts.length !== 3 || parts.some(isNaN)) {
console.error(`Invalid version format in deno.json: ${currentVersion}`);
Deno.exit(1);
}
// Increment patch version
parts[2]++;
const newVersion = parts.join(".");
denoConfig.version = newVersion;
await Deno.writeTextFile(denoConfigPath, JSON.stringify(denoConfig, null, 2) + "\n");
console.log(`Bumped version: ${currentVersion} -> ${newVersion}`);

View File

@@ -1,18 +0,0 @@
events {}
http {
server {
listen 80;
location / {
root /usr/share/nginx/html;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
}
}

View File

@@ -1,14 +0,0 @@
# Configuration
```space-lua
config.set("icalendar", {
sources = {
{
url = "http://172.22.0.3/reachcalendar.ics",
name = "TestCalendar"
}
}
})
```

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +0,0 @@
.iCalendar: Sync
# Meetings for Jan 20th, 2026.iCalendar: Sync
.
.iCalendar: Sync
.iCalendar: Sync
${template.each(query[[
from e = index.tag "ical-event"
where e.start:startsWith("2026-01-20")
order by e.start
]], function(e)
return string.format("* %s to %s: %s (TZ: %s)\n",
e.start:sub(12, 16),
e["end"]:sub(12, 16),
e.summary,
e.timezone or "UTC")
end)}
# Welcome 👋
Welcome to the wondrous world of SilverBullet.

View File

@@ -1,80 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('iCalendar Sync E2E', () => {
test('should verify iCalendar sync activity', async ({ page }) => {
const logs: string[] = [];
const errors: string[] = [];
page.on('console', msg => {
const text = msg.text();
if (msg.type() === 'error') errors.push(text);
if (text.includes('[iCalendar]')) {
logs.push(text);
console.log('Detected SB Log:', text);
}
});
// 1. Load Editor
console.log('Navigating to /');
await page.goto('/');
await page.waitForLoadState('networkidle');
console.log('Page reached, waiting for boot sequence...');
// 2. Persistent Monitoring for Sync Activity
let syncDetected = false;
let eventsSynced = 0;
const timeoutMs = 120000; // 2 minutes
const startTime = Date.now();
console.log(`Starting monitoring loop for ${timeoutMs/1000}s...`);
while (Date.now() - startTime < timeoutMs) {
// Check for notifications
const notification = page.locator('.sb-notification:has-text("Synced")');
if (await notification.count() > 0) {
const text = await notification.innerText();
console.log('Detected Sync Notification:', text);
const match = text.match(/Synced (\d+) events/);
if (match) {
eventsSynced = parseInt(match[1], 10);
if (eventsSynced > 0) {
syncDetected = true;
console.log(`SUCCESS: ${eventsSynced} events synced!`);
break;
}
}
}
// Every 30 seconds, try to "poke" it with a keyboard shortcut if not started
const elapsed = Date.now() - startTime;
if (elapsed > 30000 && elapsed < 35000 && !syncDetected) {
console.log('Auto-sync not detected yet, trying manual trigger shortcut...');
await page.keyboard.press('.');
await page.waitForTimeout(1000);
await page.keyboard.type('iCalendar: Sync');
await page.keyboard.press('Enter');
}
await page.waitForTimeout(2000);
}
// 3. Final verification
console.log('Final accumulated [iCalendar] logs:', logs);
// Check if the query rendered meetings in the UI
const meetingItems = page.locator('li:has-text("to"):has-text(":")');
const meetingCount = await meetingItems.count();
console.log(`Meetings found in UI: ${meetingCount}`);
// Filter out expected noise
const relevantErrors = errors.filter(e => !e.includes('401') && !e.includes('favicon'));
expect(relevantErrors, `Found unexpected errors: ${relevantErrors[0]}`).toHaveLength(0);
expect(syncDetected, 'iCalendar sync failed or synced 0 events').toBe(true);
expect(eventsSynced).toBeGreaterThan(0);
// Verify query rendering
expect(meetingCount).toBeGreaterThanOrEqual(12);
console.log('Test Passed.');
});
});

View File

@@ -1,6 +1,6 @@
import { assertEquals, assert } from "jsr:@std/assert"; import { assertEquals, assert } from "jsr:@std/assert";
import { convertIcsCalendar } from "https://esm.sh/ts-ics@2.4.0"; import ICAL from "ical.js";
import { expandRecurrences, resolveEventStart, localDateString } from "../icalendar.ts"; import { expandRecurrences, resolveEventStart, dateToTimezoneString } from "../icalendar.ts";
Deno.test("Integration - parse and expand real-world ICS samples", async () => { Deno.test("Integration - parse and expand real-world ICS samples", async () => {
const testDataDir = "./test_data"; const testDataDir = "./test_data";
@@ -39,30 +39,39 @@ Deno.test("Integration - parse and expand real-world ICS samples", async () => {
for (const file of files) { for (const file of files) {
console.log(` Testing file: ${file}`); console.log(` Testing file: ${file}`);
const text = await Deno.readTextFile(file); const text = await Deno.readTextFile(file);
const calendar = convertIcsCalendar(undefined, text); const jcalData = ICAL.parse(text);
const vcalendar = new ICAL.Component(jcalData);
const vevents = vcalendar.getAllSubcomponents("vevent");
assert(calendar && calendar.events, `Failed to parse ${file}`); assert(vevents.length > 0, `Failed to parse ${file} or no events found`);
for (const icsEvent of calendar.events) { for (const vevent of vevents) {
if (icsEvent.status?.toUpperCase() === "CANCELLED") continue; const icsEvent = new ICAL.Event(vevent);
const status = vevent.getFirstPropertyValue("status") as string | null;
if (status?.toUpperCase() === "CANCELLED") continue;
const finalDate = await resolveEventStart(icsEvent); const startDateUTC = icsEvent.startDate.toJSDate();
if (!finalDate) continue; const summary = icsEvent.summary;
const rrule = vevent.getFirstPropertyValue("rrule");
const exdates = vevent.getAllProperties("exdate").map((p: any) => p.getFirstValue().toJSDate().toISOString());
const localIso = localDateString(finalDate); const localIso = dateToTimezoneString(startDateUTC, "UTC");
const baseEvent = { const baseEvent = {
...icsEvent, summary,
name: icsEvent.summary || "Untitled Event", name: summary || "Untitled Event",
start: localIso, start: startDateUTC.toISOString(),
startLocal: localIso,
tag: "ical-event", tag: "ical-event",
sourceName: "IntegrationTest" sourceName: "IntegrationTest",
rrule: rrule ? rrule.toString() : undefined,
exdate: exdates.length > 0 ? exdates : undefined
}; };
try { try {
const expanded = expandRecurrences(baseEvent, 30); const expanded = expandRecurrences(baseEvent, 30);
assert(expanded.length >= 1, `Expected at least 1 occurrence for event "${icsEvent.summary}" in ${file}`); assert(expanded.length >= 1, `Expected at least 1 occurrence for event "${summary}" in ${file}`);
} catch (err) { } catch (err) {
console.error(`❌ Error expanding recurrence for event "${icsEvent.summary}" in ${file}:`, err); console.error(`❌ Error expanding recurrence for event "${summary}" in ${file}:`, err);
throw err; throw err;
} }
} }

View File

@@ -1,5 +1,5 @@
import { assertEquals, assert } from "jsr:@std/assert"; import { assertEquals, assert } from "jsr:@std/assert";
import { resolveEventStart, expandRecurrences, localDateString } from "../icalendar.ts"; import { resolveEventStart, expandRecurrences, dateToTimezoneString } from "../icalendar.ts";
const TEST_NOW = new Date("2026-01-20T12:00:00Z"); const TEST_NOW = new Date("2026-01-20T12:00:00Z");
@@ -42,24 +42,26 @@ Deno.test("Variation: Transparent Meeting (Free)", async () => {
}); });
Deno.test("Variation: Recurring Weekly (Multi-day: MO,TU,WE,TH,FR)", () => { Deno.test("Variation: Recurring Weekly (Multi-day: MO,TU,WE,TH,FR)", () => {
const start = new Date("2026-01-16T13:00:00");
const icsEvent = { const icsEvent = {
summary: "BUSY Weekly", summary: "BUSY Weekly",
start: "2026-01-16T13:00:00", start: start.toISOString(),
rrule: "FREQ=WEEKLY;UNTIL=20260814T170000Z;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;WKST=SU" rrule: "FREQ=WEEKLY;UNTIL=20260814T170000Z;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;WKST=SU"
}; };
// Use TEST_NOW to ensure the window matches // Use TEST_NOW to ensure the window matches
const results = expandRecurrences(icsEvent, 7, TEST_NOW); const results = expandRecurrences(icsEvent, 7, "UTC", TEST_NOW);
// Should have multiple occurrences per week // Should have multiple occurrences per week
assert(results.length > 1); assert(results.length > 1);
assert(results.some(r => r.start.includes("2026-01-19"))); // Monday assert(results.some(r => r.startLocal.includes("2026-01-19"))); // Monday
assert(results.some(r => r.start.includes("2026-01-20"))); // Tuesday assert(results.some(r => r.startLocal.includes("2026-01-20"))); // Tuesday
}); });
Deno.test("Variation: Recurring with WORKWEEKSTART (Outlook style)", () => { Deno.test("Variation: Recurring with WORKWEEKSTART (Outlook style)", () => {
const start = new Date("2026-01-20T08:30:00");
const icsEvent = { const icsEvent = {
summary: "Outlook Style Meeting", summary: "Outlook Style Meeting",
start: "2026-01-20T08:30:00", start: start.toISOString(),
rrule: { rrule: {
frequency: "WEEKLY", frequency: "WEEKLY",
interval: 1, interval: 1,
@@ -68,40 +70,42 @@ Deno.test("Variation: Recurring with WORKWEEKSTART (Outlook style)", () => {
} }
}; };
const results = expandRecurrences(icsEvent, 30, TEST_NOW); const results = expandRecurrences(icsEvent, 30, "UTC", TEST_NOW);
assert(results.length > 0); assert(results.length > 0);
assert(results[0].start.includes("2026-01-20")); assert(results[0].startLocal.includes("2026-01-20"));
}); });
Deno.test("Variation: Recurring with EXDATE (Exclusion)", () => { Deno.test("Variation: Recurring with EXDATE (Exclusion)", () => {
const start = new Date("2026-01-20T08:30:00");
const icsEvent = { const icsEvent = {
summary: "HPE-Veeam check-in", summary: "HPE-Veeam check-in",
start: "2026-01-20T08:30:00", start: start.toISOString(),
rrule: "FREQ=WEEKLY;UNTIL=20260324T143000Z;INTERVAL=1;BYDAY=TU;WKST=SU", rrule: "FREQ=WEEKLY;UNTIL=20260324T143000Z;INTERVAL=1;BYDAY=TU;WKST=SU",
exdate: ["2026-02-03T08:30:00"] exdate: ["2026-02-03T08:30:00"]
}; };
const results = expandRecurrences(icsEvent, 60, TEST_NOW); const results = expandRecurrences(icsEvent, 60, "UTC", TEST_NOW);
const dates = results.map(r => r.start); const dates = results.map(r => r.startLocal);
assert(dates.includes("2026-01-20T08:30:00")); assert(dates.some(d => d.includes("2026-01-20T")), "Should find first occurrence");
assert(dates.includes("2026-01-27T08:30:00")); assert(dates.some(d => d.includes("2026-01-27T")), "Should find second occurrence");
assert(!dates.includes("2026-02-03T08:30:00"), "EXDATE should be excluded"); assert(!dates.some(d => d.includes("2026-02-03T")), "EXDATE 2026-02-03 should be excluded");
assert(dates.includes("2026-02-10T08:30:00")); assert(dates.some(d => d.includes("2026-02-10T")), "Should find fourth occurrence");
}); });
Deno.test("Variation: Monthly Recurring (Last Friday)", () => { Deno.test("Variation: Monthly Recurring (Last Friday)", () => {
const start = new Date("2026-01-30T10:00:00");
const icsEvent = { const icsEvent = {
summary: "Monthly Planning", summary: "Monthly Planning",
start: "2026-01-30T10:00:00", // This is the last Friday of Jan 2026 start: start.toISOString(), // This is the last Friday of Jan 2026
rrule: "FREQ=MONTHLY;UNTIL=20260731T170000Z;INTERVAL=1;BYDAY=-1FR" rrule: "FREQ=MONTHLY;UNTIL=20260731T170000Z;INTERVAL=1;BYDAY=-1FR"
}; };
const results = expandRecurrences(icsEvent, 100, TEST_NOW); const results = expandRecurrences(icsEvent, 100, "UTC", TEST_NOW);
const dates = results.map(r => r.start); const dates = results.map(r => r.startLocal);
assert(dates.includes("2026-01-30T10:00:00")); assert(dates.some(d => d.includes("2026-01-30")), "Should find Jan occurrence");
assert(dates.includes("2026-02-27T10:00:00")); // Last Friday of Feb 2026 assert(dates.some(d => d.includes("2026-02-27")), "Should find Feb occurrence");
assert(dates.includes("2026-03-27T10:00:00")); // Last Friday of Mar 2026 assert(dates.some(d => d.includes("2026-03-27")), "Should find Mar occurrence");
}); });
Deno.test("Variation: Tentative Meeting", async () => { Deno.test("Variation: Tentative Meeting", async () => {
@@ -134,13 +138,13 @@ Deno.test("Feature: Unlimited lookback window", () => {
const start = new Date(TEST_NOW.getTime() - 500 * 86400000); // 500 days ago const start = new Date(TEST_NOW.getTime() - 500 * 86400000); // 500 days ago
const icsEvent = { const icsEvent = {
summary: "Event from 500 days ago", summary: "Event from 500 days ago",
start: localDateString(start), start: start.toISOString(),
rrule: "FREQ=DAILY;COUNT=1000" rrule: "FREQ=DAILY;COUNT=1000"
}; };
const results = expandRecurrences(icsEvent, 30, TEST_NOW); const results = expandRecurrences(icsEvent, 30, "UTC", TEST_NOW);
// Should include events from 500 days ago because there is now no limit // Should include events from 500 days ago because there is now no limit
assert(results.some(r => r.start === localDateString(start)), "Should find occurrence from 500 days ago"); assert(results.some(r => r.startLocal === dateToTimezoneString(start, "UTC")), "Should find occurrence from 500 days ago");
}); });
Deno.test("Feature: Hash Collision Prevention (Same UID/Start, Different Summary)", async () => { Deno.test("Feature: Hash Collision Prevention (Same UID/Start, Different Summary)", async () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,35 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
page.on('console', msg => {
console.log(`[BROWSER] ${msg.text()}`);
});
console.log('Logging in...');
await page.goto('http://localhost:3000/.auth');
await page.fill('input[type="text"]', 'admin');
await page.fill('input[type="password"]', 'admin');
await page.click('button:has-text("Login")');
await page.waitForNavigation();
console.log('Waiting 30s for indexing and plug activation...');
await page.waitForTimeout(30000);
// Check command palette
console.log('Opening command palette to check for iCalendar commands...');
await page.keyboard.press('Control+/');
await page.waitForTimeout(2000);
await page.keyboard.type('iCalendar:');
await page.waitForTimeout(2000);
const body = await page.innerText('body');
console.log('--- Body Output ---');
console.log(body);
await page.screenshot({ path: 'repro_check.png', fullPage: true });
await browser.close();
})();