Skip to content

test(db/schedules): edge-case + property coverage for CAS status writers and analytics aggregation - #1827

Merged
vybe merged 7 commits into
devfrom
AndriiPasternak31/issue-1771c
Jul 29, 2026
Merged

test(db/schedules): edge-case + property coverage for CAS status writers and analytics aggregation#1827
vybe merged 7 commits into
devfrom
AndriiPasternak31/issue-1771c

Conversation

@AndriiPasternak31

@AndriiPasternak31 AndriiPasternak31 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Test-only PR. Applies /edge-cases (understand → enumerate → generate → reflect → verify) to
target 3 of 7 of #1771: the precondition-guarded CAS terminal transitions in
db/schedules/executions.py + queue.py (the #1082 status-as-projection contract) and the
NULL-skip / UTC-bucketing aggregation in db/schedules/analytics.py.

  • +2410 / −0, six files, all under tests/. git diff f7aff466..HEAD -- src/ is empty — no product code touched (Run edge-case analysis & property testing (P-38) on the highest-risk core areas #1771 AC#4).
  • 129 passed, 1 xfailed. The xfail is a real bug found by this work, shipped as a strict=True xfail rather than a silent fix.
  • 38 enumerated edge cases + 12 properties (Hypothesis, incl. a RuleBasedStateMachine); 28 newly covered.

Part of #1771 (target 3 of 7). Refs #1771.

Deliberately Refs, not a closing keyword. Targets 4–7 of #1771 remain open and three
concurrent PRs reference this issue, so auto-closing on merge would be wrong. The linked issue
will not auto-promote to status-in-dev — that is intended here; bump it manually when the
last target lands.

Changes

File What
tests/unit/test_1771c_schedules_cas_edges.py Discrete CAS cases, matrix rows A1b–A16
tests/unit/test_1771c_schedules_cas_properties.py P-A1…P-A4 incl. the stateful machine + its meta-test
tests/unit/test_1771c_schedules_analytics_edges.py Discrete analytics cases, rows B1–B16
tests/unit/test_1771c_schedules_analytics_properties.py P-B1…P-B4
tests/requirements-test.txt +1 dep: hypothesis==6.161.5
tests/registry.json registers the four new files (targeted insert; 54 added / 0 removed)

No docs/ delta, and that is deliberate per Trinity Rule #4: no new capability, no API change,
no schema change, no behaviour change. The tests encode requirements that already exist
(architecture.md already documents "Status-as-projection (#1082)" and the #1107/#868/#1115
analytics entries accurately). Adding "and it has tests" would be changelog narration, which that
file's editorial rules forbid.


✅ Merge state — rebased, conflict-free

Rebased onto current origin/dev — merge-base is now the dev tip, 0 commits behind. The one
expected conflict, tests/registry.json, is resolved.

It was mechanical, not semantic: dev appended a registry entry for
test_1809_image_drift_recreate.py (and stripped the file's trailing newline) while this branch
appends four test_1771c_* entries at the same array tail. Both sides' entries are kept,
comma-separated, in the one array — nothing dropped, nothing rewritten:

  • entry count 93 (dev) + 4 (this branch) = 97, and the file parses as JSON;
  • the diff against dev is a pure insertion — 54 lines added, 0 deleted;
  • dev's no-trailing-newline form is preserved, so there is no gratuitous diff line.

Because the branch was conflicting from the start, GitHub could never build a
refs/pull/1827/merge and therefore reported zero CI checks. That is now unblocked — this is
the first time CI can actually run on this PR.

Sibling collision — resolved. All three #1771 slices now carry a byte-identical
hypothesis==6.161.5 block at one fixed position (immediately after pytest-cov>=6.0.0) in
tests/requirements-test.txt — verified by hashing the block itself (sha256
bf4d409f…b549acd, equal across 1771a / 1771b / 1771c). The slices therefore merge cleanly in
any order
and leave the file with exactly one hypothesis line: no manual dedupe, no
comment-block arbitration at merge time.


The real bug this found — negative duration_ms reaches the Overview chart (A6 / B8)

Shipped as @pytest.mark.xfail(strict=True) plus a companion characterization test pinning
today's behaviour. No product-code fix — that is out of scope per #1771 AC#4.

  • Symptom: a started_at in the future relative to the finalizing process's clock makes
    update_execution_status persist a negative duration_ms (unguarded
    completed_at − started_at), which then flows unguarded into get_agent_analytics
    observed {'avg': -299997, 'p95': -299997}.
  • Minimal repro: row running with started_at = now + 300 s
    update_execution_status(id, "success")duration_ms = -299997.
  • Why this is not a toy input: started_at and completed_at are written by different
    processes
    . The standalone src/scheduler/ container repeats the same unguarded subtraction
    at src/scheduler/database.py:514-516, and the backend runs --workers 2.
  • Why "canary G-03 already covers it" is wrong: G-03 detects the skew (severity minor).
    Nothing prevents the poisoned metric, and the analytics consumption path is entirely
    unguarded.
  • Verified honestly: re-run under --runxfail, it fails on its own asserted contract
    (assert duration >= 0, negative duration_ms persisted: -299996) — not on an unrelated error.

Note for whoever fixes it: the xfail is strict=True. When a clamp lands, this test
flips to XPASS and fails the suite. That is the intended alarm telling you to delete the
xfail marker — not a broken test.


Coverage — reported as TWO numbers, because one alone misleads

A pre-review draft of this verdict quoted a single "92%" that came from bundling nine
pre-existing neighbour test files
. That number is real but not attributable to this PR.
Both are given:

A. This PR's tests alone (attribution)

pytest tests/unit/test_1771c_*.py -q --cov-branch \
  --cov=db.schedules.executions --cov=db.schedules.queue --cov=db.schedules.analytics
→ 129 passed, 1 xfailed · TOTAL 422 stmts / 92 branch / 86%   (was 60%)
    analytics.py   96%   (was 54%)   miss 115, 234, 542->550, 658-678
    executions.py  54% file-level — in-scope methods 100% except line 417
                   (the dark claim_token arm, covered by test_1081_lease_reaper.py)
    queue.py       84%   miss 123 + out-of-scope ranges covered by neighbours

B. Bundled with the nine neighbour files (the fleet's real position)

→ 262 passed, 1 skipped, 1 xfailed · TOTAL 422 stmts / 92 branch / 90%
    analytics.py   97%  (was 92%)
    queue.py       98%
    executions.py  62% file-level; in-scope methods 100% line, BrPart 0
                   — every miss is in 7 out-of-scope sibling methods

Residual misses — attributed, not hand-waved

Miss Verdict
queue.py:123 with_for_update(skip_locked=True)structurally unreachable without TEST_POSTGRES_URL. [SQLITE-ONLY].
analytics.py:658-678 get_all_agents_schedule_countsout of declared scope (plan §3).
analytics.py:234 and 542->550 Defensive-dead, proven. Both are if not day guards over substr(started_at,1,10). The only falsy day is an empty started_at, and the query filters started_at > cutoff; SQL evaluates '' > '2026-…' as false, so it never reaches the loop. A malformed non-empty value is > cutoff but yields a truthy substr, taking the live arm. Reported, deliberately not covered.
executions.py:417 Dark claim_token CAS arm — covered by test_1081_lease_reaper.py (row A9), not by this PR.

Honest scope limits

  • SQLite-only signal. TEST_POSTGRES_URL was never set, so every DB-touching case and
    property here ran on SQLite
    . Two spots are dialect-sensitive and labelled: B9 (substr
    day-bucketing) and queue.py:123 (FOR UPDATE SKIP LOCKED, the sole uncovered line,
    structurally unreachable here). Everything else rests on standard SQL
    (COUNT/AVG NULL-skipping/CASE/integer truncation) or ASCII ISO-8601 collation, equivalent
    on both backends.
  • Mutation testing NOT run for this slice — --mutate is scoped to target 1 by the plan, and
    mutmut is unusable in this repo's sys.path layout. So "90% branch" is a coverage claim,
    not a mutation-kill claim.
  • Concurrency is deliberately out of scope — owned by
    test_1081_pull_endpoints.py::TestClaimConcurrencyC1.
  • No property claims DB-layer terminal immutability. Only CANCELLED blocks a SUCCESS
    overwrite; immutability is an upstream refactor: fire-and-forget dispatch — a hung turn holds zero backend resource #1083 property, not a DB-layer one.

Traps deliberately avoided

  • bool ⊂ int. A global isinstance(result, int) would pass for the wrong reason on every
    bool-returning writer. Assertions are therefore per return class: identity
    (is True/is False) for the 7 bool writers, isinstance(int) and not isinstance(bool) for
    the 4 rowcount writers, dict-or-None for claim_next_queued.
  • A vacuous property, caught and fixed. P-B2b asserted
    sampled is (eligible > _PERCENTILE_ROWSET_CAP) with the cap left at its production 5000
    against ≤12 seeded rows — so the True arm was unreachable for every example and half the
    biconditional passed vacuously. The cap is now drawn (1–6) and monkeypatched, with
    over/at/empty @example pins; a probe forcing sampled is False now fails, proving the arm is
    live. The constant verifiably restores to 5000 at teardown.
  • Non-vacuity of the stateful machine proven three ways, not by its green tick
    (run_state_machine_as_test prints no statistics, so a pass is weak evidence alone):
    1. neuter the invariant → the meta-test FAILS (the invariant is what catches the sabotage);
    2. remove the sabotage rule → meta-test FAILS with DID NOT RAISE AssertionError (it
      detects the injected reversal, not incidental noise);
    3. instrument the real machine → machines: 33, reaching_terminal: 30, terminal_observations: 152
      — the phantom-reversal arm is genuinely reachable.

A requirements gap the review caught

get_schedule_analytics was named a subject-under-test by both the plan and the edges file's
own docstring — yet no test invoked it. analytics.py:113-259 was entirely unexecuted under
new-tests-only coverage. Closed with 16 added cases (rows B13/B13b/B13c/B15), which is what
moved analytics attribution from 54% → 96%. Those cases also close the tool_calls JSON-shape
guards on both analytics surfaces — that column is agent-written, so every malformed shape is
reachable input and a regressed guard turns an analytics read into a 500.

Order-independence verified for real

pytest-randomly is absent locally but installed out-of-band by CI
(backend-unit-test.yml:99) — so a locally-green suite proves nothing about CI ordering. Verified
explicitly: 4 solo seeds + 2 interleaved runs with 9 neighbour files + a CI-shaped invocation,
all green
, with the venv restored afterwards.

Security

/cso --diff: zero findings.

  • Net new package surface is 1: hypothesissortedcontainers, which fakeredis already
    required.
  • It never reaches a runtime image (test-only dependency file).
  • Every production-constant patch goes through the auto-undoing monkeypatch fixture — zero
    bare module-level setattr.
  • The hypothesis==6.161.5 exact pin (rather than a floor) is deliberate: it freezes a
    reproducible derandomized input set, and is the safer supply-chain posture.

Test Plan

  • cd tests && python -m pytest unit/test_1771c_*.py -q129 passed, 1 xfailed (17.7s); re-run on the rebased branch → 129 passed, 1 xfailed (14.4s, Python 3.12, hypothesis 6.161.5)
  • Full tests/unit tier → 1 failed, 5032 passed, 17 skipped, 1 xfailed. The single red is
    a pre-existing aged-out fixture (see follow-ups), reproduced on a pristine detached
    worktree at the cut base f7aff466 with none of these files present.
  • No product-code diff: git diff origin/dev...HEAD --stat -- src/ empty (re-verified after the rebase).
  • Order-independence: 4 seeds + 2 interleaved + CI-shaped invocation, all green.
  • python3 tests/lint_sys_modules.pyOK: 203 violation(s) in 60 file(s); baseline allows 240 — no new violations, and tests/lint_sys_modules_baseline.txt is untouched (see
    below).

/verify-local deliberately skipped (explicit call, and justified): zero src/ changes, no
image-reachable inputs, and the unit tier ran in full. /verify-local exists to catch
source→image packaging gaps (#1033 class); a test-only diff has no image surface to break.

sys.modules lint — satisfied, not baselined

This PR was CONFLICTING from the moment it opened, so GitHub could never build
refs/pull/1827/merge and reported zero checks. The rebase produced this PR's first ever CI
run — which immediately failed lint (sys.modules pollution check) with 12 bare
sys.modules.pop(...) calls (3 per new file). Now green.

Fixed by satisfying the guard, not by running --regenerate-baseline: this guard has already
caught this same class in #783 / #606 / #875, and widening it on a PR whose entire point is test
rigour would be backwards. tests/lint_sys_modules_baseline.txt is unchanged.

Both eviction sites take the documented escape hatch (top-level _STUBBED_MODULE_NAMES +
autouse _restore_sys_modules, shape copied from tests/unit/test_telegram_webhook_backfill.py)
rather than monkeypatch, for a substantive reason in each case:

  • the utils* shadow clear is import-time, before any fixture exists — monkeypatch
    structurally cannot reach it;
  • the ops fixture's db.* eviction must not use monkeypatch.delitem: verified directly
    against _pytest.monkeypatch that it records no undo for a key absent on entry, so the
    freshly imported harness-bound module would stay resident for later files — strictly worse
    isolation than the explicit pop it would replace.

The fixture is a teardown-time guarantee only — the snapshot is taken before the test body, so
no in-test behaviour and no assertion changes. Confirmed empirically: the four files still report
129 passed, 1 xfailed, the same strict xfail, unchanged.


Follow-ups — flagged, NOT filed as issues

  1. The negative-duration_ms clamp (A6/B8 above). The strict=True xfail flips to XPASS the
    moment a clamp lands — that is the signal to remove the marker. Both call sites need it:
    db/schedules/executions.py and src/scheduler/database.py:514-516.
  2. tests/unit/test_1474_read_boundary_z.py::test_schedules_summary_last_run_at_normalized is
    aged out
    — belongs to no slice's fence, so it is nobody's by default. Root cause proven,
    not guessed: the fixture hard-codes NAIVE = "2026-07-06T11:00:00.207634" while
    get_agent_schedules_summary filters started_at > iso_cutoff(168), so the seeded row now
    falls outside its own 7-day window and last_run_at is None. Re-running the identical call
    with a 5-year window returns the correctly _norm_ts-normalized value — the behaviour under
    test is fine; the test aged out
    (it would have gone red around 2026-07-13). One-line fix:
    make the constant relative to nowthe same defect class this PR already fixed in its own
    B9 case
    (commit 7740a4e1).
  3. Defensive-dead branches analytics.py:234 and 542->550 — either delete the guards or
    accept them as defensive; they cannot be covered from this layer.
  4. get_all_agents_schedule_counts (analytics.py:658-678) has zero coverage and is outside
    this slice's declared scope.
  5. UNSPEC — headline success_rate is 0.0 on a zero-terminal window while per-day is
    None, so "never ran" and "failed everything" both render 0% at the headline. No requirement
    is violated (the discipline is silent on the headline) and changing it is a frontend-visible
    contract decision. Characterized, not xfailed.
  6. UNSPEC — success → success wins the CAS twice. The DB-layer SUCCESS predicate is
    status != CANCELLED, which an existing success row satisfies. Not a bug at this layer — the
    replay short-circuit is the refactor: fire-and-forget dispatch — a hung turn holds zero backend resource #1083 callback's job, and this looseness is exactly what the
    documented "a late SUCCESS overwrites a reaper LEASE_EXPIRED" guarantee depends on.
    Characterized so a future tightening is deliberate.
  7. ACCEPTED GAP — malformed started_at raises before the CAS. update_execution_status
    parses started_at before building the CAS WHERE, so a malformed non-NULL value raises
    ValueError even for a write that would have lost the CAS. Reachable at this layer
    (update_execution_to_queued copies a caller-supplied queued_at in unvalidated) but not
    from any current production caller
    (services/backlog_service.py passes utc_now_iso()).
    The None variant is unreachable — the live schema enforces NOT NULL on both backends,
    guarded by its own test since db/tables.py declares the column nullable and a Collapse schema.py + migrations.py into single source of truth (follow-up to #713) #746
    metadata-driven-DDL migration would open the AttributeError path.
  8. ruff F811 on the db_backend fixture idiom, repo-wide (~10 files). Pre-existing pattern
    in every db_harness consumer; no CI job runs ruff today. Fixing it here would diverge from
    10+ neighbours. Reported, not changed.

Full edge-case matrix (38 rows + 12 properties) — click to expand

This matrix lives only in the worktree's gitignored .plan/ directory, so it is reproduced here
in full; otherwise it dies with the worktree.

Status legend: COV already covered · NEW covered by this PR · UNSPEC spec gap (reported,
characterized, not xfailed) · BUG real bug (strict xfail) · GAP accepted gap with written reason.

Totals

Count
Enumerated (matrix rows) 38 (A: 18, B: 20)
Properties 12 (P-A1…P-A4 → 7 tests incl. 1 meta-test; P-B1…P-B4 → 7 tests)
Already covered (no new test written) 6
Newly covered 28
UNSPECIFIED (reported only) 2 (A3, B4)
Accepted gaps (written reason) 2 (A5b-None unreachable, A5b-malformed latent)
Defensive-dead branches (proven unreachable) 2 (analytics.py:234, 542->550)
Real bugs 1 (A6 / B8 — one root cause, two surfaces)

Sub-area A — CAS status writers

# Input / state Class Case Expected / observed Status
A1 prior status status-write race SUCCESS over running / failed CAS wins COV
A1b prior status status-write race SUCCESS over queued / pending_retry / skipped CAS wins NEW
A2 prior status status-write race SUCCESS over cancelled CAS loses (#671) NEW (re-pin)
A3 prior status replay double-effect SUCCESS over existing success wins again (True twice) UNSPEC
A4 prior status status-write race FAILED over ×4 terminals CAS loses COV
A5 row absence null/optional unknown execution id False, no raise NEW
A5b-i schema null/optional started_at NULL NOT NULL enforced on both backends NEW (guard)
A5b-ii started_at I/O boundary '' / garbage / out-of-range raises ValueError before the CAS GAP
A5b-iii queued_at I/O boundary caller-supplied string copied into started_at unvalidated reachability proof for A5b-ii NEW
A6 clock skew naive/aware + arithmetic started_at 300 s in the future duration_ms = -299997 persisted BUG (xfail strict)
A7 started_at time / format naive · Z · +00:00 · +05:00 no aware − naive TypeError NEW
A8 retry_count null/optional None preserves prior; 0/1/3 write through prior 7 preserved on None NEW
A9 claim_token state & concurrency token-gated CAS dark by flag, not caller-less COV
A10 prior status phantom reversal (E-02) update_execution_to_queued over ×4 terminals no-op COV
A11 double-call replay mark_execution_dispatched ×2 2nd False COV
A12 agent scope cross-tenant bulk writers scoped to one agent; empty/unknown ⇒ 0 no wildcard NEW
A13 max_age_hours numeric + lexicographic ISO exact cutoff second; 24 h; fractional; 1e6 exact-cutoff row survives (≤1 s band) NEW
A14 queued_at NULL null/optional queued row, NULL queued_at excluded by isnot(None) NEW
A15 agent scope cross-tenant get_queued_count isolation; "" ⇒ 0 scoped count NEW
A16 lease columns state find_expired_leases excludes future / NULL / terminal; ordering + limit disjoint from push rows NEW

Properties (sub-area A)

ID Shape Statement Result
P-A1 invariant preservation (stateful) Across any legal interleaving of 12 CAS writers, a terminal row is never observed non-terminal again (canary E-02 at the DB layer). Deliberately not "terminals are immutable" — that is false here. PASS (max_examples=30, stateful_step_count=8)
P-A1-meta meta-test A sabotaged machine with a raw resurrecting UPDATE must be caught PASS — proves P-A1 load-bearing
P-A2a no-crash-total 7 single-row writers return a strict bool, never raise, over the full status domain PASS — 49/49 exhaustive
P-A2b no-crash-total 4 bulk writers return a non-negative int (not bool) PASS — 28/28 exhaustive
P-A2c no-crash-total claim_next_queueddict (only from queued) or None PASS — 7/7 exhaustive
P-A3a idempotence a winning single-row call replays to False, status unchanged PASS — 49 explored, 7 winning
P-A3b idempotence a winning bulk call replays to 0 PASS — 28 explored, 6 winning
P-A3c idempotence claim_next_queued serves each queued row exactly once, FIFO, then None PASS — 4/4 exhaustive
P-A4 oracle for utc_now_iso() strings, lexicographic order chronological order PASS — 200 examples
P-A4b oracle (negative) a mixed offset spelling of the same instant breaks that equivalence — why Invariant #16 exists PASS — 200 examples

Sub-area B — analytics aggregation

# Input / state Class Case Expected / observed Status
B1 bucket config enum / dispatch _TRIGGER_BUCKETS.values() ⊆ _BUCKET_ORDER; Other last; no dupes holds today (10/10) NEW (regression guard)
B2 triggered_by enum / dispatch known · unknown · "" · None · uppercase · padded all map; unmapped ⇒ Other NEW
B3 terminal counts % with no denominator day with runs but 0 terminals success_rate is None NEW
B4 terminal counts % with no denominator headline rate on an empty agent 0.0 while per-day is None UNSPEC
B4b statuses % denominator non-terminal rows excluded from the rate denominator terminal-based NEW
B4c statuses enum alias legacy error folded into failed counts as failure NEW
B5 context_used NULL null/optional NULL-skipping AVG; all-NULL ⇒ None not zeroed NEW
B6 rowset size collection boundary cap−1 / cap / cap+1 sampled flips only above cap NEW
B6b data-source discipline sampling headline avg full-set while p95 is sampled avg=250, p95=400 NEW
B7 duration pool collection boundary 0 / 1 / 2 success rows None / value / 195 (inclusive interpolation) NEW
B7b duration_ms NULL null/optional NULL duration excluded from the pool int(None) never reached NEW
B8 duration_ms negative accumulator negative durations flow into avg and p95 {'avg': -299997, 'p95': -299997} BUG (same root as A6)
B9 started_at offset lexicographic / UTC bucketing +05:00 value → substr(...,1,10) buckets by literal prefix, not UTC instant NEW [SQLITE-ONLY]
B10 window time boundary rows at cutoff ±1 µs > is strict ⇒ exact row excluded NEW
B11 timeline collection / gap-fill hours ∈ {24,168,336,720}; zero-days contiguous, no gaps/dupes NEW
B12 message strings empty · whitespace · newline-leading · exactly 80 / 81 · emoji · combining · RTL never raises; ≤ 80 chars NEW
B13 tool_calls untrusted JSON shape get_schedule_analytics: empty · non-JSON · valid-JSON-not-a-list · list-of-scalars · dict-without-name · falsy name · non-numeric duration_ms never raises; counts only named entries NEW (added at /review)
B13c statuses aggregation branch per-schedule timeline FAILED arm success/failed day counters; cancelled/running add cost + total only NEW (added at /review)
B14 tool_calls untrusted JSON shape same shape table on get_agent_schedules_summary; NULL filtered in SQL not Python never raises; tool_call_total exact NEW (added at /review)
B15 duration pool collection boundary get_schedule_analytics 0 / 1 / 2 / 3 success rows a second implementation of B7's ladder NEW (added at /review)
B16 schedule identity tenant boundary wrong agent / soft-deleted ⇒ None (router → 404) analytics.py:115 COV

Properties (sub-area B)

ID Shape Statement Result
P-B1 conservation sum(by_type[*].total) == total_executions for any multiset of arbitrary unicode triggers — the mechanised "a new trigger never silently vanishes" PASS — 100 examples. Coverage markers from one observed run (Hypothesis re-randomises, so these are evidence of non-vacuity, not a stable contract): 80% reached Other, 54% multi-bucket
P-B1b conservation per-day stacks sum to their own day total PASS — 100 examples
P-B2 bounds 0 ≤ success_rate ≤ 1 (or None per-day); sub-counts ≤ total; zero-terminal day ⇒ None PASS — 100 examples; 11% hit the zero-terminal branch in one observed run
P-B2b bounds sample_size ≤ cap and ≤ eligible; sampled iff pool exceeds cap PASS — 100 examples. Rewritten at /review to de-vacuum (see "Traps deliberately avoided")
P-B3 invariant preservation timeline is contiguous, strictly increasing, duplicate-free, spans [now−hours, now] for any hours ∈ [1,1000] PASS — 100 examples
P-B4a no-crash-total _bucket_for_trigger total, result always ∈ _BUCKET_ORDER PASS — 200 examples
P-B4b no-crash-total _schedule_command_label total, str, ≤ 80 chars, newline-free PASS — 200 examples

🤖 Generated with Claude Code

@AndriiPasternak31

Copy link
Copy Markdown
Contributor Author

Follow-up tracking: the negative-duration_ms finding (A6 / B8) is now filed as #1832, so it survives the merge.

The issue records the point this PR body makes — that canary G-03 detects the skew at severity minor but prevents nothing, and the analytics consumption path is unguarded — and notes that a fix must clamp in both writers (db/schedules/executions.py and src/scheduler/database.py:514-516), since fixing only the backend leaves the scheduler path live. It also carries the warning that the strict=True xfail will flip to XPASS and fail the suite when a clamp lands, which is the intended alarm.

@AndriiPasternak31
AndriiPasternak31 requested review from dolho and vybe July 27, 2026 19:33
@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

AndriiPasternak31 and others added 7 commits July 28, 2026 17:56
… writers (#1771)

/edge-cases target 3, sub-area A: the #1082 status-as-projection CAS writers in
db/schedules/executions.py and db/schedules/queue.py.

- test_1771c_schedules_cas_edges.py: matrix rows A1b/A2/A3/A5/A5b/A6/A7/A8/
  A12/A13/A14/A15/A16 as parametrized cases on the db_harness (#300) full
  production schema — SQLite always, PostgreSQL when TEST_POSTGRES_URL is set.
- test_1771c_schedules_cas_properties.py: P-A1 bounded RuleBasedStateMachine
  ("terminal is absorbing" — canary E-02 at the DB layer) plus a meta-test that
  injects a phantom reversal and proves the machine reports it; P-A2/P-A3
  no-crash + idempotence asserted PER RETURN CLASS (bool / int / Optional[Dict]
  are three different contracts); P-A4 lexicographic-ISO oracle.
- hypothesis==6.161.5 added to tests/requirements-test.txt (exact pin so the
  three concurrent #1771 slices produce an identical, trivially-mergeable line).

One strict xfail: A6 — a clock-skewed started_at persists a NEGATIVE
duration_ms. No product-code fix (#1771 AC#4).

Test-only. No src/ changes.

Refs #1771

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aggregation (#1771)

/edge-cases target 3, sub-area B: db/schedules/analytics.py — get_agent_analytics
(#1107), get_schedule_analytics (#868), get_agent_schedules_summary (#1115) and
the pure leaves _bucket_for_trigger / _schedule_command_label.

- test_1771c_schedules_analytics_edges.py: matrix rows B1–B12. Highlights: B1
  pins _TRIGGER_BUCKETS.values() subset of _BUCKET_ORDER (the existing literal
  assertion in test_agent_analytics.py omits "Reminders", so it was never a
  completeness check); B6b mechanises the locked "headline avg is full-set,
  never the capped pool" discipline; B10 freezes iso_cutoff so the strict-'>'
  boundary is deterministic rather than a clock race.
- test_1771c_schedules_analytics_properties.py: P-B1 conservation (sum of
  by_type == total_executions, over arbitrary unicode triggers), P-B2 rate/count
  bounds incl. the zero-terminal-day-is-None rule, P-B3 contiguous UTC-day
  timeline over arbitrary windows, P-B4 no-crash-total on the pure leaves.
  Hypothesis `event()` markers make non-vacuity provable via
  --hypothesis-show-statistics rather than assumed.

Five first-run failures were all WRONG TESTS (my expectations/mechanisms), not
product bugs — reflection notes recorded in the docstrings.

Test-only. No src/ changes.

Refs #1771

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/update-tests tail step. Appended with ensure_ascii=True so the existing
\uXXXX escaping of 24 unrelated entries is preserved — the diff is +54/-0.

The companion .claude/agents/test-runner.md catalog sync is deliberately NOT
done: that path is inside the pinned private .claude submodule, and this wave
must not move the submodule gitlink.

Refs #1771

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review fix. B9 hard-coded a 2026-03-01/02 pair, which forced a 10-year
window to keep the rows in range — so the gap-fill loop built ~3650 day-dicts
per call, and the test would have aged out of any tighter window.

Derive the straddling instant from today instead and use a 168h window. Same
assertion, cannot age out; the file's runtime drops 8.3s -> 2.4s.

Refs #1771

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…m P-B2b (#1771)

/review findings on the #1771 target-3 edge-case slice, fixed in place.
Test-only: `git diff f7aff46..HEAD -- src/` stays empty (AC#4).

R1 REQUIREMENTS MISSING — `get_schedule_analytics` was named a
subject-under-test by the plan (§3) and by the edges file's own docstring,
yet no test invoked it: under a new-tests-only coverage run its whole body
(analytics.py:113-259) was unexecuted, and the reported 92% came from
bundling nine pre-existing neighbour files. Added rows B13/B13b/B13c/B15.

R2 Closed the residual gap the matrix itself flagged: the `tool_calls`
JSON-SHAPE guards. The column is agent-written, so valid-JSON-not-a-list,
list-of-scalars, dict-without-`name` and non-numeric `duration_ms` are all
reachable input, and a regressed guard turns an analytics read into a 500.
Neighbours cover only the `json.loads` raise. Now pinned on BOTH surfaces
(`get_schedule_analytics` + `get_agent_schedules_summary`, 20 cases).

R3 P-B2b was half-vacuous: it asserted `sampled is (eligible > cap)` with
the cap at its production 5000 and at most 12 seeded rows, so the `True` arm
was unreachable for every example. The cap is now drawn (1-6) and
monkeypatched, with over/at/empty `@example` pins. Verified load-bearing —
a probe forcing `sampled is False` fails on the explicit example.

R7 Renamed the `st` loop variable that shadowed the conventional
`hypothesis.strategies as st` alias used in the sibling properties file.

Also corrected in `.plan/edge-cases-1771c-matrix.md` (untracked by
convention): `analytics.py:234`/`241` were mis-described as tool_calls
guards (234 is the empty-day guard, 241 the FAILED timeline arm, now
covered); `542->550` is defensive-dead, not malformed-`started_at`-reachable
(`'' > cutoff` is false in SQL, so an empty `started_at` never reaches the
loop); both coverage numbers are now reported instead of only the bundled
one; and the Hypothesis `event()` percentages are labelled as one observed
run rather than a stable contract.

Declined: ruff F811 on the `db_backend` fixture import (pre-existing
repo-wide pattern in every db_harness consumer; no CI workflow runs ruff).

Refs #1771

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three concurrent #1771 slices each added `hypothesis==6.161.5` to
tests/requirements-test.txt at a different position with a different
comment, so they conflicted with each other rather than merging cleanly.

Rebuild the file from dev's copy and insert one byte-identical canonical
block at one fixed position (after pytest-cov). All three slices now carry
the same bytes in the same place, so they merge in any order and the file
keeps exactly ONE hypothesis line.

No product code, no test logic change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first CI run this PR was ever able to produce (it was CONFLICTING until
now, so GitHub could not build refs/pull/1827/merge and reported zero checks)
failed `lint (sys.modules pollution check)`: 12 bare `sys.modules.pop(...)`
calls, 3 per new file.

Fixed by satisfying the guard, NOT by widening its baseline
(`lint_sys_modules_baseline.txt` is untouched) — this guard has already caught
this same class in #783, #606 and #875, and retiring it on a test-rigour PR
would be backwards.

Both eviction sites are out of monkeypatch's reach, or actively wrong for it:

  * the `utils*` shadow clear is IMPORT-time, before any fixture exists, so
    monkeypatch structurally cannot reach it;
  * the `ops` fixture must evict `db.*` so `from db.schedules import ...`
    re-imports against the harness-bound engine. `monkeypatch.delitem` records
    NO undo for a key absent on entry (verified against _pytest.monkeypatch),
    so the freshly imported harness-bound module would stay resident for later
    files — strictly worse isolation than the explicit pop it would replace.

So both take the documented escape hatch instead: a top-level
`_STUBBED_MODULE_NAMES` list plus an autouse `_restore_sys_modules` fixture,
matching the shape of tests/unit/test_telegram_webhook_backfill.py. It is a
teardown-time guarantee only — the snapshot is taken before the test body, so
no in-test behaviour and no assertion changes.

Verified: lint reports "203 violation(s) in 60 file(s); baseline allows 240 —
no new violations"; the four files still report exactly 129 passed, 1 xfailed
(same strict xfail); `git diff origin/dev...HEAD -- src/` still empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AndriiPasternak31
AndriiPasternak31 force-pushed the AndriiPasternak31/issue-1771c branch from 4451b2e to 615182d Compare July 28, 2026 22:49

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated: test-only slice of #1771 (CAS status writers + analytics aggregation, db/schedules). Security scan clean, full pytest matrix green, sys.modules restore handled (#762 lint). Bare #1771 ref correct for the shared parent.

@vybe
vybe merged commit d676ce2 into dev Jul 29, 2026
20 checks passed
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.

2 participants