@rubytech/create-maxy-code 0.1.474 → 0.1.476

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  3. package/payload/platform/lib/storage-broker/dist/cf-exec.js +3 -2
  4. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  5. package/payload/platform/lib/storage-broker/src/cf-exec.ts +3 -2
  6. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +18 -9
  7. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/schema.sql +13 -3
  8. package/payload/platform/plugins/docs/references/calendar-booking.md +2 -2
  9. package/payload/platform/plugins/docs/references/google-guide.md +15 -6
  10. package/payload/platform/plugins/google/.claude-plugin/plugin.json +1 -1
  11. package/payload/platform/plugins/google/PLUGIN.md +60 -12
  12. package/payload/platform/plugins/google/mcp/dist/__tests__/account-audit.test.d.ts +2 -0
  13. package/payload/platform/plugins/google/mcp/dist/__tests__/account-audit.test.d.ts.map +1 -0
  14. package/payload/platform/plugins/google/mcp/dist/__tests__/account-audit.test.js +229 -0
  15. package/payload/platform/plugins/google/mcp/dist/__tests__/account-audit.test.js.map +1 -0
  16. package/payload/platform/plugins/google/mcp/dist/__tests__/account-register.test.js +9 -9
  17. package/payload/platform/plugins/google/mcp/dist/__tests__/account-register.test.js.map +1 -1
  18. package/payload/platform/plugins/google/mcp/dist/__tests__/consent-url-redirect.test.d.ts +2 -0
  19. package/payload/platform/plugins/google/mcp/dist/__tests__/consent-url-redirect.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/google/mcp/dist/__tests__/consent-url-redirect.test.js +17 -0
  21. package/payload/platform/plugins/google/mcp/dist/__tests__/consent-url-redirect.test.js.map +1 -0
  22. package/payload/platform/plugins/google/mcp/dist/__tests__/loopback-listener.test.d.ts +2 -0
  23. package/payload/platform/plugins/google/mcp/dist/__tests__/loopback-listener.test.d.ts.map +1 -0
  24. package/payload/platform/plugins/google/mcp/dist/__tests__/loopback-listener.test.js +146 -0
  25. package/payload/platform/plugins/google/mcp/dist/__tests__/loopback-listener.test.js.map +1 -0
  26. package/payload/platform/plugins/google/mcp/dist/__tests__/pending-redirect.test.d.ts +2 -0
  27. package/payload/platform/plugins/google/mcp/dist/__tests__/pending-redirect.test.d.ts.map +1 -0
  28. package/payload/platform/plugins/google/mcp/dist/__tests__/pending-redirect.test.js +19 -0
  29. package/payload/platform/plugins/google/mcp/dist/__tests__/pending-redirect.test.js.map +1 -0
  30. package/payload/platform/plugins/google/mcp/dist/__tests__/register-redirect-chain.test.d.ts +2 -0
  31. package/payload/platform/plugins/google/mcp/dist/__tests__/register-redirect-chain.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/google/mcp/dist/__tests__/register-redirect-chain.test.js +41 -0
  33. package/payload/platform/plugins/google/mcp/dist/__tests__/register-redirect-chain.test.js.map +1 -0
  34. package/payload/platform/plugins/google/mcp/dist/auth/account-audit.d.ts +44 -0
  35. package/payload/platform/plugins/google/mcp/dist/auth/account-audit.d.ts.map +1 -0
  36. package/payload/platform/plugins/google/mcp/dist/auth/account-audit.js +112 -0
  37. package/payload/platform/plugins/google/mcp/dist/auth/account-audit.js.map +1 -0
  38. package/payload/platform/plugins/google/mcp/dist/auth/loopback-listener.d.ts +17 -0
  39. package/payload/platform/plugins/google/mcp/dist/auth/loopback-listener.d.ts.map +1 -0
  40. package/payload/platform/plugins/google/mcp/dist/auth/loopback-listener.js +143 -0
  41. package/payload/platform/plugins/google/mcp/dist/auth/loopback-listener.js.map +1 -0
  42. package/payload/platform/plugins/google/mcp/dist/auth/pending-store.d.ts +9 -0
  43. package/payload/platform/plugins/google/mcp/dist/auth/pending-store.d.ts.map +1 -1
  44. package/payload/platform/plugins/google/mcp/dist/auth/pending-store.js.map +1 -1
  45. package/payload/platform/plugins/google/mcp/dist/index.js +73 -5
  46. package/payload/platform/plugins/google/mcp/dist/index.js.map +1 -1
  47. package/payload/platform/plugins/google/mcp/dist/scripts/account-audit.d.ts +2 -0
  48. package/payload/platform/plugins/google/mcp/dist/scripts/account-audit.d.ts.map +1 -0
  49. package/payload/platform/plugins/google/mcp/dist/scripts/account-audit.js +37 -0
  50. package/payload/platform/plugins/google/mcp/dist/scripts/account-audit.js.map +1 -0
  51. package/payload/platform/plugins/google/mcp/dist/tools/account-register.d.ts +20 -4
  52. package/payload/platform/plugins/google/mcp/dist/tools/account-register.d.ts.map +1 -1
  53. package/payload/platform/plugins/google/mcp/dist/tools/account-register.js +19 -9
  54. package/payload/platform/plugins/google/mcp/dist/tools/account-register.js.map +1 -1
  55. package/payload/platform/plugins/google/references/auth.md +93 -14
  56. package/payload/platform/plugins/scheduling/PLUGIN.md +2 -2
  57. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js +158 -23
  58. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map +1 -1
  59. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-schema-migration.test.d.ts +2 -0
  60. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-schema-migration.test.d.ts.map +1 -0
  61. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-schema-migration.test.js +200 -0
  62. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-schema-migration.test.js.map +1 -0
  63. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-sync-provision.test.d.ts +2 -0
  64. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-sync-provision.test.d.ts.map +1 -0
  65. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-sync-provision.test.js +309 -0
  66. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-sync-provision.test.js.map +1 -0
  67. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/d1-command.test.js +219 -19
  68. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/d1-command.test.js.map +1 -1
  69. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +94 -11
  70. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
  71. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +106 -22
  72. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
  73. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.d.ts +109 -0
  74. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.d.ts.map +1 -0
  75. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.js +213 -0
  76. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-sync-provision.js.map +1 -0
  77. package/payload/platform/plugins/scheduling/mcp/dist/lib/d1-command.d.ts +80 -8
  78. package/payload/platform/plugins/scheduling/mcp/dist/lib/d1-command.d.ts.map +1 -1
  79. package/payload/platform/plugins/scheduling/mcp/dist/lib/d1-command.js +202 -8
  80. package/payload/platform/plugins/scheduling/mcp/dist/lib/d1-command.js.map +1 -1
  81. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts +6 -4
  82. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts.map +1 -1
  83. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +334 -142
  84. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  85. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts +21 -3
  86. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts.map +1 -1
  87. package/payload/platform/services/claude-session-manager/dist/activity-rows.js +6 -2
  88. package/payload/platform/services/claude-session-manager/dist/activity-rows.js.map +1 -1
  89. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  90. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +43 -5
  91. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  92. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +9 -0
  93. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
  94. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +78 -0
  95. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
  96. package/payload/server/{chunk-Y6V45R6M.js → chunk-LBCMFD4O.js} +173 -29
  97. package/payload/server/maxy-edge.js +1 -1
  98. package/payload/server/public/activity.html +6 -6
  99. package/payload/server/public/assets/{AdminLoginScreens-B44KbEuC.js → AdminLoginScreens-DximwPlS.js} +1 -1
  100. package/payload/server/public/assets/{AdminShell-BjxV2qkb.js → AdminShell-BehbH-Oa.js} +1 -1
  101. package/payload/server/public/assets/{Checkbox-DSeZNGNA.js → Checkbox-DM-eHqOS.js} +1 -1
  102. package/payload/server/public/assets/activity-hDwUeQd6.js +1 -0
  103. package/payload/server/public/assets/{admin-B4LpTCur.js → admin-CAkDnGdk.js} +1 -1
  104. package/payload/server/public/assets/{browser-CEFvaWwI.js → browser-DXl8hOoi.js} +1 -1
  105. package/payload/server/public/assets/{calendar-B3zo6V34.js → calendar-DP6hn4-6.js} +1 -1
  106. package/payload/server/public/assets/chat-DhOfVGYM.js +1 -0
  107. package/payload/server/public/assets/chevron-left-DYNBoLii.js +1 -0
  108. package/payload/server/public/assets/data-bq8fTo_s.js +1 -0
  109. package/payload/server/public/assets/{graph-RaOrnSf2.js → graph-CHcYoEJ5.js} +1 -1
  110. package/payload/server/public/assets/{graph-labels-GlTEIYJj.js → graph-labels-CZykslZM.js} +1 -1
  111. package/payload/server/public/assets/{maximize-2-CHDa8p5h.js → maximize-2-31AZEcMS.js} +1 -1
  112. package/payload/server/public/assets/{operator-DtqHY4jV.js → operator-BGGpdsO_.js} +1 -1
  113. package/payload/server/public/assets/{page-CkhFj8GQ.js → page-BloC6ygA.js} +1 -1
  114. package/payload/server/public/assets/{page-DxnWgPtd.js → page-DOeiiqbR.js} +1 -1
  115. package/payload/server/public/assets/{public-D5TUfQ3I.js → public-CGgO6IZv.js} +1 -1
  116. package/payload/server/public/assets/{rotate-ccw-BJjVAQQY.js → rotate-ccw-BlAi6iFg.js} +1 -1
  117. package/payload/server/public/assets/{tasks-CM9pYYG1.js → tasks-C1giFRRL.js} +1 -1
  118. package/payload/server/public/assets/{time-entry-format-C5RYEYjM.js → time-entry-format-CiUUQdq7.js} +1 -1
  119. package/payload/server/public/assets/{triangle-alert-DLfIsuo_.js → triangle-alert-CVqKuLoa.js} +1 -1
  120. package/payload/server/public/assets/{useCopyFeedback-7yKeiAzV.js → useCopyFeedback-ksyUe_g1.js} +1 -1
  121. package/payload/server/public/assets/{useSelectionMode-CWlh6AEq.js → useSelectionMode-BPrBkdTd.js} +1 -1
  122. package/payload/server/public/assets/{useSubAccountSwitcher-B2n9QPVI.css → useSubAccountSwitcher-K4kYeN6i.css} +1 -1
  123. package/payload/server/public/assets/{useVoiceRecorder-j3ZArZ2H.js → useVoiceRecorder-CTXP8oTW.js} +1 -1
  124. package/payload/server/public/browser.html +5 -5
  125. package/payload/server/public/calendar.html +6 -6
  126. package/payload/server/public/chat.html +12 -12
  127. package/payload/server/public/data.html +11 -11
  128. package/payload/server/public/graph.html +11 -11
  129. package/payload/server/public/index.html +14 -14
  130. package/payload/server/public/operator.html +14 -14
  131. package/payload/server/public/public.html +12 -12
  132. package/payload/server/public/tasks.html +5 -5
  133. package/payload/server/server.js +547 -423
  134. package/payload/server/public/assets/activity-CdipMTDp.js +0 -1
  135. package/payload/server/public/assets/chat-5tJdo9i_.js +0 -1
  136. package/payload/server/public/assets/chevron-left-BJllrPBj.js +0 -1
  137. package/payload/server/public/assets/data-Du79EgR-.js +0 -1
  138. /package/payload/server/public/assets/{useSubAccountSwitcher-CBlKgwms.js → useSubAccountSwitcher-DzgzTRRn.js} +0 -0
