Skip to content

updating crawler watchdog branch to test - #18

Merged
agrorithms merged 22 commits into
crawler-as-watchdog-reschedulefrom
main
Jul 30, 2026
Merged

updating crawler watchdog branch to test#18
agrorithms merged 22 commits into
crawler-as-watchdog-reschedulefrom
main

Conversation

@agrorithms

Copy link
Copy Markdown
Owner

No description provided.

allowing more active sessions to show on active-sessions page
Lockfile churn from a prior `npm install` that resolved newer patch
versions of existing dev dependencies (eslint internals and friends).
Isolated in its own commit so it stays separable from the Vitest
install that follows.
Captures the recon findings that contradicted the original brief (the
ended_at cutover already shipped; isFullClear is dead code; the
leaderboard SQL lives in leaderboard-cache.ts; request() has no retry
logic) along with the ten decisions taken in response.

Written down because the commits that follow will not match the brief
they came from, and the reason should not live only in a chat log.
Vitest over Jest: it runs TypeScript and ESM natively with no babel or
ts-jest layer, reads the existing tsconfig's path aliases, and does not
contend with Next.js 16's bundler.

- vitest.config.ts: node environment, colocated src/**/*.test.ts plus
  tests/**/*.test.ts, explicit @/ -> src/ alias mirroring tsconfig,
  v8 coverage reporter with no thresholds.
- npm test / test:watch / test:coverage.
- Renamed test-maintenance-cycle -> e2e:maintenance (same command) so
  `npm test` means fast, hermetic, no network and nothing else. The only
  other references to the old name are in untracked .codex/ transcripts.
- Global network guard fails any test reaching the real internet without
  stubbing fetch, with its own tests covering the re-arm-after-stub case.

Gate verified: a passing run, then a flipped assertion failing with a
readable diff, then the throwaway deleted. eslint and tsc --noEmit both
clean on the new files; no flat-config override was needed.
Fixtures are captured from the live API rather than hand-authored so
they encode Bungie's real quirks — absent startingPhaseIndex, entry
counts above six, activity durations that disagree with per-player
time. A synthetic PGCR would only encode our beliefs about the API,
which is what the fixtures exist to check.

Every instance ID was selected by querying the local database for runs
that actually exhibit the target property, so each case is a real
observed run. The non-raid fixture probes forward from a known raid id
until isRaidActivityHash rejects one.

The script prints the salient fields per capture (entry count,
completion count, fromBeginning, duration vs max time played, missing
names) so a capture that fails to exhibit its stated property is
visible rather than assumed.

Run by the maintainer via `npm run capture-fixtures`; the API key is
read from .env by the script and never handled directly.
Test databases are real SQLite files in a per-file temp directory, not
`:memory:`. SQLite silently downgrades journal_mode=WAL to 'memory' for
in-memory databases, so `:memory:` would exercise different journal
semantics than production and undercut the reason for using a real
database at all. Verified, not assumed. See ADR 0003.

The path is set in a setupFile because DB_PATH is a module-level const
resolved at import time; setting it there rather than inside a helper
means test files can use ordinary static imports. It also relocates
DATA_DIR, isolating maintenance-state.json — getDb() reads that file on
every single call and would throw DatabaseMaintenanceError across the
whole suite if the real database were mid-vacuum.

Seeding goes through insertFullPGCR rather than raw INSERTs, because
that is where ended_at is derived and last_seen_at advanced. Raw inserts
would produce rows production could never create.

Fixtures for realism, builders for permutation.
Against a real database, not a mock — better-sqlite3 opens a fresh one
in about a millisecond, and mocking would validate the scaffolding
instead of the SQL.

Covers membership (checkpoint runs, zero-completion runs, non-finishers,
and runs with no derivable end time are all excluded), the cutoff
boundary judged on end time rather than start, raid filtering, the
three-key tie-break, competition-style rank assignment, and the display
name path.

Two notes on the brief this came from: fullClearsOnly has no false
branch to test — it is forced true on every path — and the SQL lives in
lib/cache/leaderboard-cache.ts, not lib/db/queries.ts.

