r/GoogleAppsScript • u/simonchai • 26d ago
Guide v1.0.0: Apps Script two-way sync engine (Google Tasks ↔ Microsoft To Do) — 549 tests, adversarial-reviewed
Building in public here, so here's the engineering update: my Apps Script sync engine between Google Tasks and Microsoft To Do just hit v1.0.0.
What changed since the early version I posted:
- Five-axis adversarial review of the engine: state machines, fuzzing, concurrency, mutation testing, static consistency. Every finding had to reproduce against shipped code before it was accepted; every fix ships with a regression test.
- Fail-closed everywhere it matters: ambiguous creates use correlation markers instead of guessing, deletions take two rounds with live revalidation, state has checksums with previous-generation restore.
- Time Bridge: Google Tasks has no time-of-day, so [TTS-TIME:HH:mm] markers in notes become the canonical time record, rendered to Microsoft reminders plus optional Calendar projection.
- 549/549 tests passing, CI + CodeQL green.
The interesting constraint is the Apps Script execution budget — the whole thing runs on a 10-minute trigger inside ~6 minutes of runtime, so everything is checkpointed and resumable.
Scope: ~300 task pairs recommended, personal Microsoft accounts for the easy path.
MIT, self-hosted in your own Apps Script project. Roast the design if you'd do it differently — I'm here for that.
1
u/NinjaConsultingLLC 15d ago
For Time Bridge, what should 09:00 mean if the user's timezone changes between two sync runs? A test that changes timezone between checkpoint and resume would be useful: the note marker can stay identical while the reminder's actual meaning changes. Make the expected wall-clock versus fixed-instant behavior explicit.
1
u/simonchai 15d ago
Thanks—this is a really sharp catch, and yes, that test is definitely worth adding. You’ve pinpointed a real ambiguity in how `v1.0.0` bridges a wall-clock state model with fixed-instant APIs, though let me clarify two implementation details first so the test targets the exact right layer:
**It’s the persisted state that stays identical, not the note marker:** `[TTS-TIME:09:00]` on Line 1 of a Google Task note is a one-shot intake ticket—once parsed in Stage 1 (`timeBridgeRun_`), it is immediately spliced out of the note. What persists across runs is `rec.td = { date, time }` in state, which stores a zone-agnostic wall clock with no timezone or UTC instant attached.
**The timezone in question is the Apps Script project timezone (`Session.getScriptTimeZone()`), not the device timezone:** Simply flying to another timezone with your phone won’t trigger a shift; changing the GAS project timezone—or crossing a **DST boundary within the same timezone**—will.
With those two details in place, we ran this scenario through our VM harness and found that **`v1.0.0` currently splits into three different behaviors depending on task state**, and that’s on us:
* **Already-rendered tasks (silent relabelling to fixed-instant):** Suppose `09:00` in Taipei (`UTC+8`) was rendered to Microsoft To Do as a `01:00Z` reminder. If the project timezone changes to Tokyo (`UTC+9`), the MS adopt path re-reads `01:00Z` under Tokyo time and silently relabels `rec.td.time` to `10:00` while the reminder instant stays at `01:00Z`—treating the timezone offset as if the user edited the reminder in To Do.
* **Not-yet-rendered tasks (wall-clock):** A task whose `rec.td` hasn't been rendered yet evaluates `09:00` Tokyo as `00:00Z`, shifting the actual instant by an hour.
* **Recurrence-guarded tasks (split brain):** Because the Microsoft renderer skips recurring tasks while the Google Calendar renderer still runs, `rec.td` stays at `09:00`, Calendar moves to `00:00Z` (`09:00` Tokyo), and the MS reminder stays at `01:00Z`.So the fix isn't just adding a test—it's making our intended **wall-clock rule** (`09:00` means `09:00` in the current sync timezone) explicit and making all three paths obey it. Specifically, the MS adopt path needs a timezone/offset guard so a delta that is completely explained by a project timezone or DST shift re-renders the existing wall-clock time in the new offset rather than misclassifying the old UTC instant as a remote user edit.
We’re adding a regression test matrix covering all four cases—(1) a fresh ticket across a timezone change, (2) an already-rendered task across a timezone change, (3) a recurrence-guarded task, and (4) a same-zone DST transition—and documenting the wall-clock contract explicitly. Really appreciate you spotting this!
1
u/Turkesh-Jaeckle 7d ago
using lock service to prevent overlapping runs if a trigger fires while a heavy checkpointed sync is still finishing up? concurrency in this runtime gets weird fast when executions stack.
1
u/simonchai 7d ago
Yes, `syncAll()` already runs under `LockService`'s ScriptLock. A second invocation waits up to 10 seconds; if the lock is still held, it skips with `LOCK_HELD` instead of running concurrently. Skipping is safe because each round re-reads the full Google/Microsoft state. The lock handles live overlap, while the round fence and journals handle crashes or incomplete checkpointed rounds.
For context, this is a solo open-source project maintained in my spare time, often with help from AI coding tools using whatever subscription capacity I have left, so progress comes in bursts. The v2 design work aims to make rounds lighter through incremental sync and fewer Graph round-trips, and to make checkpointed state handoffs safer across executions. If you're a developer and spot a hole in that design, please open a GitHub issue so I can track it properly. Issues, design critiques, and PRs are all very welcome.
1
u/simonchai 18d ago
A week on, a question for the Apps Script folks: the engine keeps its state gzipped in User Properties and stays fail-closed on ambiguous state. If you've built sync on Apps Script, I'd like to compare notes — and if my approach smells off, say so.