@@ -133,17 +133,40 @@ Either reading `false` on a brand that ships both is the mis-stamp signature.
133
133
 
134
134
  ### Connecting an account, once the brand ships a client
135
135
 
136
- `google-account-register` returns a consent URL. The user opens it on any
137
- device and approves. Google redirects to `http://127.0.0.1:8571/?code=…`, which
138
- **fails to load by design** — nothing listens there, and the consenting browser
139
- is the user's own phone. The user copies the `code` value out of the address bar
140
- of that failed page and passes it to `google-account-register-submit`, which
141
- exchanges it in-process. `google-account-register-poll` reports the outcome at
142
- any point.
143
-
144
- That failed-page step is a real UX cost, priced and accepted: some mobile
145
- browsers hide the URL of a failed navigation. It is the price of a client type
146
- whose secret can legitimately ship to customer devices.
136
+ `google-account-register` returns a consent URL. The user opens it on any device
137
+ and approves. What happens next depends on **where** they approved, and both
138
+ paths are supported.
139
+
140
+ **On the install's own machine.** Register binds a single-shot listener on
141
+ `127.0.0.1` at an OS-assigned port and puts that port in the consent URL, so
142
+ Google's redirect reaches it. The listener verifies `state`, completes the
143
+ exchange in-process, and renders a page naming the connected account. Nothing is
144
+ pasted. The page asks the user to return to the chat and say so, because the
145
+ agent has no callback and learns the outcome from
146
+ `google-account-register-poll`.
147
+
148
+ **On another device.** A phone cannot route to the install's loopback, so the
149
+ redirect fails to load exactly as before. The user copies the whole address-bar
150
+ URL and passes it to `google-account-register-submit`, which exchanges it
151
+ in-process. This path is not a degraded mode; it is the designed route for the
152
+ Pi deployment, where the admin always consents from a phone.
153
+
154
+ That failed-page step remains a real UX cost on the cross-device path, priced
155
+ and accepted: some mobile browsers hide the URL of a failed navigation. It is
156
+ the price of a client type whose secret can legitimately ship to customer
157
+ devices, and it cannot be removed — the phone has no route to the install.
158
+
159
+ **The port is no longer fixed**, and the consent URL is no longer deterministic.
160
+ RFC 8252 §7.3 is what permits this: Google ignores the port when matching a
161
+ loopback redirect, so binding `:0` is free. It also retires a latent collision,
162
+ since a fixed port is shared by every brand and two co-resident brands would
163
+ contend for it. `http://127.0.0.1:8571` survives as the fallback when the bind
164
+ fails, which keeps an install that cannot bind working exactly as it did before.
165
+
166
+ Because the port varies per registration, the pending entry stores the redirect
167
+ it issued and the exchange sends that stored value back. Google compares the two
168
+ byte for byte, so a registration whose listener bound a port cannot be completed
169
+ against the constant.
147
170
 