One test pins a defect rather than asserting correctness:
formatDisplayName drops the #Code when the code is 0, because it guards
with a truthiness check. Reported, not fixed.
The brief asked for parity tests guarding an in-flight cutover from the
run_durations CTE to pgcrs.ended_at. That cutover already shipped in
610408e and no run_durations SQL remains in src/, so there is nothing
left to guard.

Retargeted to the code that replaced it: the three-tier duration
fallback in computeActivityDurationSeconds and the future-end-time
corruption guard, asserted both directly and through insertFullPGCR.

Covers Tier 1 winning over per-player time, a zero duration being
treated as unusable, late-joiner offsets, non-completers counting toward
duration (the CTE excluded them), the Tier 2 collapse when start offsets
are absent, Tier 3 yielding NULL, the future-end-time discard with its
clock-skew headroom, and last_seen_at advancing monotonically.
Mocked at the fetch boundary only. Stubbing our own modules would test
the stubs; stubbing fetch tests the response handling that decides
whether a raid gets ingested or dropped.

Correction to the brief: request() has no retry logic. It classifies a
failure and, when Bungie signals throttling, pauses the shared per-key
rate limiter — the throttle applies to the key, not to the one request
that saw it. The tests follow that actual behaviour.

- maintenance: the SystemDisabled predicate, with its negative cases
  (privacy restriction, plain 5xx, timeout, error-shaped impostor)
  carrying as much weight as the positive one. A false positive pauses
  the whole crawler for a blip; a false negative burns thousands of
  doomed requests.
- client: payload handling, the typed-vs-untyped error distinction that
  the maintenance predicate depends on, non-JSON Cloudflare bodies, and
  each of the four pause paths (ThrottleSeconds, 429 with and without
  Retry-After, and the 1672 game-server backoff that reports no
  duration).
- rate-limiter: FIFO serialization, and pauses that catch queued
  waiters, extend a sleep already in progress, and never shorten an
  existing pause.

Timing is asserted with fake timers, so the suite stays fast.
First workflow in the repo. Node 22, matching local and production.

