Skip to content

feat(updater): tier 4 — autonomous update in maintenance window (#7607)#7753

Merged
JohnMcLear merged 13 commits into
developfrom
issue-7607-tier-4-autonomous
May 17, 2026
Merged

feat(updater): tier 4 — autonomous update in maintenance window (#7607)#7753
JohnMcLear merged 13 commits into
developfrom
issue-7607-tier-4-autonomous

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

Ships Tier 4 (autonomous) of the four-tier auto-update design from
docs/superpowers/specs/2026-04-25-auto-update-design.md (§"Tier 4 —
autonomous"). Completes the work tracked in #7607 after tiers 1 (#7601),
2 (#7704), and 3 (#7720).

  • New pure module MaintenanceWindow.ts (parseWindow, inWindow,
    nextWindowStart) covers same-day, cross-midnight, and DST cases.
  • UpdatePolicy.canAutonomous now requires a parse-valid
    updates.maintenanceWindow. Missing/invalid windows degrade to Tier 3
    (canAuto: true) with explicit reason values
    maintenance-window-missing / maintenance-window-invalid. The
    terminal rollback-failed state still wins.
  • Scheduler.decideSchedule snaps scheduledFor forward to the next
    window opening when grace would otherwise land outside the window.
  • Scheduler.decideTriggerApply returns a new
    {action: 'defer', nextStart, reason: 'outside-maintenance-window'}
    when fire-time has slipped outside the window; index.ts persists the
    new scheduledFor and re-arms the timer.
  • Settings adds updates.maintenanceWindow: {start, end, tz} | null,
    defaulting to null. Documented in settings.json.template and
    settings.json.docker.

Phase tracking — this PR currently lands the backend for Tier 4.
Follow-up commits on the same PR will land:

  • Admin UI: MaintenanceWindowPicker.tsx, scheduled-panel "deferred
    until" subtitle, "configure window" banner, i18n keys under
    update.window.*.
  • GET /admin/update/status surfaces nextWindowOpensAt.
  • Mocha integration: outside-window queues, entering-window fires,
    window-closes-mid-grace defers.
  • doc/admin/updates.md Tier 4 section, runbook smoke entry,
    CHANGELOG.md Unreleased.

Plan

docs/superpowers/plans/2026-05-15-auto-update-pr4-tier4-autonomous.md
(committed in this PR) maps the spec section to concrete files and
verification steps task-by-task.

Test plan

  • vitest unit — MaintenanceWindow.test.ts (22 cases: parser,
    same-day, cross-midnight, tz=utc vs local, DST host-clock notes).
  • vitest unit — UpdatePolicy.test.ts extended (missing window,
    invalid window, lower-tier ignore, rollback-failed precedence).
  • vitest unit — Scheduler.test.ts extended (snap-forward, in-window
    no-snap, canAutonomous=false bypass, defer at fire, fire in window,
    email dedupe across defer).
  • Full backend-new suite: 629 passed (35 files).
  • pnpm exec tsc --noEmit clean.
  • Admin Playwright spec for the window picker + scheduled deferral.
  • Mocha window-boundary integration spec.
  • Manual smoke runbook Tier 4 section against a disposable VM.

Closes #7607 once the follow-up commits land and CI is green.

🤖 Generated with Claude Code

JohnMcLear and others added 6 commits May 15, 2026 10:48
…7607)

Maps PR 4 of the auto-update design spec (§"Tier 4 — autonomous") to concrete
files, tasks, and verification steps. Subsequent commits scaffold against this
plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tier 4

Pure module: parseWindow, inWindow, nextWindowStart. Supports tz=local|utc
and cross-midnight ranges. Used by upcoming Scheduler + UpdatePolicy changes.

22 vitest unit tests cover format validation, same-day + cross-midnight
boundaries, and host-local vs UTC clock comparisons. DST handling is
absorbed by JS Date constructor's wall-clock normalization (documented in
the file header).

Refs #7607

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires MaintenanceWindow into the existing tier 3 backend so autonomous
updates only fire while `now` is inside `updates.maintenanceWindow`.

UpdatePolicy
  - new optional `maintenanceWindow` input
  - canAutonomous flips on only for git+tier=autonomous+parse-valid window
  - new reasons `maintenance-window-missing` / `maintenance-window-invalid`
  - rollback-failed still wins over window denial

Scheduler
  - decideSchedule snaps scheduledFor forward to nextWindowStart when
    canAutonomous + grace lands outside the window
  - decideTriggerApply returns a new `{action: 'defer'}` when canAutonomous
    + fire-time is outside the window; carries nextStart for the runner
  - canAutonomous=false preserves Tier 3 behavior unchanged

index.ts wires settings.updates.maintenanceWindow through both passes and
re-arms the timer on defer. Status endpoint surface (nextWindowOpensAt) +
admin UI picker land in a follow-up commit.

Settings adds `maintenanceWindow: {start, end, tz} | null`, defaulting to
null. settings.json.template / settings.json.docker document the shape.

Tests
  - 22 vitest cases for MaintenanceWindow already cover the math
  - 4 new UpdatePolicy cases for the window outcomes
  - 6 new Scheduler cases for tier-4 schedule/trigger paths
  - Full backend-new suite: 629 passed (35 files)

Refs #7607

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nner

GET /admin/update/status now returns:
  - `maintenanceWindow`: the parsed window object (admin sessions only)
  - `nextWindowOpensAt`: ISO of the next window opening when tier=autonomous

UpdatePage
  - new "Maintenance window" section when tier=autonomous, shows current
    window summary + next opens at, or "Not configured" when unset
  - scheduled panel now appends a "deferred until <iso>" line when the
    backend has snapped scheduledFor to the next window opening

UpdateBanner
  - new variant when tier=autonomous and policy.reason is
    `maintenance-window-missing` or `maintenance-window-invalid`, linking
    to /admin/update

i18n
  - 8 new keys under `update.banner.*`, `update.page.policy.*`,
    `update.page.scheduled.*`, `update.window.*` (en.json only;
    translations follow via the usual locale workflow)

Interactive picker is intentionally deferred — admins edit
`updates.maintenanceWindow` via the parsed JSONC settings editor (#7709).
A follow-up commit may add a thin write-through component if the JSONC
round-trip turns out to be too rough for typical operators.

Refs #7607

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CHANGELOG: flip Tier 4 from "designed, not yet implemented" to current.
Document maintenanceWindow shape, snap-forward, defer-at-fire, and the
two missing/invalid policy reasons.

doc/admin/updates.md: new "Tier 4 — autonomous in a maintenance window"
section with config example, policy gating, DST/timezone notes, admin UI
behavior.

runbook: §12 walks a disposable VM through missing-window, malformed,
outside-window deferral, fire-at-opening, and window-closes-mid-grace.
Adds five sign-off checklist items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mocha integration covering the four scenarios called out in the spec
§"Tier 4 — autonomous":

  - outside-window: decideSchedule snaps scheduledFor forward to the
    next opening and the snapped value round-trips through saveState
  - inside-window at fire-time: decideTriggerApply returns fire
  - window-closes-mid-grace: decideTriggerApply returns defer with
    nextStart at the next opening; persisted state moves forward
  - cancel during deferred-grace: state returns to idle, and the next
    decideSchedule pass re-emits a schedule snapped to the next opening

All 4 cases passing locally under tsx mocha.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JohnMcLear and others added 2 commits May 15, 2026 12:20
Replaces the (would send email) stub introduced in PR #7601 with a
nodemailer-backed transport. The dependency is lazy-imported so installs
that don't set mail.host pay no runtime cost.

Settings additions
  - new top-level mail block: host, port, secure, from, auth (user/pass)
  - mail.host=null keeps the legacy log-only behaviour; the Notifier
    still updates dedupe state so we don't re-evaluate every tick
  - settings.json.template documents the shape inline
  - settings.json.docker reads MAIL_HOST / MAIL_FROM / MAIL_PORT /
    MAIL_SECURE from env so operators can configure via container env

Transport
  - lazy import('nodemailer') on first send
  - transport cached by host; settings reload picks up new host without
    needing a restart
  - send errors are swallowed (logged warn) so a transient SMTP failure
    can never poison the surrounding updater state machine
  - successful sends log at info; legacy "(would send email)" path
    remains the visible signal when mail is disabled

Refs #7607

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before mutating the working tree, runPreflight now reads the target tag's
package.json via `git show <tag>:package.json` and verifies that
process.versions.node satisfies its engines.node range. Failures land at
preflight-failed cleanly (no rollback needed — nothing has changed yet).

Motivation: a release that bumps the Node floor used to either fail
mid-`pnpm install` (which then rolls back successfully) or restart on the
new build and crash in the boot path (which then rolls back via the
health-check timer). Both paths recover, but they burn a drain + restart
cycle on a condition we can reject upfront.

Implementation
  - new PreflightReason `node-engine-mismatch`
  - new dep `readTargetEnginesNode(tag)` — runs the git-show as a child
    process with stdio captured to a string; missing tag / missing file /
    malformed JSON / missing engines.node all resolve to null (treated as
    "no constraint, pass")
  - uses existing semver dep with includePrerelease: true
  - new PreflightInput field `currentNodeVersion`; threaded from
    process.versions.node in both wirings (scheduler + manual apply)
  - check runs *after* signature verification so we trust the package.json
  - PreflightResult carries an optional `detail` string; applyPipeline
    appends it to the lastResult.reason so the admin UI shows e.g.
    "node-engine-mismatch: target requires Node >=26.0.0, running 25.0.0"

Tests: 6 new vitest cases (no engines.node, satisfies, fails below floor,
caret range, loose-spaced range, ordering after signature). Full
backend-new: 635 passed (was 629).

Refs #7607

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before this commit, only the terminal rollback-failed state emailed the
admin. Auto-recovered failures (rolled-back-install-failed, rolled-back-
build-failed, rolled-back-health-check, rolled-back-crash-loop) and pre-
flight-failed surfaced only via the /admin/update banner — so a 3am
autonomous update that failed because of, say, a Node engine bump would
roll back silently and stay invisible until the admin next logged in.

Notifier
  - new EmailKinds: 'update-preflight-failed', 'update-rolled-back',
    'update-rollback-failed'
  - new pure decideOutcomeEmail(input) → {toSend, newState}
  - dedupe key `<outcome>:<targetTag>` in EmailSendLog.lastFailureKey:
    same outcome on same tag emits one email per cycle (kills retry-loop
    spam); a different outcome or different tag resets the key
  - rollback-failed always fires (terminal — overrides dedupe)
  - state.ts validator + loadState backfill the new field for legacy
    state files (Tier 1/2/3 installs upgrading in place)

Wiring
  - new index.ts helper notifyApplyFailure() loads state, runs the pure
    notifier, sends (via the nodemailer-backed sendEmailViaSmtp from the
    previous commit), persists the new dedupe key — all best-effort
  - schedulerTriggerApply: fires on applyUpdate returning preflight-failed
    or rolled-back
  - /admin/update/apply HTTP handler: same
  - boot path in expressCreateServer: if state.lastResult is a failure
    outcome we haven't already emailed about, fire then. Covers:
      - health-check timeout rollback (timer expired between boots)
      - crash-loop forced rollback caught on a later boot
      - preflight-failed where the process didn't get to email before exit
      - unacknowledged rollback-failed terminal

Tests
  - 8 new vitest cases for decideOutcomeEmail (adminEmail=null, each
    outcome's content, dedupe by tag, dedupe by outcome, rollback-failed
    bypass)
  - Full backend-new suite: 643 passed (was 635)

Refs #7607

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnMcLear JohnMcLear marked this pull request as ready for review May 15, 2026 12:41
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Tier 4 — autonomous update in maintenance window with SMTP notifications

✨ Enhancement 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• Implements **Tier 4 (autonomous)** auto-update feature with maintenance window support, completing
  the four-tier auto-update design
• New pure module MaintenanceWindow.ts with parseWindow, inWindow, and nextWindowStart
  functions handling same-day, cross-midnight, and DST cases
• UpdatePolicy.canAutonomous now requires valid updates.maintenanceWindow; missing/invalid
  windows degrade to Tier 3 with explicit reason values
• Scheduler.decideSchedule snaps scheduledFor forward to next window opening;
  decideTriggerApply defers when fire-time slips outside window
• SMTP email transport integration via nodemailer with failure outcome notifications and dedupe
  tracking
• Node engine version compatibility check in preflight validation
• Settings adds updates.maintenanceWindow: {start, end, tz} | null and mail SMTP configuration
• Admin UI surfaces maintenance window info, deferred-until subtitle, and misconfiguration banner
• Comprehensive test coverage: 22 MaintenanceWindow unit tests, Tier 4 scheduler/policy/notifier
  tests, integration tests for window boundaries
• Documentation: Tier 4 section in doc/admin/updates.md, smoke test runbook, implementation plan,
  i18n keys, and CHANGELOG entry
Diagram
flowchart LR
  Settings["Settings<br/>maintenanceWindow<br/>mail config"]
  Policy["UpdatePolicy<br/>canAutonomous gated<br/>by window"]
  Scheduler["Scheduler<br/>snap-forward<br/>defer at fire"]
  Notifier["Notifier<br/>failure emails<br/>dedupe tracking"]
  Preflight["Preflight<br/>node engine check"]
  Email["SMTP Transport<br/>nodemailer"]
  AdminUI["Admin UI<br/>window display<br/>deferred subtitle<br/>config banner"]
  
  Settings -- "provides window" --> Policy
  Policy -- "gates autonomous" --> Scheduler
  Scheduler -- "triggers apply" --> Preflight
  Preflight -- "failure outcome" --> Notifier
  Notifier -- "sends via" --> Email
  Email -- "delivery" --> AdminUI
  Settings -- "mail config" --> Email
  Scheduler -- "defer action" --> AdminUI
Loading

Grey Divider

File Changes

1. src/node/updater/MaintenanceWindow.ts ✨ Enhancement +105/-0

Tier 4 maintenance window parsing and time-boundary logic

• New pure module implementing Tier 4 maintenance-window math with parseWindow, inWindow, and
 nextWindowStart functions
• Handles same-day, cross-midnight, and DST cases for both UTC and local timezones
• Validates window format (HH:MM times, valid timezone) and rejects malformed inputs

src/node/updater/MaintenanceWindow.ts


2. src/node/updater/UpdatePolicy.ts ✨ Enhancement +36/-12

Tier 4 autonomous permission gated by maintenance window

• Extended PolicyInput to accept optional maintenanceWindow parameter
• evaluatePolicy now gates canAutonomous on valid maintenance window; missing/invalid windows
 degrade to Tier 3 with explicit reason values
• Terminal rollback-failed state still takes precedence over window denial

src/node/updater/UpdatePolicy.ts


3. src/node/updater/Scheduler.ts ✨ Enhancement +42/-6

Scheduler snap-forward and defer logic for maintenance windows

• Added maintenanceWindow parameter to DecideScheduleInput interface
• decideSchedule snaps scheduledFor forward to next window opening when grace lands outside
 window
• New TriggerApplyDecision action defer with nextStart and reason when fire-time is outside
 window
• decideTriggerApply checks window membership and defers if needed

src/node/updater/Scheduler.ts


View more (27)
4. src/node/updater/Notifier.ts ✨ Enhancement +77/-1

Failure outcome email notification and dedupe logic

• Added new EmailKind values for failure outcomes: update-preflight-failed,
 update-rolled-back, update-rollback-failed
• New decideOutcomeEmail function handles failure notification with dedupe key
 <outcome>:<targetTag>rollback-failed always fires (overrides dedupe) as terminal state requiring intervention

src/node/updater/Notifier.ts


5. src/node/updater/preflight.ts ✨ Enhancement +32/-2

Node engine version compatibility check in preflight

• Added node-engine-mismatch to PreflightReason type
• New currentNodeVersion input parameter and readTargetEnginesNode dependency for reading
 target's engines.node
• Node version check runs after signature verification, fails if current version doesn't satisfy
 target's semver range

src/node/updater/preflight.ts


6. src/node/updater/types.ts ✨ Enhancement +19/-0

Type definitions for maintenance windows and failure tracking

• New MaintenanceWindow interface with start, end (HH:MM format), and tz field
• Added lastFailureKey to EmailSendLog for failure outcome dedupe tracking
• Updated EMPTY_STATE to initialize lastFailureKey to null

src/node/updater/types.ts


7. src/node/updater/state.ts ✨ Enhancement +11/-5

State validation and backfill for failure tracking field

• Updated isValidEmail validator to accept optional lastFailureKey field for backwards
 compatibility
• loadState backfills missing lastFailureKey to null when loading older state files

src/node/updater/state.ts


8. src/node/updater/index.ts ✨ Enhancement +166/-6

SMTP email delivery and failure notification integration

• Implemented SMTP email transport with nodemailer; cached transport rebuilds only if host changes
• sendEmailViaSmtp now actually sends mail via configured SMTP or logs would-send when
 unconfigured
• New notifyApplyFailure function sends failure outcome emails with dedupe state persistence
• schedulerTriggerApply handles new defer action by persisting new scheduledFor and re-arming
 timer
• Boot-time failure notification fires pending emails from previous run's failures
• buildSchedulerApplyDeps adds currentNodeVersion and readTargetEnginesNode for preflight

src/node/updater/index.ts


9. src/node/updater/applyPipeline.ts ✨ Enhancement +7/-3

Preflight failure reason enrichment with detail field

• Preflight failures now include optional detail field (e.g. node version mismatch info) in reason
 string
• Reason string appends detail when present for richer admin UI messaging

src/node/updater/applyPipeline.ts


10. src/node/utils/Settings.ts ⚙️ Configuration changes +35/-0

Settings for maintenance window and SMTP mail configuration

• Added updates.maintenanceWindow setting with shape {start, end, tz} or null, defaulting to
 null
• New mail settings object with host, port, secure, from, and optional auth for SMTP
 configuration
• Both settings documented with comments explaining Tier 4 and mail behavior

src/node/utils/Settings.ts


11. src/node/hooks/express/updateStatus.ts ✨ Enhancement +14/-0

Update status endpoint surfaces maintenance window info

• Added maintenanceWindow field to policy evaluation input
• New response fields maintenanceWindow (parsed window for admin) and nextWindowOpensAt (ISO
 timestamp)
• Non-admin requests receive null for both fields; admin-only when tier is autonomous

src/node/hooks/express/updateStatus.ts


12. src/node/hooks/express/updateActions.ts ✨ Enhancement +30/-1

Manual apply endpoint integrates node engine check and failure notifications

• Added readTargetEnginesNode dependency implementation using git show <tag>:package.json
• Preflight input now includes currentNodeVersion parameter
• Manual apply path calls notifyApplyFailure for preflight-failed and rolled-back outcomes

src/node/hooks/express/updateActions.ts


13. src/tests/backend-new/specs/updater/MaintenanceWindow.test.ts 🧪 Tests +130/-0

Comprehensive unit tests for maintenance window math

• 22 test cases covering parseWindow validation (format, tz, zero-length rejection)
• inWindow tests for same-day, cross-midnight, and local-tz windows
• nextWindowStart tests for same-day, cross-midnight, and local-tz calculations

src/tests/backend-new/specs/updater/MaintenanceWindow.test.ts


14. src/tests/backend-new/specs/updater/Scheduler.test.ts 🧪 Tests +96/-0

Tier 4 scheduler snap-forward and defer test cases

• New Tier 4 test suite with 5 cases: snap-forward, in-window no-snap, canAutonomous bypass, defer
 at fire, fire in window
• Tests verify dedupe behavior across defer/re-schedule cycles

src/tests/backend-new/specs/updater/Scheduler.test.ts


15. src/tests/backend-new/specs/updater/UpdatePolicy.test.ts 🧪 Tests +45/-0

Tier 4 maintenance window policy gating tests

• New test suite for Tier 4 window gating: missing window, invalid window, lower-tier ignore,
 rollback-failed precedence
• Verifies canAutonomous degrades correctly with appropriate reason values

src/tests/backend-new/specs/updater/UpdatePolicy.test.ts


16. src/tests/backend-new/specs/updater/Notifier.test.ts 🧪 Tests +77/-1

Failure outcome email notification tests

• New decideOutcomeEmail test suite with 8 cases covering all failure outcomes
• Tests dedupe behavior, terminal state override, and re-emission on different tags/outcomes

src/tests/backend-new/specs/updater/Notifier.test.ts


17. src/tests/backend-new/specs/updater/preflight.test.ts 🧪 Tests +56/-0

Node engine version compatibility check tests

• New Node engine check test suite with 5 cases: no constraint, satisfied range, unsatisfied floor,
 caret ranges, loose ranges
• Verifies engine check runs after signature verification (not before)

src/tests/backend-new/specs/updater/preflight.test.ts


18. src/tests/backend/specs/updater-window-integration.ts 🧪 Tests +147/-0

Tier 4 maintenance window boundary integration tests

• New integration test suite with 4 cases: snap scheduledFor forward, fire inside window, defer at
 window close, cancel during deferred grace
• Tests state persistence and timer re-arming across window boundaries

src/tests/backend/specs/updater-window-integration.ts


19. admin/src/store/store.ts ✨ Enhancement +9/-0

Admin store types for maintenance window display

• New MaintenanceWindow interface matching backend shape
• UpdateStatusPayload adds maintenanceWindow and nextWindowOpensAt fields for Tier 4 UI

admin/src/store/store.ts


20. src/locales/en.json 📝 Documentation +9/-0

Internationalization strings for Tier 4 maintenance window UI

• New i18n keys for maintenance window policy reasons and banners
• Keys for window configuration display, next-opening timestamp, and deferred-until subtitle

src/locales/en.json


21. doc/admin/updates.md 📝 Documentation +41/-1

Tier 4 autonomous maintenance window documentation

• Updated Tier 4 description from "designed, not yet implemented" to full feature documentation
• New section explaining window configuration, gate mechanics, DST/timezone notes, and admin UI
 behavior

doc/admin/updates.md


22. src/package.json Dependencies +2/-0

Dependencies for SMTP email transport

• Added nodemailer@^8.0.7 dependency for SMTP email delivery
• Added @types/nodemailer@^8.0.0 dev dependency for TypeScript support

src/package.json


23. pnpm-lock.yaml Dependencies +81/-69

Dependency lock file updates for nodemailer

• Locked nodemailer@8.0.7 and @types/nodemailer@8.0.0 versions
• Updated transitive dependency resolution for debug and https-proxy-agent

pnpm-lock.yaml


24. docs/superpowers/plans/2026-05-15-auto-update-pr4-tier4-autonomous.md 📝 Documentation +224/-0

Tier 4 autonomous updates implementation plan with task breakdown

• New comprehensive implementation plan document for Tier 4 autonomous updates in maintenance
 windows
• Defines 8 concrete tasks: settings schema, MaintenanceWindow module, UpdatePolicy extension,
 Scheduler gating, status endpoint wiring, admin UI components, integration tests, and documentation
• Includes detailed verification steps, test cases, and cross-cutting checks before PR submission
• Maps specification requirements to concrete file modifications and i18n keys

docs/superpowers/plans/2026-05-15-auto-update-pr4-tier4-autonomous.md


25. docs/superpowers/specs/2026-04-25-auto-update-runbook.md 📝 Documentation +46/-0

Tier 4 smoke test runbook section with verification steps

• Adds new §12 "Tier 4 — autonomous in a maintenance window" smoke test section
• Documents setup steps: missing window banner, malformed window handling, outside-window deferral,
 fire-at-opening, and window-closes-mid-grace scenarios
• Includes expected behaviors for each step and sign-off checklist items
• Provides concrete examples with time-based window configurations

docs/superpowers/specs/2026-04-25-auto-update-runbook.md


26. settings.json.template ⚙️ Configuration changes +29/-1

Add maintenanceWindow and mail settings to template

• Adds maintenanceWindow: null field to the updates settings object
• Includes detailed comment explaining the field's purpose, shape, and cross-midnight/DST behavior
• Documents that null value disables Tier 4 (downgrades to Tier 3)
• Also adds new mail configuration section with SMTP transport settings

settings.json.template


27. admin/src/pages/UpdatePage.tsx ✨ Enhancement +43/-0

Add maintenance window display and deferred-until subtitle

• Adds "deferred until" subtitle when scheduled update is gated by maintenance window (Tier 4)
• Renders new "Maintenance window" section when tier === 'autonomous'
• Displays current window configuration (start, end, tz) and next window opening time
• Shows "Not configured" message when window is unset

admin/src/pages/UpdatePage.tsx


28. CHANGELOG.md 📝 Documentation +3/-1

Document Tier 4 autonomous updates feature in changelog

• Adds new entry under Unreleased for Tier 4 (autonomous in maintenance window) feature
• Documents support for cross-midnight windows and DST transitions
• Explains policy degradation to Tier 3 when window is missing or malformed
• References issue #7607 closure

CHANGELOG.md


29. settings.json.docker ⚙️ Configuration changes +15/-1

Add maintenanceWindow and mail settings to Docker template

• Adds maintenanceWindow: null field to the updates settings object
• Adds new mail configuration section with environment variable interpolation for SMTP settings
• Includes comments explaining mail transport behavior and lazy-loading

settings.json.docker


30. admin/src/components/UpdateBanner.tsx ✨ Enhancement +17/-0

Add maintenance window misconfiguration banner for Tier 4

• Adds new banner variant for Tier 4 when maintenance window is missing or invalid
• Checks policy.reason for 'maintenance-window-missing' or 'maintenance-window-invalid'
• Displays localized message and link to /update configuration page
• Positioned before generic "update available" banner to prioritize window misconfiguration
 awareness

admin/src/components/UpdateBanner.tsx


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 15, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Remediation recommended

1. Misleading deferred subtitle ✓ Resolved 🐞 Bug ≡ Correctness
Description
UpdatePage renders the "Outside maintenance window" subtitle for autonomous tier whenever
scheduledFor is more than 60 seconds away, which is also true for a normal in-window grace period
(e.g., 15 minutes), so the UI can incorrectly claim the delay is due to being outside the window.
Code

admin/src/pages/UpdatePage.tsx[R195-207]

+          {/* Tier 4: when the next fire is gated by a maintenance window, the
+              backend persists `scheduledFor` to the next opening. Surface that
+              so the admin understands why the countdown is long. */}
+          {us.tier === 'autonomous' && us.nextWindowOpensAt
+              && new Date(scheduled.scheduledFor).getTime()
+                 > Date.now() + 60 * 1000 && (
+            <p className="update-scheduled-deferred">
+              <Trans
+                i18nKey="update.page.scheduled.deferred_until"
+                values={{at: us.nextWindowOpensAt}}
+              />
+            </p>
+          )}
Evidence
The UI currently uses a fixed time-distance heuristic, but the scheduler sets `scheduledFor = now +
grace when that timestamp is already inside the window; with documented preApplyGraceMinutes: 15`,
this makes scheduledFor naturally far in the future without any window deferral, triggering the
misleading subtitle.

admin/src/pages/UpdatePage.tsx[186-207]
src/node/updater/Scheduler.ts[79-88]
src/node/hooks/express/updateStatus.ts[115-119]
doc/admin/updates.md[202-208]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`UpdatePage` currently shows the maintenance-window deferral subtitle based on a fixed `scheduledFor > now + 60s` heuristic. This misfires when the update is scheduled inside the maintenance window but has a grace period longer than 60s (common per docs), leading to incorrect admin messaging.
## Issue Context
Backend Tier 4 only snaps `scheduledFor` to the next window opening if `now + grace` is outside the window; otherwise it keeps `scheduledFor = now + grace`. The status endpoint also always provides `nextWindowOpensAt` for autonomous tier (when the window parses), so the UI needs a stronger signal than “scheduledFor is far away”.
## Fix Focus Areas
- admin/src/pages/UpdatePage.tsx[186-207]
- src/node/updater/Scheduler.ts[79-88]
- src/node/hooks/express/updateStatus.ts[115-119]
- doc/admin/updates.md[202-208]
### Suggested implementation direction
Change the subtitle condition to reflect *actual deferral*, e.g. only show it when `scheduled.scheduledFor` is effectively the next window opening (compare equality to `us.nextWindowOpensAt`, possibly with a small tolerance), rather than using a fixed `> now + 60s` threshold.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Preflight detail dropped ✓ Resolved 🐞 Bug ≡ Correctness
Description
applyUpdate() saves a detailed preflight failure reason (reasonStr) to state and logs but
returns only pf.reason, causing /admin/update/apply responses and failure-notification emails to
lose the detail that was just computed.
Code

src/node/updater/applyPipeline.ts[R92-103]

+      // Append the optional `detail` (e.g. "target requires Node >=26.0.0,
+      // running 25.0.0" for node-engine-mismatch) so the admin UI shows a
+      // version-specific message without requiring a separate API field.
+      const reasonStr = pf.detail ? `${pf.reason}: ${pf.detail}` : pf.reason;
   await deps.saveState({
     ...preState,
-        execution: {status: 'preflight-failed', targetTag, reason: pf.reason, at},
-        lastResult: {targetTag, fromSha: '', outcome: 'preflight-failed', reason: pf.reason, at},
+        execution: {status: 'preflight-failed', targetTag, reason: reasonStr, at},
+        lastResult: {targetTag, fromSha: '', outcome: 'preflight-failed', reason: reasonStr, at},
   });
-      deps.appendLog(`[${at}] PREFLIGHT_FAILED ${pf.reason}`);
+      deps.appendLog(`[${at}] PREFLIGHT_FAILED ${reasonStr}`);
   return {outcome: 'preflight-failed', reason: pf.reason};
 }
Evidence
applyUpdate() persists reasonStr but returns pf.reason; the HTTP handler forwards
result.reason into notify/email and the 409 response, and the Notifier email body embeds that
reason string verbatim.

src/node/updater/applyPipeline.ts[89-103]
src/node/hooks/express/updateActions.ts[274-301]
src/node/updater/Notifier.ts[156-162]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
On preflight failure, `applyUpdate()` computes `reasonStr` (including optional `pf.detail`) and persists it, but returns `{reason: pf.reason}`. Callers (HTTP route + notify path) use the returned `result.reason`, so they miss the detail string.
## Issue Context
- State/logs store the enriched string, so the admin UI eventually shows it.
- Immediate HTTP 409 responses and `notifyApplyFailure()` emails use the returned value and therefore lose important diagnostics (e.g., Node engine mismatch details).
## Fix Focus Areas
- src/node/updater/applyPipeline.ts[89-103]
- src/node/hooks/express/updateActions.ts[274-301]
- src/node/updater/Notifier.ts[156-162]
### Suggested implementation direction
- Change `applyUpdate()` to return `reason: reasonStr` for `preflight-failed`.
- Ensure any downstream callers that surface `result.reason` (HTTP + email) now get the enriched detail.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. SMTP cache misses config ✓ Resolved 🐞 Bug ☼ Reliability
Description
sendEmailViaSmtp() only rebuilds the cached nodemailer transport when settings.mail.host
changes, so runtime changes to port/secure/auth after reloadSettings() will keep using a
stale transport configuration.
Code

src/node/updater/index.ts[R46-78]

+/**
+ * Cached nodemailer transport. Built on first use when `settings.mail.host` is
+ * set; never imported when mail is disabled (keeps boot costs predictable for
+ * installs that don't care about outbound mail).
+ *
+ * Rebuilt automatically only if the cached host doesn't match current
+ * settings — a settings reload mid-process therefore picks up new mail config
+ * without requiring a restart.
+ */
+let transportCache: {host: string; transporter: {sendMail: (m: any) => Promise<any>}} | null = null;
+
+const buildTransport = async (host: string) => {
+  const {default: nodemailer} = await import('nodemailer');
+  return nodemailer.createTransport({
+    host,
+    port: Number(settings.mail.port) || 587,
+    secure: !!settings.mail.secure,
+    auth: settings.mail.auth ?? undefined,
+  });
+};
+
const sendEmailViaSmtp = async (to: string, subject: string, body: string): Promise<void> => {
-  // Etherpad core has no built-in SMTP. PR 1 ships the dedupe machinery without an actual sender;
-  // subsequent PRs can wire nodemailer or rely on a notification plugin.
-  logger.info(`(would send email) to=${to} subject="${subject}"`);
-  void body;
+  const host = settings.mail.host;
+  if (!host || !settings.mail.from) {
+    // Mail not configured. Log so operators running the runbook can confirm
+    // the Notifier fired even without delivery, and the dedupe state still
+    // advances so we don't re-evaluate the same trigger every tick.
+    logger.info(`(would send email) to=${to} subject="${subject}"`);
+    return;
+  }
+  if (!transportCache || transportCache.host !== host) {
+    transportCache = {host, transporter: await buildTransport(host)};
+  }
Evidence
The transport cache invalidation condition checks only host, while transport construction uses
port/secure/auth. Because reloadSettings() is invoked at runtime via the admin settings
flow, those fields can change without triggering a cache rebuild.

src/node/updater/index.ts[46-78]
src/node/hooks/express/adminsettings.ts[395-401]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The cached nodemailer transport is invalidated only on host changes. If an operator updates SMTP credentials or port (without changing host) and triggers `reloadSettings()`, future emails will continue using the old transport settings.
## Issue Context
The admin settings flow can call `reloadSettings()` at runtime, so mail settings are expected to be mutable without a process restart.
## Fix Focus Areas
- src/node/updater/index.ts[46-78]
- src/node/hooks/express/adminsettings.ts[395-401]
### Suggested implementation direction
- Expand the cache key to include `host`, `port`, `secure`, and a stable representation of `auth` (and optionally `from`).
- Alternatively, clear `transportCache` whenever `reloadSettings()` runs (if there is a suitable hook point), forcing a rebuild on next send.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Resolves conflicts in src/package.json (@types/node bumped on develop,
nodemailer added on this branch — keep both) and pnpm-lock.yaml
(regenerated from the merged package.json via `pnpm install --lockfile-only`).

Local verification: tsc --noEmit clean, vitest backend-new 643/643 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnMcLear JohnMcLear requested a review from SamTV12345 May 16, 2026 17:37
JohnMcLear and others added 2 commits May 16, 2026 18:48
- UpdatePage: only show "deferred until" subtitle when scheduledFor
  actually matches nextWindowOpensAt. The previous `scheduledFor >
  now + 60s` heuristic misfired during a normal in-window 15-min
  grace period.
- applyPipeline: return the enriched preflight reason (`reason:
  detail`) instead of only `pf.reason`, so /admin/update/apply 409
  bodies and failure-notify emails preserve diagnostics like the
  Node engine mismatch detail.
- updater/index: key the cached nodemailer transport on the full
  set of SMTP options (host + port + secure + auth) so runtime
  changes to port/credentials via reloadSettings() invalidate
  the cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Windows with Plugins (24)

Failed stage: Run the backend tests [❌]

Failed test name: authorLoad returns paginated rows

Failure summary:

The GitHub Action failed because pnpm test -- --exit reported 7 failing tests and exited with code 7
(##[error]Process completed with exit code 7.).
All 7 failures are timeouts in
D:\a\etherpad\etherpad\src\tests\backend\specs\admin\anonymizeAuthorSocket.ts, each ending with:

Error: Timeout of 120000ms exceeded. For async tests and hooks, ensure "done()" is called; if
returning a Promise, ensure it resolves.
The failing tests are:
- authorLoad returns paginated rows

- anonymizeAuthorPreview returns counters without flipping erased
- anonymizeAuthor commits when the
flag is enabled
- anonymizeAuthor returns {error: "disabled"} when flag is off
-
anonymizeAuthorPreview returns {error: "disabled"} when flag is off
- authorLoad returns {error:
"disabled"} when flag is off
- handlers do not crash on payload-less emits

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

427:  �[36;1mpowershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"�[0m
428:  shell: C:\Program Files\PowerShell\7\pwsh.EXE -command ". '{0}'"
429:  env:
430:  PNPM_HOME: C:\Users\runneradmin\setup-pnpm\node_modules\.bin
431:  ##[endgroup]
432:  ##[group]Run mkdir -p "D:\a\etherpad\etherpad/node-report"
433:  �[36;1mmkdir -p "D:\a\etherpad\etherpad/node-report"�[0m
434:  �[36;1m# --exit forces process.exit(failures) after the suite completes,�[0m
435:  �[36;1m# closing the post-suite event-loop drain window where Windows +�[0m
436:  �[36;1m# Node 24 hard-kills the process. Scoped to Windows so Linux/local�[0m
437:  �[36;1m# runs still surface real handle leaks via natural drain.�[0m
438:  �[36;1mpnpm test -- --exit�[0m
439:  shell: C:\Program Files\Git\bin\bash.EXE --noprofile --norc -e -o pipefail {0}
440:  env:
441:  PNPM_HOME: C:\Users\runneradmin\setup-pnpm\node_modules\.bin
442:  NODE_OPTIONS: --report-on-fatalerror --report-uncaught-exception --report-on-signal --report-compact --report-directory=D:\a\etherpad\etherpad/node-report
443:  ##[endgroup]
...

473:  �[32m[2026-05-17T12:32:47.029] [INFO] plugins - �[39mLoading plugin ep_etherpad-lite...
474:  �[32m[2026-05-17T12:32:47.031] [INFO] plugins - �[39mLoaded 15 plugins
475:  �[32m[2026-05-17T12:32:47.524] [INFO] server - �[39mInstalled plugins: ep_align@11.0.29, ep_author_hover@11.0.28, ep_font_color@0.0.137, ep_cursortrace@3.1.59, ep_font_size@0.4.109, ep_hash_auth@11.0.24, ep_plugin_helpers@0.5.3, ep_spellcheck@0.0.101, ep_headings2@0.2.119, ep_table_of_contents@0.4.7, ep_set_title_on_pad@0.7.6, ep_markdown@12.0.9, ep_readonly_guest@1.0.53, ep_subscript_and_superscript@0.3.67
476:  �[32m[2026-05-17T12:32:47.526] [INFO] settings - �[39mReport bugs at https://github.com/ether/etherpad/issues
477:  �[32m[2026-05-17T12:32:47.526] [INFO] settings - �[39mYour Etherpad version is 3.0.0 (4902f13)
478:  �[32m[2026-05-17T12:32:48.432] [INFO] updater - �[39mupdater: install method = git, tier = notify
479:  �[32m[2026-05-17T12:32:48.435] [INFO] http - �[39mHTTP server listening for connections
480:  �[32m[2026-05-17T12:32:48.436] [INFO] settings - �[39mYou can access your Etherpad instance at http://localhost:0/
481:  �[32m[2026-05-17T12:32:48.436] [INFO] settings - �[39mThe plugin admin page is at http://localhost:0/admin/plugins
482:  �[32m[2026-05-17T12:32:48.436] [INFO] server - �[39mEtherpad is running
483:  D:\a\etherpad\etherpad\src\tests\backend\specs\admin\anonymizeAuthorSocket.ts
484:  �[32m[2026-05-17T12:32:48.447] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user guest
485:  1) authorLoad returns paginated rows
486:  2) anonymizeAuthorPreview returns counters without flipping erased
487:  3) anonymizeAuthor commits when the flag is enabled
488:  4) anonymizeAuthor returns {error: "disabled"} when flag is off
489:  5) anonymizeAuthorPreview returns {error: "disabled"} when flag is off
490:  6) authorLoad returns {error: "disabled"} when flag is off
491:  7) handlers do not crash on payload-less emits
492:  D:\a\etherpad\etherpad\src\tests\backend\specs\admin\authorSearch.ts
493:  ✔ returns an empty page when the pattern matches nothing
494:  ✔ matches by name substring
495:  ✔ matches by mapper substring (joins mapper2author)
496:  ✔ hides erased authors by default and includes them when asked
497:  ✔ sorts by lastSeen
498:  ✔ caps results at 1000 and reports cappedAt (125ms)
499:  D:\a\etherpad\etherpad\src\tests\backend\specs\anonymizeAuthor.ts
500:  ✔ zeroes the display identity on globalAuthor:<id>
501:  ✔ drops token2author and mapper2author mappings pointing at the author
502:  ✔ is idempotent — second call returns zero counters
503:  ✔ returns zero counters for an unknown authorID
504:  ✔ re-runs the sweep when a prior call errored before setting erased=true
505:  ✔ dryRun returns the same counter shape but does not mutate the record
...

515:  truncated mode
516:  ✔ zeros the last octet of v4
517:  ✔ keeps the first /48 of a compressed v6
518:  ✔ keeps the first /48 of a fully written v6
519:  ✔ truncates v4 inside a v4-mapped v6
520:  ✔ returns ANONYMOUS for a non-IP string
521:  empty / null input
522:  ✔ returns ANONYMOUS for null in full mode
523:  ✔ returns ANONYMOUS for '' in full mode
524:  ✔ returns ANONYMOUS for null in truncated mode
525:  ✔ returns ANONYMOUS for '' in truncated mode
526:  ✔ returns ANONYMOUS for null in anonymous mode
527:  ✔ returns ANONYMOUS for '' in anonymous mode
528:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\anonymizeAuthor.ts
529:  ✔ anonymizeAuthor zeroes the author and returns counters
530:  ✔ anonymizeAuthor with missing authorID returns an error
531:  ✔ anonymizeAuthor returns an apierror when gdprAuthorErasure is disabled
532:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\api.ts
...

541:  ✔ declares a top-level tags array with all expected resource groups
542:  ✔ tags every operation with at least one non-empty tag
543:  ✔ summarizes every operation
544:  ✔ advertises only POST per path (downstream tooling cleanliness)
545:  runtime backward compatibility (GET + POST still routed)
546:  ✔ GET requests still reach the API handler
547:  ✔ POST requests still reach the API handler
548:  ✔ REST-style /rest/<ver>/pad/checkToken still resolves
549:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\appendTextAuthor.ts
550:  ✔ appendText with authorId attributes the text to that author
551:  ✔ appendText without authorId does not attribute to any author (93ms)
552:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\characterEncoding.ts
553:  Sanity checks
554:  ✔ can connect
555:  ✔ finds the version tag
556:  ✔ errors with invalid OAuth token
557:  ✔ errors with unprivileged OAuth token
558:  Tests
...

611:  at <anonymous> (D:\a\etherpad\etherpad\src\static\js\pluginfw\hooks.ts:273:18)
612:  at new Promise (<anonymous>)
613:  at callHookFnAsync (D:\a\etherpad\etherpad\src\static\js\pluginfw\hooks.ts:236:16)
614:  at <anonymous> (D:\a\etherpad\etherpad\src\static\js\pluginfw\hooks.ts:351:54)
615:  at Array.map (<anonymous>)
616:  at Object.exports.aCallAll (D:\a\etherpad\etherpad\src\static\js\pluginfw\hooks.ts:351:13)
617:  at getHTMLFromAtext (D:\a\etherpad\etherpad\src\node\utils\ExportHtml.ts:507:19)
618:  at async Object.getPadHTML (D:\a\etherpad\etherpad\src\node\utils\ExportHtml.ts:43:10)
619:  at async Object.exports.getHTML (D:\a\etherpad\etherpad\src\node\db\API.ts:296:14)
620:  at async handler (D:\a\etherpad\etherpad\src\node\hooks\express\openapi.ts:768:20)
621:  at async OpenAPIBackend.handleRequest (D:\a\etherpad\etherpad\node_modules\.pnpm\openapi-backend@5.16.1\node_modules\openapi-backend\src\backend.ts:313:27)
622:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\hooks\express\openapi.ts:814:22)
623:  ✔ get the HTML of Pad with emojis
624:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\chat.ts
625:  API Versioning
626:  ✔ errors if can not connect
627:  Chat functionality
...

1096:  - exports DOC from imported DOCX
1097:  - exports DOCX from imported DOCX
1098:  - Tries to import .pdf that uses soffice
1099:  - exports PDF
1100:  - Tries to import .odt that uses soffice
1101:  - exports ODT
1102:  malformed .etherpad files are rejected
1103:  �[33m[2026-05-17T12:46:53.204] [WARN] ImportEtherpad - �[39m(pad kdzj5) import contained 1 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
1104:  �[32m[2026-05-17T12:46:53.206] [INFO] settings - �[39m<ref *2> Response {
1105:  _events: [Object: null prototype] {},
1106:  _eventsCount: 0,
1107:  _maxListeners: undefined,
1108:  res: <ref *1> IncomingMessage {
1109:  _events: {
1110:  close: [Function: bound emit],
1111:  error: [Array],
1112:  data: [Array],
...

1115:  },
1116:  _readableState: ReadableState {
1117:  highWaterMark: 16384,
1118:  buffer: [],
1119:  bufferIndex: 0,
1120:  length: 0,
1121:  pipes: [],
1122:  awaitDrainWriters: null,
1123:  Symbol(kState): 201070460,
1124:  Symbol(kDecoderValue): [StringDecoder],
1125:  Symbol(kEncodingValue): 'utf8'
1126:  },
1127:  _maxListeners: undefined,
1128:  socket: Socket {
1129:  connecting: false,
1130:  _hadError: false,
1131:  _parent: null,
1132:  _host: 'localhost',
1133:  _closeAfterHandlingError: false,
1134:  _events: [Object],
...

1187:  'ETag',
1188:  'W/"3e-c22+Nq37KkPTgt2Yh0qQIIkF+C0"',
1189:  'Connection',
1190:  'close'
1191:  ],
1192:  rawTrailers: [],
1193:  joinDuplicateHeaders: undefined,
1194:  aborted: false,
1195:  upgrade: false,
1196:  url: '',
1197:  method: null,
1198:  statusCode: 200,
1199:  statusMessage: 'OK',
1200:  client: Socket {
1201:  connecting: false,
1202:  _hadError: false,
1203:  _parent: null,
1204:  _host: 'localhost',
1205:  _closeAfterHandlingError: false,
1206:  _events: [Object],
...

1282:  upgradeOrConnect: false,
1283:  parser: null,
1284:  maxHeadersCount: null,
1285:  reusedSocket: false,
1286:  host: 'localhost',
1287:  protocol: 'http:',
1288:  Symbol(shapeMode): false,
1289:  Symbol(kCapture): false,
1290:  Symbol(kBytesWritten): 0,
1291:  Symbol(kNeedDrain): false,
1292:  Symbol(corked): 0,
1293:  Symbol(kChunkedBuffer): [],
1294:  Symbol(kChunkedLength): 0,
1295:  Symbol(kSocket): [Socket],
1296:  Symbol(kOutHeaders): [Object: null prototype],
1297:  Symbol(errored): null,
1298:  Symbol(kHighWaterMark): 16384,
...

1409:  upgradeOrConnect: false,
1410:  parser: null,
1411:  maxHeadersCount: null,
1412:  reusedSocket: false,
1413:  host: 'localhost',
1414:  protocol: 'http:',
1415:  Symbol(shapeMode): false,
1416:  Symbol(kCapture): false,
1417:  Symbol(kBytesWritten): 0,
1418:  Symbol(kNeedDrain): false,
1419:  Symbol(corked): 0,
1420:  Symbol(kChunkedBuffer): [],
1421:  Symbol(kChunkedLength): 0,
1422:  Symbol(kSocket): [Socket],
1423:  Symbol(kOutHeaders): [Object: null prototype],
1424:  Symbol(errored): null,
1425:  Symbol(kHighWaterMark): 16384,
...

1460:  Symbol(kCapture): false,
1461:  Symbol(kHeaders): [Object],
1462:  Symbol(kHeadersCount): 22,
1463:  Symbol(kTrailers): null,
1464:  Symbol(kTrailersCount): 0
1465:  },
1466:  _resBuffered: true,
1467:  response: [Circular *2],
1468:  called: true,
1469:  Symbol(shapeMode): false,
1470:  Symbol(kCapture): false
1471:  },
1472:  req: <ref *3> ClientRequest {
1473:  _events: [Object: null prototype] {
1474:  drain: [Function],
1475:  error: [Function (anonymous)],
1476:  finish: [Function: requestOnFinish]
...

1570:  upgradeOrConnect: false,
1571:  parser: null,
1572:  maxHeadersCount: null,
1573:  reusedSocket: false,
1574:  host: 'localhost',
1575:  protocol: 'http:',
1576:  Symbol(shapeMode): false,
1577:  Symbol(kCapture): false,
1578:  Symbol(kBytesWritten): 0,
1579:  Symbol(kNeedDrain): false,
1580:  Symbol(corked): 0,
1581:  Symbol(kChunkedBuffer): [],
1582:  Symbol(kChunkedLength): 0,
1583:  Symbol(kSocket): Socket {
1584:  connecting: false,
1585:  _hadError: false,
1586:  _parent: null,
1587:  _host: 'localhost',
1588:  _closeAfterHandlingError: false,
1589:  _events: [Object],
...

1610:  Symbol(shapeMode): true,
1611:  Symbol(kCapture): false,
1612:  Symbol(kSetNoDelay): true,
1613:  Symbol(kSetKeepAlive): false,
1614:  Symbol(kSetKeepAliveInitialDelay): 0,
1615:  Symbol(kSetTOS): undefined,
1616:  Symbol(kBytesRead): 0,
1617:  Symbol(kBytesWritten): 0
1618:  },
1619:  Symbol(kOutHeaders): [Object: null prototype] {
1620:  host: [Array],
1621:  'accept-encoding': [Array],
1622:  'content-type': [Array],
1623:  'content-length': [Array]
1624:  },
1625:  Symbol(errored): null,
1626:  Symbol(kHighWaterMark): 16384,
...

1651:  'x-ratelimit-limit': '999999',
1652:  'x-ratelimit-remaining': '999974',
1653:  date: 'Sun, 17 May 2026 12:46:53 GMT',
1654:  'x-ratelimit-reset': '1779022073',
1655:  'content-type': 'application/json; charset=utf-8',
1656:  'content-length': '62',
1657:  etag: 'W/"3e-c22+Nq37KkPTgt2Yh0qQIIkF+C0"',
1658:  connection: 'close'
1659:  },
1660:  statusCode: 200,
1661:  status: 200,
1662:  statusType: 2,
1663:  info: false,
1664:  ok: true,
1665:  redirect: false,
1666:  clientError: false,
1667:  serverError: false,
1668:  error: false,
1669:  created: false,
...

1692:  ✔ missing attrib in pool
1693:  ✔ extra attrib in pool
1694:  ✔ changeset refers to non-existent attrib
1695:  ✔ pad atext does not match
1696:  ✔ missing chat message
1697:  revisions are supported in txt and html export
1698:  �[33m[2026-05-17T12:46:53.230] [WARN] ImportEtherpad - �[39m(pad kdzj5) import contained 3 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
1699:  �[32m[2026-05-17T12:46:53.235] [INFO] settings - �[39mExporting pad "kdzj5" in txt format
1700:  �[32m[2026-05-17T12:46:53.239] [INFO] settings - �[39mExporting pad "kdzj5" in txt format
1701:  ✔ txt request rev 1
1702:  �[32m[2026-05-17T12:46:53.243] [INFO] settings - �[39mExporting pad "kdzj5" in txt format
1703:  ✔ txt request rev 2
1704:  �[32m[2026-05-17T12:46:53.247] [INFO] settings - �[39mExporting pad "kdzj5" in txt format
1705:  ✔ txt request rev 1test returns rev 1
1706:  �[32m[2026-05-17T12:46:53.252] [INFO] settings - �[39mExporting pad "kdzj5" in txt format
1707:  ✔ txt request rev test1 returns 500 with error message
1708:  �[32m[2026-05-17T12:46:53.256] [INFO] settings - �[39mExporting pad "kdzj5" in txt format
1709:  ✔ txt request rev 5 returns head rev
1710:  �[32m[2026-05-17T12:46:53.260] [INFO] settings - �[39mExporting pad "kdzj5" in html format
1711:  ✔ html request rev 1
1712:  �[32m[2026-05-17T12:46:53.266] [INFO] settings - �[39mExporting pad "kdzj5" in html format
1713:  ✔ html request rev 2
1714:  �[32m[2026-05-17T12:46:53.274] [INFO] settings - �[39mExporting pad "kdzj5" in html format
1715:  ✔ html request rev 1test returns rev 1
1716:  �[32m[2026-05-17T12:46:53.280] [INFO] settings - �[39mExporting pad "kdzj5" in html format
1717:  ✔ html request rev test1 results in 500 response
1718:  �[32m[2026-05-17T12:46:53.284] [INFO] settings - �[39mExporting pad "kdzj5" in html format
1719:  ✔ html request rev 5 returns head rev
1720:  Import authorization checks
1721:  ✔ !authn !exist -> create (118ms)
1722:  ✔ !authn exist -> replace (110ms)
1723:  �[32m[2026-05-17T12:46:53.522] [INFO] http - �[39mFailed authentication from IP ANONYMOUS
1724:  ✔ authn anonymous !exist -> fail
1725:  �[32m[2026-05-17T12:46:53.529] [INFO] http - �[39mFailed authentication from IP ANONYMOUS
1726:  ✔ authn anonymous exist -> fail
1727:  �[32m[2026-05-17T12:46:53.536] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
1728:  ✔ authn user create !exist -> create (112ms)
1729:  �[32m[2026-05-17T12:46:53.649] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
1730:  ✔ authn user modify !exist -> fail
1731:  �[32m[2026-05-17T12:46:53.653] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
1732:  ✔ authn user readonly !exist -> fail
1733:  �[32m[2026-05-17T12:46:53.658] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
1734:  ✔ authn user create exist -> replace (117ms)
1735:  �[32m[2026-05-17T12:46:53.777] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
1736:  ✔ authn user modify exist -> replace (109ms)
1737:  �[32m[2026-05-17T12:46:53.887] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
1738:  ✔ authn user readonly exist -> fail
1739:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\instance.ts
1740:  Connectivity for instance-level API tests
1741:  ✔ can connect
1742:  getStats
1743:  ✔ Gets the stats of a running instance
1744:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\jwtAdminClaim.ts
1745:  JWT admin claim enforcement (authorization_code grant)
1746:  ✔ rejects a token with admin=false
1747:  ✔ rejects a token with no admin claim
1748:  ✔ accepts a token with admin=true (happy path)
1749:  ✔ rejects an unsigned / tampered token
1750:  ✔ rejects a request with no Authorization header
1751:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\pad.ts
1752:  Sanity checks
1753:  ✔ errors with invalid oauth token
1754:  Tests
1755:  ✔ deletes a Pad that does not exist
1756:  ✔ creates a new Pad
1757:  ✔ gets revision count of Pad
1758:  ✔ gets saved revisions count of Pad
1759:  ✔ gets saved revision list of Pad
1760:  ✔ get the HTML of Pad
1761:  ✔ list all pads
1762:  ✔ deletes the Pad
1763:  ✔ list all pads again
1764:  ✔ get the HTML of a Pad -- Should return a failure
1765:  ✔ creates a new Pad with text
...

1782:  ✔ Sets text on a pad Id
1783:  ✔ Gets text on a pad Id
1784:  ✔ Sets text on a pad Id including an explicit newline
1785:  ✔ Gets text on a pad Id and doesn't have an excess newline
1786:  ✔ Gets when pad was last edited
1787:  ✔ Move a Pad to a different Pad ID
1788:  ✔ Gets text from new pad
1789:  ✔ Move pad back to original ID
1790:  ✔ Get text using original ID
1791:  ✔ Get last edit of original ID
1792:  ✔ Append text to a pad Id
1793:  ✔ getText of old revision
1794:  ✔ Sets the HTML of a Pad attempting to pass ugly HTML
1795:  ✔ Pad with complex nested lists of different types
1796:  ✔ Pad with white space between list items
1797:  ✔ errors if pad can be created
1798:  ✔ copies the content of a existent pad
1799:  ✔ does not add an useless revision
1800:  ✔ creates a new Pad with empty text
1801:  ✔ deletes with empty text
1802:  copyPadWithoutHistory
1803:  ✔ returns a successful response
1804:  ✔ creates a new pad with the same content as the source pad
1805:  ✔ copying to a non-existent group throws an error
1806:  ✔ source and destination attribute pools are independent (79ms)
1807:  copying to an existing pad
1808:  �[91m[2026-05-17T12:46:54.366] [ERROR] settings - �[39merroring out without force
1809:  ✔ force=false fails
1810:  ✔ force=true succeeds
1811:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\restoreRevision.ts
1812:  �[33m[2026-05-17T12:46:54.395] [WARN] settings - �[39mAuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead 
1813:  at Object.exports.getAuthor4Token (D:\a\etherpad\etherpad\src\node\db\AuthorManager.ts:175:12)
1814:  at Context.<anonymous> (D:\a\etherpad\etherpad\src\tests\backend\specs\api\restoreRevision.ts:32:36)
1815:  v1.2.11
1816:  - content matches
1817:  ✔ authorId ignored
1818:  v1.3.0
1819:  ✔ change is attributed to given authorId
1820:  ✔ authorId can be omitted
1821:  D:\a\etherpad\etherpad\src\tests\backend\specs\api\sessionsAndGroups.ts
1822:  API Versioning
1823:  ✔ errors if can not connect
1824:  API: Group creation and deletion
...

1887:  �[32m[2026-05-17T12:46:54.669] [INFO] access - �[39m[LEAVE] pad:testChatPad socket:rAsQRY2N0Lr_RWCDAAAD IP:ANONYMOUS authorID:a.6OPErRQTxVEJcAXK
1888:  �[33m[2026-05-17T12:46:54.704] [WARN] message - �[39mclient sent author token via CLIENT_READY message; cookie migration will take effect on next HTTP response. See docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md
1889:  �[32m[2026-05-17T12:46:54.706] [INFO] access - �[39m[CREATE] pad:testChatPad socket:RiYooYk4-AEPXjWAAAAF IP:ANONYMOUS authorID:a.ZYKChRW6cvsi9Mp8
1890:  ✔ pad
1891:  �[32m[2026-05-17T12:46:54.709] [INFO] access - �[39m[LEAVE] pad:testChatPad socket:RiYooYk4-AEPXjWAAAAF IP:ANONYMOUS authorID:a.ZYKChRW6cvsi9Mp8
1892:  �[33m[2026-05-17T12:46:54.736] [WARN] message - �[39mclient sent author token via CLIENT_READY message; cookie migration will take effect on next HTTP response. See docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md
1893:  �[32m[2026-05-17T12:46:54.737] [INFO] access - �[39m[CREATE] pad:testChatPad socket:hwy0iZSvwTaj_rXJAAAH IP:ANONYMOUS authorID:a.yAnP8nRDWViykBfT
1894:  ✔ padId
1895:  �[32m[2026-05-17T12:46:54.740] [INFO] access - �[39m[LEAVE] pad:testChatPad socket:hwy0iZSvwTaj_rXJAAAH IP:ANONYMOUS authorID:a.yAnP8nRDWViykBfT
1896:  �[33m[2026-05-17T12:46:54.767] [WARN] message - �[39mclient sent author token via CLIENT_READY message; cookie migration will take effect on next HTTP response. See docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md
1897:  �[32m[2026-05-17T12:46:54.769] [INFO] access - �[39m[CREATE] pad:testChatPad socket:mKPLHKPixRcpAnBmAAAJ IP:ANONYMOUS authorID:a.6LYlSSsoEEXRELT3
1898:  ✔ mutations propagate
1899:  �[32m[2026-05-17T12:46:54.773] [INFO] access - �[39m[LEAVE] pad:testChatPad socket:mKPLHKPixRcpAnBmAAAJ IP:ANONYMOUS authorID:a.6LYlSSsoEEXRELT3
1900:  D:\a\etherpad\etherpad\src\tests\backend\specs\clientvar_rev_consistency.ts
1901:  �[33m[2026-05-17T12:46:54.800] [WARN] settings - �[39mbypassing socket.io authentication and authorization checks due to settings.loadTest
1902:  �[91m[2026-05-17T12:46:54.800] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
1903:  �[32m[2026-05-17T12:46:54.800] [INFO] access - �[39m[ENTER] pad:3SM8uxtmcy socket:DNeWnxQ1cOvE6SvLAAAL IP:ANONYMOUS authorID:a.2Q911dfaBGI1nebu
1904:  �[32m[2026-05-17T12:46:54.803] [INFO] access - �[39m[LEAVE] pad:3SM8uxtmcy socket:DNeWnxQ1cOvE6SvLAAAL IP:ANONYMOUS authorID:a.2Q911dfaBGI1nebu
1905:  ✔ CLIENT_VARS rev matches initialAttributedText state at that exact rev
1906:  �[33m[2026-05-17T12:46:54.847] [WARN] settings - �[39mbypassing socket.io authentication and authorization checks due to settings.loadTest
1907:  �[91m[2026-05-17T12:46:54.848] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
1908:  �[32m[2026-05-17T12:46:54.848] [INFO] access - �[39m[ENTER] pad:A0IsQjkDxv socket:FLU8IAu2nJlH8cX5AAAN IP:ANONYMOUS authorID:a.FeMRtuXxbr5GzfoJ
1909:  �[32m[2026-05-17T12:46:55.765] [INFO] access - �[39m[LEAVE] pad:A0IsQjkDxv socket:FLU8IAu2nJlH8cX5AAAN IP:ANONYMOUS authorID:a.FeMRtuXxbr5GzfoJ
1910:  ✔ CLIENT_VARS stays consistent under concurrent edits during handshake (delay race) (962ms)
1911:  �[33m[2026-05-17T12:46:55.789] [WARN] settings - �[39mbypassing socket.io authentication and authorization checks due to settings.loadTest
1912:  �[91m[2026-05-17T12:46:55.790] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
1913:  �[32m[2026-05-17T12:46:55.790] [INFO] access - �[39m[CREATE] pad:KUMSmJGRoW socket:2BaHp6dW7Dx6tOKIAAAP IP:ANONYMOUS authorID:a.H8cDSuF9xYfbpiuK
...

1940:  textColorFromBackgroundColor — invariant
1941:  ✔ always picks whichever of black/white gives the higher contrast
1942:  D:\a\etherpad\etherpad\src\tests\backend\specs\compactPad.ts
1943:  API.compactPad()
1944:  ✔ collapses all history when keepRevisions is omitted
1945:  ✔ keeps only the last N revisions when keepRevisions is a number
1946:  ✔ rejects negative keepRevisions
1947:  ✔ rejects non-numeric keepRevisions
1948:  ✔ rejects fractional keepRevisions
1949:  ✔ refuses to run when cleanup.enabled is false
1950:  HTTP API dispatch (1.3.1)
1951:  ✔ passes keepRevisions from query string into compactPad
1952:  ✔ collapses all history when keepRevisions is absent from URL
1953:  runCompactAll (bin/compactAllPads loop)
1954:  ✔ parses --keep / --dry-run / no args
1955:  �[91m[2026-05-17T12:46:56.347] [ERROR] settings - �[39m--keep expects a non-negative integer; got abc
1956:  �[91m[2026-05-17T12:46:56.347] [ERROR] settings - �[39m--keep expects a non-negative integer; got -1
1957:  ✔ rejects --keep with non-integer / negative / unknown args
1958:  ✔ compacts every pad and tallies before/after revisions
1959:  ✔ honours --keep N by passing it through to compactPad
1960:  ✔ --dry-run does not call compactPad
1961:  ✔ keeps going when one pad fails to compact
1962:  ✔ keeps going when one pad fails the pre-flight count
1963:  ✔ reports listAllPads failure without iterating
1964:  ✔ handles an empty instance
1965:  ✔ end-to-end against the real HTTP handler
1966:  runCompactStale (bin/compactStalePads loop)
1967:  ✔ parses --older-than / --keep / --dry-run
1968:  �[91m[2026-05-17T12:46:56.380] [ERROR] settings - �[39m--older-than is required
1969:  �[91m[2026-05-17T12:46:56.380] [ERROR] settings - �[39m--older-than is required
1970:  �[91m[2026-05-17T12:46:56.380] [ERROR] settings - �[39m--older-than expects a non-negative integer; got abc
1971:  �[91m[2026-05-17T12:46:56.380] [ERROR] settings - �[39m--older-than expects a non-negative integer; got -1
1972:  ✔ rejects missing / invalid --older-than and unknown args
1973:  ✔ only compacts pads older than the cutoff
1974:  ✔ honours --keep N for stale pads
1975:  ✔ --dry-run does not call compactPad on stale pads
1976:  ✔ keeps going when one stale pad fails to compact
1977:  ✔ counts a getLastEdited failure as a failure but keeps going
1978:  ✔ reports listAllPads failure without iterating
1979:  ✔ handles an empty instance
1980:  ✔ handles an instance where every pad is fresh
1981:  ✔ skips a pad that gets edited between selection and compaction
1982:  ✔ counts a getLastEdited recheck failure as a failure
1983:  ✔ --older-than 0 treats every pad as stale
...

2011:  ✔ text matches
2012:  ✔ alines match
2013:  ✔ attributes are sorted in canonical order
2014:  A single completely empty line break within an ol should reset count if OL is closed off..
2015:  ✔ text matches
2016:  ✔ alines match
2017:  ✔ attributes are sorted in canonical order
2018:  A single <p></p> should create a new line
2019:  ✔ text matches
2020:  ✔ alines match
2021:  ✔ attributes are sorted in canonical order
2022:  Tests if ols properly get line numbers when in a normal OL #2
2023:  ✔ text matches
2024:  ✔ alines match
2025:  ✔ attributes are sorted in canonical order
2026:  First item being an UL then subsequent being OL will fail
2027:  - text matches
...

2136:  ✔ alines match
2137:  ✔ attributes are sorted in canonical order
2138:  nbsp preserved across span boundary
2139:  ✔ text matches
2140:  ✔ alines match
2141:  ✔ attributes are sorted in canonical order
2142:  D:\a\etherpad\etherpad\src\tests\backend\specs\ensureAuthorTokenCookie.ts
2143:  ✔ mints a fresh t.* token when the cookie is absent
2144:  ✔ reuses the cookie value and does not emit Set-Cookie when already set
2145:  ✔ sets Secure when the request is HTTPS
2146:  ✔ uses SameSite=None when embedded cross-site (Sec-Fetch-Site: cross-site)
2147:  ✔ ignores an invalid existing cookie and mints a fresh one
2148:  D:\a\etherpad\etherpad\src\tests\backend\specs\export.ts
2149:  �[32m[2026-05-17T12:46:56.702] [INFO] settings - �[39mExporting pad "testExportPad" in doc format
2150:  �[32m[2026-05-17T12:46:56.717] [INFO] LibreOffice - �[39m[9888] Converting C:\Users\RUNNER~1\AppData\Local\Temp/etherpad_export_87217ed8c9b7d195f0fadff65bdac6e9.html to odt in C:\Users\RUNNER~1\AppData\Local\Temp
2151:  �[91m[2026-05-17T12:46:56.722] [ERROR] LibreOffice - �[39m[9888] stderr: The system cannot find the path specified.
2152:  �[91m[2026-05-17T12:46:56.725] [ERROR] LibreOffice - �[39m[9888] Conversion failed: Error: Failed to spawn /bin/false: spawn /bin/false ENOENT
2153:  at exports (D:\a\etherpad\etherpad\src\node\utils\run_cmd.ts:124:48)
2154:  at doConvertTask (D:\a\etherpad\etherpad\src\node\utils\LibreOffice.ts:38:13)
2155:  at D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:151:38
2156:  at D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:4017:13
2157:  at Object.process (D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:1680:21)
2158:  at D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:1532:23
2159:  at D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:74:45
2160:  �[91m[2026-05-17T12:46:56.725] [ERROR] settings - �[39mExport error: Error: Failed to spawn /bin/false: spawn /bin/false ENOENT
2161:  at exports (D:\a\etherpad\etherpad\src\node\utils\run_cmd.ts:124:48)
2162:  at doConvertTask (D:\a\etherpad\etherpad\src\node\utils\LibreOffice.ts:38:13)
2163:  at D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:151:38
2164:  at D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:4017:13
2165:  at Object.process (D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:1680:21)
2166:  at D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:1532:23
2167:  at D:\a\etherpad\etherpad\node_modules\.pnpm\async@3.2.6\node_modules\async\dist\async.js:74:45 {
2168:  code: 'ENOENT'
2169:  }
2170:  ✔ returns 500 on export error
2171:  native DOCX export (#7538)
2172:  �[32m[2026-05-17T12:46:56.731] [INFO] settings - �[39mExporting pad "testExportPad" in docx format
2173:  ✔ returns a valid DOCX archive (PK zip signature) (331ms)
2174:  �[32m[2026-05-17T12:46:57.062] [INFO] settings - �[39mExporting pad "testExportPad" in docx format
2175:  ✔ sends the Word-processing-ml content-type
2176:  native PDF export (#7538)
2177:  �[32m[2026-05-17T12:46:57.084] [INFO] settings - �[39mExporting pad "testExportPad" in pdf format
2178:  ✔ returns a valid %PDF- document (144ms)
2179:  �[32m[2026-05-17T12:46:57.229] [INFO] settings - �[39mExporting pad "testExportPad" in pdf format
2180:  ✔ sends application/pdf content-type
2181:  odt without soffice (#7538)
2182:  �[91m[2026-05-17T12:46:57.241] [ERROR] settings - �[39mImpossible to export pad "testExportPad" in odt format. There is no converter configured
2183:  ✔ returns the "not enabled" message for odt
...

2746:  at new Promise (<anonymous>)
2747:  at callHookFnAsync (D:\a\etherpad\etherpad\src\static\js\pluginfw\hooks.ts:236:16)
2748:  at <anonymous> (D:\a\etherpad\etherpad\src\static\js\pluginfw\hooks.ts:351:54)
2749:  at Array.map (<anonymous>)
2750:  at Object.exports.aCallAll (D:\a\etherpad\etherpad\src\static\js\pluginfw\hooks.ts:351:13)
2751:  at getHTMLFromAtext (D:\a\etherpad\etherpad\src\node\utils\ExportHtml.ts:507:19)
2752:  at async Object.getPadHTML (D:\a\etherpad\etherpad\src\node\utils\ExportHtml.ts:43:10)
2753:  at async Context.<anonymous> (D:\a\etherpad\etherpad\src\tests\backend\specs\export_list.ts:128:18)
2754:  ✔ nested ordered list counters reset when closing levels
2755:  D:\a\etherpad\etherpad\src\tests\backend\specs\favicon.ts
2756:  ✔ uses custom favicon if set (relative pathname)
2757:  ✔ uses custom favicon from url
2758:  ✔ uses custom favicon if set (absolute pathname)
2759:  ✔ falls back if custom favicon is missing
2760:  ✔ uses skin favicon if present
2761:  �[91m[2026-05-17T12:46:57.461] [ERROR] settings - �[39m(node:4368) [DEP0147] DeprecationWarning: In future versions of Node.js, fs.rmdir(path, { recursive: true }) will be removed. Use fs.rm(path, { recursive: true }) instead
2762:  (Use `node --trace-deprecation ...` to show where the warning was created)
...

3115:  ✔ defer call cb(unrejectedPromise) then defer call to cb(resolvedPromise) (diff. outcomes) -> log+throw
3116:  ✔ defer call cb(unrejectedPromise) then defer call to cb(rejectedPromise) (diff. outcomes) -> log+throw
3117:  ✔ defer call cb(unrejectedPromise) then defer call to cb(rejectedPromise) (same outcome) -> only log
3118:  ✔ defer call cb(unrejectedPromise) then defer call to cb(unresolvedPromise) (diff. outcomes) -> log+throw
3119:  ✔ defer call cb(unrejectedPromise) then defer call cb(unrejectedPromise) (diff. outcomes) -> log+throw
3120:  ✔ defer call cb(unrejectedPromise) then defer call cb(unrejectedPromise) (same outcome) -> only log
3121:  hooks.aCallAll
3122:  basic behavior
3123:  ✔ calls all asynchronously, returns values in order
3124:  ✔ passes hook name
3125:  ✔ undefined context -> {}
3126:  ✔ null context -> {}
3127:  ✔ context unmodified
3128:  aCallAll callback
3129:  ✔ exception in callback rejects
3130:  ✔ propagates error on exception
3131:  ✔ propagages null error on success
3132:  ✔ propagages results on success
...

3226:  order of records does not matter
3227:  �[33m[2026-05-17T12:46:58.653] [WARN] ImportEtherpad - �[39m(pad BpDVdLl4Lz) import contained 1 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
3228:  ✔ [0,1,2]
3229:  �[33m[2026-05-17T12:46:58.655] [WARN] ImportEtherpad - �[39m(pad TBFiiRM89D) import contained 1 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
3230:  ✔ [0,2,1]
3231:  �[33m[2026-05-17T12:46:58.656] [WARN] ImportEtherpad - �[39m(pad NZKp4Olp8L) import contained 1 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
3232:  ✔ [1,0,2]
3233:  �[33m[2026-05-17T12:46:58.658] [WARN] ImportEtherpad - �[39m(pad jat9zuKW9D) import contained 1 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
3234:  ✔ [1,2,0]
3235:  �[33m[2026-05-17T12:46:58.659] [WARN] ImportEtherpad - �[39m(pad 89evnIvCtG) import contained 1 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
3236:  ✔ [2,0,1]
3237:  �[33m[2026-05-17T12:46:58.661] [WARN] ImportEtherpad - �[39m(pad nYivwGWqrV) import contained 1 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
3238:  ✔ [2,1,0]
3239:  old .etherpad imports without author metadata
3240:  �[33m[2026-05-17T12:46:58.662] [WARN] ImportEtherpad - �[39m(pad ryXRdoLr4a) import contained 1 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
3241:  ✔ imports without error when revision lacks meta.author
3242:  �[33m[2026-05-17T12:46:58.664] [WARN] ImportEtherpad - �[39m(pad 7wWgSxFxlP) import contained 1 unattributed insert op(s); rewriting them with the system author to satisfy the appendRevision invariant. Source pad id: testing.
...

3262:  ✔ src/node/handler/PadMessageHandler.ts does not log a raw IP
3263:  ✔ src/node/handler/SocketIORouter.ts does not log a raw IP
3264:  ✔ src/node/hooks/express/webaccess.ts does not log a raw IP
3265:  ✔ src/node/hooks/express/importexport.ts does not log a raw IP
3266:  invalid ipLogging falls back to anonymous at load time
3267:  ✔ rejects an unknown mode
3268:  ✔ rejects null
3269:  D:\a\etherpad\etherpad\src\tests\backend\specs\largePaste.ts
3270:  ✔ can set and retrieve 50,000 characters of text on a pad
3271:  D:\a\etherpad\etherpad\src\tests\backend\specs\LinkInstaller.ts
3272:  readFileSync with plain paths (bug fix)
3273:  ✔ reads a plugin package.json using a plain file path and utf-8
3274:  ✔ path.join produces a plain string path, not a URL object
3275:  addSubDependency-style resolution
3276:  ✔ recursively resolves nested dependencies from package.json files
3277:  error handling when package.json is missing
3278:  ✔ logs an error instead of crashing when package.json does not exist
3279:  D:\a\etherpad\etherpad\src\tests\backend\specs\lowerCasePadIds.ts
3280:  not activated
3281:  ✔ do nothing
3282:  activated
3283:  ✔ lowercase pad ids
3284:  �[91m[2026-05-17T12:46:58.772] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3285:  �[32m[2026-05-17T12:46:58.773] [INFO] access - �[39m[CREATE] pad:ALREADYexistingPad socket:Jb20DU_DBcp2qGHJAAAR IP:ANONYMOUS authorID:a.yStuArrMK8xENS76
3286:  �[91m[2026-05-17T12:46:58.804] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3287:  �[32m[2026-05-17T12:46:58.804] [INFO] access - �[39m[CREATE] pad:alreadyexistingpad socket:l8XRHSkHGsS0CZXAAAAT IP:ANONYMOUS authorID:a.sXIK07jxz67Wet7I
3288:  ✔ keeps old pads accessible (57ms)
3289:  �[32m[2026-05-17T12:46:58.807] [INFO] access - �[39m[LEAVE] pad:alreadyexistingpad socket:l8XRHSkHGsS0CZXAAAAT IP:ANONYMOUS authorID:a.sXIK07jxz67Wet7I
3290:  �[32m[2026-05-17T12:46:58.807] [INFO] access - �[39m[LEAVE] pad:ALREADYexistingPad socket:Jb20DU_DBcp2qGHJAAAR IP:ANONYMOUS authorID:a.yStuArrMK8xENS76
3291:  �[91m[2026-05-17T12:46:58.852] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3292:  �[32m[2026-05-17T12:46:58.852] [INFO] access - �[39m[CREATE] pad:maliciousattempt socket:UPLqLvX8W5RtNrZuAAAV IP:ANONYMOUS authorID:a.TRnTElgxAvayuyYT
3293:  ✔ disallow creation of different case pad-name via socket connection (45ms)
3294:  �[32m[2026-05-17T12:46:58.854] [INFO] access - �[39m[LEAVE] pad:maliciousattempt socket:UPLqLvX8W5RtNrZuAAAV IP:ANONYMOUS authorID:a.TRnTElgxAvayuyYT
3295:  D:\a\etherpad\etherpad\src\tests\backend\specs\messages.ts
3296:  CHANGESET_REQ
3297:  �[91m[2026-05-17T12:46:58.889] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3298:  �[32m[2026-05-17T12:46:58.889] [INFO] access - �[39m[ENTER] pad:a8ITWhZwjM socket:5Ws0QMJCcAMJ5kH_AAAX IP:ANONYMOUS authorID:a.Uomya95U73pk8rOX
3299:  �[91m[2026-05-17T12:46:58.919] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3300:  �[32m[2026-05-17T12:46:58.919] [INFO] access - �[39m[ENTER] pad:a8ITWhZwjM socket:lCyKXFAuseykAJCtAAAZ IP:ANONYMOUS authorID:a.AeWFXp6Yq8uLWRJS
3301:  ✔ users are unable to read changesets from other pads
3302:  �[32m[2026-05-17T12:46:59.933] [INFO] access - �[39m[LEAVE] pad:a8ITWhZwjM socket:5Ws0QMJCcAMJ5kH_AAAX IP:ANONYMOUS authorID:a.Uomya95U73pk8rOX
3303:  �[32m[2026-05-17T12:46:59.933] [INFO] access - �[39m[LEAVE] pad:a8ITWhZwjM socket:lCyKXFAuseykAJCtAAAZ IP:ANONYMOUS authorID:a.AeWFXp6Yq8uLWRJS
3304:  �[91m[2026-05-17T12:46:59.975] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3305:  �[32m[2026-05-17T12:46:59.975] [INFO] access - �[39m[ENTER] pad:S89wu2X2oF socket:vtVTkikmdCWEUbXXAAAb IP:ANONYMOUS authorID:a.SP6rATcf4MVD4qrC
3306:  �[91m[2026-05-17T12:46:59.996] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3307:  �[32m[2026-05-17T12:46:59.996] [INFO] access - �[39m[ENTER] pad:S89wu2X2oF socket:EK-hqOfWHd4fQvvOAAAd IP:ANONYMOUS authorID:a.BZt4ZxPJQJoX9PWJ
3308:  �[91m[2026-05-17T12:47:01.019] [ERROR] socket.io - �[39mError handling pad message from EK-hqOfWHd4fQvvOAAAd: Error: CHANGESET_REQ: rev is not a number
3309:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:612:11)
3310:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\handler\SocketIORouter.ts:85:14)
3311:  ✔ CHANGESET_REQ: verify revNum is a number (regression)
3312:  �[32m[2026-05-17T12:47:01.021] [INFO] access - �[39m[LEAVE] pad:S89wu2X2oF socket:vtVTkikmdCWEUbXXAAAb IP:ANONYMOUS authorID:a.SP6rATcf4MVD4qrC
3313:  �[32m[2026-05-17T12:47:01.021] [INFO] access - �[39m[LEAVE] pad:S89wu2X2oF socket:EK-hqOfWHd4fQvvOAAAd IP:ANONYMOUS authorID:a.BZt4ZxPJQJoX9PWJ
3314:  �[91m[2026-05-17T12:47:01.126] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3315:  �[32m[2026-05-17T12:47:01.126] [INFO] access - �[39m[ENTER] pad:6BHnLALdUq socket:0BUvD4xxiuDAgLNCAAAf IP:ANONYMOUS authorID:a.TSAl27KlBNcYCmLN
3316:  �[91m[2026-05-17T12:47:01.151] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3317:  �[32m[2026-05-17T12:47:01.151] [INFO] access - �[39m[ENTER] pad:6BHnLALdUq socket:d79lNNV8_FnLFDbQAAAh IP:ANONYMOUS authorID:a.AtYULAiSwaWOSFdB
3318:  ✔ CHANGESET_REQ: revNum is converted to number if possible (regression)
3319:  �[32m[2026-05-17T12:47:02.161] [INFO] access - �[39m[LEAVE] pad:6BHnLALdUq socket:0BUvD4xxiuDAgLNCAAAf IP:ANONYMOUS authorID:a.TSAl27KlBNcYCmLN
3320:  �[32m[2026-05-17T12:47:02.161] [INFO] access - �[39m[LEAVE] pad:6BHnLALdUq socket:d79lNNV8_FnLFDbQAAAh IP:ANONYMOUS authorID:a.AtYULAiSwaWOSFdB
3321:  �[91m[2026-05-17T12:47:02.188] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3322:  �[32m[2026-05-17T12:47:02.188] [INFO] access - �[39m[ENTER] pad:FSK451eI9M socket:GAFWtBhpRSmWC4tSAAAj IP:ANONYMOUS authorID:a.JjmjYGTVRGIKahra
3323:  �[91m[2026-05-17T12:47:02.220] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3324:  �[32m[2026-05-17T12:47:02.220] [INFO] access - �[39m[ENTER] pad:FSK451eI9M socket:3mTtgM028MQG9wG9AAAl IP:ANONYMOUS authorID:a.CGAAT0Hw3CBvc3PG
3325:  ✔ CHANGESET_REQ: revNum 2 is converted to head rev 1 (regression)
3326:  �[32m[2026-05-17T12:47:03.229] [INFO] access - �[39m[LEAVE] pad:FSK451eI9M socket:GAFWtBhpRSmWC4tSAAAj IP:ANONYMOUS authorID:a.JjmjYGTVRGIKahra
3327:  �[32m[2026-05-17T12:47:03.229] [INFO] access - �[39m[LEAVE] pad:FSK451eI9M socket:3mTtgM028MQG9wG9AAAl IP:ANONYMOUS authorID:a.CGAAT0Hw3CBvc3PG
3328:  USER_CHANGES
3329:  �[91m[2026-05-17T12:47:03.258] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3330:  �[32m[2026-05-17T12:47:03.258] [INFO] access - �[39m[ENTER] pad:KoZWXgZOyt socket:H0R8FuwlN4v3vt-TAAAn IP:ANONYMOUS authorID:a.L2iuQePOySKZdIuw
3331:  �[91m[2026-05-17T12:47:03.296] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3332:  �[32m[2026-05-17T12:47:03.296] [INFO] access - �[39m[ENTER] pad:KoZWXgZOyt socket:fLnTwqDe8-vIItmnAAAp IP:ANONYMOUS authorID:a.VIFqKBXCRYx0s90f
3333:  ✔ changes are applied
3334:  �[32m[2026-05-17T12:47:04.310] [INFO] access - �[39m[LEAVE] pad:KoZWXgZOyt socket:H0R8FuwlN4v3vt-TAAAn IP:ANONYMOUS authorID:a.L2iuQePOySKZdIuw
3335:  �[32m[2026-05-17T12:47:04.311] [INFO] access - �[39m[LEAVE] pad:KoZWXgZOyt socket:fLnTwqDe8-vIItmnAAAp IP:ANONYMOUS authorID:a.VIFqKBXCRYx0s90f
3336:  �[91m[2026-05-17T12:47:04.344] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3337:  �[32m[2026-05-17T12:47:04.344] [INFO] access - �[39m[ENTER] pad:fzg006HVzv socket:uT_yCKfZm4AHwWYxAAAr IP:ANONYMOUS authorID:a.KrY9DJy8droTIEO4
3338:  �[91m[2026-05-17T12:47:04.386] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3339:  �[32m[2026-05-17T12:47:04.386] [INFO] access - �[39m[ENTER] pad:fzg006HVzv socket:5-VvqWPAVioJqh-fAAAt IP:ANONYMOUS authorID:a.WhV8KFVFwTPY9Ovq
3340:  �[33m[2026-05-17T12:47:05.399] [WARN] message - �[39mFailed to apply USER_CHANGES from author a.KrY9DJy8droTIEO4 (socket uT_yCKfZm4AHwWYxAAAr) on pad fzg006HVzv: Error: Not a changeset: this is not a valid changeset
3341:  at error (D:\a\etherpad\etherpad\src\static\js\Changeset.ts:64:13)
3342:  at unpack (D:\a\etherpad\etherpad\src\static\js\Changeset.ts:363:44)
3343:  at checkRep (D:\a\etherpad\etherpad\src\static\js\Changeset.ts:246:20)
3344:  at handleUserChanges (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:840:5)
3345:  ✔ bad changeset is rejected
3346:  �[32m[2026-05-17T12:47:05.401] [INFO] access - �[39m[LEAVE] pad:fzg006HVzv socket:uT_yCKfZm4AHwWYxAAAr IP:ANONYMOUS authorID:a.KrY9DJy8droTIEO4
3347:  �[32m[2026-05-17T12:47:05.401] [INFO] access - �[39m[LEAVE] pad:fzg006HVzv socket:5-VvqWPAVioJqh-fAAAt IP:ANONYMOUS authorID:a.WhV8KFVFwTPY9Ovq
3348:  �[91m[2026-05-17T12:47:05.430] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3349:  �[32m[2026-05-17T12:47:05.431] [INFO] access - �[39m[ENTER] pad:Q2Xmn16UJP socket:bq1Toe58UEPcMbGpAAAv IP:ANONYMOUS authorID:a.c90wTNHZmm70siCQ
3350:  �[91m[2026-05-17T12:47:05.462] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3351:  �[32m[2026-05-17T12:47:05.462] [INFO] access - �[39m[ENTER] pad:Q2Xmn16UJP socket:kBFuAioQWPw6p7bxAAAx IP:ANONYMOUS authorID:a.j43Wm9oPRbP6XbFA
3352:  �[33m[2026-05-17T12:47:06.475] [WARN] message - �[39mFailed to apply USER_CHANGES from author a.c90wTNHZmm70siCQ (socket bq1Toe58UEPcMbGpAAAv) on pad Q2Xmn16UJP: Error: Author a.c90wTNHZmm70siCQ submitted an insert without an author attribute in changeset Z:1>5+5$hello
3353:  at handleUserChanges (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:887:15)
3354:  ✔ insert without author attribute is rejected
3355:  �[32m[2026-05-17T12:47:06.476] [INFO] access - �[39m[LEAVE] pad:Q2Xmn16UJP socket:bq1Toe58UEPcMbGpAAAv IP:ANONYMOUS authorID:a.c90wTNHZmm70siCQ
3356:  �[32m[2026-05-17T12:47:06.476] [INFO] access - �[39m[LEAVE] pad:Q2Xmn16UJP socket:kBFuAioQWPw6p7bxAAAx IP:ANONYMOUS authorID:a.j43Wm9oPRbP6XbFA
3357:  �[91m[2026-05-17T12:47:06.506] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3358:  �[32m[2026-05-17T12:47:06.506] [INFO] access - �[39m[ENTER] pad:GhmgIGpoaT socket:6CEtrxU947dJMInrAAAz IP:ANONYMOUS authorID:a.omGxqJp8JTV08pUo
3359:  �[91m[2026-05-17T12:47:06.537] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3360:  �[32m[2026-05-17T12:47:06.537] [INFO] access - �[39m[ENTER] pad:GhmgIGpoaT socket:vWEzZAwIDwDwN3ddAAA1 IP:ANONYMOUS authorID:a.CyMxfBoTdGA95HW0
3361:  �[33m[2026-05-17T12:47:07.541] [WARN] message - �[39mFailed to apply USER_CHANGES from author a.omGxqJp8JTV08pUo (socket 6CEtrxU947dJMInrAAAz) on pad GhmgIGpoaT: Error: Author a.omGxqJp8JTV08pUo tried to submit changes as author a.etherpad-system in changeset Z:1>5*0+5$hello
3362:  at handleUserChanges (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:874:17)
3363:  ✔ insert claiming the reserved system author is rejected
3364:  �[32m[2026-05-17T12:47:07.542] [INFO] access - �[39m[LEAVE] pad:GhmgIGpoaT socket:6CEtrxU947dJMInrAAAz IP:ANONYMOUS authorID:a.omGxqJp8JTV08pUo
3365:  �[32m[2026-05-17T12:47:07.543] [INFO] access - �[39m[LEAVE] pad:GhmgIGpoaT socket:vWEzZAwIDwDwN3ddAAA1 IP:ANONYMOUS authorID:a.CyMxfBoTdGA95HW0
3366:  �[91m[2026-05-17T12:47:07.583] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3367:  �[32m[2026-05-17T12:47:07.583] [INFO] access - �[39m[ENTER] pad:OtuMM9aYjh socket:R0t-4R3CaWkNE-MmAAA3 IP:ANONYMOUS authorID:a.T9VVhOgPRlq2PPfM
3368:  �[91m[2026-05-17T12:47:07.613] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3369:  �[32m[2026-05-17T12:47:07.613] [INFO] access - �[39m[ENTER] pad:OtuMM9aYjh socket:Fz7A9bF7HuUVtvLEAAA5 IP:ANONYMOUS authorID:a.MR0aLEvifLhURXd1
3370:  �[33m[2026-05-17T12:47:08.628] [WARN] message - �[39mFailed to apply USER_CHANGES from author a.T9VVhOgPRlq2PPfM (socket R0t-4R3CaWkNE-MmAAA3) on pad OtuMM9aYjh: Error: Rejected USER_CHANGES whose application would leave the pad without a trailing '\n' (length 7). Every USER_CHANGES must preserve the "doc ends with \n" invariant.
3371:  at handleUserChanges (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:960:13)
3372:  ✔ changeset that would strand the trailing \n is rejected
3373:  �[32m[2026-05-17T12:47:08.629] [INFO] access - �[39m[LEAVE] pad:OtuMM9aYjh socket:R0t-4R3CaWkNE-MmAAA3 IP:ANONYMOUS authorID:a.T9VVhOgPRlq2PPfM
3374:  �[32m[2026-05-17T12:47:08.630] [INFO] access - �[39m[LEAVE] pad:OtuMM9aYjh socket:Fz7A9bF7HuUVtvLEAAA5 IP:ANONYMOUS authorID:a.MR0aLEvifLhURXd1
3375:  �[91m[2026-05-17T12:47:08.657] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3376:  �[32m[2026-05-17T12:47:08.657] [INFO] access - �[39m[ENTER] pad:Kh4fzOnu2L socket:uCM9kcJyhujOokfvAAA7 IP:ANONYMOUS authorID:a.bFHKOc5bzYuMToSb
3377:  �[91m[2026-05-17T12:47:08.688] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3378:  �[32m[2026-05-17T12:47:08.688] [INFO] access - �[39m[ENTER] pad:Kh4fzOnu2L socket:QjbwjpZaU7QTlDdPAAA9 IP:ANONYMOUS authorID:a.CLNCAoVXpP5N7Hsr
3379:  ✔ retransmission is accepted, has no effect
3380:  �[32m[2026-05-17T12:47:09.697] [INFO] access - �[39m[LEAVE] pad:Kh4fzOnu2L socket:uCM9kcJyhujOokfvAAA7 IP:ANONYMOUS authorID:a.bFHKOc5bzYuMToSb
3381:  �[32m[2026-05-17T12:47:09.698] [INFO] access - �[39m[LEAVE] pad:Kh4fzOnu2L socket:QjbwjpZaU7QTlDdPAAA9 IP:ANONYMOUS authorID:a.CLNCAoVXpP5N7Hsr
3382:  �[91m[2026-05-17T12:47:09.725] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3383:  �[32m[2026-05-17T12:47:09.725] [INFO] access - �[39m[ENTER] pad:PmyTutjG7p socket:XcHGoByXM1pP8JFYAAA_ IP:ANONYMOUS authorID:a.E5h7nZdJBpKEZlUh
3384:  �[91m[2026-05-17T12:47:09.750] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3385:  �[32m[2026-05-17T12:47:09.751] [INFO] access - �[39m[ENTER] pad:PmyTutjG7p socket:lfLguoAFqkzClNmIAABB IP:ANONYMOUS authorID:a.VhnYaS8CZfEjEtGF
3386:  ✔ identity changeset is accepted, has no effect
3387:  �[32m[2026-05-17T12:47:10.767] [INFO] access - �[39m[LEAVE] pad:PmyTutjG7p socket:XcHGoByXM1pP8JFYAAA_ IP:ANONYMOUS authorID:a.E5h7nZdJBpKEZlUh
3388:  �[32m[2026-05-17T12:47:10.768] [INFO] access - �[39m[LEAVE] pad:PmyTutjG7p socket:lfLguoAFqkzClNmIAABB IP:ANONYMOUS authorID:a.VhnYaS8CZfEjEtGF
3389:  �[91m[2026-05-17T12:47:10.802] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3390:  �[32m[2026-05-17T12:47:10.802] [INFO] access - �[39m[ENTER] pad:oAnvqZ2VCm socket:71uX8m27ExjQsEJuAABD IP:ANONYMOUS authorID:a.E56TzthMOWfTybiv
3391:  �[91m[2026-05-17T12:47:10.843] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3392:  �[32m[2026-05-17T12:47:10.843] [INFO] access - �[39m[ENTER] pad:oAnvqZ2VCm socket:8CW2bfdPYVBpy6N_AABF IP:ANONYMOUS authorID:a.B6V1LbZXAGvNgJNW
3393:  ✔ non-identity changeset with no net change is accepted, has no effect
3394:  �[32m[2026-05-17T12:47:11.851] [INFO] access - �[39m[LEAVE] pad:oAnvqZ2VCm socket:71uX8m27ExjQsEJuAABD IP:ANONYMOUS authorID:a.E56TzthMOWfTybiv
3395:  �[32m[2026-05-17T12:47:11.851] [INFO] access - �[39m[LEAVE] pad:oAnvqZ2VCm socket:8CW2bfdPYVBpy6N_AABF IP:ANONYMOUS authorID:a.B6V1LbZXAGvNgJNW
3396:  �[91m[2026-05-17T12:47:11.873] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3397:  �[32m[2026-05-17T12:47:11.873] [INFO] access - �[39m[ENTER] pad:Sbs1ZafyW1 socket:k1EseiYtz5dZCocFAABH IP:ANONYMOUS authorID:a.HtSm5sN0FS0ns00g
3398:  �[91m[2026-05-17T12:47:11.920] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3399:  �[32m[2026-05-17T12:47:11.920] [INFO] access - �[39m[ENTER] pad:Sbs1ZafyW1 socket:PB7YMqGsmFCxBvl6AABJ IP:ANONYMOUS authorID:a.8AlCg63IuigI2Sad
3400:  �[91m[2026-05-17T12:47:12.935] [ERROR] socket.io - �[39mError handling pad message from PB7YMqGsmFCxBvl6AABJ: Error: COLLABROOM: write attempt on read-only pad
3401:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:612:11)
3402:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\handler\SocketIORouter.ts:85:14)
3403:  �[91m[2026-05-17T12:47:12.939] [ERROR] socket.io - �[39mError handling pad message from PB7YMqGsmFCxBvl6AABJ: Error: COLLABROOM: write attempt on read-only pad
3404:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:612:11)
...

3801:  �[32m[2026-05-17T12:47:19.210] [INFO] access - �[39m[CREATE] pad:pad socket:8GbQDq6yY-AbbTLDAABj IP:ANONYMOUS authorID:a.8yOTsrqtKCDFsh7i username:user
3802:  �[32m[2026-05-17T12:47:19.213] [INFO] access - �[39m[LEAVE] pad:pad socket:8GbQDq6yY-AbbTLDAABj IP:ANONYMOUS authorID:a.8yOTsrqtKCDFsh7i username:user
3803:  �[32m[2026-05-17T12:47:19.214] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3804:  �[32m[2026-05-17T12:47:19.241] [INFO] access - �[39m[CREATE] pad:pad socket:i6RJDq-ym3cTe8lyAABl IP:ANONYMOUS authorID:a.8vccDXvCIJP4qN7I username:user
3805:  ✔ authn user read-only /p/pad -> 200, ok (63ms)
3806:  �[32m[2026-05-17T12:47:19.243] [INFO] access - �[39m[LEAVE] pad:pad socket:i6RJDq-ym3cTe8lyAABl IP:ANONYMOUS authorID:a.8vccDXvCIJP4qN7I username:user
3807:  �[32m[2026-05-17T12:47:19.246] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3808:  �[32m[2026-05-17T12:47:19.289] [INFO] access - �[39m[CREATE] pad:pad socket:M84xMZit6jmLSFe0AABn IP:ANONYMOUS authorID:a.3mbycHrJngjPRZ7l username:user
3809:  ✔ authz user /p/pad -> 200, ok (46ms)
3810:  �[32m[2026-05-17T12:47:19.291] [INFO] access - �[39m[LEAVE] pad:pad socket:M84xMZit6jmLSFe0AABn IP:ANONYMOUS authorID:a.3mbycHrJngjPRZ7l username:user
3811:  �[32m[2026-05-17T12:47:19.294] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3812:  �[32m[2026-05-17T12:47:19.321] [INFO] access - �[39m[CREATE] pad:päd socket:A_yUnRjzqrkc8AfIAABp IP:ANONYMOUS authorID:a.NWWKn3HOE3TMrgLF username:user
3813:  ✔ supports pad names with characters that must be percent-encoded
3814:  �[32m[2026-05-17T12:47:19.323] [INFO] access - �[39m[LEAVE] pad:päd socket:A_yUnRjzqrkc8AfIAABp IP:ANONYMOUS authorID:a.NWWKn3HOE3TMrgLF username:user
3815:  Abnormal access attempts
3816:  �[32m[2026-05-17T12:47:19.327] [INFO] http - �[39mFailed authentication from IP ANONYMOUS
3817:  �[33m[2026-05-17T12:47:19.367] [WARN] message - �[39mclient sent author token via CLIENT_READY message; cookie migration will take effect on next HTTP response. See docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md
3818:  �[91m[2026-05-17T12:47:19.368] [ERROR] socket.io - �[39mError handling pad message from RJ61Q9gnvPATtKevAABr: Error: access denied
3819:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:508:11)
3820:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\handler\SocketIORouter.ts:85:14)
3821:  ✔ authn anonymous /p/pad -> 401, error (43ms)
3822:  �[32m[2026-05-17T12:47:19.372] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3823:  �[32m[2026-05-17T12:47:19.388] [INFO] access - �[39m[CREATE] pad:pad socket:3ZCi4M4C9Wg_e60KAABt IP:ANONYMOUS authorID:a.eWsnTmwgYtA7TRbE username:user
3824:  �[32m[2026-05-17T12:47:19.391] [INFO] access - �[39m[LEAVE] pad:pad socket:3ZCi4M4C9Wg_e60KAABt IP:ANONYMOUS authorID:a.eWsnTmwgYtA7TRbE username:user
3825:  �[32m[2026-05-17T12:47:19.392] [INFO] http - �[39mFailed authentication from IP ANONYMOUS
3826:  �[33m[2026-05-17T12:47:19.431] [WARN] message - �[39mclient sent author token via CLIENT_READY message; cookie migration will take effect on next HTTP response. See docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md
3827:  �[91m[2026-05-17T12:47:19.431] [ERROR] socket.io - �[39mError handling pad message from f7tGKUp3D_xdVwcRAABv: Error: access denied
3828:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:508:11)
3829:  at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
3830:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\handler\SocketIORouter.ts:85:14)
3831:  ✔ authn anonymous read-only /p/pad -> 401, error (62ms)
3832:  �[33m[2026-05-17T12:47:19.526] [WARN] message - �[39mclient sent author token via CLIENT_READY message; cookie migration will take effect on next HTTP response. See docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md
3833:  �[91m[2026-05-17T12:47:19.527] [ERROR] socket.io - �[39mError handling pad message from CMS1IMDWwMbxkIzaAABx: Error: access denied
3834:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:508:11)
3835:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\handler\SocketIORouter.ts:85:14)
3836:  ✔ authn !cookie -> error (94ms)
3837:  �[32m[2026-05-17T12:47:19.530] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3838:  �[91m[2026-05-17T12:47:19.549] [ERROR] socket.io - �[39mError handling pad message from S6U76zFYCQdKjjN7AABz: Error: access denied
3839:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:508:11)
3840:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\handler\SocketIORouter.ts:85:14)
3841:  ✔ authorization bypass attempt -> error
3842:  Authorization levels via authorize hook
3843:  �[32m[2026-05-17T12:47:19.562] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3844:  �[32m[2026-05-17T12:47:19.581] [INFO] access - �[39m[CREATE] pad:pad socket:MnOOVyr4EPcnY58wAAB1 IP:ANONYMOUS authorID:a.LARKiaF5oa4BHSSs username:user
3845:  ✔ level='create' -> can create
3846:  �[32m[2026-05-17T12:47:19.591] [INFO] access - �[39m[LEAVE] pad:pad socket:MnOOVyr4EPcnY58wAAB1 IP:ANONYMOUS authorID:a.LARKiaF5oa4BHSSs username:user
3847:  �[32m[2026-05-17T12:47:19.594] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3848:  �[32m[2026-05-17T12:47:19.639] [INFO] access - �[39m[CREATE] pad:pad socket:mtErWkqNv5W9pDtlAAB3 IP:ANONYMOUS authorID:a.NB9m5AhHU0gxHdyb username:user
3849:  ✔ level=true -> can create (48ms)
3850:  �[32m[2026-05-17T12:47:19.641] [INFO] access - �[39m[LEAVE] pad:pad socket:mtErWkqNv5W9pDtlAAB3 IP:ANONYMOUS authorID:a.NB9m5AhHU0gxHdyb username:user
3851:  �[32m[2026-05-17T12:47:19.644] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3852:  �[91m[2026-05-17T12:47:19.676] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3853:  �[32m[2026-05-17T12:47:19.676] [INFO] access - �[39m[CREATE] pad:pad socket:jEbTCsTbNxRk7QZqAAB5 IP:ANONYMOUS authorID:a.a0eYQk31OYooNboK username:user
3854:  ✔ level='modify' -> can modify
3855:  �[32m[2026-05-17T12:47:19.677] [INFO] access - �[39m[LEAVE] pad:pad socket:jEbTCsTbNxRk7QZqAAB5 IP:ANONYMOUS authorID:a.a0eYQk31OYooNboK username:user
3856:  �[32m[2026-05-17T12:47:19.680] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3857:  �[91m[2026-05-17T12:47:19.717] [ERROR] socket.io - �[39mError handling pad message from Ujs7ETzpkygFJJCNAAB7: Error: access denied
3858:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:508:11)
3859:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\handler\SocketIORouter.ts:85:14)
3860:  ✔ level='create' settings.editOnly=true -> unable to create (39ms)
3861:  �[32m[2026-05-17T12:47:19.720] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3862:  �[91m[2026-05-17T12:47:19.740] [ERROR] socket.io - �[39mError handling pad message from UB9jmUMaM9LlZj3NAAB9: Error: access denied
3863:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:508:11)
3864:  at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
3865:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\handler\SocketIORouter.ts:85:14)
3866:  ✔ level='modify' settings.editOnly=false -> unable to create
3867:  �[32m[2026-05-17T12:47:19.751] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3868:  �[91m[2026-05-17T12:47:19.770] [ERROR] socket.io - �[39mError handling pad message from Y3MTQC_XSbX6eyVPAAB_: Error: access denied
3869:  at Object.exports.handleMessage (D:\a\etherpad\etherpad\src\node\handler\PadMessageHandler.ts:508:11)
3870:  at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
3871:  at async <anonymous> (D:\a\etherpad\etherpad\src\node\handler\SocketIORouter.ts:85:14)
3872:  ✔ level='readOnly' -> unable to create
3873:  �[32m[2026-05-17T12:47:19.775] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3874:  �[91m[2026-05-17T12:47:19.791] [ERROR] message - �[39mThere is no author for authorId: a.etherpad-system. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802
3875:  �[32m[2026-05-17T12:47:19.791] [INFO] access - �[39m[CREATE] pad:pad socket:sVWdYNeNmRuHczh5AACB IP:ANONYMOUS authorID:a.x48fMXX1VSkyc65E username:user
3876:  ✔ level='readOnly' -> unable to modify
3877:  �[32m[2026-05-17T12:47:19.797] [INFO] access - �[39m[LEAVE] pad:pad socket:sVWdYNeNmRuHczh5AACB IP:ANONYMOUS authorID:a.x48fMXX1VSkyc65E username:user
3878:  Authorization levels via user settings
3879:  �[32m[2026-05-17T12:47:19.800] [INFO] http - �[39mSuccessful authentication from IP ANONYMOUS for user user
3880:  �[32m[2026-05-17T12:47:19.829] [INFO] access - �[39m[CREATE] pad:pad socket:d1DkEwJGVJupUs06AACD IP:ANONYMOUS authorID:a.n1ETQ...

@JohnMcLear JohnMcLear merged commit 962bfe8 into develop May 17, 2026
31 of 35 checks passed
@JohnMcLear JohnMcLear deleted the issue-7607-tier-4-autonomous branch May 17, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clean update path

1 participant