148
171
  ---
149
172
 
@@ -318,7 +341,60 @@ Two divergences from Outlook are load-bearing:
318
341
  credentials.
319
342
 
320
343
  Refresh runs **in-process** against Google, within 5 minutes of access-token
321
- expiry. Nothing in `platform/ui` participates.
344
+ expiry. No `platform/ui` code mints, exchanges or refreshes a credential; its
345
+ only Google surface is the standing pending audit below, which reads the age of
346
+ `pending-oauth.enc` and never decrypts it.
347
+
348
+ ---
349
+
350
+ ## Standing audits
351
+
352
+ Both states these cover are unobservable by any event: an abandoned registration
353
+ fires nothing, and a refresh token expiring is a date passing, not an action.
354
+ `runAccountRegisterPoll` emits `op=pending-stranded` only when somebody polls —
355
+ and the case worth catching is the one nobody polls.
356
+
357
+ - **Stranded pending.** In-process in `platform/ui`
358
+ (`server/google-pending-audit.ts`), every 5 minutes, over every account uuid
359
+ under `data/accounts`. Any `pending-oauth.enc` at or past
360
+ `PENDING_LIFETIME_SEC` (600s) emits
361
+ `[google-oauth] op=pending-stranded account=<id> ageSec=<N>`. It runs in-process
362
+ rather than in the spawned script precisely so the signal does not depend on a
363
+ spawn being healthy — the same rationale as Outlook's `devicecode-stranded`
364
+ audit. A pending file that vanishes mid-scan (a concurrent submit clearing it)
365
+ is skipped per-account; it must never abort the cycle, because that would
366
+ suppress the heartbeat and read as the audit being dead.
367
+ - **Refresh TTL.** Spawned `mcp/dist/scripts/account-audit.js`, hourly, one
368
+ `[google-account-audit] account=<id> email=<addr> refreshTtlDays=<D> status=<…>`
369
+ per connected Google account. It is a spawned script, not in-process, because
370
+ classifying a token means decrypting the blob with the per-account AES key and
371
+ `TokenStore` is the only holder of that contract. An unreadable blob is skipped,
372
+ never fatal — one corrupt store must not blind the audit to every other account.
373
+
374
+ **Heartbeats, and why they are not optional.** Each audit emits a heartbeat every
375
+ cycle, including empty ones. Without it, an audit that has stopped running looks
376
+ exactly like an audit reporting all-clear, and the operator learns nothing until
377
+ an account has already fallen off. The absence of a heartbeat across more than
378
+ one interval is the failure signal. A pending cycle that throws logs
379
+ `op=pending-audit-error` — a deliberately distinct token, so a broken cycle is
380
+ never miscounted as a healthy heartbeat by a reader grepping `op=pending-audit`.
381
+
382
+ **What `refreshTtlDays` is, exactly.** Days remaining on Google's 180-day
383
+ inactivity revocation clock, computed as `180 - daysSince(lastRefresh)` and
384
+ clamped so a clock that ran ahead cannot report more than 180. `TokenBlob` stores
385
+ no refresh-token expiry because Google issues none, so this inactivity clock is
386
+ the only bounded quantity available locally. It does **not** predict the other
387
+ revocation causes — the user revoking access, a password change, the app being
388
+ unpublished — none of which are visible from disk. `refreshToken: null` is
389
+ reported `expired` with zero days however recently it was written, since such a
390
+ connection dies with its access token about an hour out.
391
+
392
+ Diagnostic path:
393
+
394
+ ```
395
+ ~/<brand>-code/platform/scripts/logs-read.sh --tail server 200 \
396
+ | grep -E '\[google-oauth\]|\[google-account-audit\]'
397
+ ```
322
398
 
323
399
  ---
324
400
 
@@ -330,8 +406,11 @@ The earlier hosted-callback model built a hosted HTTPS callback at
330
406
  Desktop client cannot register an HTTPS redirect, so all of it is deleted:
331
407
  `platform/ui/server/routes/google-oauth.ts` and its test, the
332
408
  `/api/google/oauth/callback` allowlist entry, the mount in `server/index.ts`,