Adds `tsc --noEmit` beyond the brief: nothing else typechecks the repo,
since vitest transpiles without checking and a full `next build` is too
slow for a per-push gate. The tsconfig includes **/*.ts, so test files
are checked too.

The e2e maintenance harness is excluded — it spawns long-running
processes against a mock Bungie server.

Documented in the workflow: CI is x86_64 while production is ARM64
(Oracle A1 Flex), so native-module differences in better-sqlite3 will
not be caught here.
The brief made ProcessedPGCR.isFullClear the centrepiece here, calling
its three-way || the riskiest logic in the codebase. It is dead code:
computed, returned, and never read. fetchAndStorePGCR persists Bungie's
raw activityWasStartedFromBeginning instead, and every leaderboard
filters on that column.

It is also wrong. Bungie no longer sends startingPhaseIndex (absent on
all 827k stored rows), so the `=== undefined` branch fires
unconditionally and reports every run as a full clear — including the
568k checkpoint runs. Harmless only because nothing consumes it; wiring
it up would inflate every leaderboard by roughly 2.2x.

So Phase 3 tests the signal that actually ships:

- pgcr.test.ts: the per-entry .some() completion check, director hash vs
  referenceId fallback, raid key resolution, ISO-to-unix conversion
  (including a DST boundary, to pin that no local offset leaks in), and
  player extraction tolerating a withheld global display name.
- full-clear-flag.test.ts: that checkpoint runs persist as 0, that
  starting_phase_index is inert, and that the full-clear flag stays
  independent of whether anyone finished.

isFullClear is left untested on purpose, with the reasoning recorded in
the test file — pinning dead behaviour would only make its removal
harder. Flagged for deletion in a future change, not deleted here.
ADR 0003 — test databases are real SQLite files, not `:memory:`. SQLite
silently downgrades journal_mode=WAL to 'memory' for in-memory
databases, so `:memory:` would exercise different journal semantics than
production and give up most of the reason for using a real database.

ADR 0004 — mock only at the network boundary. No vi.mock of our own
modules, real database, seeding through insertFullPGCR. Written down
because mocking one's own modules is the default habit elsewhere, so the
absence needs explaining before someone adds it back.

CONTEXT.md — Full Clear sharpened to name the only authoritative signal,
since the codebase held two competing implementations of the term and
the glossary did not say which was canonical. Added Checkpoint Run (the
majority of observed raids, previously unnamed) and Completion (the unit
leaderboards actually rank by, and narrower than "finished a raid").

decisions.md — the recon findings that reshaped the work, plus four
defects reported rather than fixed: dead-and-wrong isFullClear,
formatDisplayName dropping #Code on a zero code, getDb() reading the
maintenance state file on every call, and CLAUDE.md describing a runtime
manifest dependency that does not exist.

Also brings ADRs 0001 and 0002 into git; they were written earlier but
never committed.
Eight real Bungie responses, captured verbatim. Capturing them corrected
three claims that had been inferred from the database rather than
observed at the source. The conclusions held; the mechanisms did not.

- startingPhaseIndex is SENT, always 0 — not absent. Present on every
  fixture including three confirmed checkpoint runs. The database showed
  0 everywhere because the writer coerces with `|| 0`, which had
  flattened the evidence. So isFullClear's `=== 0` branch fires, not its
  `=== undefined` branch. Still true for 100% of runs; the defect is
  unchanged. Corrected in the docs and test comments.
- A six-entry raid report can belong to two players with three
  characters each. pgcr_players is keyed (instance_id, membership_id)
  with INSERT OR IGNORE, so only each player's first entry survives —
  981s stored where their longest character played 1494s. Those columns
  are read nowhere in src/, so it is latent. Duration derivation is
  unaffected: it runs before the dedupe.
- Bungie can withhold identity entirely: nineteen entries, all
  isPublic false with membershipType 0 and no name field at all. Not a
  missing global name with a platform fallback — no fallback exists.
  Ingestion stores all nineteen with NULL names, which is correct.

Two fixtures renamed to what they actually are: the "duration
divergence" case is really the multi-character case (the gap is 8%), and
the "no duration" case is really an absurd 27384s duration on an
18-minute run. There is no true Tier 3 fixture; builders cover it.
…ock-aware

The two counts now navigate to /leaderboard and /active-sessions. They carry no
styling of their own — colour, weight and underline all inherit, so the strip is
pixel-identical for mouse and touch users; a focus-visible ring is the only
addition, and it fires for keyboard navigation only.

Deletes FooterStatus, whose "Updated Xs ago" the StatsBar ticker supersedes.

These were never duplicates, though: the footer reported the *crawler heartbeat*
(how stale the data is) while the StatsBar ticker reports *this tab's last fetch*
(how stale the page is). They diverge exactly when it matters — a dead crawler
left the bar reading "Updates paused - Updated 8s ago". So the freshness slot now
picks its clock: page age while live, data age once the heartbeat lapses, and
nothing at all when the crawler is down and its age is unknown (the "Maintenance"
label already says everything true there).

That three-way choice is extracted to selectFreshness() so it can be tested under
vitest's node environment, which has no DOM by design.

Docs: CONTEXT.md gains Heartbeat / Data Freshness / Page Freshness — the missing
vocabulary that let one phrase mean two things in the first place. ADR 0002 notes
that its count-vs-list rule now extends to navigation: the links deliberately
don't override a reader's saved time range or raid filters.
feat: link StatsBar counts to their pages; make the freshness slot cl…
Resolves two doc conflicts:
- tests/README.md: both sides documented the DB_PATH guard at the same
  anchor. Kept main's fuller explanation of why setupFiles order is
  load-bearing, followed by the branch's recovery note for the error.
- docs/decisions.md: the branch's note cited a code-layout line and a
  'Raid Detection' section that main's CLAUDE.md trim has since removed.
  Reworded to point at the surviving conventions bullet.
@agrorithms
agrorithms merged commit 2eb14f1 into crawler-as-watchdog-reschedule Jul 30, 2026
2 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.

1 participant