Implement unified time testing with controllable app_now() function#329
Conversation
…aster) Rebases #329 against current master. The original branch (commit 8507fe6) had 28 file conflicts because master refactored most of the same code paths over the 60+ commits since the PR was opened, and its migration 0033 collides with master's existing 0033–0093. This rebase keeps the foundation only — the two genuinely new things — and defers the wide Time-effect threading through 16 source modules because those modules have changed shape on master and need re-applying against current signatures, not a merge. Foundation included: - static/migrations/0094_app_now_function.sql: - app_now() reads the 'app.current_time' GUC, falls back to NOW(). - Updates set_updated_at / new_anomaly_proc / check_triggered_query_monitors / check_tests_to_trigger to call app_now() instead of now()/current_timestamp. - Preserves master's later changes that the original PR clobbered: new_anomaly_proc keeps the 1-hour batching delay (migration 0049). - Renamed from 0033 because the slot is long-taken. - src/Pkg/TestClock.hs: - IORef-backed clock with advanceTime / setTestTime / getTestTime. - runMutableTime: Time-effect interpreter that reads from the IORef. - syncConnectionTime / runWithTimeSyncedPool: pool wrapper that sets the GUC on each connection checkout so app_now() in triggers matches. Deferred (open follow-up): plumbing TestClock through Pkg.TestUtils, threading the Time effect into the queries currently using NOW() at the SQL level, and converting test specs from runFrozenTime to runMutableTime.
89e9c97 to
8eafc90
Compare
Code Review: PR #329 — Unified time testing with
|
…aster) Rebases #329 against current master. The original branch (commit 8507fe6) had 28 file conflicts because master refactored most of the same code paths over the 60+ commits since the PR was opened, and its migration 0033 collides with master's existing 0033–0093. This rebase keeps the foundation only — the two genuinely new things — and defers the wide Time-effect threading through 16 source modules because those modules have changed shape on master and need re-applying against current signatures, not a merge. Foundation included: - static/migrations/0094_app_now_function.sql: - app_now() reads the 'app.current_time' GUC, falls back to NOW(). - Updates set_updated_at / new_anomaly_proc / check_triggered_query_monitors / check_tests_to_trigger to call app_now() instead of now()/current_timestamp. - Preserves master's later changes that the original PR clobbered: new_anomaly_proc keeps the 1-hour batching delay (migration 0049). - Renamed from 0033 because the slot is long-taken. - src/Pkg/TestClock.hs: - IORef-backed clock with advanceTime / setTestTime / getTestTime. - runMutableTime: Time-effect interpreter that reads from the IORef. - syncConnectionTime / runWithTimeSyncedPool: pool wrapper that sets the GUC on each connection checkout so app_now() in triggers matches. Deferred (open follow-up): plumbing TestClock through Pkg.TestUtils, threading the Time effect into the queries currently using NOW() at the SQL level, and converting test specs from runFrozenTime to runMutableTime.
d5b0733 to
62eb053
Compare
|
Code Review: PR 329 - Unified time testing with app_now() OVERVIEW This PR adds the foundational layer for controllable time in tests: a PostgreSQL app_now() function driven by a GUC, and a TestClock Haskell module. The approach is sound. However, the diff is materially incomplete vs. the PR description. The description lists 15+ changed files (TestUtils wiring, TestResources.testClock, runWithTimeSyncedPool, query-function Time constraints, test updates) but the actual diff contains only 3 files. TestUtils.hs still calls runFrozenTime on lines 542/832/846 and has no TestClock integration. Until those follow-up changes land, the new module is dead code. CORRECTNESS: set_config transaction scope syncConnectionTime calls set_config with third argument true, making the GUC transaction-local. In autocommit mode (the default for postgresql-simple connections not inside an explicit BEGIN), each statement is its own transaction, so the GUC resets immediately after set_config returns, before the next query can read it. The docstring says resets when connection returns to the pool, which is misleading. For the pattern to work, syncConnectionTime must be called within the same explicit transaction as the queries that need to see the test time. If the intended use is per-connection-checkout, consider false (session-scoped) instead, or document the transaction requirement explicitly. This needs resolving before wiring into the pool wrapper. CODE SUCCINCTNESS (eta-reductions available) Current: More concise: In runMutableTime, CurrentTime -> liftIO (readIORef (unTestClock clock)) removes the extra hop through getTestTime. app_now() VOLATILITY Declared STABLE but reads current_setting (a session variable). VOLATILE is more accurate since PostgreSQL may cache STABLE results across repeated calls in a single query. Recommend VOLATILE unless there is a measured performance reason to keep STABLE. SMALLER NITS
SUMMARY The migration and TestClock module are clean. Two things to resolve: (1) land the TestUtils/query-function integration the PR description promises, or explicitly scope the PR down to foundation-only; and (2) clarify set_config transaction-scope semantics before the pool-wrapper is wired in. |
Builds on the foundation in the previous commit. Tests now share one
mutable clock per spec via 'tr.trTestClock' instead of each call site
seeding a fresh frozen clock. New helpers expose the advancement API
('advanceTestTime', 'advanceMinutes', 'advanceHours', 'advanceDays',
'getTestTime'); 'runTestBg t tr' still accepts a 'UTCTime' for backward
compatibility but now resets the shared clock to that time rather than
constructing a throwaway one.
Signature cleanup:
- 'effToServantHandlerTest' takes a 'TestClock' parameter (drives the
Time effect via 'runMutableTime' instead of 'runFrozenTime').
- 'toServantResponse' / 'toBaseServantResponse' take 'TestResources'
directly. Old call sites passing
toBaseServantResponse tr.trATCtx tr.trLogger
are now just
toBaseServantResponse tr
Same simplification for 'testServant' which collapses to an alias.
External callers updated: ApiV1Spec, ClientMetadataSpec,
BusinessFlowsSpec, BotTestHelpers (drops a now-unused
'runFrozenTime' import).
Deferred for a follow-up — wholesale SQL 'NOW()' → 'Time.currentTime'
threading through model query functions. That's per-function refactor
work that fits better as incremental commits against current master
signatures.
Code ReviewGood overall design — the Bug / correctness
Stale module docThe
Minor style
-- current
runTestBg t TestResources{..} = \action -> do
setTestTime trTestClock t
...
-- cleaner — move the arg into the binding
runTestBg t TestResources{..} action = do
setTestTime trTestClock t
...Magic number seconds — advanceMinutes tr n = advanceTestTime tr (fromIntegral n * nominalDay / 1440)
advanceHours tr n = advanceTestTime tr (fromIntegral n * nominalDay / 24)
advanceDays tr n = advanceTestTime tr (fromIntegral n * nominalDay)
SQL
The SummaryFoundation is solid. The two blockers are: (1) clean up the stale |
…lpers
Sites that called the old toBaseServantResponse/toServantResponse with
separate (AuthContext, Logger[, sessAndHeader]) args:
- Pages/Bots/DiscordSpec, Pages/Bots/WorkflowsSpec (Discord half) —
these inject a modified Config.env; switched to record-update on
TestResources: toBaseServantResponse tr{trATCtx = testCtx}.
- Pages/Bots/{Whatsapp,Workflows} (Whatsapp/Slack halves),
Pages/GitSyncSpec, Opentelemetry/GrpcIngestionSpec — straightforward
drop to toBaseServantResponse tr / toServantResponse tr.
- Pages/BusinessFlowsSpec: the one direct effToServantHandlerTest
call now passes tr.trTestClock.
Also fix the lint warning that broke CI: inline the lambda parameter in
runTestBg (hlint Redundant-lambda).
Code Review: Unified Time Testing with
|
Adds test infrastructure and 8 integration tests that exercise the new controllable test clock (PR #329) across log patterns, anomalies, notifications, digest flush, and expiry jobs. TestUtils additions: runTestBgNoReset / runAllBackgroundJobsNoReset — multi-tick variants that don't snap the clock back to a fixed time. captureNotifs — clock-preserving notification capture. ingestLogAt / ingestErrorAt / ingestTraceAt — timestamp records from the live test clock instead of taking a UTCTime. SQL conversions (from raw now() to #{now}): ExpireShareEvents, runNotificationDigest hourly flush, selectIssues 24h chart series. Migration 0095 swaps now() → app_now() in log_auto_resolve_activity trigger so auto-resolve activity timestamps honour the test clock. New tests: LogPatternsSpec 13 — acknowledgeIssue opens 24h cooldown LogPatternsSpec 14 — pruneStaleLogPatterns 30-day boundary LogPatternsSpec 15 — autoAcknowledgeStaleNewPatterns 7-day boundary NotificationsSpec 3a — hourly rate-limit bucket rollover AnomaliesSpec — selectIssues 24h period filter DigestFlushSpec (new) — hourly digest flush honours test clock ExpirySpec (new) — share-event 30+48h boundary, replay 30d TTL
Summary
This PR introduces a unified time testing system that allows tests to control time across both Haskell effects and PostgreSQL triggers/defaults. It replaces the frozen time approach with a mutable, advanceable test clock that syncs with PostgreSQL's
app_now()function via a custom GUC variable.Key Changes
Database Migration
static/migrations/0033_app_now_function.sqlwhich:app_now()function that reads fromapp.current_timeGUC variable, falling back toNOW()in productionset_updated_at()trigger to useapp_now()new_anomaly_proc(),check_triggered_query_monitors(), andcheck_tests_to_trigger()procedures to useapp_now()for background job schedulingNew Test Clock Module
src/Pkg/TestClock.hsproviding:TestClocktype backed by anIORef UTCTimeadvanceTimeandsetTestTimefor controlling the test clockrunMutableTimeto interpret theTimeeffect from a mutable clocksyncConnectionTimeto sync the test clock to PostgreSQL's GUC on each connection checkoutrunWithTimeSyncedPoolto wrap connection pools with automatic time synchronizationTest Infrastructure Updates
Pkg.TestUtilsto:TestClockparameter in background job runnersadvanceTestTime,advanceMinutes,advanceHours,advanceDays)TestClockinTestResourcesrunMutableTimeandrunWithTimeSyncedPoolinstead ofrunFrozenTimeQuery Updates for Time Consistency
Time :> esconstraint and useTime.currentTimeinstead ofNOW():Models.Apis.RequestDumps:getRequestDumpForReports,getRequestDumpsForPreviousReportPeriodModels.Telemetry.Telemetry:getDataPointsData,getMetricChartListDataModels.Apis.Anomalies:countAnomalies,acknowledgeAnomaliesModels.Apis.Issues:updateIssueWithNewAnomaly,updateIssueEnhancementModels.Apis.LogPatterns:acknowledgeLogPatternsModels.Apis.Monitors:updateQMonitorTriggeredStateModels.Projects.Projects:projectCacheByIdModels.Projects.ProjectMembersandModels.Projects.ProjectApiKeys: Similar updatesTest Updates
TestClockto background job runners and response handlersgetTestTimeinstead ofgetCurrentTimefor test assertionsrunFrozenTimein favor of mutable clockImplementation Details
set_config('app.current_time', ?, true)which makes the setting transaction-scoped and auto-resets when connections return to the poolNOW()callshttps://claude.ai/code/session_019Mn7XLed4oq6j7dWqciekH