333
- and the `MAXY_UI_INTERNAL_PORT` dependency. There is zero Google surface in
334
- `platform/ui`, and a test asserts the public host still refuses that path.
409
+ and the `MAXY_UI_INTERNAL_PORT` dependency. No OAuth surface remains in
410
+ `platform/ui` no route, no exchange, no credential and a test asserts the
411
+ public host still refuses that path. The one `platform/ui` file that mentions
412
+ Google is `server/google-pending-audit.ts`, which reads the age of a pending file
413
+ and is not reachable over HTTP at all.
335
414
 
336
415
  **Devices touched by the earlier plugin may still hold
337
416
  `platform/config/google-house.env`, containing the retired Web client's live
@@ -182,13 +182,13 @@ Dispatch results are recorded on the Event node (`lastDispatchResult`, `lastDisp
182
182
 
183
183
  ## Reconciling public bookings (D1 → graph)
184
184
 
185
- `scheduling/mcp/dist/scripts/reconcile-bookings.js` turns public bookings into calendar meetings. The booking site (deployed by the cloudflare `calendar-site` skill) writes each submission to a Cloudflare D1 `bookings` table at the edge, so capture survives a brief Pi outage. This binary pulls accepted, unswept rows from each account's D1, writes one `:Meeting {source:'booking', uid:'booking-<id>'}` per row (idempotent — a re-run writes no duplicate), links the booker as a `:Person` via `[:ATTENDED]` (respecting the global email-uniqueness skip), notifies the admin (email; WhatsApp only when a thread already exists, never cold), then marks the row swept.
185
+ `scheduling/mcp/dist/scripts/reconcile-bookings.js` turns public bookings into calendar meetings. The booking site (deployed by the cloudflare `calendar-site` skill) writes each submission to a Cloudflare D1 `bookings` table at the edge, so capture survives a brief Pi outage. This binary pulls eligible rows (`status='accepted' AND kind='booking'`) from each account's D1, claims the ones with no matching `:Meeting`, writes one `:Meeting {source:'booking', uid:'booking-<id>'}` per row (idempotent — a re-run writes no duplicate), links the booker as a `:Person` via `[:ATTENDED]` (respecting the global email-uniqueness skip), notifies the admin (email; WhatsApp only when a thread already exists, never cold), then marks the row swept. A row is claimed on the absence of its meeting rather than on `swept`, so a meeting that is later deleted or lost is rewritten on the next run; `swept` records only that a write was attempted. `kind='block'` rows reserve a slot and are never reconciled.
186
186
 
187
187
  It is **armed by the always-running `platform/ui` server on an interval**, because the Pi has no other periodic surface (the same gap `check-due-events` has). A PID lock at `<configDir>/reconcile-bookings.lock` prevents overlapping runs. It registers **no agent tool** — there is nothing for the agent to call — so the tool-surface registries are untouched.
188
188
 
189
189
  Env: `PLATFORM_ROOT`, `NEO4J_URI` (and `NEO4J_PASSWORD` or `config/.neo4j-password`). Per account it reads `data/accounts/<id>/calendar-availability.json` (for `bookingDbName`). The Cloudflare credential is read house-only from `config/cloudflare-house.env`; that credential is a minter, so the calendar mints a `calendar-d1` (D1-only) token from it via `cf-token.sh` and shells `wrangler` for the D1 reads/writes on that scoped token, never the minter directly.
190
190
 
191
- Observability: one `[calendar-booking]` line per step keyed by `bookingId` (`reconcile-claim`, `meeting-write`, `notify`, `booked`), and one `[calendar-reconcile] account=… pendingUnreconciled=… orphanMeetings=…` audit line per account per run. A `pendingUnreconciled` that persists across runs means reconcile is stalled; `orphanMeetings` means a booking meeting has no backing D1 row.
191
+ Observability: one `[calendar-booking]` line per step keyed by `bookingId` (`reconcile-claim`, `meeting-write`, `notify`, `booked`), and one `[calendar-reconcile] account=… eligibleExamined=… pendingUnreconciled=… orphanMeetings=…` audit line per account per run. `eligibleExamined` separates "nothing to do" from "verified clean". A `pendingUnreconciled` that persists across runs means those rows are failing to become meetings rather than merely lagging — the `attempt=` count on the claim and failure lines says how many times each has been tried. `orphanMeetings` means a booking meeting has no backing eligible D1 row.
192
192
 
193
193
  ## Ingesting calendar archives (.ics → graph)
194
194
 
@@ -5,7 +5,8 @@
5
5
  // account always resolves or creates its own Person for the booking
6
6
  // email, never a foreign account's).
7
7
  import { describe, it, expect, vi } from "vitest";
8
- import { reconcileOne, auditCounts, bookingUid, ensureConnectorSyncEvent, countPublishedConflicts } from "../booking-reconcile.js";
8
+ import { readFile } from "node:fs/promises";
9
+ import { reconcileOne, auditCounts, bookingUid, ensureConnectorSyncEvent, classifyPublishedDrift, selectUnreconciled, fetchBookingMeetingUids, ELIGIBLE_BOOKING_PREDICATE, ELIGIBLE_BOOKING_SELECT, BOOKING_SCHEMA_MIGRATION, isMissingColumnError, isDuplicateColumnError, } from "../booking-reconcile.js";
9
10
  const booking = {
10
11
  bookingId: "abc",
11
12
  slotStart: "2026-06-22T09:00:00Z",
@@ -52,20 +53,82 @@ describe("reconcileOne", () => {
52
53
  expect(r.created).toBe(false);
53
54
  });
54
55
  });
56
+ describe("eligibility predicate", () => {
57
+ // Asserting the constant equals its own literal would be a tautology. What
58
+ // matters is that it qualifies on BOTH axes: status alone counted block rows
59
+ // forever (1806), and kind alone would sweep cancelled rows.
60
+ it("qualifies on status and on kind", () => {
61
+ expect(ELIGIBLE_BOOKING_PREDICATE).toMatch(/status\s*=\s*'accepted'/);
62
+ expect(ELIGIBLE_BOOKING_PREDICATE).toMatch(/kind\s*=\s*'booking'/);
63
+ });
64
+ // The load-bearing property of 1806: there is ONE definition, and the read
65
+ // both call sites use is built from it rather than restating it.
66
+ it("builds the sweep/audit read from that predicate and asks for attempts", () => {
67
+ expect(ELIGIBLE_BOOKING_SELECT).toContain(ELIGIBLE_BOOKING_PREDICATE);
68
+ expect(ELIGIBLE_BOOKING_SELECT).toMatch(/\battempts\b/);
69
+ expect(ELIGIBLE_BOOKING_SELECT).toMatch(/ORDER BY id/);
70
+ });
71
+ // 1807 — the sweep must not consult swept. A predicate carrying it would make
72
+ // a booking whose meeting went missing unreachable again.
73
+ it("does not filter on swept", () => {
74
+ expect(ELIGIBLE_BOOKING_SELECT).not.toMatch(/WHERE[\s\S]*\bswept\b/);
75
+ });
76
+ });
77
+ describe("selectUnreconciled", () => {
78
+ const rows = [{ bookingId: "reconciled" }, { bookingId: "lost" }];
79
+ // Task 1807 — the Rob Mead case in fixture form. This row is swept=1 in D1;
80
+ // the sweep must claim it anyway, because its meeting is absent. The old
81
+ // swept=0 predicate returns the empty set here.
82
+ it("claims an eligible row whose meeting is absent", () => {
83
+ const claimed = selectUnreconciled(rows, new Set([bookingUid("reconciled")]));
84
+ expect(claimed.map((r) => r.bookingId)).toEqual(["lost"]);
85
+ });
86
+ // The mutation guard the task requires. A predicate that returned every
87
+ // accepted row would pass the test above and fail this one.
88
+ it("does not claim a row whose meeting is present", () => {
89
+ const claimed = selectUnreconciled(rows, new Set([bookingUid("reconciled"), bookingUid("lost")]));
90
+ expect(claimed).toEqual([]);
91
+ });
92
+ it("claims everything when the graph holds no booking meetings", () => {
93
+ expect(selectUnreconciled(rows, new Set()).map((r) => r.bookingId)).toEqual(["reconciled", "lost"]);
94
+ });
95
+ });
55
96
  describe("auditCounts", () => {
56
- it("counts pending (D1 ahead) and orphans (graph ahead)", async () => {
57
- const run = vi.fn(async () => ({
58
- records: [{ get: () => ["booking-a", "booking-b"] }],
97
+ it("counts pending (D1 ahead) and orphans (graph ahead)", () => {
98
+ const counts = auditCounts(["a", "c"], ["booking-a", "booking-b"]);
99
+ // 'c' eligible in D1 but no meeting -> pending 1; 'booking-b' meeting with no D1 row -> orphan 1.
100
+ expect(counts).toEqual({ eligibleExamined: 2, pendingUnreconciled: 1, orphanMeetings: 1 });
101
+ });
102
+ it("is clean when D1 and graph agree", () => {
103
+ expect(auditCounts(["a", "b"], ["booking-a", "booking-b"])).toEqual({
104
+ eligibleExamined: 2,
105
+ pendingUnreconciled: 0,
106
+ orphanMeetings: 0,
107
+ });
108
+ });
109
+ // Task 1806 observability — an audit that returns the same value for "clean"
110
+ // and "no input" is not an audit. eligibleExamined is what separates them.
111
+ it("distinguishes nothing-to-do from verified-clean", () => {
112
+ const empty = auditCounts([], []);
113
+ const clean = auditCounts(["a"], ["booking-a"]);
114
+ expect(empty.pendingUnreconciled).toBe(clean.pendingUnreconciled);
115
+ expect(empty.eligibleExamined).toBe(0);
116
+ expect(clean.eligibleExamined).toBe(1);
117
+ });
118
+ });
119
+ describe("fetchBookingMeetingUids", () => {
120
+ it("reads this account's booking meeting uids", async () => {
121
+ const run = vi.fn(async (_cypher, _params) => ({
122
+ records: [{ get: () => ["booking-a"] }],
59
123
  }));
60
- const counts = await auditCounts({ run }, "acct-1", ["a", "c"]);
61
- // 'c' accepted in D1 but no meeting -> pending 1; 'booking-b' meeting with no D1 row -> orphan 1.
62
- expect(counts.pendingUnreconciled).toBe(1);
63
- expect(counts.orphanMeetings).toBe(1);
124
+ const uids = await fetchBookingMeetingUids({ run }, "acct-1");
125
+ expect(uids).toEqual(["booking-a"]);
126
+ expect(run.mock.calls[0][0]).toMatch(/source: 'booking'/);
127
+ expect(run.mock.calls[0][1]).toEqual({ accountId: "acct-1" });
64
128
  });
65
- it("is clean when D1 and graph agree", async () => {
66
- const run = vi.fn(async () => ({ records: [{ get: () => ["booking-a", "booking-b"] }] }));
67
- const counts = await auditCounts({ run }, "acct-1", ["a", "b"]);
68
- expect(counts).toEqual({ pendingUnreconciled: 0, orphanMeetings: 0 });
129
+ it("returns an empty list when the account has no booking meetings", async () => {
130
+ const run = vi.fn(async () => ({ records: [] }));
131
+ expect(await fetchBookingMeetingUids({ run }, "acct-1")).toEqual([]);
69
132
  });
70
133
  });
71
134
  describe("ensureConnectorSyncEvent", () => {
@@ -84,10 +147,11 @@ describe("ensureConnectorSyncEvent", () => {
84
147
  };
85
148
  return { session: session, calls };
86
149
  }
150
+ const WA = { channel: "whatsapp", destination: "+44 7504 472444" };
87
151
  it("creates the recurring sync event when none exists", async () => {
88
152
  const { session, calls } = sessionReturning(0);
89
- const res = await ensureConnectorSyncEvent(session, "acct-1", "+44 7504 472444");
90
- expect(res).toBe("created");
153
+ const res = await ensureConnectorSyncEvent(session, "acct-1", WA);
154
+ expect(res).toBe("provisioned");
91
155
  const merge = calls.find((c) => /CREATE \(e:Event/.test(c.cypher));
92
156
  expect(merge).toBeTruthy();
93
157
  expect(merge.params.accountId).toBe("acct-1");
@@ -96,28 +160,99 @@ describe("ensureConnectorSyncEvent", () => {
96
160
  expect(merge.params.recurrence).toBe("*/30 * * * *");
97
161
  expect(String(merge.params.agentPrompt)).toMatch(/connector/i);
98
162
  });
163
+ // Task 1809 — the channel is whatever the caller resolved, not a hardcoded
164
+ // 'whatsapp'. An account reachable only on Telegram provisions on Telegram.
165
+ it("writes the resolved channel verbatim rather than assuming WhatsApp", async () => {
166
+ const { session, calls } = sessionReturning(0);
167
+ const res = await ensureConnectorSyncEvent(session, "acct-1", {
168
+ channel: "telegram",
169
+ destination: "123456789",
170
+ });
171
+ expect(res).toBe("provisioned");
172
+ const merge = calls.find((c) => /CREATE \(e:Event/.test(c.cypher));
173
+ expect(merge.params.agentChannel).toBe("telegram");
174
+ expect(merge.params.agentDestination).toBe("123456789");
175
+ });
99
176
  it("is a no-op when the sync event already exists", async () => {
100
177
  const { session, calls } = sessionReturning(1);
101
- const res = await ensureConnectorSyncEvent(session, "acct-1", "+44 7504 472444");
102
- expect(res).toBe("exists");
178
+ const res = await ensureConnectorSyncEvent(session, "acct-1", WA);
179
+ expect(res).toBe("already-provisioned");
103
180
  expect(calls.some((c) => /CREATE \(e:Event/.test(c.cypher))).toBe(false);
104
181
  });
105
182
  });
106
- describe("countPublishedConflicts", () => {
183
+ describe("booking schema migration", () => {
184
+ it("adds both columns and backfills known machine-written block rows", () => {
185
+ expect(BOOKING_SCHEMA_MIGRATION).toEqual([
186
+ "ALTER TABLE bookings ADD COLUMN kind TEXT NOT NULL DEFAULT 'booking'",
187
+ "ALTER TABLE bookings ADD COLUMN attempts INTEGER NOT NULL DEFAULT 0",
188
+ "UPDATE bookings SET kind='block' WHERE note='Auto-block: Google Calendar'",
189
+ ]);
190
+ });
191
+ // The backfill runs once, at column creation. Running control flow keys on
192
+ // `kind`, never on the note — the string appears here and nowhere else. This
193
+ // is the boundary Task 1806 drew when it rejected note-matching as a
194
+ // predicate, pinned mechanically rather than promised in a comment.
195
+ it("reads the note in the migration only", async () => {
196
+ const src = await readFile(new URL("../booking-reconcile.ts", import.meta.url), "utf8");
197
+ expect(src.split("Auto-block: Google Calendar").length - 1).toBe(1);
198
+ });
199
+ it("recognises a missing-column failure so the migration can be triggered", () => {
200
+ expect(isMissingColumnError("D1_ERROR: no such column: kind at offset 62")).toBe(true);
201
+ expect(isMissingColumnError("no such table: bookings")).toBe(false);
202
+ expect(isMissingColumnError("Authentication error [code: 10000]")).toBe(false);
203
+ });
204
+ it("recognises a duplicate-column failure so a partial migration is a no-op", () => {
205
+ expect(isDuplicateColumnError("duplicate column name: kind")).toBe(true);
206
+ expect(isDuplicateColumnError("no such column: kind")).toBe(false);
207
+ });
208
+ });
209
+ describe("classifyPublishedDrift", () => {
107
210
  const busy = [{ start: "2026-06-22T10:00:00Z", end: "2026-06-22T11:00:00Z" }];
108
- it("counts a published slot that overlaps a connector-busy interval", () => {
211
+ it("reports verified with the conflict when a published slot overlaps busy", () => {
109
212
  const published = [
110
213
  { start: "2026-06-22T10:30:00Z", end: "2026-06-22T11:30:00Z" }, // overlaps
111
214
  { start: "2026-06-22T12:00:00Z", end: "2026-06-22T13:00:00Z" }, // clear
112
215
  ];
113
- expect(countPublishedConflicts(published, busy)).toBe(1);
216
+ expect(classifyPublishedDrift(published, busy)).toEqual({
217
+ outcome: "verified",
218
+ published: 2,
219
+ busy: 1,
220
+ conflicts: 1,
221
+ });
114
222
  });
115
- it("returns 0 when no published slot overlaps busy", () => {
223
+ it("reports verified with 0 conflicts when both sets are non-empty and disjoint", () => {
116
224
  const published = [{ start: "2026-06-22T09:00:00Z", end: "2026-06-22T10:00:00Z" }]; // touches edge, no overlap
117
- expect(countPublishedConflicts(published, busy)).toBe(0);
225
+ expect(classifyPublishedDrift(published, busy)).toEqual({
226
+ outcome: "verified",
227
+ published: 1,
228
+ busy: 1,
229
+ conflicts: 0,
230
+ });
231
+ });
232
+ it("reports no-busy-data when there is nothing to compare published slots against", () => {
233
+ const published = [{ start: "2026-06-22T10:30:00Z", end: "2026-06-22T11:30:00Z" }];
234
+ expect(classifyPublishedDrift(published, [])).toEqual({
235
+ outcome: "no-busy-data",
236
+ published: 1,
237
+ busy: 0,
238
+ conflicts: 0,
239
+ });
240
+ });
241
+ it("reports no-published-slots when nothing has been published", () => {
242
+ expect(classifyPublishedDrift([], busy)).toEqual({
243
+ outcome: "no-published-slots",
244
+ published: 0,
245
+ busy: 1,
246
+ conflicts: 0,
247
+ });
118
248
  });
119
- it("returns 0 for an empty published set", () => {
120
- expect(countPublishedConflicts([], busy)).toBe(0);
249
+ it("reports no-published-slots when both sets are empty", () => {
250
+ expect(classifyPublishedDrift([], [])).toEqual({
251
+ outcome: "no-published-slots",
252
+ published: 0,
253
+ busy: 0,
254
+ conflicts: 0,
255
+ });
121
256
  });
122
257
  });
123
258
  //# sourceMappingURL=booking-reconcile.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"booking-reconcile.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/booking-reconcile.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"booking-reconcile.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/booking-reconcile.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=booking-schema-migration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-schema-migration.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/booking-schema-migration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,200 @@
1
+ // Task 1806/1807 — the bookings schema migration, executed against a real
2
+ // SQLite database built from the calendar-site template's own schema.sql, the
3
+ // way `wrangler d1 execute` applies it to D1.
4
+ //
5
+ // A hand-written fake table would model the shape we are trying to prove we
6
+ // produce, which is the failure the sibling cloudflare suite records: 1689
7
+ // shipped a UNIQUE-violating upload because its fake accepted what the real
8
+ // schema rejected. The same argument applies here with more force — the whole
9
+ // defect class this task closes is "the code's idea of the population and the
10
+ // database's idea of it disagree".
11
+ //
12
+ // This also pins the cross-plugin contract: the template schema (cloudflare
13
+ // plugin) and the eligibility predicate (scheduling plugin) must agree on the
14
+ // column names, and nothing else in the tree checks that.
15
+ import { describe, it, expect } from "vitest";
16
+ import { execFile } from "node:child_process";
17
+ import { promisify } from "node:util";
18
+ import { mkdtemp } from "node:fs/promises";
19
+ import { tmpdir } from "node:os";
20
+ import { resolve, dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { BOOKING_SCHEMA_MIGRATION, ELIGIBLE_BOOKING_SELECT, isMissingColumnError, isDuplicateColumnError, } from "../booking-reconcile.js";
23
+ import { createD1Client } from "../d1-command.js";
24
+ const run = promisify(execFile);
25
+ const TEMPLATE_SCHEMA = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../../cloudflare/skills/calendar-site/template/schema.sql");
26
+ /** A real SQLite database. `legacy: true` builds the table as it exists on an
27
+ * already-provisioned install — the template minus the two columns this task
28
+ * adds — because those installs never re-run schema.sql. */
29
+ async function db(dir, opts) {
30
+ const root = await mkdtemp(resolve(tmpdir(), dir));
31
+ const path = resolve(root, "bookings.db");
32
+ if (opts.legacy) {
33
+ await run("sqlite3", [
34
+ path,
35
+ `CREATE TABLE bookings (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT, bookingId TEXT UNIQUE NOT NULL,
37
+ slotStart TEXT NOT NULL, slotEnd TEXT NOT NULL, name TEXT NOT NULL,
38
+ email TEXT NOT NULL, note TEXT, status TEXT NOT NULL DEFAULT 'accepted',
39
+ createdAt TEXT NOT NULL, swept INTEGER NOT NULL DEFAULT 0);`,
40
+ ]);
41
+ }
42
+ else {
43
+ // `.read` because schema.sql opens with a `--` comment, which sqlite3 would
44
+ // otherwise parse as a CLI option.
45
+ await run("sqlite3", [path, `.read ${TEMPLATE_SCHEMA}`]);
46
+ }
47
+ const exec = async (sql) => {
48
+ try {
49
+ return (await run("sqlite3", [path, sql])).stdout.trim();
50
+ }
51
+ catch (e) {
52
+ // sqlite3 reports SQL errors on stderr with a non-zero exit, the same
53
+ // shape wrangler surfaces and d1Query now propagates.
54
+ throw new Error(String(e.stderr ?? e.message).trim());
55
+ }
56
+ };
57
+ return { exec };
58
+ }
59
+ /** The live maxy population, from the device read on 2026-07-19: 24 genuine
60
+ * bookings, 3 Google Calendar block rows, 1 admin block row. Plus Rob Mead —
61
+ * the sitedesk row whose meeting went missing, which is the case 1807 exists
62
+ * for and is byte-identical in D1 to a block row. */
63
+ function seed() {
64
+ const rows = [];
65
+ for (let i = 0; i < 24; i++) {
66
+ rows.push(`('josh-${i}','s','e','Josh Goff','josh.goff@gethumantra.com','','accepted','t',1)`);
67
+ }
68
+ for (let i = 0; i < 3; i++) {
69
+ rows.push(`('gcal-${i}','s','e','Google Calendar block','gcal-block@x.test','Auto-block: Google Calendar','accepted','t',1)`);
70
+ }
71
+ rows.push(`('admin-0','s','e','Joel Smalley','joel@x.test','Blocked by admin','accepted','t',1)`);
72
+ rows.push(`('rob-mead','s','e','Rob Mead','rob@x.test','','accepted','t',1)`);
73
+ return `INSERT INTO bookings (bookingId,slotStart,slotEnd,name,email,note,status,createdAt,swept) VALUES ${rows.join(",")};`;
74
+ }
75
+ describe("bookings schema migration against real SQLite", () => {
76
+ it("the template already satisfies the eligibility predicate, so a fresh install needs no migration", async () => {
77
+ const d = await db("bookings-fresh-", { legacy: false });
78
+ await d.exec(seed());
79
+ // Pins the cross-plugin contract: if the template drops or renames either
80
+ // column, or the predicate names a column the template lacks, this fails.
81
+ expect(await d.exec(`${ELIGIBLE_BOOKING_SELECT};`)).toContain("josh-0");
82
+ });
83
+ it("a legacy database fails the eligible read, and the failure is the one the migration answers", async () => {
84
+ const d = await db("bookings-legacy-", { legacy: true });
85
+ await d.exec(seed());
86
+ await expect(d.exec(`${ELIGIBLE_BOOKING_SELECT};`)).rejects.toThrow();
87
+ const err = await d.exec(`${ELIGIBLE_BOOKING_SELECT};`).catch((e) => e.message);
88
+ // The classifier must match the real sqlite/D1 wording, not a wording we
89
+ // imagined. Matching the wrong string means the migration never fires.
90
+ expect(isMissingColumnError(err)).toBe(true);
91
+ });
92
+ it("migrating a legacy database classifies machine-written block rows and nothing else", async () => {
93
+ const d = await db("bookings-migrate-", { legacy: true });
94
+ await d.exec(seed());
95
+ for (const sql of BOOKING_SCHEMA_MIGRATION)
96
+ await d.exec(`${sql};`);
97
+ expect(await d.exec("SELECT kind, count(*) FROM bookings GROUP BY kind ORDER BY kind;")).toBe("block|3\nbooking|26");
98
+ // The admin block row keeps kind='booking' on purpose. It already has a
99
+ // meeting, so dropping it from the eligible population would turn a false
100
+ // pendingUnreconciled into a false orphanMeetings.
101
+ expect(await d.exec("SELECT kind FROM bookings WHERE bookingId='admin-0';")).toBe("booking");
102
+ // Rob Mead is indistinguishable from a block row on every column except the
103
+ // note. He must survive as eligible, or 1807's whole case is lost.
104
+ expect(await d.exec("SELECT kind FROM bookings WHERE bookingId='rob-mead';")).toBe("booking");
105
+ expect(await d.exec("SELECT attempts FROM bookings WHERE bookingId='rob-mead';")).toBe("0");
106
+ });
107
+ it("no Google Calendar block row is left eligible after migrating", async () => {
108
+ const d = await db("bookings-noblock-", { legacy: true });
109
+ await d.exec(seed());
110
+ for (const sql of BOOKING_SCHEMA_MIGRATION)
111
+ await d.exec(`${sql};`);
112
+ const eligible = await d.exec(`${ELIGIBLE_BOOKING_SELECT};`);
113
+ expect(eligible).not.toContain("gcal-");
114
+ expect(eligible).toContain("rob-mead");
115
+ });
116
+ it("re-running completes a partial migration rather than leaving block rows eligible", async () => {
117
+ // The Critical case. If the ALTERs land and the backfill fails, the columns
118
+ // now exist, so the eligible read succeeds and a missing-column trigger
119
+ // would never fire again — leaving every block row eligible for the sweep
120
+ // to reconcile into a meeting. The dispatcher therefore gates on a marker
121
+ // written only after all three statements succeed; this asserts the
122
+ // property that gate depends on, namely that a re-run finishes the job.
123
+ const d = await db("bookings-partial-", { legacy: true });
124
+ await d.exec(seed());
125
+ await d.exec(`${BOOKING_SCHEMA_MIGRATION[0]};`);
126
+ await d.exec(`${BOOKING_SCHEMA_MIGRATION[1]};`);
127
+ // Backfill did not run: every block row is still eligible.
128
+ expect(await d.exec("SELECT count(*) FROM bookings WHERE kind='block';")).toBe("0");
129
+ expect(await d.exec(`${ELIGIBLE_BOOKING_SELECT};`)).toContain("gcal-0");
130
+ // Re-run the whole migration, tolerating duplicate-column exactly as the
131
+ // dispatcher does.
132
+ for (const sql of BOOKING_SCHEMA_MIGRATION) {
133
+ await d.exec(`${sql};`).catch((e) => {
134
+ if (!isDuplicateColumnError(e.message))
135
+ throw e;
136
+ return "";
137
+ });
138
+ }
139
+ expect(await d.exec("SELECT count(*) FROM bookings WHERE kind='block';")).toBe("3");
140
+ expect(await d.exec(`${ELIGIBLE_BOOKING_SELECT};`)).not.toContain("gcal-0");
141
+ });
142
+ it("a duplicate-column ALTER is recognised, so a re-run is a no-op not an abort", async () => {
143
+ const d = await db("bookings-rerun-", { legacy: true });
144
+ await d.exec(seed());
145
+ for (const sql of BOOKING_SCHEMA_MIGRATION)
146
+ await d.exec(`${sql};`);
147
+ const err = await d.exec(`${BOOKING_SCHEMA_MIGRATION[0]};`).catch((e) => e.message);
148
+ expect(isDuplicateColumnError(err)).toBe(true);
149
+ expect(isMissingColumnError(err)).toBe(false);
150
+ for (const sql of BOOKING_SCHEMA_MIGRATION) {
151
+ await d.exec(`${sql};`).catch((e) => {
152
+ if (!isDuplicateColumnError(e.message))
153
+ throw e;
154
+ return "";
155
+ });
156
+ }
157
+ expect(await d.exec("SELECT kind, count(*) FROM bookings GROUP BY kind ORDER BY kind;")).toBe("block|3\nbooking|26");
158
+ });
159
+ // The migration only fires if the transport surfaces D1's own error text. Task
160
+ // 1805 replaced the wrangler spawn with the HTTP API mid-sprint, so the string
161
+ // this classifier reads now comes from a different layer entirely. If a future
162
+ // transport summarises the failure instead of relaying it, the migration
163
+ // silently never runs and every block row becomes sweep-eligible — the exact
164
+ // regression this task exists to prevent, reintroduced from underneath.
165
+ it("the D1 HTTP transport relays a missing-column error the classifier can read", async () => {
166
+ const body = JSON.stringify({
167
+ result: [],
168
+ success: false,
169
+ errors: [{ code: 7500, message: "no such column: attempts at offset 62" }],
170
+ });
171
+ const fetchFn = async (url) => {
172
+ // The name lookup succeeds; the statement is what fails.
173
+ const ok = url.includes("/d1/database?");
174
+ return {
175
+ ok: true,
176
+ status: 200,
177
+ chunks: (async function* () {
178
+ yield new TextEncoder().encode(ok ? JSON.stringify({ success: true, result: [{ uuid: "db-uuid", name: "maxy-bookings" }] }) : body);
179
+ })(),
180
+ };
181
+ };
182
+ const d1 = createD1Client({ accountId: "acct", token: "t", fetchFn });
183
+ // Resolving is itself a failure here: the statement must reject, or the
184
+ // dispatcher would treat a missing column as an empty booking set.
185
+ const err = await d1.query("maxy-bookings", ELIGIBLE_BOOKING_SELECT).then(() => "", (e) => e.message);
186
+ expect(err).not.toBe("");
187
+ expect(isMissingColumnError(err)).toBe(true);
188
+ expect(isDuplicateColumnError(err)).toBe(false);
189
+ });
190
+ it("attempts survives the migration as a counter the sweep can increment", async () => {
191
+ const d = await db("bookings-attempts-", { legacy: true });
192
+ await d.exec(seed());
193
+ for (const sql of BOOKING_SCHEMA_MIGRATION)
194
+ await d.exec(`${sql};`);
195
+ await d.exec("UPDATE bookings SET attempts = attempts + 1 WHERE bookingId='rob-mead';");
196
+ await d.exec("UPDATE bookings SET attempts = attempts + 1 WHERE bookingId='rob-mead';");
197
+ expect(await d.exec("SELECT attempts FROM bookings WHERE bookingId='rob-mead';")).toBe("2");
198
+ });
199
+ });
200
+ //# sourceMappingURL=booking-schema-migration.test.js.map