Electron Stagewright docs

ADR-017: Clock control plugin via a transport clock seam

Status: Accepted (Playwright transport; CDP-transport clock deferred)

Context

An agent driving an Electron app cannot test time-dependent UI deterministically. "Does the session banner appear after 30 seconds?", "does the countdown reach zero?", "does the 'last updated 5 minutes ago' label roll over?", "what happens at midnight?" — answering these by waiting real wall-clock time is slow and flaky, and some states (a far-future expiry) can't be reached at all. The transport capability matrix has reserved canControlClock since ADR-003, but it had no seam and no consumer (the CDP transport even declared it aspirationally true). This is the clock analog of the network capture plugin (ADR-016): the same "a transport seam + a capability gate + a plugin that drives it" shape.

Decision

1. A dedicated clock seam on the transport, gated by canControlClock

TransportSession gains a clock seam — installClock(options), setFixedTime(time), setSystemTime(time), advanceClock(ms), runClockFor(ms), pauseClockAt(time), resumeClock(). The Playwright transport implements it via page.clock (its fake-timer controller, which overrides the renderer's Date / setTimeout / setInterval), and flips canControlClock from false to true — the capability's first consumer.

@electron-stagewright/plugin-clock drives that seam: clock_install, clock_set_time, clock_set_system_time, clock_advance, clock_run_for, clock_pause, clock_resume, clock_status. The plugin keeps the orchestration (the per-session install lifecycle, the gate, error envelopes) in TypeScript; the transport owns the actual clock.

A seam — not eval — because the fake clock must override the renderer's timer globals transparently and survive across calls; that is a transport concern, not arbitrary JavaScript, so it should not inherit the eval threat model or the --allow-eval opt-in.

2. Gated by canControlClock, install-before-use, NOT --allow-eval gated

Clock control alters app behaviour (it changes the time the app sees and fires its timers), so it is bounded the same way as the other modify-capable plugins: the canControlClock capability and the operator-loaded plugin. It is not a secret surface, so there is no redaction concern.

Rationale

Alternatives considered

Consequences

References