diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..168a1da --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: test + +# Fast, hermetic checks only. The maintenance-cycle harness +# (`npm run e2e:maintenance`) is deliberately excluded: it spawns real +# crawler and scanner processes against a mock Bungie server and runs for +# minutes, which is not what a per-push gate is for. +# +# NOTE ON ARCHITECTURE: this runs on x86_64, while production is ARM64 +# (Oracle A1 Flex). better-sqlite3 is a native module, so CI compiles and +# tests a different binary than production runs. Native-level differences +# in better-sqlite3 will not be caught here. + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + # `ci`, not `install`: the lockfile is the source of truth for + # deployments, and a CI run that silently resolved different versions + # would be testing something production never sees. + - run: npm ci + + - run: npm run lint + + # Nothing else typechecks the repo — `npm test` transpiles without + # checking, and a full `next build` is too slow for this gate. + - run: npx tsc --noEmit + + - run: npm test diff --git a/.gitignore b/.gitignore index 18fbaa3..4a10d02 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,7 @@ localhost.key # Sentry Config File .env.sentry-build-plugin -certificates \ No newline at end of file +certificates + +#handoff docs +docs/handoffs/ \ No newline at end of file diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..d87a1cb --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,56 @@ +# Destiny Farm Finder + +Tracks Destiny 2 raid activity in near real time: which players are raiding right now, and who +has completed what. A set of background crawlers observes the Bungie API and writes to SQLite; +the web app only reads. + +## Language + +**Fireteam**: +A group of players playing a Destiny activity together. The unit users care about — every card on +the active-sessions page is one fireteam. +_Avoid_: party, group, team, squad, lobby + +**Active Session**: +A fireteam observed to be inside a raid right now. Ceases to be active when the crawler confirms +the raid ended, or when the observation goes stale. +_Avoid_: live session, current activity, in-progress raid + +**Roster**: +The players making up a fireteam, as reported by Bungie. May be incomplete — Bungie does not +always disclose every member — so a roster of one usually means limited visibility rather than a +genuine solo run. +_Avoid_: party members, participants, players in session + +**Tracked Player**: +A player the system knows about and will poll for activity. Identified by `Name#Code`; a player +becomes tracked by being discovered in a raid alongside someone already tracked. +_Avoid_: user, account, member + +**Raid**: +Destiny's six-player endgame activity, and the only activity type the leaderboards and the +active-sessions list cover. Other activities are observed but never displayed. +_Avoid_: activity (too broad), instance + +**Full Clear**: +A raid played from the first encounter through the final boss, as opposed to joining at a +checkpoint. Only Bungie's own report that the activity began at the start establishes this — no +other signal is authoritative, and one that looks like it is has been wrong since Bungie stopped +publishing it. +_Avoid_: complete run, fresh run + +**Checkpoint Run**: +A raid entered partway through, at a saved encounter. Observed and stored like any other run, but +never counted toward a leaderboard. The majority of raids we see. +_Avoid_: partial run, CP run + +**Completion**: +One full clear finished by a particular player, counted once per raid instance however many +characters they brought to it. The unit every leaderboard ranks by. A player being present for a +cleared raid is not enough — they must have finished it themselves. +_Avoid_: clear, kill, run + +**Farm**: +Repeatedly replaying a single raid encounter or checkpoint for rewards, rather than progressing +through the raid. The activity the site is named for. +_Avoid_: grind, rerun diff --git a/docs/adr/0001-fireteam-denominated-display-cap.md b/docs/adr/0001-fireteam-denominated-display-cap.md new file mode 100644 index 0000000..8d903a9 --- /dev/null +++ b/docs/adr/0001-fireteam-denominated-display-cap.md @@ -0,0 +1,24 @@ +# Active-session limits are denominated in fireteams, not rows + +`active_sessions` is keyed by `membership_id`, so a single fireteam produces up to six rows — one +per tracked player in it. The read path originally capped those raw rows (`ORDER BY started_at +DESC LIMIT 200`) and deduped into fireteams afterwards, which meant the limit was spent on +duplicates and, because it sorted by start time, evicted the longest-running raids first. In +practice ~1000 live rows rendered ~110 cards and nothing older than about five minutes was ever +visible. We now scan a generous bound of raw rows, dedupe into fireteams, and only then apply the +user-facing cap — which is counted in fireteams. + +## Consequences + +- Two separate limits exist and must not be collapsed into one: `ACTIVE_SESSION_ROW_SCAN_LIMIT` + (raw rows, default 3000) and `ACTIVE_SESSION_DISPLAY_LIMIT` (fireteams, default 600). + Re-introducing a single `LIMIT` in SQL restores the bug. +- The row scan is ordered by `checked_at DESC`, not `started_at DESC`. It is served by + `idx_active_sessions_checked_at`, and if the bound is ever hit it sheds the *stalest* rows — + the ones closest to ageing out — instead of the longest-running raids. +- Dedupe happens before name enrichment, so the display-name lookup covers only the fireteams + actually rendered rather than every row scanned. +- The row bound has roughly 2x headroom over what the crawler can produce: at + `CRAWLER_SESSION_POLLING_LIMIT` rows per cycle across the 900s freshness window, at most ~1500 + rows can be fresh simultaneously. Raising the polling limit materially should prompt a review + of this bound. diff --git a/docs/adr/0002-session-count-reports-true-total.md b/docs/adr/0002-session-count-reports-true-total.md new file mode 100644 index 0000000..91e0556 --- /dev/null +++ b/docs/adr/0002-session-count-reports-true-total.md @@ -0,0 +1,22 @@ +# The active-session count reports the true total, not the number of cards shown + +`countActiveRaidSessions` feeds the nav StatsBar and the OG share cards, while +`/api/active-sessions` feeds the page. These deliberately no longer agree: the count reports every +live fireteam, whereas the list is capped at `ACTIVE_SESSION_DISPLAY_LIMIT`. The headline number +answers "how busy is Destiny right now", which is the question a share card and a stats bar are +actually asking; capping it to whatever happened to fit on screen would understate real activity +and undersell the site. + +This is a deliberate exception to the invariant that `dedupe.ts` was written to protect — that the +count and the list collapse duplicate rows identically. That invariant still holds: both go through +`getDedupedActiveSessions`, so they can never disagree about *what a fireteam is*. Only the cap +differs. + +## Consequences + +- `/api/active-sessions` returns `total` (all live fireteams) alongside `shown` (those under the + cap), so the page can disclose the difference rather than hiding sessions silently. +- A discrepancy between the StatsBar number and the visible card count is expected when the cap + bites, and is not a bug to be "fixed" by capping the count. +- The server logs a warning whenever the cap bites, since the default (600) sits close to observed + prod volume and the gap would otherwise be invisible. diff --git a/docs/adr/0003-tests-run-against-a-real-sqlite-file.md b/docs/adr/0003-tests-run-against-a-real-sqlite-file.md new file mode 100644 index 0000000..788039e --- /dev/null +++ b/docs/adr/0003-tests-run-against-a-real-sqlite-file.md @@ -0,0 +1,45 @@ +# Tests run against a real SQLite file, not `:memory:` + +The test suite gives each test file its own throwaway database in a `mkdtemp` +directory, pointed at by `RAID_TRACKER_DB_PATH`, rather than using SQLite's +`:memory:` database. `:memory:` looks like the obvious choice — faster, no +cleanup — so the reason for not using it needs recording. + +## Why + +SQLite cannot put an in-memory database into WAL mode. `PRAGMA journal_mode = WAL` +returns `memory` for `:memory:` and `wal` for a file, silently: + +``` +:memory: journal_mode = WAL -> 'memory' +file journal_mode = WAL -> 'wal' +``` + +Production runs WAL. The entire justification for testing against a real database +rather than a mock is that it validates the real SQL under real semantics, so +running the suite under a different journal mode gives up most of what the +approach was bought for. + +A temp *directory* rather than just a temp file, because `DATA_DIR` in +`src/lib/maintenance/state.ts` derives from `dirname(RAID_TRACKER_DB_PATH)`. +Relocating the database therefore relocates `maintenance-state.json` for free. +That is not incidental: `getDb()` calls `isDbQuiesceActive()` on *every* +invocation, which reads that file from disk — so a suite pointed at the real data +directory would throw `DatabaseMaintenanceError` from every test if it happened +to run while a maintenance vacuum was in progress. + +The path is set in a Vitest `setupFile` rather than inside a helper, because +`DB_PATH` is a module-level constant resolved at import time. Setting it before +the test file's own imports run is what lets test files use ordinary static +imports instead of `await import()` throughout. + +## Consequences + +- Test databases cost a `mkdtemp` plus `initializeSchema()` per test file — + roughly 5–15 ms on tmpfs. At this suite's size that is a few milliseconds + overall, well below the value of matching production semantics. +- Temp directories leak into the system temp dir if a test process is killed + before `afterAll` runs. Harmless, and the OS clears them. +- The schema under test is the production schema by construction: `getDb()` runs + `initializeSchema()`, including the `ended_at` migration guard and the Phase 3 + indexes. There is no second schema definition that can drift. diff --git a/docs/adr/0004-mock-only-at-the-network-boundary.md b/docs/adr/0004-mock-only-at-the-network-boundary.md new file mode 100644 index 0000000..d3d0d42 --- /dev/null +++ b/docs/adr/0004-mock-only-at-the-network-boundary.md @@ -0,0 +1,41 @@ +# Mock only at the network boundary + +Tests stub `fetch` and nothing else. There is no `vi.mock()` of any module in +`src/`, and the database is real rather than faked. This is a deliberate +constraint, not an oversight — mocking our own modules is the default habit in +most test suites, so the absence needs explaining before someone helpfully adds it. + +## Why + +Leaderboard integrity is the product. The failure that matters here is not a +crash but a silently wrong row set, and that failure lives in exactly the places +mocking would erase: the SQL, and the shape of what Bungie actually returns. + +- **Mocking `@/lib/db/queries` would test the mock.** A test asserting that + `getLeaderboard` returns what the mock was told to return proves nothing about + whether the SQL selects the right runs. `better-sqlite3` opens a database in + about a millisecond, so a real one is both faster than the mock scaffolding and + actually load-bearing. +- **Seeding goes through `insertFullPGCR`, not raw INSERTs.** All four production + ingestion sources funnel through that function, and it is where `ended_at` is + derived and `players.last_seen_at` advanced. Raw inserts would let tests build + rows that production could never produce, so the tests would pass against + impossible data. +- **`fetch` is the one boundary worth faking.** It is genuinely external, genuinely + slow, rate-limited, and returns different data every day. Everything on our side + of it is ours to verify. + +A setup file (`tests/setup/no-network.ts`) replaces `fetch` with a thrower before +every test, so a test that reaches the real internet fails loudly instead of +quietly burning Bungie API quota and going flaky against live data. + +## Consequences + +- Test databases are real; see ADR 0003 for why they are files rather than + `:memory:`. +- Tests that need a specific Bungie response stub `fetch` explicitly. The guard + records itself as the original, so the block is restored automatically for the + next test with no per-file cleanup. +- Fixtures are captured from the live API rather than hand-authored, so they + encode Bungie's real quirks instead of our beliefs about them. Builders in + `tests/helpers/` cover permutations where only one field needs to vary. diff --git a/docs/decisions.md b/docs/decisions.md new file mode 100644 index 0000000..7c09c4f --- /dev/null +++ b/docs/decisions.md @@ -0,0 +1,239 @@ +# Decisions + +Running log of decisions that aren't big enough for an ADR, or that live partly outside the +codebase (infrastructure, Cloudflare config) where a code comment can't reach them. + +--- + +## 2026-07-26 — HTTP caching for the real-time API routes + +### Symptom + +After deploying the fireteam-denominated display cap (`f760d58`), the browser stopped showing +live numbers: + +- `/api/live-stats` and `/api/active-sessions?limit=600` both served as `200 OK (from disk cache)`. +- The StatsBar's full-clear count and active-fireteam count sat unchanged for roughly an hour. +- `/api/active-sessions?limit=600` fired **twice** per poll — one from disk cache, one a real + 200 or 304. + +### The display-cap change was not the cause + +Confirmed, not assumed: + +``` +git show f760d58 -- src/app/api/active-sessions/route.ts \ + src/app/api/live-stats/route.ts \ + src/lib/http/cache.ts + | grep -E '^[-+].*(withCache|withNoStore|Cache|max-age|dynamic)' +→ no matches +``` + +`src/lib/http/cache.ts` had not been touched since 2026-05-29 (`51414df`). The +`withCache(…, 10, 30)` on active-sessions and `withCache(…, 15, 30)` on live-stats predate that +work by two months. The change altered *which* numbers those endpoints return, never how they are +cached. + +The origin was also verified healthy — three cache-busted fetches 18s apart returned 408, 412 and +411 fireteams with an advancing `timestamp`. Nothing was frozen server-side. + +### Root cause + +`cacheControl()` emits `public, max-age=0, s-maxage=N, stale-while-revalidate=M`. Nothing in the +repo has ever emitted a non-zero `max-age`. The value reaching the browser was being rewritten by +Cloudflare, and the three endpoints differed in a way that pinned it exactly: + +| endpoint | Cloudflare cache rule | `max-age` at the browser | `cf-cache-status` | +|---|---|---|---| +| `/api/status` | none | `0` — origin value, untouched | `DYNAMIC` | +| `/api/active-sessions` | Browser TTL: override, 1s | `1` | `HIT` / `EXPIRED` | +| `/api/live-stats` | Browser TTL: **unset** | `14400` | `HIT` / `EXPIRED` | + +Leaving Browser TTL unset in a cache rule does **not** pass the origin header through. It falls +back to the zone-level Browser Cache TTL (Caching → Configuration), whose default is 4 hours — +hence `max-age=14400`, hence a browser entitled to serve the stat bar from disk for four hours +without asking. `/api/status`, which has no cache rule and is therefore not eligible for cache, +proves the contrast: its `max-age=0` arrives unmodified. + +### Decision: fix it at Cloudflare, not at the origin + +~15s of caching is wanted, not merely tolerated — it shields the SQLite dedupe pass from repeated +polling across two PM2 workers. Cloudflare is the correct layer to express "cache 15s at the edge, +never in the browser", because it can separate edge TTL from browser TTL. The origin header cannot. + +**Applied:** Browser TTL → *override origin, 1 second* on the `/api/live-stats` cache rule, +mirroring the rule already on `/api/active-sessions`. Verified afterwards: + +``` +/api/live-stats cache-control: public, max-age=1, s-maxage=15, stale-while-revalidate=30 +/api/active-sessions cache-control: public, max-age=1, s-maxage=10, stale-while-revalidate=30 +``` + +The four-hour freeze is gone. + +### Trap: do not "fix" this with `withNoStore` + +The obvious-looking origin fix — switching both routes to `withNoStore` — was written, tested +against this analysis, and **reverted deliberately**. The `/api/live-stats` rule uses Edge TTL +*"use cache-control header if present, bypass cache if not"*. A `no-store` response makes +Cloudflare **bypass the edge cache entirely**, destroying the 15s edge caching the rule exists to +provide. The origin header and the cache rule have to be designed together; changing one in +isolation fights the other. + +`export const dynamic = 'force-dynamic'` was reverted for a different reason: it was never the +problem. Next.js 15+ does not cache GET route handlers by default, and prod was measurably dynamic +already. Harmless, but it would have been misleading documentation of a cause that wasn't real. + +### Still outstanding: the double fetch + +Cloudflare's Browser TTL override rewrites `max-age` but passes `stale-while-revalidate` through +untouched: + +``` +cache-control: public, max-age=1, s-maxage=10, stale-while-revalidate=30 + ^^^^^^^^^ Cloudflare ^^^^^^^^^^^^^^^^^^^^^^ origin, unmodified +``` + +Chrome implements SWR. Past `max-age=1` the copy is stale, so each 30s poll hands the page the +stale disk copy *immediately* and fires a background revalidation — the two network rows, and data +rendered up to ~31s old. This is the origin's `stale-while-revalidate=30` doing exactly what it +says; the directive is simply wrong for an endpoint that is polled on a fixed interval. + +**Recommended origin change (not yet made):** keep `withCache`, drop the +`stale-while-revalidate` term for the two real-time endpoints. Resulting behaviour: + +- Browser: `max-age` pinned to 1s by the cache rule, no SWR → every poll is a real conditional + request. One network row. +- Cloudflare edge: absorbs those polls at its configured TTL, one origin query per ~15s. +- Origin: unchanged cost. + +Open question before doing it: whether to drop SWR globally from `cacheControl()` or only for +these two. SWR is defensible on the slower-moving endpoints, so a second helper +(`withCacheNoStale`, or an optional third argument) is probably better than changing the shared +one. + +### Latent exposure elsewhere + +These still send `public` to the browser and would freeze the same way if a cache rule without a +Browser TTL were ever added for them: + +- `src/app/api/leaderboard/route.ts:41` +- `src/app/api/players/[membershipType]/[membershipId]/route.ts:176` +- `src/app/api/raids/route.ts:14` +- `src/app/api/status/route.ts:36` (healthy path only) + +Slower-moving data, so a stale read is less visible — but a player page stuck for four hours is the +same failure. **Durable mitigation:** set the zone-level Browser Cache TTL to *Respect Existing +Headers*. While it stays at the 4-hour default, every future cache rule written without an explicit +Browser TTL inherits this bug. + +### Consequences + +- Cloudflare cache rules are load-bearing configuration for this app, and they are not in the repo. + A rule added or edited without a Browser TTL reintroduces a multi-hour client-side freeze that + looks exactly like an origin bug and cannot be reproduced locally. +- When a "stale data" report arrives, check `cf-cache-status` and the `cache-control` actually + received before reading any application code. `curl -sS -D - -o /dev/null ` against prod + settles origin-vs-edge-vs-browser in one request; a cache-busted fetch confirms the origin + independently. +- `s-maxage` in `cacheControl()` is only honoured where a cache rule makes the path eligible. + Endpoints with no rule (`/api/status`) are `DYNAMIC` and their `s-maxage` is inert. + +--- + +## 2026-07-26 — Testing framework: what got built, and what the brief got wrong + +The repo had no unit test framework. Vitest is now wired up with tests covering `processPGCR`, +the leaderboard query, `ended_at` derivation, Bungie error handling, and the rate limiter. Full +plan of record: `docs/testing-framework-plan.md`. Strategy decisions: ADR 0003 (real SQLite files, +not `:memory:`) and ADR 0004 (mock only at the network boundary). + +Recorded here are the findings that changed the shape of the work, and the defects found along the +way that were **not** fixed. + +### `ProcessedPGCR.isFullClear` is dead code, and would be wrong if used + +`src/lib/crawler/pgcr.ts:36` derives `isFullClear` from a three-way `||`. Nothing reads it. +`fetchAndStorePGCR` persists Bungie's raw `activityWasStartedFromBeginning`, and every leaderboard +filters on that column (`leaderboard-cache.ts:175`, `queries.ts:760/781/820/855`). + +It is also incorrect. Bungie now reports `startingPhaseIndex: 0` on every PGCR — verified against +live API captures, including confirmed checkpoint runs where `activityWasStartedFromBeginning` is +`false`. The field is present but inert. So the `startingPhaseIndex === 0` branch fires +unconditionally and `isFullClear` is `true` for 100% of runs. Of those, 568,648 have +`activity_was_started_from_beginning = 0`, i.e. they are checkpoint runs. Wiring this field into +the writer would inflate every full-clear leaderboard by roughly 2.2×. + +**Action:** delete the field in a future change. Deliberately left untested — pinning dead +behaviour would only make removal harder. The reasoning is duplicated in +`src/lib/crawler/pgcr.test.ts` so whoever finds it there does not have to come looking here. + +### `formatDisplayName` drops the `#Code` when the code is zero + +`src/lib/cache/leaderboard-cache.ts:135` guards with +`if (entry.bungieGlobalDisplayName && entry.bungieGlobalDisplayNameCode)`. A code of `0` is falsy, +so the branch is skipped and the player renders as a bare name. CLAUDE.md calls the full +`Name#Code` form load-bearing and notes partial names were a real bug before. + +Pinned as current behaviour in `tests/db/leaderboard.test.ts`, labelled `BUG:` — not endorsed. Not +fixed here because the brief said to report defects rather than fix them, and because whether a +`#0000` code is reachable in Bungie's namespace was not established. + +### `getDb()` hits the filesystem on every call + +`isDbQuiesceActive()` reads `data/maintenance-state.json` from disk on **every** `getDb()` +invocation, not just on open. Not a correctness bug — but it is why test isolation has to relocate +`DATA_DIR` and not merely the database file, since a suite running during a maintenance vacuum +would otherwise fail every test with `DatabaseMaintenanceError`. + +### CLAUDE.md's raid-detection description is inaccurate + +CLAUDE.md states raid detection "matches `activityHash` against the manifest cache +(`data/manifest-cache.json`)". Nothing reads that file. `RAID_DEFINITIONS` is a hardcoded literal +in `src/lib/bungie/manifest.ts:16`; `setup-manifest` only *writes* the cache, for human review +before hand-editing the literal. Convenient for tests — raid detection is fully hermetic — but the +documentation implies a runtime dependency that does not exist. + +### The `ended_at` cutover was already complete + +The brief described it as in-flight and asked for a parity safety net across ~10 SQL sites. It +shipped in `610408e`; zero `run_durations` references remain in `src/`. That phase was retargeted +to testing `computeActivityDurationSeconds` — the tiered derivation that replaced the CTE — which +is where a wrong row set would now come from. + +### Zero-completion runs are the dominant shape + +456,009 of 827,076 stored PGCRs (55%) have no player with `completed = 1`. Not a defect, but worth +knowing before reasoning about any query that joins through completions: the "no completions" case +is the common path, not an edge case. + +### Addendum, same day — what capturing real fixtures corrected + +Three claims above were inferred from the database and turned out to be wrong at the source. The +conclusions held; the mechanisms did not. + +**`startingPhaseIndex` is sent, and is always `0`.** Not absent, as first written. It is present on +every captured PGCR including three confirmed checkpoint runs +(`activityWasStartedFromBeginning: false`). The database showed `0` everywhere because the writer +coerces with `|| 0`, which had flattened the evidence. So `isFullClear`'s `=== 0` branch fires +rather than its `=== undefined` branch — the field is still `true` for 100% of runs, so nothing +about the defect changes. + +**A player can appear several times in one report.** `pgcr-multi-character-garden.json` has six +entries belonging to **two** people, three characters each. `pgcr_players` is keyed +`(instance_id, membership_id)` with `INSERT OR IGNORE`, so only each player's *first* entry is +stored: that player's `time_played_seconds` records 981s when their longest character played 1494s. +`kills`, `deaths`, `assists` and `time_played_seconds` are written but **read nowhere in `src/`**, +so this is latent rather than user-visible. Duration derivation is unaffected — it runs on the +in-memory entries before the dedupe. + +**Bungie can withhold identity entirely.** `pgcr-missing-bungie-name.json` has nineteen entries, +every one arriving as `isPublic: false`, `membershipType: 0`, with **no `displayName` and no +`bungieGlobalDisplayName`**. This is not "the global name is missing so fall back to the platform +name" — there is no fallback left, and `formatDisplayName` ends up rendering a raw membership id. +Ingestion handles it correctly: all nineteen rows store with NULL names rather than being rejected, +which is right, since the run itself is real. + +Note also that `types.ts` declares `UserInfoCard.displayName` and +`DestinyPostGameCarnageReportData.startingPhaseIndex` as required, and both are optional in +practice. The type is more confident than the API. diff --git a/package-lock.json b/package-lock.json index 33a207c..b2ba0c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,13 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitest/coverage-v8": "^4.1.10", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", "tsx": "^4.21.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.10" } }, "node_modules/@alloc/quick-lru": { @@ -265,22 +267,32 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, "dependencies": { @@ -288,9 +300,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -783,15 +795,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -824,9 +836,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", - "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, "license": "MIT", "dependencies": { @@ -836,8 +848,8 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -848,9 +860,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", - "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { @@ -1758,6 +1770,317 @@ "node": ">=14" } }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-commonjs": { "version": "28.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", @@ -2662,6 +2985,13 @@ "webpack": ">=5.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2943,9 +3273,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -2963,15 +3293,33 @@ "@types/node": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" } }, "node_modules/@types/eslint-scope": { @@ -3232,16 +3580,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -3597,6 +3945,160 @@ "win32" ] }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -3829,9 +4331,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -4080,6 +4582,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -4087,6 +4599,35 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4207,9 +4748,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -4374,6 +4915,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4837,8 +5388,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -4965,25 +5515,25 @@ } }, "node_modules/eslint": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", - "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.3", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -5002,7 +5552,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -5417,6 +5967,16 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5468,9 +6028,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "funding": [ { "type": "github", @@ -5782,15 +6342,15 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { @@ -5967,6 +6527,13 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -6516,6 +7083,45 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -6582,9 +7188,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -7044,6 +7650,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7170,9 +7817,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -7496,6 +8143,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7635,6 +8296,13 @@ "node": "20 || >=22" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7665,9 +8333,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "dev": true, "funding": [ { @@ -7685,7 +8353,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7983,6 +8651,40 @@ "node": ">=0.10.0" } }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, "node_modules/rollup": { "version": "4.62.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", @@ -8404,6 +9106,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -8486,6 +9195,13 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stacktrace-parser": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", @@ -8498,6 +9214,13 @@ "node": ">=6" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -8807,15 +9530,32 @@ } } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -8855,6 +9595,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9194,15 +9944,482 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" }, "engines": { "node": ">=10.13.0" @@ -9411,6 +10628,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 2f67e92..25bb59f 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,11 @@ "verify-phase3-cutover": "npx tsx scripts/verify-phase3-cutover.ts", "drop-phase3-orphan-indexes": "npx tsx scripts/drop-phase3-orphan-indexes.ts", "cleanup-types": "npx tsx scripts/cleanup/cleanup-membership-types.ts", - "test-maintenance-cycle": "npx tsx scripts/test-maintenance-cycle.ts", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "e2e:maintenance": "npx tsx scripts/test-maintenance-cycle.ts", + "capture-fixtures": "npx tsx scripts/capture-pgcr-fixture.ts", "find-private": "npx tsx scripts/find-private-session-players.ts" }, "dependencies": { @@ -43,10 +47,12 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitest/coverage-v8": "^4.1.10", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", "tsx": "^4.21.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.10" } } diff --git a/scripts/capture-pgcr-fixture.ts b/scripts/capture-pgcr-fixture.ts new file mode 100644 index 0000000..bf58903 --- /dev/null +++ b/scripts/capture-pgcr-fixture.ts @@ -0,0 +1,193 @@ +import 'dotenv/config'; +import fs from 'fs'; +import path from 'path'; +import { BungieEndpoints } from '../src/lib/bungie/endpoints'; +import { isRaidActivityHash } from '../src/lib/bungie/manifest'; +import { readActivityDurationSeconds } from '../src/lib/bungie/pgcr-stats'; +import type { DestinyPostGameCarnageReportData } from '../src/lib/bungie/types'; + +/** + * Captures real PGCR JSON from Bungie into tests/fixtures/. + * + * Fixtures are captured rather than hand-authored so they encode Bungie's actual + * quirks — absent fields, entry counts above six, durations that disagree with + * per-player time. A synthetic PGCR only encodes our beliefs about the API, which + * is exactly what the fixtures exist to check. + * + * PGCR data is public; the captured files are committed as-is. + * + * The instance IDs below were selected by querying the local database for runs + * exhibiting each property, so every case is a real observed run rather than a + * hypothetical. Run with: npm run capture-fixtures + */ + +const FIXTURE_DIR = path.join(process.cwd(), 'tests', 'fixtures'); + +interface Target { + file: string; + instanceId: string; + why: string; +} + +const TARGETS: Target[] = [ + { + file: 'pgcr-fullclear-salvations-edge.json', + instanceId: '17091392013', + why: 'Baseline happy path: six players, started from the beginning, completed.', + }, + { + file: 'pgcr-checkpoint-root-of-nightmares.json', + instanceId: '17091462346', + why: 'Checkpoint run — activityWasStartedFromBeginning is false. Must be excluded from full-clear leaderboards.', + }, + { + file: 'pgcr-zero-completions-vault-of-glass.json', + instanceId: '17091467640', + why: 'No entry has completed = 1. The dominant shape in the table (55% of stored PGCRs).', + }, + { + file: 'pgcr-partial-completion-last-wish.json', + instanceId: '17091283535', + why: 'Some entries completed, some did not. `completed` is per-entry; ANY completion counts.', + }, + { + file: 'pgcr-multi-character-garden.json', + instanceId: '17091200569', + why: 'Six entries but only two distinct players — three characters each. Also a mild Tier 1 vs Tier 2 duration gap (2069s vs 2037s).', + }, + { + file: 'pgcr-absurd-duration-crotas-end.json', + instanceId: '17091316490', + why: 'Reports a 27384s (7.6h) activity duration for a run where nobody played past 1093s. The megalobby corruption that FUTURE_ENDED_SKEW_SECONDS exists to reject.', + }, + { + file: 'pgcr-missing-bungie-name.json', + instanceId: '16975643976', + why: 'At least one entry lacks bungieGlobalDisplayName. Player extraction must tolerate it.', + }, +]; + +// Instance IDs are broadly sequential, so a raid's neighbours are almost always +// other activity types. We probe forward from a known raid until isRaidActivityHash +// rejects one, giving a genuine non-raid PGCR without needing a curated id. +// Safely within Number.MAX_SAFE_INTEGER (~9e15), so plain arithmetic is fine here. +const NON_RAID_PROBE_START = 17091392014; +const NON_RAID_PROBE_ATTEMPTS = 12; +const NON_RAID_FILE = 'pgcr-non-raid.json'; + +function requireApiKey(): string { + const key = process.env.BUNGIE_API_KEY; + if (!key) { + console.error('[ERROR] BUNGIE_API_KEY is not set. Add it to .env and re-run.'); + process.exit(1); + } + return key; +} + +async function fetchPGCR( + instanceId: string, + apiKey: string +): Promise { + const response = await fetch(BungieEndpoints.getPGCR(instanceId), { + headers: { 'X-API-Key': apiKey }, + signal: AbortSignal.timeout(30_000), + }); + + if (!response.ok) { + console.error(` [ERROR] HTTP ${response.status} for instance ${instanceId}`); + return null; + } + + const body = await response.json(); + if (body.ErrorCode !== 1) { + console.error(` [ERROR] Bungie ErrorCode ${body.ErrorCode} (${body.ErrorStatus}) for ${instanceId}`); + return null; + } + + return body.Response as DestinyPostGameCarnageReportData; +} + +/** Reports the fields each fixture is supposed to demonstrate, so a capture that + * silently fails to exhibit its property is visible rather than assumed. */ +function describe(pgcr: DestinyPostGameCarnageReportData): string { + const hash = pgcr.activityDetails.directorActivityHash || pgcr.activityDetails.referenceId; + const entries = pgcr.entries || []; + const completedCount = entries.filter((e) => e.values?.completed?.basic?.value === 1).length; + const maxTimePlayed = entries.reduce( + (max, e) => Math.max(max, e.values?.timePlayedSeconds?.basic?.value || 0), + 0 + ); + // Uses the production reader so the reported value is the one the writer + // would actually see, not a second interpretation of the same JSON. + const duration = readActivityDurationSeconds(entries); + const missingNames = entries.filter((e) => !e.player?.destinyUserInfo?.bungieGlobalDisplayName).length; + + return [ + `raid=${isRaidActivityHash(hash) ? 'yes' : 'NO '}`, + `hash=${hash}`, + `entries=${entries.length}`, + `completed=${completedCount}`, + `fromBeginning=${pgcr.activityWasStartedFromBeginning}`, + `startingPhaseIndex=${pgcr.startingPhaseIndex}`, + `durationSec=${duration ?? 'ABSENT'}`.padEnd(20), + `maxTimePlayed=${maxTimePlayed}`, + `missingNames=${missingNames}`, + ].join(' '); +} + +async function capture(target: Target, apiKey: string): Promise { + console.log(`\n${target.file} (instance ${target.instanceId})`); + const pgcr = await fetchPGCR(target.instanceId, apiKey); + if (!pgcr) return false; + + fs.writeFileSync(path.join(FIXTURE_DIR, target.file), JSON.stringify(pgcr, null, 2)); + console.log(` ${describe(pgcr)}`); + return true; +} + +async function captureNonRaid(apiKey: string): Promise { + console.log(`\n${NON_RAID_FILE} (probing forward from ${NON_RAID_PROBE_START})`); + + for (let i = 0; i < NON_RAID_PROBE_ATTEMPTS; i++) { + const instanceId = String(NON_RAID_PROBE_START + i); + const pgcr = await fetchPGCR(instanceId, apiKey); + if (!pgcr) continue; + + const hash = pgcr.activityDetails.directorActivityHash || pgcr.activityDetails.referenceId; + if (isRaidActivityHash(hash)) { + console.log(` ${instanceId} is a raid — probing next`); + continue; + } + + fs.writeFileSync(path.join(FIXTURE_DIR, NON_RAID_FILE), JSON.stringify(pgcr, null, 2)); + console.log(` captured instance ${instanceId}`); + console.log(` ${describe(pgcr)}`); + return true; + } + + console.error(` [ERROR] No non-raid activity found in ${NON_RAID_PROBE_ATTEMPTS} attempts.`); + return false; +} + +async function main() { + const apiKey = requireApiKey(); + fs.mkdirSync(FIXTURE_DIR, { recursive: true }); + + console.log('Capturing PGCR fixtures from Bungie into tests/fixtures/'); + console.log('Check the reported fields below against each fixture\'s stated purpose.'); + + let ok = 0; + for (const target of TARGETS) { + if (await capture(target, apiKey)) ok++; + } + if (await captureNonRaid(apiKey)) ok++; + + const total = TARGETS.length + 1; + console.log(`\n${ok}/${total} fixtures captured.`); + if (ok < total) { + console.log('Some captures failed — the missing cases will need a synthetic fixture instead.'); + process.exit(1); + } +} + +main(); diff --git a/src/lib/bungie/client.test.ts b/src/lib/bungie/client.test.ts new file mode 100644 index 0000000..6a7a135 --- /dev/null +++ b/src/lib/bungie/client.test.ts @@ -0,0 +1,213 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { BungieAPIError, BungieClient } from './client'; + +/** + * Mocked at the fetch boundary only — never at our own module boundaries. + * Stubbing `getPGCR` would test the stub; stubbing `fetch` tests the response + * handling that actually decides whether a raid gets ingested or dropped. + * + * `request()` does no retrying. What it does is classify a failure and, when + * Bungie signals throttling, pause the shared per-key rate limiter — because the + * throttle applies to the key, not to the one request that happened to see it. + * These tests cover that classification and that dispatch. + */ + +const PGCR_BODY = { + Response: { activityDetails: { instanceId: '123' } }, + ErrorCode: 1, + ErrorStatus: 'Success', + Message: 'Ok', + ThrottleSeconds: 0, +}; + +function jsonResponse(body: unknown, init?: ResponseInit): Response { + return new Response(JSON.stringify(body), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + ...init, + }); +} + +/** High RPS so ordinary spacing never interferes with what a test is asserting. */ +function makeClient(): BungieClient { + return new BungieClient('test-api-key', 1000); +} + +afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); +}); + +describe('successful responses', () => { + it('returns the parsed payload', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse(PGCR_BODY))); + + const result = await makeClient().getPGCR('123'); + + expect(result.Response.activityDetails.instanceId).toBe('123'); + }); + + it('authenticates with the API key header', async () => { + const fetchMock = vi.fn().mockResolvedValue(jsonResponse(PGCR_BODY)); + vi.stubGlobal('fetch', fetchMock); + + await makeClient().getPGCR('123'); + + const [, init] = fetchMock.mock.calls[0]; + expect(init.headers['X-API-Key']).toBe('test-api-key'); + }); + + it('requests the PGCR from the stats host', async () => { + // PGCRs live on stats.bungie.net, not www.bungie.net. Getting this wrong + // fails every ingestion path at once. + const fetchMock = vi.fn().mockResolvedValue(jsonResponse(PGCR_BODY)); + vi.stubGlobal('fetch', fetchMock); + + await makeClient().getPGCR('123'); + + expect(String(fetchMock.mock.calls[0][0])).toContain( + 'stats.bungie.net/Platform/Destiny2/Stats/PostGameCarnageReport/123/' + ); + }); +}); + +describe('Bungie-level errors', () => { + it('raises a typed error carrying Bungie\'s own code and status', async () => { + // The type matters: isBungieSystemDisabledError is instanceof-based, so a + // generic Error here would stop the crawler ever pausing for maintenance. + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + jsonResponse({ + Response: null, + ErrorCode: 5, + ErrorStatus: 'SystemDisabled', + Message: 'This system is temporarily disabled.', + ThrottleSeconds: 0, + }) + ) + ); + + const error = await makeClient().getPGCR('123').catch((e) => e); + + expect(error).toBeInstanceOf(BungieAPIError); + expect(error.errorCode).toBe(5); + expect(error.errorStatus).toBe('SystemDisabled'); + }); + + it('raises an untyped error for a plain HTTP failure', async () => { + // A 5xx is not a Bungie-level error — the body may not even be JSON — so it + // must not masquerade as one. + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(new Response('gateway timeout', { status: 504 })) + ); + + const error = await makeClient().getPGCR('123').catch((e) => e); + + expect(error).toBeInstanceOf(Error); + expect(error).not.toBeInstanceOf(BungieAPIError); + expect(error.message).toContain('504'); + }); + + it('survives a non-JSON error body without masking the failure', async () => { + // Cloudflare serves HTML error pages. The 1672 inspection parses the body, + // so it must swallow the parse failure and still throw. + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(new Response('

502

', { status: 502 })) + ); + + await expect(makeClient().getPGCR('123')).rejects.toThrow(/502/); + }); +}); + +describe('throttle handling pauses the whole key', () => { + it('defers the next request after Bungie reports a throttle', async () => { + vi.useFakeTimers(); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(jsonResponse({ ...PGCR_BODY, ThrottleSeconds: 4 })) + ); + const client = makeClient(); + + await client.getPGCR('123'); + + const second = trackSettled(client.getPGCR('456')); + await vi.advanceTimersByTimeAsync(3999); + expect(second.settled).toBe(false); + + await vi.advanceTimersByTimeAsync(20); + expect(second.settled).toBe(true); + }); + + it('honours Retry-After on an HTTP 429', async () => { + vi.useFakeTimers(); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + new Response('rate limited', { status: 429, headers: { 'Retry-After': '7' } }) + ) + ); + const client = makeClient(); + + await client.getPGCR('123').catch(() => {}); + + const second = trackSettled(client.getPGCR('456').catch(() => {})); + await vi.advanceTimersByTimeAsync(6999); + expect(second.settled).toBe(false); + + await vi.advanceTimersByTimeAsync(20); + expect(second.settled).toBe(true); + }); + + it('falls back to a five second pause when 429 omits Retry-After', async () => { + vi.useFakeTimers(); + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('rate limited', { status: 429 }))); + const client = makeClient(); + + await client.getPGCR('123').catch(() => {}); + + const second = trackSettled(client.getPGCR('456').catch(() => {})); + await vi.advanceTimersByTimeAsync(4999); + expect(second.settled).toBe(false); + + await vi.advanceTimersByTimeAsync(20); + expect(second.settled).toBe(true); + }); + + it('imposes its own backoff for a game-server throttle, which reports no duration', async () => { + // ErrorCode 1672 arrives as a 503 with ThrottleSeconds: 0. Bungie tells us + // to back off without saying how long, so retrying immediately would just + // earn another 503. Default self-imposed pause is 2s. + vi.useFakeTimers(); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ ErrorCode: 1672, ErrorStatus: 'DestinyThrottledByGameServer', ThrottleSeconds: 0 }), + { status: 503 } + ) + ) + ); + const client = makeClient(); + + await client.getPGCR('123').catch(() => {}); + + const second = trackSettled(client.getPGCR('456').catch(() => {})); + await vi.advanceTimersByTimeAsync(1999); + expect(second.settled).toBe(false); + + await vi.advanceTimersByTimeAsync(20); + expect(second.settled).toBe(true); + }); +}); + +function trackSettled(promise: Promise): { settled: boolean } { + const state = { settled: false }; + promise.then( + () => { state.settled = true; }, + () => { state.settled = true; } + ); + return state; +} diff --git a/src/lib/bungie/maintenance.test.ts b/src/lib/bungie/maintenance.test.ts new file mode 100644 index 0000000..bba0c74 --- /dev/null +++ b/src/lib/bungie/maintenance.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { BungieAPIError } from './client'; +import { isBungieSystemDisabledError } from './maintenance'; + +/** + * This predicate decides whether the crawler and scanner pause for Bungie's + * weekly maintenance window or keep hammering a dead API. The cost of a false + * negative is thousands of doomed requests; the cost of a false positive is a + * needless multi-minute pause during an ordinary blip. Both matter, so the + * negative cases below are as important as the positive one. + */ +describe('isBungieSystemDisabledError', () => { + it('recognises Bungie signalling that the platform is down for maintenance', () => { + const error = new BungieAPIError(5, 'SystemDisabled', 'This system is temporarily disabled.'); + + expect(isBungieSystemDisabledError(error)).toBe(true); + }); + + it('ignores a different Bungie-level error', () => { + // A privacy restriction is a per-player condition, not a platform outage. + // Pausing the whole crawler for one private profile would be a real bug. + const error = new BungieAPIError(1665, 'DestinyPrivacyRestriction', 'Profile is private.'); + + expect(isBungieSystemDisabledError(error)).toBe(false); + }); + + it('ignores a plain HTTP failure', () => { + // request() throws a generic Error for non-2xx responses, so a 500 never + // reaches here as a BungieAPIError. Bungie 5xx storms are common and must + // not be mistaken for scheduled maintenance. + const error = new Error('Bungie API error 500: Internal Server Error'); + + expect(isBungieSystemDisabledError(error)).toBe(false); + }); + + it('ignores a request timeout', () => { + const error = new DOMException('The operation was aborted due to timeout', 'TimeoutError'); + + expect(isBungieSystemDisabledError(error)).toBe(false); + }); + + it('ignores an error-shaped object that merely claims the right status', () => { + // The check is instanceof-based, so a plain object carrying the same + // fields is deliberately not enough. + const impostor = { name: 'BungieAPIError', errorCode: 5, errorStatus: 'SystemDisabled' }; + + expect(isBungieSystemDisabledError(impostor)).toBe(false); + }); + + it('ignores non-errors entirely', () => { + expect(isBungieSystemDisabledError(null)).toBe(false); + expect(isBungieSystemDisabledError(undefined)).toBe(false); + expect(isBungieSystemDisabledError('SystemDisabled')).toBe(false); + }); +}); diff --git a/src/lib/crawler/pgcr.test.ts b/src/lib/crawler/pgcr.test.ts new file mode 100644 index 0000000..cdad575 --- /dev/null +++ b/src/lib/crawler/pgcr.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from 'vitest'; +import { processPGCR } from './pgcr'; +import { + NON_RAID_HASH, + RAID_HASH, + buildEntry, + buildFireteam, + buildPGCR, +} from '../../../tests/helpers/pgcr-builder'; + +/** + * `processPGCR` is pure — JSON in, object out — so it needs no setup and is the + * right first target. It is the funnel every ingested raid passes through. + * + * NOT COVERED HERE, DELIBERATELY: `ProcessedPGCR.isFullClear`. That field is + * computed and returned but never read by anything — `fetchAndStorePGCR` + * persists Bungie's raw `activityWasStartedFromBeginning` instead, and every + * leaderboard filters on that column. The derivation is also wrong: Bungie now + * reports `startingPhaseIndex: 0` on every PGCR — including checkpoint runs, as + * the captured fixtures show — so the `=== 0` branch fires unconditionally and + * reports every run as a full clear. It is dead code slated for removal, so pinning its behaviour here would + * only make that removal harder. The signal that actually decides leaderboard + * membership is covered in tests/db/full-clear-flag.test.ts. + * See docs/decisions.md. + */ + +describe('run completion', () => { + it('counts the run as completed when a single member finished', () => { + // `completed` is per-entry and the check is an .some(): five people can + // leave and the run still counts, which is correct — the raid was cleared. + const pgcr = buildPGCR({ entries: buildFireteam({ size: 6, completions: 1 }) }); + + expect(processPGCR(pgcr).completed).toBe(true); + }); + + it('counts the run as not completed when nobody finished', () => { + // 55% of stored PGCRs land here, so this is the common path, not the edge. + const pgcr = buildPGCR({ entries: buildFireteam({ size: 6, completions: 0 }) }); + + expect(processPGCR(pgcr).completed).toBe(false); + }); + + it('counts the run as not completed when it has no entries at all', () => { + expect(processPGCR(buildPGCR({ entries: [] })).completed).toBe(false); + }); + + it('treats a missing completion stat as not completed', () => { + // Old PGCRs omit stats rather than reporting zero. + const entry = buildEntry(); + delete entry.values.completed; + + expect(processPGCR(buildPGCR({ entries: [entry] })).completed).toBe(false); + }); +}); + +describe('activity identification', () => { + it('prefers the director activity hash', () => { + const pgcr = buildPGCR({ activityHash: RAID_HASH, referenceId: NON_RAID_HASH }); + + expect(processPGCR(pgcr).activityHash).toBe(RAID_HASH); + }); + + it('falls back to the reference id when the director hash is absent', () => { + // Bungie reports directorActivityHash as 0 for some older activities, and + // `||` treats that as absent — which is the intended behaviour here. + const pgcr = buildPGCR({ activityHash: 0, referenceId: RAID_HASH }); + + expect(processPGCR(pgcr).activityHash).toBe(RAID_HASH); + }); + + it('resolves a known raid to its key', () => { + const pgcr = buildPGCR({ activityHash: RAID_HASH }); + + expect(processPGCR(pgcr).raidKey).toBe('salvations_edge'); + }); + + it('leaves the raid key unset for an activity that is not a raid', () => { + // The crawler drops non-raids on this basis, so a wrong answer here either + // floods the database with strikes or silently discards real raids. + const pgcr = buildPGCR({ activityHash: NON_RAID_HASH }); + + expect(processPGCR(pgcr).raidKey).toBeUndefined(); + }); + + it('carries the instance id through unchanged', () => { + // Instance ids exceed 2^31 and are handled as strings throughout; any + // numeric coercion would corrupt them. + const pgcr = buildPGCR({ instanceId: '17091392013' }); + + expect(processPGCR(pgcr).instanceId).toBe('17091392013'); + }); +}); + +describe('period conversion', () => { + it('converts Bungie\'s ISO timestamp to unix seconds', () => { + const pgcr = buildPGCR({ period: '2026-07-26T12:00:00Z' }); + + expect(processPGCR(pgcr).period).toBe(Math.floor(Date.UTC(2026, 6, 26, 12, 0, 0) / 1000)); + }); + + it('is unaffected by a daylight-saving boundary', () => { + // Bungie reports UTC, which has no DST. This pins that the conversion does + // not pick up the host's local offset — a machine in a DST-observing zone + // would otherwise shift every run by an hour twice a year, quietly moving + // runs across leaderboard cutoffs. + const pgcr = buildPGCR({ period: '2026-03-29T01:30:00Z' }); + + expect(processPGCR(pgcr).period).toBe(Math.floor(Date.UTC(2026, 2, 29, 1, 30, 0) / 1000)); + }); + + it('truncates sub-second precision rather than rounding up', () => { + const pgcr = buildPGCR({ period: '2026-07-26T12:00:00.999Z' }); + + expect(processPGCR(pgcr).period).toBe(Math.floor(Date.UTC(2026, 6, 26, 12, 0, 0) / 1000)); + }); +}); + +describe('player extraction', () => { + it('keeps one record per entry', () => { + const pgcr = buildPGCR({ entries: buildFireteam({ size: 6 }) }); + + expect(processPGCR(pgcr).players).toHaveLength(6); + }); + + it('preserves the parts that make up a Name#Code identity', () => { + // Player identity is Name#Code throughout the system; dropping either half + // here produces the partial-name bug the project has hit before. + const entry = buildEntry({ + membershipId: '4611686018400000001', + bungieGlobalDisplayName: 'Guardian', + bungieGlobalDisplayNameCode: 42, + }); + + const [player] = processPGCR(buildPGCR({ entries: [entry] })).players; + + expect(player.bungieGlobalDisplayName).toBe('Guardian'); + expect(player.bungieGlobalDisplayNameCode).toBe(42); + }); + + it('tolerates an entry with no global display name', () => { + // Bungie withholds it for some accounts. Extraction must not throw; the + // downstream display path falls back to the platform display name. + const entry = buildEntry({ + displayName: 'LegacyName', + bungieGlobalDisplayName: null, + bungieGlobalDisplayNameCode: null, + }); + + const [player] = processPGCR(buildPGCR({ entries: [entry] })).players; + + expect(player.bungieGlobalDisplayName).toBeUndefined(); + expect(player.displayName).toBe('LegacyName'); + }); + + it('retains every member of an oversized fireteam', () => { + // Entry counts above six are normal: players joining and leaving each get + // an entry, and the database has raids with seven or more. + const pgcr = buildPGCR({ entries: buildFireteam({ size: 9 }) }); + + expect(processPGCR(pgcr).players).toHaveLength(9); + }); +}); diff --git a/src/lib/utils/rate-limiter.test.ts b/src/lib/utils/rate-limiter.test.ts new file mode 100644 index 0000000..c61be11 --- /dev/null +++ b/src/lib/utils/rate-limiter.test.ts @@ -0,0 +1,160 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { RateLimiter } from './rate-limiter'; + +/** + * The limiter is the only thing standing between the crawler/scanner worker pools + * and Bungie's per-key rate limits. Its two guarantees — that concurrent waiters + * cannot claim the same slot, and that a pause applies to the whole key rather + * than to one request — are both timing behaviours, so they are tested with fake + * timers rather than real sleeps. + */ + +afterEach(() => { + vi.useRealTimers(); +}); + +describe('request spacing', () => { + it('lets the first request through without waiting', async () => { + vi.useFakeTimers(); + const limiter = new RateLimiter(10); + + const granted = trackSettled(limiter.wait()); + await vi.advanceTimersByTimeAsync(0); + + expect(granted.settled).toBe(true); + }); + + it('spaces the next request by the configured interval', async () => { + vi.useFakeTimers(); + const limiter = new RateLimiter(10); // 100ms between grants + + limiter.wait(); + const second = trackSettled(limiter.wait()); + + await vi.advanceTimersByTimeAsync(99); + expect(second.settled).toBe(false); + + await vi.advanceTimersByTimeAsync(1); + expect(second.settled).toBe(true); + }); + + it('serializes concurrent waiters instead of letting them burst', async () => { + // Without the FIFO promise chain, ten simultaneous callers would each read + // the same nextSlot and fire at once — the exact burst the limiter exists + // to prevent. + vi.useFakeTimers(); + const limiter = new RateLimiter(10); + + const order: number[] = []; + const waiters = Array.from({ length: 5 }, (_, i) => + limiter.wait().then(() => order.push(i)) + ); + + await vi.advanceTimersByTimeAsync(500); + await Promise.all(waiters); + + expect(order).toEqual([0, 1, 2, 3, 4]); + }); +}); + +describe('pausing the key', () => { + it('defers a request that had not started waiting yet', async () => { + vi.useFakeTimers(); + const limiter = new RateLimiter(1000); // spacing is negligible here + + limiter.pauseFor(5); + const granted = trackSettled(limiter.wait()); + + await vi.advanceTimersByTimeAsync(4999); + expect(granted.settled).toBe(false); + + await vi.advanceTimersByTimeAsync(1); + expect(granted.settled).toBe(true); + }); + + it('extends a wait that is already in progress', async () => { + // The loop in wait() re-reads nextSlot after each sleep precisely so a + // pause landing mid-sleep is honoured. Without that re-read, a request + // that was already sleeping would fire straight into a throttled key. + vi.useFakeTimers(); + const limiter = new RateLimiter(1000); + + limiter.pauseFor(2); + const granted = trackSettled(limiter.wait()); + + await vi.advanceTimersByTimeAsync(1000); + limiter.pauseFor(5); // arrives while the first wait is still sleeping + + await vi.advanceTimersByTimeAsync(1000); + expect(granted.settled).toBe(false); + + await vi.advanceTimersByTimeAsync(4000); + expect(granted.settled).toBe(true); + }); + + it('holds back every queued waiter, not just the one that saw the throttle', async () => { + // Bungie throttles the key, not the request. A pause that only affected + // the caller who observed it would let the rest of the pool keep hammering. + vi.useFakeTimers(); + const limiter = new RateLimiter(1000); + + // Let one waiter through first, so the pause below is demonstrably + // affecting a queue rather than simply being set before any activity. + const first = trackSettled(limiter.wait()); + await vi.advanceTimersByTimeAsync(0); + expect(first.settled).toBe(true); + + const queued = [trackSettled(limiter.wait()), trackSettled(limiter.wait())]; + limiter.pauseFor(3); + + await vi.advanceTimersByTimeAsync(2999); + expect(queued.map((w) => w.settled)).toEqual([false, false]); + + // Both are released once the pause lifts; the extra tick covers the + // normal inter-request spacing between the two of them. + await vi.advanceTimersByTimeAsync(20); + expect(queued.every((w) => w.settled)).toBe(true); + }); + + it('catches a waiter that has been queued but not yet granted', async () => { + // Grants are handed out on a microtask, so a pause issued in the same tick + // as wait() still applies — the caller is queued, not yet through. + vi.useFakeTimers(); + const limiter = new RateLimiter(1000); + + const granted = trackSettled(limiter.wait()); + limiter.pauseFor(3); + + await vi.advanceTimersByTimeAsync(2999); + expect(granted.settled).toBe(false); + + await vi.advanceTimersByTimeAsync(1); + expect(granted.settled).toBe(true); + }); + + it('never shortens an existing pause', async () => { + // pauseFor takes the max, so a 1s game-server backoff arriving during a + // 10s throttle must not cut the longer pause short. + vi.useFakeTimers(); + const limiter = new RateLimiter(1000); + + limiter.pauseFor(10); + limiter.pauseFor(1); + const granted = trackSettled(limiter.wait()); + + await vi.advanceTimersByTimeAsync(9999); + expect(granted.settled).toBe(false); + + await vi.advanceTimersByTimeAsync(1); + expect(granted.settled).toBe(true); + }); +}); + +function trackSettled(promise: Promise): { settled: boolean } { + const state = { settled: false }; + promise.then( + () => { state.settled = true; }, + () => { state.settled = true; } + ); + return state; +} diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..901f2a3 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,111 @@ +# Tests + +## Running them + +```bash +npm test # everything, once. Fast, hermetic, no network. +npm run test:watch # re-runs on save +npm run test:coverage # adds a coverage table. No thresholds — it's a diagnostic, not a gate. + +npx vitest run tests/db/leaderboard.test.ts # one file +npx vitest run -t 'checkpoint' # tests whose name matches +``` + +`npm test` is meant to stay fast and reachable from anywhere. It never touches the network and +never touches the real database — if it ever does either, that's a bug in the test, not a +tolerable shortcut. + +## `npm test` vs `npm run e2e:maintenance` + +Two different things that both deserve to exist. + +| | `npm test` | `npm run e2e:maintenance` | +|---|---|---| +| What | Unit and query-level tests | The maintenance-cycle harness | +| Speed | Under a second | Minutes | +| Needs | Nothing | Spawns real crawler/scanner processes against a mock Bungie server | +| In CI | Yes | No — too slow, too many moving parts | + +`scripts/test-maintenance-cycle.ts` predates this framework and is correct for what it does. It is +not being ported or absorbed into Vitest. It was only renamed, from `test-maintenance-cycle` to +`e2e:maintenance`, so that `npm test` unambiguously means "fast, hermetic, no network". + +## Layout + +``` +tests/ +├── db/ tests that need a database +├── fixtures/ captured Bungie PGCR JSON + README +├── helpers/ builders, seeding, db access +└── setup/ global setup: db path, network guard +src/**/*.test.ts pure-logic tests, next to what they cover +``` + +Colocated or under `tests/`? Colocate when the test needs nothing but the module — it moves with +the code it covers. Put it under `tests/` when it needs a database, a fixture, or a helper. + +## The two ground rules + +**Never mock our own modules.** `vi.mock('@/lib/db/queries')` tests the mock. The database is real +— `better-sqlite3` opens one in about a millisecond, faster than the mock scaffolding it replaces, +and it validates the actual SQL. See [ADR 0004](../docs/adr/0004-mock-only-at-the-network-boundary.md). + +**Mock `fetch`, and only `fetch`.** `tests/setup/no-network.ts` replaces it with a thrower before +every test, so a test reaching the real internet fails loudly rather than quietly burning Bungie +API quota. Stub it deliberately when you need a response: + +```ts +vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('{"ErrorCode":1}'))); +``` + +No cleanup needed — the guard re-arms itself before the next test. + +## Adding a test + +**Pure logic** — colocate it, import directly, done: + +```ts +// src/lib/crawler/pgcr.test.ts +import { processPGCR } from './pgcr'; +import { buildPGCR, buildFireteam } from '../../../tests/helpers/pgcr-builder'; + +it('counts the run as completed when a single member finished', () => { + const pgcr = buildPGCR({ entries: buildFireteam({ size: 6, completions: 1 }) }); + expect(processPGCR(pgcr).completed).toBe(true); +}); +``` + +**Anything touching the database** — reset in `beforeEach`, seed through the helpers: + +```ts +// tests/db/whatever.test.ts +import { resetTestDb } from '../helpers/db'; +import { seedRun, seedPlayer, hoursAgo } from '../helpers/seed'; + +beforeEach(() => { resetTestDb(); }); + +it('excludes a checkpoint run', () => { + seedRun({ instanceId: '1', completedBy: ['p1'], startedFromBeginning: false }); + expect(runLeaderboardRows(24, [], 10)).toEqual([]); +}); +``` + +Each test file gets its own throwaway database in a temp directory, created with the production +schema. You don't have to set it up — `tests/setup/test-db-path.ts` handles it before your imports +run. A real file rather than `:memory:` for a specific reason: +[ADR 0003](../docs/adr/0003-tests-run-against-a-real-sqlite-file.md). + +`seedRun` goes through `insertFullPGCR`, the same chokepoint all four production ingestion sources +use — so seeded rows are rows production could actually create. Don't reach for raw `INSERT`s. + +**Fixtures or builders?** Fixtures when the point is "this is what Bungie really sends". Builders +when you need to vary one field across cases. See [fixtures/README.md](./fixtures/README.md). + +## Writing them + +Name a test as a claim about behaviour, not a description of code. `excludes a run that nobody +completed` beats `test filter logic`. When you read a failure at 2am, the name is what you get. + +Comment the *why* when it isn't obvious from the name — especially when a test pins behaviour +that's wrong but current. Those are labelled `BUG:` and say so in the comment, so nobody "fixes" +the test instead of the code. diff --git a/tests/db/ended-at-derivation.test.ts b/tests/db/ended-at-derivation.test.ts new file mode 100644 index 0000000..87f7f66 --- /dev/null +++ b/tests/db/ended-at-derivation.test.ts @@ -0,0 +1,191 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { FUTURE_ENDED_SKEW_SECONDS, computeActivityDurationSeconds } from '@/lib/db/queries'; +import { getDb } from '@/lib/db'; +import { resetTestDb } from '../helpers/db'; +import { hoursAgo, readPgcrRow, seedPlayer, seedRun } from '../helpers/seed'; + +/** + * `pgcrs.ended_at` is the denormalized run end time (period + duration) that + * replaced the old `run_durations` CTE in the phase 3b reader cutover (610408e). + * Every leaderboard, the recent-completions list, and the completion-time stats + * now filter and sort on it, so a wrong derivation does not crash anything — it + * silently returns the wrong set of runs. That is the failure mode this file + * exists to catch. + */ + +beforeEach(() => { + resetTestDb(); +}); + +describe('computeActivityDurationSeconds', () => { + it('prefers Bungie\'s activity-level duration over per-player time', () => { + const players = [{ startSeconds: 0, timePlayedSeconds: 900 }]; + + expect(computeActivityDurationSeconds(2400, players)).toBe(2400); + }); + + it('falls back to per-player time when the activity duration is absent', () => { + const players = [ + { startSeconds: 0, timePlayedSeconds: 900 }, + { startSeconds: 0, timePlayedSeconds: 1500 }, + ]; + + expect(computeActivityDurationSeconds(null, players)).toBe(1500); + }); + + it('treats a zero activity duration as unusable rather than as a real value', () => { + // `> 0` not `!= null`: a zero duration is Bungie reporting nothing useful, + // and accepting it would pin ended_at to the run's start time. + const players = [{ startSeconds: 0, timePlayedSeconds: 1500 }]; + + expect(computeActivityDurationSeconds(0, players)).toBe(1500); + }); + + it('counts a late joiner\'s offset so the run is not measured from their arrival', () => { + // The player who joined 1200s in and played 600s establishes a 1800s run, + // even though nobody's individual time exceeds 900s. + const players = [ + { startSeconds: 0, timePlayedSeconds: 900 }, + { startSeconds: 1200, timePlayedSeconds: 600 }, + ]; + + expect(computeActivityDurationSeconds(null, players)).toBe(1800); + }); + + it('considers players who did not complete, not just those who did', () => { + // The pre-cutover CTE filtered on completed = 1. This deliberately does not: + // someone who left before the end still bounds how long the activity ran. + const players = [ + { startSeconds: 0, timePlayedSeconds: 2400 }, + { startSeconds: 0, timePlayedSeconds: 600 }, + ]; + + expect(computeActivityDurationSeconds(null, players)).toBe(2400); + }); + + it('collapses to the longest time played when no start offsets are reported', () => { + const players = [ + { startSeconds: null, timePlayedSeconds: 900 }, + { timePlayedSeconds: 1500 }, + ]; + + expect(computeActivityDurationSeconds(null, players)).toBe(1500); + }); + + it('reports no duration at all for an empty PGCR', () => { + expect(computeActivityDurationSeconds(null, [])).toBeNull(); + }); + + it('reports no duration when every player has zero time played', () => { + const players = [ + { startSeconds: 0, timePlayedSeconds: 0 }, + { startSeconds: 0, timePlayedSeconds: 0 }, + ]; + + expect(computeActivityDurationSeconds(null, players)).toBeNull(); + }); +}); + +describe('ended_at as persisted by insertFullPGCR', () => { + it('stores the run end as start plus duration', () => { + const period = hoursAgo(3); + seedRun({ instanceId: '1', period, completedBy: ['p1'], activityDurationSeconds: 1800 }); + + expect(readPgcrRow('1')?.ended_at).toBe(period + 1800); + }); + + it('leaves the end time unknown when no duration can be derived', () => { + // Tier 3. A NULL ended_at drops the run from every leaderboard, because + // they all filter `ended_at >= cutoff`. That exclusion is intended: a run + // with no derivable end time cannot be placed in a time window. + seedRun({ + instanceId: '1', + completedBy: ['p1'], + activityDurationSeconds: null, + timePlayedSeconds: 0, + }); + + expect(readPgcrRow('1')?.ended_at).toBeNull(); + }); + + it('discards a future end time as corrupt', () => { + // Bungie reports absurd durations for farm/checkpoint megalobby instances + // — multi-day "activities". A PGCR is only ingested after the run ended, + // so an end time beyond now is malformed by definition. + seedRun({ + instanceId: '1', + period: hoursAgo(1), + completedBy: ['p1'], + activityDurationSeconds: 30 * 24 * 3600, + }); + + expect(readPgcrRow('1')?.ended_at).toBeNull(); + }); + + it('keeps a just-finished run whose end time is barely ahead of the ingest clock', () => { + // Clock skew between Bungie and the crawler must not be mistaken for + // corruption, so the guard allows an hour of headroom. + const period = Math.floor(Date.now() / 1000); + const duration = FUTURE_ENDED_SKEW_SECONDS - 60; + seedRun({ instanceId: '1', period, completedBy: ['p1'], activityDurationSeconds: duration }); + + expect(readPgcrRow('1')?.ended_at).toBe(period + duration); + }); +}); + +describe('players.last_seen_at maintenance', () => { + it('advances to the end time of a newly ingested run', () => { + const period = hoursAgo(2); + seedPlayer('p1'); + seedRun({ instanceId: '1', period, completedBy: ['p1'], activityDurationSeconds: 1800 }); + + expect(readLastSeen('p1')).toBe(period + 1800); + }); + + it('never moves backwards when an older run is ingested late', () => { + // The scanner backfills runs out of order, so an old PGCR routinely arrives + // after a newer one. Moving last_seen_at backwards would demote an active + // player into the cold crawl bucket. + const recent = hoursAgo(1); + seedPlayer('p1'); + seedRun({ instanceId: '1', period: recent, completedBy: ['p1'], activityDurationSeconds: 1800 }); + seedRun({ instanceId: '2', period: hoursAgo(50), completedBy: ['p1'], activityDurationSeconds: 1800 }); + + expect(readLastSeen('p1')).toBe(recent + 1800); + }); + + it('is untouched by a run with no derivable end time', () => { + seedPlayer('p1'); + seedRun({ + instanceId: '1', + completedBy: ['p1'], + activityDurationSeconds: null, + timePlayedSeconds: 0, + }); + + expect(readLastSeen('p1')).toBeFalsy(); + }); + + it('advances for a player who did not complete the run', () => { + // last_seen_at tracks presence, not achievement — someone who joined and + // left was still online, and the crawl buckets care about that. + const period = hoursAgo(2); + seedPlayer('p2'); + seedRun({ + instanceId: '1', + period, + completedBy: ['p1'], + incompleteBy: ['p2'], + activityDurationSeconds: 1800, + }); + + expect(readLastSeen('p2')).toBe(period + 1800); + }); +}); + +function readLastSeen(membershipId: string): number | null { + const row = getDb() + .prepare('SELECT last_seen_at FROM players WHERE membership_id = ?') + .get(membershipId) as { last_seen_at: number | null } | undefined; + return row?.last_seen_at ?? null; +} diff --git a/tests/db/full-clear-flag.test.ts b/tests/db/full-clear-flag.test.ts new file mode 100644 index 0000000..85fb0aa --- /dev/null +++ b/tests/db/full-clear-flag.test.ts @@ -0,0 +1,66 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { resetTestDb } from '../helpers/db'; +import { readPgcrRow, seedRun } from '../helpers/seed'; + +/** + * What actually decides whether a run counts as a full clear. + * + * There are two candidate signals in the codebase and only one of them is real: + * + * ProcessedPGCR.isFullClear computed, never persisted, always true + * pgcrs.activity_was_started_from_beginning persisted, and what every + * leaderboard filters on + * + * The first is dead code. Bungie reports `startingPhaseIndex: 0` on every PGCR, + * including confirmed checkpoint runs (see tests/real-pgcrs.test.ts), so + * `isFullClear`'s `=== 0` branch fires unconditionally and reports every run as + * a full clear — including the 568k that are checkpoint runs. Nothing reads it, so nothing breaks today; + * wiring it up would inflate every leaderboard by roughly 2.2x. + * + * These tests pin the signal that ships, so that if anyone ever "tidies" the + * writer by using the derived field instead, the failure is loud. + * See docs/decisions.md. + */ + +beforeEach(() => { + resetTestDb(); +}); + +describe('the persisted full-clear flag', () => { + it('records a run started from the beginning as a full clear', () => { + seedRun({ instanceId: '1', completedBy: ['p1'], startedFromBeginning: true }); + + expect(readPgcrRow('1')?.activity_was_started_from_beginning).toBe(1); + }); + + it('records a checkpoint run as not a full clear', () => { + // The case ProcessedPGCR.isFullClear gets wrong. If the writer ever + // switched to that field, this would flip to 1 and the run would start + // appearing on full-clear leaderboards. + seedRun({ instanceId: '1', completedBy: ['p1'], startedFromBeginning: false }); + + expect(readPgcrRow('1')?.activity_was_started_from_beginning).toBe(0); + }); + + it('stores a zero starting phase index regardless of the run type', () => { + // Bungie reports startingPhaseIndex as 0 for every run and the writer coerces + // it with `|| 0` anyway, so the column is 0 for every row in production. Pinned so nobody + // writes a query that assumes this column still discriminates anything. + seedRun({ instanceId: '1', completedBy: ['p1'], startedFromBeginning: true }); + seedRun({ instanceId: '2', completedBy: ['p1'], startedFromBeginning: false }); + + expect(readPgcrRow('1')?.starting_phase_index).toBe(0); + expect(readPgcrRow('2')?.starting_phase_index).toBe(0); + }); + + it('keeps the full-clear flag independent of whether anyone finished', () => { + // Two orthogonal facts: how the run was entered, and whether it was + // cleared. The leaderboards require both, so conflating them would either + // admit checkpoint clears or exclude legitimate ones. + seedRun({ instanceId: '1', incompleteBy: ['p1'], startedFromBeginning: true }); + + const row = readPgcrRow('1'); + expect(row?.activity_was_started_from_beginning).toBe(1); + expect(row?.completed).toBe(0); + }); +}); diff --git a/tests/db/leaderboard.test.ts b/tests/db/leaderboard.test.ts new file mode 100644 index 0000000..c3a6aa1 --- /dev/null +++ b/tests/db/leaderboard.test.ts @@ -0,0 +1,237 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { runLeaderboardRows } from '@/lib/cache/leaderboard-cache'; +import { resetTestDb } from '../helpers/db'; +import { seedPlayer, seedRun } from '../helpers/seed'; + +/** + * The leaderboard is the product. A crash here would be noticed within minutes; + * a silently wrong row set would not be noticed at all, which is why these tests + * assert membership and ordering rather than "it returned something". + * + * Raid hashes are real values from RAID_DEFINITIONS in bungie/manifest.ts. + */ +const SALVATIONS_EDGE = 2192826039; +const CROTAS_END = 1566480315; + +const DURATION = 1800; +const HOURS_BACK = 24; + +/** Places a run so it ends exactly `offsetSeconds` relative to the 24h cutoff. + * Negative lands inside the window, positive lands outside (further in the past). */ +function endingRelativeToCutoff(offsetSeconds: number): number { + const cutoff = Math.floor(Date.now() / 1000) - HOURS_BACK * 3600; + return cutoff - offsetSeconds - DURATION; +} + +beforeEach(() => { + resetTestDb(); +}); + +describe('who appears on the leaderboard', () => { + it('counts a raid instance once per player, however many entries they have', () => { + // pgcr_players is keyed (instance_id, membership_id), so a player running + // two characters through one instance collapses to a single row at insert + // and COUNT(DISTINCT instance_id) keeps it that way if that ever changes. + seedRun({ instanceId: '1', completedBy: ['p1'] }); + seedRun({ instanceId: '2', completedBy: ['p1'] }); + + const rows = runLeaderboardRows(HOURS_BACK, [], 10); + + expect(rows).toHaveLength(1); + expect(rows[0].completions).toBe(2); + }); + + it('excludes a checkpoint run', () => { + // The live full-clear signal. Note this is Bungie's raw + // activityWasStartedFromBeginning, not ProcessedPGCR.isFullClear — that + // field is computed, never persisted, and would report every run as a + // full clear. See docs/testing-framework-plan.md. + seedRun({ instanceId: '1', completedBy: ['p1'], startedFromBeginning: false }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)).toEqual([]); + }); + + it('excludes a player who was present but did not finish', () => { + seedRun({ instanceId: '1', completedBy: ['p1'], incompleteBy: ['p2'] }); + + const rows = runLeaderboardRows(HOURS_BACK, [], 10); + + expect(rows.map((r) => r.membershipId)).toEqual(['p1']); + }); + + it('excludes a run that nobody completed', () => { + // 55% of stored PGCRs have zero completions, so this is the single most + // common shape in the table rather than an edge case. + seedRun({ instanceId: '1', incompleteBy: ['p1', 'p2'] }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)).toEqual([]); + }); + + it('excludes a run with no derivable end time', () => { + // ended_at IS NULL fails `ended_at >= cutoff`, so the run cannot be placed + // in any time window and drops out entirely. + seedRun({ + instanceId: '1', + completedBy: ['p1'], + activityDurationSeconds: null, + timePlayedSeconds: 0, + }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)).toEqual([]); + }); +}); + +describe('the time window', () => { + it('includes a run that ended just inside the cutoff', () => { + seedRun({ + instanceId: '1', + period: endingRelativeToCutoff(-60), + completedBy: ['p1'], + activityDurationSeconds: DURATION, + }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)).toHaveLength(1); + }); + + it('excludes a run that ended just outside the cutoff', () => { + seedRun({ + instanceId: '1', + period: endingRelativeToCutoff(60), + completedBy: ['p1'], + activityDurationSeconds: DURATION, + }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)).toEqual([]); + }); + + it('judges the window by when a run ended, not when it started', () => { + // A long run that began before the cutoff but finished inside it counts. + // This is precisely what the ended_at denormalization exists to express. + const cutoff = Math.floor(Date.now() / 1000) - HOURS_BACK * 3600; + seedRun({ + instanceId: '1', + period: cutoff - 600, + completedBy: ['p1'], + activityDurationSeconds: 1200, + }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)).toHaveLength(1); + }); +}); + +describe('raid filtering', () => { + it('counts only the requested raid', () => { + seedRun({ instanceId: '1', completedBy: ['p1'], activityHash: SALVATIONS_EDGE }); + seedRun({ instanceId: '2', completedBy: ['p1'], activityHash: CROTAS_END }); + + const rows = runLeaderboardRows(HOURS_BACK, ['salvations_edge'], 10); + + expect(rows[0].completions).toBe(1); + }); + + it('counts every raid when no filter is given', () => { + seedRun({ instanceId: '1', completedBy: ['p1'], activityHash: SALVATIONS_EDGE }); + seedRun({ instanceId: '2', completedBy: ['p1'], activityHash: CROTAS_END }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)[0].completions).toBe(2); + }); + + it('counts the union when several raids are requested', () => { + seedRun({ instanceId: '1', completedBy: ['p1'], activityHash: SALVATIONS_EDGE }); + seedRun({ instanceId: '2', completedBy: ['p1'], activityHash: CROTAS_END }); + + const rows = runLeaderboardRows(HOURS_BACK, ['salvations_edge', 'crotas_end'], 10); + + expect(rows[0].completions).toBe(2); + }); +}); + +describe('ordering and limit', () => { + it('ranks the most completions first', () => { + seedRun({ instanceId: '1', completedBy: ['p1', 'p2'] }); + seedRun({ instanceId: '2', completedBy: ['p1'] }); + + const rows = runLeaderboardRows(HOURS_BACK, [], 10); + + expect(rows.map((r) => r.membershipId)).toEqual(['p1', 'p2']); + }); + + it('breaks a tie in favour of whoever got there first', () => { + // lastClearAt ASC. A player who reached three clears an hour ago outranks + // one who reached three a minute ago, so a stale PGCR discovered late still + // slots its player at their true historical position. + seedRun({ instanceId: '1', period: hoursBeforeNow(10), completedBy: ['early'] }); + seedRun({ instanceId: '2', period: hoursBeforeNow(1), completedBy: ['late'] }); + + const rows = runLeaderboardRows(HOURS_BACK, [], 10); + + expect(rows.map((r) => r.membershipId)).toEqual(['early', 'late']); + }); + + it('breaks a remaining tie by membership id so the order is never arbitrary', () => { + const period = hoursBeforeNow(5); + seedRun({ instanceId: '1', period, completedBy: ['bbb', 'aaa'] }); + + const rows = runLeaderboardRows(HOURS_BACK, [], 10); + + expect(rows.map((r) => r.membershipId)).toEqual(['aaa', 'bbb']); + }); + + it('returns at most the requested number of players', () => { + seedRun({ instanceId: '1', completedBy: ['p1', 'p2', 'p3'] }); + + expect(runLeaderboardRows(HOURS_BACK, [], 2)).toHaveLength(2); + }); +}); + +describe('rank assignment', () => { + it('gives tied players the same rank and skips the ranks they consumed', () => { + // Competition ranking: 1, 2, 2, 4 — not 1, 2, 2, 3. + seedRun({ instanceId: '1', period: hoursBeforeNow(5), completedBy: ['top', 'mid1', 'mid2', 'low'] }); + seedRun({ instanceId: '2', period: hoursBeforeNow(5), completedBy: ['top', 'mid1', 'mid2'] }); + seedRun({ instanceId: '3', period: hoursBeforeNow(5), completedBy: ['top'] }); + + const rows = runLeaderboardRows(HOURS_BACK, [], 10); + + expect(rows.map((r) => [r.membershipId, r.completions, r.rank])).toEqual([ + ['top', 3, 1], + ['mid1', 2, 2], + ['mid2', 2, 2], + ['low', 1, 4], + ]); + }); +}); + +describe('display names', () => { + it('renders the full Name#Code, zero-padding the code to four digits', () => { + seedPlayer('p1', 'Guardian', 42); + seedRun({ instanceId: '1', completedBy: ['p1'] }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)[0].displayName).toBe('Guardian#0042'); + }); + + it('falls back to the name recorded on the run when the player is not yet crawled', () => { + // A player appears in pgcr_players the moment a run is ingested, but only + // enters the players table once crawled — which is why the join is a LEFT + // one and why this fallback is load-bearing rather than defensive. + seedRun({ instanceId: '1', completedBy: ['p1'] }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)[0].displayName).toBe('Guardian-p1'); + }); + + it('BUG: drops the #Code entirely when the code is zero', () => { + // formatDisplayName guards with `&& entry.bungieGlobalDisplayNameCode`, so a + // code of 0 is falsy and the branch is skipped, yielding a partial name. + // CLAUDE.md calls the full Name#Code form load-bearing and notes partial + // names were a real bug before. Pinned as current behaviour, not endorsed — + // see docs/decisions.md. + seedPlayer('p1', 'Guardian', 0); + seedRun({ instanceId: '1', completedBy: ['p1'] }); + + expect(runLeaderboardRows(HOURS_BACK, [], 10)[0].displayName).toBe('Guardian'); + }); +}); + +function hoursBeforeNow(hours: number): number { + return Math.floor(Date.now() / 1000) - Math.round(hours * 3600); +} diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 0000000..3614fc0 --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,69 @@ +# PGCR fixtures + +Real Bungie PGCR responses, captured verbatim on 2026-07-26. PGCR data is public, so these are +committed as-is. None are synthetic. + +## Why captured, not hand-written + +A hand-authored PGCR encodes what we *believe* the API returns. These fixtures exist to check that +belief, so writing them ourselves would defeat the point. + +That is not a theoretical concern. Capturing these corrected three things we had wrong, each +inferred from the database rather than observed at the source: + +- `startingPhaseIndex` is **present and always `0`**, including on confirmed checkpoint runs. We + had assumed Bungie stopped sending it, because the writer's `|| 0` had flattened the evidence. +- A "six player" raid report can belong to **two people** with three characters each. +- "No derivable duration" was really **an absurd duration** — 27384s reported for an 18-minute run. + +Use a fixture when the point is "this is what Bungie really sends". Use a builder from +`../helpers/pgcr-builder.ts` when you need to vary one field across several cases. Fixtures for +realism, builders for permutation. + +## Capturing + +```bash +npm run capture-fixtures +``` + +Reads `BUNGIE_API_KEY` from `.env` and rewrites every fixture below. + +The script prints the salient fields for each capture — entry count, completion count, +`fromBeginning`, duration versus longest time played, missing names — so a fixture that no longer +demonstrates its stated case is visible rather than silently wrong. Check that output against the +table below whenever you re-capture. Bungie does eventually stop serving old PGCRs; if a capture +starts failing, pick a replacement instance and update `scripts/capture-pgcr-fixture.ts`. + +Every instance ID was chosen by querying the production database for runs exhibiting the property +in question, so each is a real observed run rather than a hypothetical. + +## The fixtures + +| File | Instance | What makes it interesting | +|---|---|---| +| `pgcr-fullclear-salvations-edge.json` | 17091392013 | Baseline. Six players, six completions, started from the beginning. | +| `pgcr-checkpoint-root-of-nightmares.json` | 17091462346 | `activityWasStartedFromBeginning: false` — a checkpoint run, which every leaderboard must exclude. Seven entries. | +| `pgcr-zero-completions-vault-of-glass.json` | 17091467640 | No entry has `completed = 1`. The most common shape in the table: 55% of stored PGCRs. | +| `pgcr-partial-completion-last-wish.json` | 17091283535 | Two entries, one completion. `completed` is per-entry and ANY completion counts the run. | +| `pgcr-multi-character-garden.json` | 17091200569 | **Six entries, two distinct players** — three characters each. Also a mild Tier 1 vs Tier 2 duration gap (2069s reported, 2037s derivable). | +| `pgcr-absurd-duration-crotas-end.json` | 17091316490 | Reports a **27384s (7.6 hour)** duration for a run where nobody played past 1093s. The megalobby corruption `FUTURE_ENDED_SKEW_SECONDS` exists to reject. | +| `pgcr-missing-bungie-name.json` | 16975643976 | **Nineteen entries, every one anonymous** — `isPublic: false`, `membershipType: 0`, and no name field of any kind. Not "the global name is missing", but no identity at all. | +| `pgcr-non-raid.json` | 17091392014 | A non-raid activity, so `isRaidActivityHash` rejects it. Found by probing forward from a known raid instance. | + +## Two cases with no fixture + +**A checkpoint run identified by `startingPhaseIndex > 0`.** The original brief asked for this. +No such PGCR exists to capture — the field is `0` on every one of the 827,076 rows in production +*and* on every fixture above, including the three that are genuinely checkpoint runs. Checkpoint +runs are identified by `activityWasStartedFromBeginning` instead, which +`pgcr-checkpoint-root-of-nightmares.json` covers. + +**A true Tier 3 run, where no duration is derivable at all.** Every captured PGCR reports a usable +`activityDurationSeconds`. `pgcr-absurd-duration-crotas-end.json` was originally captured expecting +this case — its database row has a NULL `ended_at` — but the NULL came from the future-end-time +guard rejecting a corrupt duration, not from Tier 3. Tier 3 is covered by builders in +`tests/db/ended-at-derivation.test.ts`. + +Note that the absurd-duration fixture's NULL-ness is **time dependent**: the guard compares against +the ingest clock, so re-seeding that run today produces a non-NULL `ended_at`. Do not write a test +asserting NULL from that fixture — use a builder, which controls the period explicitly. diff --git a/tests/fixtures/pgcr-absurd-duration-crotas-end.json b/tests/fixtures/pgcr-absurd-duration-crotas-end.json new file mode 100644 index 0000000..3398b6a --- /dev/null +++ b/tests/fixtures/pgcr-absurd-duration-crotas-end.json @@ -0,0 +1,786 @@ +{ + "period": "2026-07-26T20:20:25Z", + "startingPhaseIndex": 0, + "activityWasStartedFromBeginning": true, + "activityDifficultyTier": -1, + "activityDetails": { + "referenceId": 1507509200, + "directorActivityHash": 1507509200, + "instanceId": "17091316490", + "mode": 4, + "modes": [ + 7, + 4 + ], + "isPrivate": false, + "membershipType": 0 + }, + "entries": [ + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/d914fab82fe3f1d6f751627e04338f51.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018457300313", + "displayName": "chingmugga", + "bungieGlobalDisplayName": "chingmugga", + "bungieGlobalDisplayNameCode": 1299 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 1607, + "emblemHash": 1907674138 + }, + "characterId": "2305843009535885726", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 27384, + "displayValue": "7h 36m" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 0, + "displayValue": "" + } + }, + "startSeconds": { + "basic": { + "value": 199, + "displayValue": "3m 19s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 27, + "displayValue": "0m 27s" + } + }, + "playerCount": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/97d1647338c17bdddc79bfac4ed01519.jpg", + "crossSaveOverride": 2, + "applicableMembershipTypes": [ + 3, + 6, + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018527901876", + "displayName": "Lutchet", + "bungieGlobalDisplayName": "Lutchet", + "bungieGlobalDisplayNameCode": 5129 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 3887404748, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 4132147348 + }, + "characterId": "2305843010300344547", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 27384, + "displayValue": "7h 36m" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 8250665239436292000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 0, + "displayValue": "0m 0s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1093, + "displayValue": "18m 13s" + } + }, + "playerCount": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/d0d8a24c8a8143747adb192caaa43a5f.jpg", + "crossSaveOverride": 2, + "applicableMembershipTypes": [ + 3, + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018491329287", + "displayName": "RedsWinter", + "bungieGlobalDisplayName": "Red", + "bungieGlobalDisplayNameCode": 7227 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 545, + "emblemHash": 383734238 + }, + "characterId": "2305843010331984142", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "opponentsDefeated": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "efficiency": { + "basic": { + "value": 1.5, + "displayValue": "1.50" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 1.5, + "displayValue": "1.50" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 1.5, + "displayValue": "1.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 27384, + "displayValue": "7h 36m" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 8250665239436292000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 3, + "displayValue": "0m 3s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 507, + "displayValue": "8m 27s" + } + }, + "playerCount": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 1280894514, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.3333333333333333, + "displayValue": "33%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/775aba88c3235c8a38d68ec69f1f810c.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 3 + ], + "isPublic": true, + "membershipType": 3, + "membershipId": "4611686018538347503", + "displayName": "tana", + "bungieGlobalDisplayName": "Enchant", + "bungieGlobalDisplayNameCode": 4152 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 3903070392 + }, + "characterId": "2305843010785034770", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 206, + "displayValue": "206" + } + }, + "opponentsDefeated": { + "basic": { + "value": 206, + "displayValue": "206" + } + }, + "efficiency": { + "basic": { + "value": 206, + "displayValue": "206.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 206, + "displayValue": "206.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 206, + "displayValue": "206.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 27384, + "displayValue": "7h 36m" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 8250665239436292000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 3, + "displayValue": "0m 3s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 516, + "displayValue": "8m 36s" + } + }, + "playerCount": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3293207827, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 43, + "displayValue": "43" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.09302325581395349, + "displayValue": "9%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 161, + "displayValue": "161" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 9, + "displayValue": "9" + } + } + } + } + } + ], + "teams": [] +} \ No newline at end of file diff --git a/tests/fixtures/pgcr-checkpoint-root-of-nightmares.json b/tests/fixtures/pgcr-checkpoint-root-of-nightmares.json new file mode 100644 index 0000000..4e7d04e --- /dev/null +++ b/tests/fixtures/pgcr-checkpoint-root-of-nightmares.json @@ -0,0 +1,1490 @@ +{ + "period": "2026-07-26T21:22:51Z", + "startingPhaseIndex": 0, + "activityWasStartedFromBeginning": false, + "activityDifficultyTier": -1, + "activityDetails": { + "referenceId": 2381413764, + "directorActivityHash": 2381413764, + "instanceId": "17091462346", + "mode": 4, + "modes": [ + 7, + 4 + ], + "isPrivate": false, + "membershipType": 0 + }, + "entries": [ + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/bebd5209420449f9036e731b4a65f361.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 2, + 3, + 6, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018430340724", + "displayName": "MBlack475", + "bungieGlobalDisplayName": "MBlack475", + "bungieGlobalDisplayNameCode": 5321 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 2803282938, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 165005424 + }, + "characterId": "2305843009262976722", + "values": { + "assists": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 27, + "displayValue": "27" + } + }, + "opponentsDefeated": { + "basic": { + "value": 34, + "displayValue": "34" + } + }, + "efficiency": { + "basic": { + "value": 34, + "displayValue": "34.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 27, + "displayValue": "27.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 30.5, + "displayValue": "30.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 291, + "displayValue": "4m 51s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -708723304309311500, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 3, + "displayValue": "0m 3s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 288, + "displayValue": "4m 48s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 42435996, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 13, + "displayValue": "13" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 1471212226, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 14, + "displayValue": "14" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.07142857142857142, + "displayValue": "7%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/9d0b71aea62aa6ab003d095f52c64872.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 3 + ], + "isPublic": true, + "membershipType": 3, + "membershipId": "4611686018517087279", + "displayName": "Armogeddon", + "bungieGlobalDisplayName": "Armogeddon", + "bungieGlobalDisplayNameCode": 6928 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 3228576704 + }, + "characterId": "2305843009871374016", + "values": { + "assists": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "efficiency": { + "basic": { + "value": 0.3333333333333333, + "displayValue": "0.33" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0.16666666666666666, + "displayValue": "0.17" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 291, + "displayValue": "4m 51s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -8931696098840895000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 289, + "displayValue": "4m 49s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/4b4f1ab72cf309112748faa6940e8a41.jpg", + "crossSaveOverride": 3, + "applicableMembershipTypes": [ + 1, + 6, + 3 + ], + "isPublic": true, + "membershipType": 3, + "membershipId": "4611686018497795565", + "displayName": "sammers", + "bungieGlobalDisplayName": "copycat", + "bungieGlobalDisplayNameCode": 8146 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 3887404748, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 3800278197 + }, + "characterId": "2305843010074994078", + "values": { + "assists": { + "basic": { + "value": 12, + "displayValue": "12" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "kills": { + "basic": { + "value": 27, + "displayValue": "27" + } + }, + "opponentsDefeated": { + "basic": { + "value": 39, + "displayValue": "39" + } + }, + "efficiency": { + "basic": { + "value": 9.75, + "displayValue": "9.75" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 6.75, + "displayValue": "6.75" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 8.25, + "displayValue": "8.25" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 291, + "displayValue": "4m 51s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -708723304309311500, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 4, + "displayValue": "0m 4s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 287, + "displayValue": "4m 47s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 1715391576, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 23, + "displayValue": "23" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.08695652173913043, + "displayValue": "9%" + } + } + } + }, + { + "referenceId": 1802315656, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 2591746970, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/9f121959eafcbbef4796bdea398f8e48.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 3 + ], + "isPublic": true, + "membershipType": 3, + "membershipId": "4611686018527934833", + "displayName": "Vick XCII", + "bungieGlobalDisplayName": "Vick XCII", + "bungieGlobalDisplayNameCode": 130 + }, + "characterClass": "Titan", + "classHash": 3655393761, + "raceHash": 3887404748, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 908153540 + }, + "characterId": "2305843010606614390", + "values": { + "assists": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "opponentsDefeated": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "efficiency": { + "basic": { + "value": 2.5, + "displayValue": "2.50" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 2, + "displayValue": "2.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 2.25, + "displayValue": "2.25" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 291, + "displayValue": "4m 51s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -708723304309311500, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 54, + "displayValue": "0m 54s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 237, + "displayValue": "3m 57s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3418719964, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/4dff669ed6d7273fa751997d09cf2525.jpg", + "crossSaveOverride": 2, + "applicableMembershipTypes": [ + 3, + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018471369112", + "displayName": "cheng_hang_low", + "bungieGlobalDisplayName": "Chaz", + "bungieGlobalDisplayNameCode": 8734 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 2803282938, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 1784442048 + }, + "characterId": "2305843010641374831", + "values": { + "assists": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 15, + "displayValue": "15" + } + }, + "opponentsDefeated": { + "basic": { + "value": 20, + "displayValue": "20" + } + }, + "efficiency": { + "basic": { + "value": 20, + "displayValue": "20.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 15, + "displayValue": "15.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 17.5, + "displayValue": "17.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 291, + "displayValue": "4m 51s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -708723304309311500, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 3, + "displayValue": "0m 3s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 288, + "displayValue": "4m 48s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 4174431791, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 15, + "displayValue": "15" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.4666666666666667, + "displayValue": "47%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/31d60aedc5ebdd94c369d1b4f352cf45.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 3 + ], + "isPublic": true, + "membershipType": 3, + "membershipId": "4611686018527934833", + "displayName": "Vick XCII", + "bungieGlobalDisplayName": "Vick XCII", + "bungieGlobalDisplayNameCode": 130 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 526, + "emblemHash": 3508476927 + }, + "characterId": "2305843010783094914", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 291, + "displayValue": "4m 51s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -708723304309311500, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 0, + "displayValue": "0m 0s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 24, + "displayValue": "0m 24s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/24e9133c9cc157853762de5a2c3853aa.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 3 + ], + "isPublic": true, + "membershipType": 3, + "membershipId": "4611686018557234021", + "displayName": "DICEMAN", + "bungieGlobalDisplayName": "DICEMAN", + "bungieGlobalDisplayNameCode": 4465 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 406, + "emblemHash": 1907674137 + }, + "characterId": "2305843010783414340", + "values": { + "assists": { + "basic": { + "value": 11, + "displayValue": "11" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 20, + "displayValue": "20" + } + }, + "opponentsDefeated": { + "basic": { + "value": 31, + "displayValue": "31" + } + }, + "efficiency": { + "basic": { + "value": 15.5, + "displayValue": "15.50" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 10, + "displayValue": "10.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 12.75, + "displayValue": "12.75" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 291, + "displayValue": "4m 51s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -708723304309311500, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 289, + "displayValue": "4m 49s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 1863583117, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 15, + "displayValue": "15" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.2, + "displayValue": "20%" + } + } + } + }, + { + "referenceId": 3285784871, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + } + ], + "teams": [] +} \ No newline at end of file diff --git a/tests/fixtures/pgcr-fullclear-salvations-edge.json b/tests/fixtures/pgcr-fullclear-salvations-edge.json new file mode 100644 index 0000000..5f86471 --- /dev/null +++ b/tests/fixtures/pgcr-fullclear-salvations-edge.json @@ -0,0 +1,2031 @@ +{ + "period": "2026-07-26T20:43:06Z", + "startingPhaseIndex": 0, + "activityWasStartedFromBeginning": true, + "activityDifficultyTier": -1, + "activityDetails": { + "referenceId": 1541433876, + "directorActivityHash": 1541433876, + "instanceId": "17091392013", + "mode": 4, + "modes": [ + 7, + 4 + ], + "isPrivate": false, + "membershipType": 0 + }, + "entries": [ + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/6bc9bafcd714a0f5501554b740ba497c.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 3, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018445419097", + "displayName": "Achryllic", + "bungieGlobalDisplayName": "War", + "bungieGlobalDisplayNameCode": 6141 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 3887404748, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 1576, + "emblemHash": 3228576714 + }, + "characterId": "2305843009311474238", + "values": { + "assists": { + "basic": { + "value": 33, + "displayValue": "33" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 226, + "displayValue": "226" + } + }, + "opponentsDefeated": { + "basic": { + "value": 259, + "displayValue": "259" + } + }, + "efficiency": { + "basic": { + "value": 129.5, + "displayValue": "129.50" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 113, + "displayValue": "113.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 121.25, + "displayValue": "121.25" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1246, + "displayValue": "20m 46s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -3445485178621350400, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1244, + "displayValue": "20m 44s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 42435996, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 73, + "displayValue": "73" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.0547945205479452, + "displayValue": "5%" + } + } + } + }, + { + "referenceId": 1111334348, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 16, + "displayValue": "16" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.25, + "displayValue": "25%" + } + } + } + }, + { + "referenceId": 393652859, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 3460576091, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "100%" + } + } + } + }, + { + "referenceId": 2386208942, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 25, + "displayValue": "25" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.24, + "displayValue": "24%" + } + } + } + }, + { + "referenceId": 3211806999, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "100%" + } + } + } + }, + { + "referenceId": 2697143634, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 37, + "displayValue": "37" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.05405405405405406, + "displayValue": "5%" + } + } + } + }, + { + "referenceId": 4069880346, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 1085743380, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 22, + "displayValue": "22" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 11, + "displayValue": "11" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 28, + "displayValue": "28" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/bb345c3f323449ebb123569b41bb4738.jpg", + "crossSaveOverride": 3, + "applicableMembershipTypes": [ + 6, + 3 + ], + "isPublic": true, + "membershipType": 3, + "membershipId": "4611686018468703758", + "displayName": "Azrael", + "bungieGlobalDisplayName": "Azrael", + "bungieGlobalDisplayNameCode": 7707 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 3887404748, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 2962546551 + }, + "characterId": "2305843009486464357", + "values": { + "assists": { + "basic": { + "value": 25, + "displayValue": "25" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "kills": { + "basic": { + "value": 140, + "displayValue": "140" + } + }, + "opponentsDefeated": { + "basic": { + "value": 165, + "displayValue": "165" + } + }, + "efficiency": { + "basic": { + "value": 33, + "displayValue": "33.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 28, + "displayValue": "28.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 30.5, + "displayValue": "30.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1246, + "displayValue": "20m 46s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -3445485178621350400, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1244, + "displayValue": "20m 44s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 42435996, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 66, + "displayValue": "66" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.030303030303030304, + "displayValue": "3%" + } + } + } + }, + { + "referenceId": 1111334348, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "100%" + } + } + } + }, + { + "referenceId": 1363886209, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 14, + "displayValue": "14" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 480368036, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 1085743380, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 1303313141, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 2905188646, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 4049127142, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.2, + "displayValue": "20%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 11, + "displayValue": "11" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 11, + "displayValue": "11" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/25c54e5c6474e6f7f9dc34ba9ea6daf4.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 3, + 6, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018495002284", + "displayName": "MCR7425", + "bungieGlobalDisplayName": "mc", + "bungieGlobalDisplayNameCode": 4377 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 898834093, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 545, + "emblemHash": 3228576706 + }, + "characterId": "2305843009681024548", + "values": { + "assists": { + "basic": { + "value": 34, + "displayValue": "34" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "kills": { + "basic": { + "value": 152, + "displayValue": "152" + } + }, + "opponentsDefeated": { + "basic": { + "value": 186, + "displayValue": "186" + } + }, + "efficiency": { + "basic": { + "value": 62, + "displayValue": "62.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 50.666666666666664, + "displayValue": "50.67" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 56.333333333333336, + "displayValue": "56.33" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1246, + "displayValue": "20m 46s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -3445485178621350400, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1244, + "displayValue": "20m 44s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 42435996, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 62, + "displayValue": "62" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 2905188646, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.3157894736842105, + "displayValue": "32%" + } + } + } + }, + { + "referenceId": 1363886209, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 2198166292, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 2697143634, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 22, + "displayValue": "22" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.09090909090909091, + "displayValue": "9%" + } + } + } + }, + { + "referenceId": 393652859, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 8, + "displayValue": "8" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 15, + "displayValue": "15" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/128d5d5874db84e28da2ae1a5d27e385.jpg", + "crossSaveOverride": 2, + "applicableMembershipTypes": [ + 3, + 6, + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018516535925", + "displayName": "Drop_remax", + "bungieGlobalDisplayName": "remax 么", + "bungieGlobalDisplayNameCode": 5629 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 548, + "emblemHash": 2979324135 + }, + "characterId": "2305843009895274489", + "values": { + "assists": { + "basic": { + "value": 31, + "displayValue": "31" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 8, + "displayValue": "8" + } + }, + "kills": { + "basic": { + "value": 109, + "displayValue": "109" + } + }, + "opponentsDefeated": { + "basic": { + "value": 140, + "displayValue": "140" + } + }, + "efficiency": { + "basic": { + "value": 17.5, + "displayValue": "17.50" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 13.625, + "displayValue": "13.63" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 15.5625, + "displayValue": "15.56" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1246, + "displayValue": "20m 46s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -3445485178621350400, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1244, + "displayValue": "20m 44s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 42435996, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 29, + "displayValue": "29" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.10344827586206896, + "displayValue": "10%" + } + } + } + }, + { + "referenceId": 3821409356, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 3211624072, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 8, + "displayValue": "8" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.375, + "displayValue": "38%" + } + } + } + }, + { + "referenceId": 1363886209, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 2697143634, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 29, + "displayValue": "29" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.10344827586206896, + "displayValue": "10%" + } + } + } + }, + { + "referenceId": 4049127142, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.3333333333333333, + "displayValue": "33%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 13, + "displayValue": "13" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 13, + "displayValue": "13" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 8, + "displayValue": "8" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/2f77520720175fc8796152ae5d623404.jpg", + "crossSaveOverride": 2, + "applicableMembershipTypes": [ + 1, + 3, + 6, + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018440253889", + "displayName": "bianok219", + "bungieGlobalDisplayName": "Bionic™", + "bungieGlobalDisplayNameCode": 7184 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 165005427 + }, + "characterId": "2305843010004194199", + "values": { + "assists": { + "basic": { + "value": 22, + "displayValue": "22" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "kills": { + "basic": { + "value": 133, + "displayValue": "133" + } + }, + "opponentsDefeated": { + "basic": { + "value": 155, + "displayValue": "155" + } + }, + "efficiency": { + "basic": { + "value": 38.75, + "displayValue": "38.75" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 33.25, + "displayValue": "33.25" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 36, + "displayValue": "36.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1246, + "displayValue": "20m 46s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -3445485178621350400, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1244, + "displayValue": "20m 44s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 42435996, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 38, + "displayValue": "38" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.02631578947368421, + "displayValue": "3%" + } + } + } + }, + { + "referenceId": 2905188646, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 4207120603, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 44, + "displayValue": "44" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 1363886209, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 2198166292, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 3211806999, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "100%" + } + } + } + }, + { + "referenceId": 3245446311, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "100%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 15, + "displayValue": "15" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/19bcc057f9c9fb0bfedcfee2a3c0be16.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 6, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018556576248", + "displayName": "KirkActually697", + "bungieGlobalDisplayName": "ChrisActually", + "bungieGlobalDisplayNameCode": 8703 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 118, + "emblemHash": 4183788701 + }, + "characterId": "2305843010764464176", + "values": { + "assists": { + "basic": { + "value": 46, + "displayValue": "46" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "kills": { + "basic": { + "value": 74, + "displayValue": "74" + } + }, + "opponentsDefeated": { + "basic": { + "value": 120, + "displayValue": "120" + } + }, + "efficiency": { + "basic": { + "value": 20, + "displayValue": "20.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 12.333333333333334, + "displayValue": "12.33" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 16.166666666666668, + "displayValue": "16.17" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1246, + "displayValue": "20m 46s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": -3445485178621350400, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 0, + "displayValue": "0m 0s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1246, + "displayValue": "20m 46s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 2697143634, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 48, + "displayValue": "48" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.08333333333333333, + "displayValue": "8%" + } + } + } + }, + { + "referenceId": 4069880346, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 42435996, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 8, + "displayValue": "8" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 3623686757, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + } + ], + "teams": [] +} \ No newline at end of file diff --git a/tests/fixtures/pgcr-missing-bungie-name.json b/tests/fixtures/pgcr-missing-bungie-name.json new file mode 100644 index 0000000..58b926a --- /dev/null +++ b/tests/fixtures/pgcr-missing-bungie-name.json @@ -0,0 +1,3764 @@ +{ + "period": "2026-06-26T15:11:41Z", + "startingPhaseIndex": 0, + "activityWasStartedFromBeginning": false, + "activityDifficultyTier": 0, + "activityDetails": { + "referenceId": 1516551982, + "directorActivityHash": 1516551982, + "instanceId": "16975643976", + "mode": 4, + "modes": [ + 7, + 4 + ], + "isPrivate": false, + "membershipType": 0 + }, + "entries": [ + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018430950974" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 545, + "emblemHash": 690263481 + }, + "characterId": "2305843009261736687", + "values": { + "assists": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 21, + "displayValue": "21" + } + }, + "opponentsDefeated": { + "basic": { + "value": 30, + "displayValue": "30" + } + }, + "efficiency": { + "basic": { + "value": 15, + "displayValue": "15.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 10.5, + "displayValue": "10.50" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 12.75, + "displayValue": "12.75" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 376, + "displayValue": "6m 16s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 357, + "displayValue": "5m 57s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3407395594, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.5263157894736842, + "displayValue": "53%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018452637778" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 548, + "emblemHash": 3919847954 + }, + "characterId": "2305843009265760620", + "values": { + "assists": { + "basic": { + "value": 13, + "displayValue": "13" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 12, + "displayValue": "12" + } + }, + "opponentsDefeated": { + "basic": { + "value": 25, + "displayValue": "25" + } + }, + "efficiency": { + "basic": { + "value": 25, + "displayValue": "25.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 12, + "displayValue": "12.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 18.5, + "displayValue": "18.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 228, + "displayValue": "3m 48s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3647341740, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.2, + "displayValue": "20%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018434018062" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 3888032083 + }, + "characterId": "2305843009269399822", + "values": { + "assists": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 11, + "displayValue": "11" + } + }, + "opponentsDefeated": { + "basic": { + "value": 17, + "displayValue": "17" + } + }, + "efficiency": { + "basic": { + "value": 8.5, + "displayValue": "8.50" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 5.5, + "displayValue": "5.50" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 7, + "displayValue": "7.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 456, + "displayValue": "7m 36s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 269, + "displayValue": "4m 29s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3326135421, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018428476796" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 546, + "emblemHash": 4178714189 + }, + "characterId": "2305843009271105030", + "values": { + "assists": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 16, + "displayValue": "16" + } + }, + "opponentsDefeated": { + "basic": { + "value": 18, + "displayValue": "18" + } + }, + "efficiency": { + "basic": { + "value": 18, + "displayValue": "18.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 16, + "displayValue": "16.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 17, + "displayValue": "17.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 1020, + "displayValue": "17m 0s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 385, + "displayValue": "6m 25s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3413860063, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 16, + "displayValue": "16" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.1875, + "displayValue": "19%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018436710777" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 0, + "lightLevel": 0, + "emblemHash": 0 + }, + "characterId": "2305843009271719945", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 0, + "displayValue": "" + } + }, + "startSeconds": { + "basic": { + "value": 430, + "displayValue": "7m 10s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 12, + "displayValue": "0m 12s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018451197748" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 543, + "emblemHash": 54004488 + }, + "characterId": "2305843009286289179", + "values": { + "assists": { + "basic": { + "value": 14, + "displayValue": "14" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 13, + "displayValue": "13" + } + }, + "opponentsDefeated": { + "basic": { + "value": 27, + "displayValue": "27" + } + }, + "efficiency": { + "basic": { + "value": 27, + "displayValue": "27.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 13, + "displayValue": "13.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 20, + "displayValue": "20.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 247, + "displayValue": "4m 7s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 859869931, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.2857142857142857, + "displayValue": "29%" + } + } + } + }, + { + "referenceId": 4019651319, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018473875915" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 543, + "emblemHash": 3888032083 + }, + "characterId": "2305843009325624769", + "values": { + "assists": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "opponentsDefeated": { + "basic": { + "value": 8, + "displayValue": "8" + } + }, + "efficiency": { + "basic": { + "value": 8, + "displayValue": "8.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 4, + "displayValue": "4.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 6, + "displayValue": "6.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 219, + "displayValue": "3m 39s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 2111625436, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018486712448" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 0, + "emblemHash": 788073490 + }, + "characterId": "2305843009423796145", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 13, + "displayValue": "13" + } + }, + "opponentsDefeated": { + "basic": { + "value": 13, + "displayValue": "13" + } + }, + "efficiency": { + "basic": { + "value": 13, + "displayValue": "13.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 13, + "displayValue": "13.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 13, + "displayValue": "13.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 1110, + "displayValue": "18m 30s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 308, + "displayValue": "5m 8s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3961462214, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.25, + "displayValue": "25%" + } + } + } + }, + { + "referenceId": 3285784871, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.25, + "displayValue": "25%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018452495462" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 0, + "lightLevel": 0, + "emblemHash": 0 + }, + "characterId": "2305843009469094139", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 0, + "displayValue": "" + } + }, + "startSeconds": { + "basic": { + "value": 402, + "displayValue": "6m 42s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1, + "displayValue": "0m 1s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018488805347" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 543, + "emblemHash": 298334056 + }, + "characterId": "2305843009489095266", + "values": { + "assists": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "opponentsDefeated": { + "basic": { + "value": 17, + "displayValue": "17" + } + }, + "efficiency": { + "basic": { + "value": 17, + "displayValue": "17.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 7, + "displayValue": "7.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 12, + "displayValue": "12.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 2, + "displayValue": "0m 2s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 206, + "displayValue": "3m 26s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 4289226715, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.3333333333333333, + "displayValue": "33%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018499052863" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 1661191192 + }, + "characterId": "2305843009698594810", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 38, + "displayValue": "38" + } + }, + "opponentsDefeated": { + "basic": { + "value": 38, + "displayValue": "38" + } + }, + "efficiency": { + "basic": { + "value": 38, + "displayValue": "38.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 38, + "displayValue": "38.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 38, + "displayValue": "38.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 955, + "displayValue": "15m 55s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 352, + "displayValue": "5m 52s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 2140635451, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 26, + "displayValue": "26" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 3725585710, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 2298039571, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018494052601" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 543, + "emblemHash": 707041059 + }, + "characterId": "2305843009891714279", + "values": { + "assists": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "opponentsDefeated": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "efficiency": { + "basic": { + "value": 6, + "displayValue": "6.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 3, + "displayValue": "3.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 4.5, + "displayValue": "4.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 961, + "displayValue": "16m 1s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 423, + "displayValue": "7m 3s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018439434003" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 545, + "emblemHash": 3888032092 + }, + "characterId": "2305843010150764320", + "values": { + "assists": { + "basic": { + "value": 8, + "displayValue": "8" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 24, + "displayValue": "24" + } + }, + "opponentsDefeated": { + "basic": { + "value": 32, + "displayValue": "32" + } + }, + "efficiency": { + "basic": { + "value": 16, + "displayValue": "16.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 12, + "displayValue": "12.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 14, + "displayValue": "14.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 382, + "displayValue": "6m 22s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 353, + "displayValue": "5m 53s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3736001860, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 17, + "displayValue": "17" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 3211624072, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "100%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018479746031" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 543, + "emblemHash": 2770607178 + }, + "characterId": "2305843010152994261", + "values": { + "assists": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "opponentsDefeated": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "efficiency": { + "basic": { + "value": 6, + "displayValue": "6.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 3, + "displayValue": "3.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 4.5, + "displayValue": "4.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 1068, + "displayValue": "17m 48s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 332, + "displayValue": "5m 32s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 1041028434, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.3333333333333333, + "displayValue": "33%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018436426616" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 548, + "emblemHash": 707041059 + }, + "characterId": "2305843010441474057", + "values": { + "assists": { + "basic": { + "value": 45, + "displayValue": "45" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "kills": { + "basic": { + "value": 50, + "displayValue": "50" + } + }, + "opponentsDefeated": { + "basic": { + "value": 95, + "displayValue": "95" + } + }, + "efficiency": { + "basic": { + "value": 15.833333333333334, + "displayValue": "15.83" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 8.333333333333334, + "displayValue": "8.33" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 12.083333333333334, + "displayValue": "12.08" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 0, + "displayValue": "0m 0s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1702, + "displayValue": "28m 22s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 2708806099, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 28, + "displayValue": "28" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 8, + "displayValue": "8" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.2857142857142857, + "displayValue": "29%" + } + } + } + }, + { + "referenceId": 2366022261, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "kills": { + "basic": { + "value": 12, + "displayValue": "12" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018532088071" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 545, + "emblemHash": 298334061 + }, + "characterId": "2305843010531094103", + "values": { + "assists": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 17, + "displayValue": "17" + } + }, + "opponentsDefeated": { + "basic": { + "value": 27, + "displayValue": "27" + } + }, + "efficiency": { + "basic": { + "value": 13.5, + "displayValue": "13.50" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 8.5, + "displayValue": "8.50" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 11, + "displayValue": "11.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 294, + "displayValue": "4m 54s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 415, + "displayValue": "6m 55s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 214545213, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 12, + "displayValue": "12" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018511194064" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 0, + "lightLevel": 0, + "emblemHash": 0 + }, + "characterId": "2305843010552655045", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 0, + "displayValue": "" + } + }, + "startSeconds": { + "basic": { + "value": 1030, + "displayValue": "17m 10s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 27, + "displayValue": "0m 27s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018554475758" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 543, + "emblemHash": 3888032095 + }, + "characterId": "2305843010709554387", + "values": { + "assists": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "opponentsDefeated": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "efficiency": { + "basic": { + "value": 5, + "displayValue": "5.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 3, + "displayValue": "3.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 4, + "displayValue": "4.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 4, + "displayValue": "0m 4s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 209, + "displayValue": "3m 29s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3245446311, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "crossSaveOverride": 0, + "isPublic": false, + "membershipType": 0, + "membershipId": "4611686018463558704" + }, + "classHash": 0, + "raceHash": 0, + "genderHash": 0, + "characterLevel": 50, + "lightLevel": 545, + "emblemHash": 3888032083 + }, + "characterId": "2305843010781834041", + "values": { + "assists": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "opponentsDefeated": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "efficiency": { + "basic": { + "value": 9, + "displayValue": "9.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 6, + "displayValue": "6.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 7.5, + "displayValue": "7.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1734, + "displayValue": "28m 54s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 3911111240297402000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 394, + "displayValue": "6m 34s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 306, + "displayValue": "5m 6s" + } + }, + "playerCount": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 193009988, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + } + ], + "teams": [] +} \ No newline at end of file diff --git a/tests/fixtures/pgcr-multi-character-garden.json b/tests/fixtures/pgcr-multi-character-garden.json new file mode 100644 index 0000000..2face8f --- /dev/null +++ b/tests/fixtures/pgcr-multi-character-garden.json @@ -0,0 +1,1120 @@ +{ + "period": "2026-07-26T19:25:17Z", + "startingPhaseIndex": 0, + "activityWasStartedFromBeginning": true, + "activityDifficultyTier": -1, + "activityDetails": { + "referenceId": 1042180643, + "directorActivityHash": 1042180643, + "instanceId": "17091200569", + "mode": 4, + "modes": [ + 7, + 4 + ], + "isPrivate": false, + "membershipType": 0 + }, + "entries": [ + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/3d3f3a5b8a73880956d13f31ee544e3a.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018462874397", + "displayName": "MistaHates4s", + "bungieGlobalDisplayName": "Oryx's Taken Glue", + "bungieGlobalDisplayNameCode": 9062 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 479, + "emblemHash": 2026109716 + }, + "characterId": "2305843009283987028", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 2069, + "displayValue": "34m 29s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -4671784694761834000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 789, + "displayValue": "13m 9s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 981, + "displayValue": "16m 21s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/fb7cf21487dffd363ec02127c4993346.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018462874397", + "displayName": "MistaHates4s", + "bungieGlobalDisplayName": "Oryx's Taken Glue", + "bungieGlobalDisplayNameCode": 9062 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 3887404748, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 543, + "emblemHash": 46275857 + }, + "characterId": "2305843009320728729", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 2069, + "displayValue": "34m 29s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -4671784694761834000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 543, + "displayValue": "9m 3s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1494, + "displayValue": "24m 54s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/da283135842de6983df5ab6c595ce428.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 3, + 6, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018473876278", + "displayName": "GiornoDrinkT", + "bungieGlobalDisplayName": "Savathun's Onlyfans", + "bungieGlobalDisplayNameCode": 2533 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 543, + "emblemHash": 787024997 + }, + "characterId": "2305843009325894788", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 2069, + "displayValue": "34m 29s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -4671784694761834000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 889, + "displayValue": "14m 49s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 557, + "displayValue": "9m 17s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/18dc6c427a49dc411027a22b413b8b7c.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 3, + 6, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018473876278", + "displayName": "GiornoDrinkT", + "bungieGlobalDisplayName": "Savathun's Onlyfans", + "bungieGlobalDisplayNameCode": 2533 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 475, + "emblemHash": 298334061 + }, + "characterId": "2305843009675195762", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 2069, + "displayValue": "34m 29s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -4671784694761834000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 684, + "displayValue": "11m 24s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 988, + "displayValue": "16m 28s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/4d07bd0a923964dd344ec238776460f1.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018462874397", + "displayName": "MistaHates4s", + "bungieGlobalDisplayName": "Oryx's Taken Glue", + "bungieGlobalDisplayNameCode": 9062 + }, + "characterClass": "Titan", + "classHash": 3655393761, + "raceHash": 3887404748, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 451, + "emblemHash": 2565108496 + }, + "characterId": "2305843009678825697", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 2069, + "displayValue": "34m 29s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -4671784694761834000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 995, + "displayValue": "16m 35s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 549, + "displayValue": "9m 9s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/e270170e30415d608ea6a4901bd84495.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 3, + 6, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018473876278", + "displayName": "GiornoDrinkT", + "bungieGlobalDisplayName": "Savathun's Onlyfans", + "bungieGlobalDisplayNameCode": 2533 + }, + "characterClass": "Titan", + "classHash": 3655393761, + "raceHash": 3887404748, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 538, + "emblemHash": 1230660640 + }, + "characterId": "2305843009918354004", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 25, + "displayValue": "25" + } + }, + "opponentsDefeated": { + "basic": { + "value": 25, + "displayValue": "25" + } + }, + "efficiency": { + "basic": { + "value": 25, + "displayValue": "25.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 25, + "displayValue": "25.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 25, + "displayValue": "25.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 2069, + "displayValue": "34m 29s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -4671784694761834000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 0, + "displayValue": "0m 0s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1916, + "displayValue": "31m 56s" + } + }, + "playerCount": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 2965080304, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 12, + "displayValue": "12" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + } + ], + "teams": [] +} \ No newline at end of file diff --git a/tests/fixtures/pgcr-non-raid.json b/tests/fixtures/pgcr-non-raid.json new file mode 100644 index 0000000..1d9a3b8 --- /dev/null +++ b/tests/fixtures/pgcr-non-raid.json @@ -0,0 +1,906 @@ +{ + "period": "2026-07-26T20:45:34Z", + "startingPhaseIndex": 0, + "activityWasStartedFromBeginning": true, + "activityDifficultyTier": 9, + "selectedSkullHashes": [ + 3830595819, + 550488408, + 2468650119, + 510222748, + 295800916, + 1701682929, + 295800916, + 295800916, + 295800916, + 295800916, + 3105737563, + 1903043004, + 3591337835, + 1042708060, + 3076476604, + 4147307117, + 295800916, + 295800916, + 295800916, + 449004569, + 295800916, + 295800916, + 3830595819, + 550488408, + 2468650119, + 510222748 + ], + "activityDetails": { + "referenceId": 935938264, + "directorActivityHash": 935938264, + "instanceId": "17091392014", + "mode": 3, + "modes": [ + 7, + 3, + 18 + ], + "isPrivate": false, + "membershipType": 1 + }, + "entries": [ + { + "standing": 0, + "score": { + "basic": { + "value": 15909, + "displayValue": "15,909" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/2eb4d5248601cee5a31eeb4404413345.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 2, + 3, + 5, + 6, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018463396679", + "displayName": "Axell776", + "bungieGlobalDisplayName": "Axell776", + "bungieGlobalDisplayNameCode": 9816 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 707041070 + }, + "characterId": "2305843009267324081", + "values": { + "assists": { + "basic": { + "value": 24, + "displayValue": "24" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 72, + "displayValue": "72" + } + }, + "opponentsDefeated": { + "basic": { + "value": 96, + "displayValue": "96" + } + }, + "efficiency": { + "basic": { + "value": 96, + "displayValue": "96.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 72, + "displayValue": "72.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 84, + "displayValue": "84.00" + } + }, + "score": { + "basic": { + "value": 15909, + "displayValue": "15,909" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1063, + "displayValue": "17m 43s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": 7834403886865681000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 0, + "displayValue": "0m 0s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1063, + "displayValue": "17m 43s" + } + }, + "playerCount": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "teamScore": { + "basic": { + "value": 10909, + "displayValue": "10,909" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 1018012078, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 27, + "displayValue": "27" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.3333333333333333, + "displayValue": "33%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 30, + "displayValue": "30" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "assists": { + "basic": { + "value": 24, + "displayValue": "24" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 72, + "displayValue": "72" + } + }, + "score": { + "basic": { + "value": 15909.0673828125, + "displayValue": "15,909" + } + }, + "partial_score": { + "basic": { + "value": 10909.0673828125, + "displayValue": "10,909" + } + }, + "display_total_multiplier": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "display_team_multiplier": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "display_time_bonus_points": { + "basic": { + "value": 909.0673828125, + "displayValue": "909" + } + }, + "performance_grade_multiplier": { + "basic": { + "value": 1, + "displayValue": "1" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 15909, + "displayValue": "15,909" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/2ebb5872f93aec5b5b74cb407ad69913.jpg", + "crossSaveOverride": 2, + "applicableMembershipTypes": [ + 1, + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018447808357", + "displayName": "Mr_Kelevra_", + "bungieGlobalDisplayName": "Kelevra IX", + "bungieGlobalDisplayNameCode": 8817 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 545, + "emblemHash": 723818653 + }, + "characterId": "2305843009279218509", + "values": { + "assists": { + "basic": { + "value": 25, + "displayValue": "25" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 50, + "displayValue": "50" + } + }, + "opponentsDefeated": { + "basic": { + "value": 75, + "displayValue": "75" + } + }, + "efficiency": { + "basic": { + "value": 75, + "displayValue": "75.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 50, + "displayValue": "50.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 62.5, + "displayValue": "62.50" + } + }, + "score": { + "basic": { + "value": 15909, + "displayValue": "15,909" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1063, + "displayValue": "17m 43s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": 7834403886865681000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 4, + "displayValue": "0m 4s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1059, + "displayValue": "17m 39s" + } + }, + "playerCount": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "teamScore": { + "basic": { + "value": 10909, + "displayValue": "10,909" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 2188764214, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.4, + "displayValue": "40%" + } + } + } + }, + { + "referenceId": 3176697588, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.3157894736842105, + "displayValue": "32%" + } + } + } + }, + { + "referenceId": 954563454, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.2, + "displayValue": "20%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "assists": { + "basic": { + "value": 25, + "displayValue": "25" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 50, + "displayValue": "50" + } + }, + "score": { + "basic": { + "value": 15909.0673828125, + "displayValue": "15,909" + } + }, + "partial_score": { + "basic": { + "value": 10909.0673828125, + "displayValue": "10,909" + } + }, + "display_total_multiplier": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "display_team_multiplier": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "display_time_bonus_points": { + "basic": { + "value": 909.0673828125, + "displayValue": "909" + } + }, + "performance_grade_multiplier": { + "basic": { + "value": 1, + "displayValue": "1" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 15909, + "displayValue": "15,909" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/c47a032605d0fe2d20f81b4d79c8e8e1.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 3, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018537946823", + "displayName": "Morgoth2405", + "bungieGlobalDisplayName": "Morgoth2405", + "bungieGlobalDisplayNameCode": 2529 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 2803282938, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 545, + "emblemHash": 690263481 + }, + "characterId": "2305843010621614327", + "values": { + "assists": { + "basic": { + "value": 27, + "displayValue": "27" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 79, + "displayValue": "79" + } + }, + "opponentsDefeated": { + "basic": { + "value": 106, + "displayValue": "106" + } + }, + "efficiency": { + "basic": { + "value": 106, + "displayValue": "106.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 79, + "displayValue": "79.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 92.5, + "displayValue": "92.50" + } + }, + "score": { + "basic": { + "value": 15909, + "displayValue": "15,909" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 1063, + "displayValue": "17m 43s" + } + }, + "completionReason": { + "basic": { + "value": 0, + "displayValue": "Objective Completed" + } + }, + "fireteamId": { + "basic": { + "value": 7834403886865681000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 4, + "displayValue": "0m 4s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 1059, + "displayValue": "17m 39s" + } + }, + "playerCount": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "teamScore": { + "basic": { + "value": 10909, + "displayValue": "10,909" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 71057630, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.16666666666666666, + "displayValue": "17%" + } + } + } + }, + { + "referenceId": 1435808083, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 17, + "displayValue": "17" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.17647058823529413, + "displayValue": "18%" + } + } + } + }, + { + "referenceId": 42435996, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.3333333333333333, + "displayValue": "33%" + } + } + } + }, + { + "referenceId": 2069224589, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 19, + "displayValue": "19" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 8, + "displayValue": "8" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "assists": { + "basic": { + "value": 27, + "displayValue": "27" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 79, + "displayValue": "79" + } + }, + "score": { + "basic": { + "value": 15909.0673828125, + "displayValue": "15,909" + } + }, + "partial_score": { + "basic": { + "value": 10909.0673828125, + "displayValue": "10,909" + } + }, + "display_total_multiplier": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "display_team_multiplier": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "display_time_bonus_points": { + "basic": { + "value": 909.0673828125, + "displayValue": "909" + } + }, + "performance_grade_multiplier": { + "basic": { + "value": 1, + "displayValue": "1" + } + } + } + } + } + ], + "teams": [] +} \ No newline at end of file diff --git a/tests/fixtures/pgcr-partial-completion-last-wish.json b/tests/fixtures/pgcr-partial-completion-last-wish.json new file mode 100644 index 0000000..7ab02e8 --- /dev/null +++ b/tests/fixtures/pgcr-partial-completion-last-wish.json @@ -0,0 +1,377 @@ +{ + "period": "2026-07-26T20:17:19Z", + "startingPhaseIndex": 0, + "activityWasStartedFromBeginning": true, + "activityDifficultyTier": -1, + "activityDetails": { + "referenceId": 2122313384, + "directorActivityHash": 2122313384, + "instanceId": "17091283535", + "mode": 4, + "modes": [ + 7, + 4 + ], + "isPrivate": false, + "membershipType": 0 + }, + "entries": [ + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/78a94beb470d9b8d41e82972f5999c9f.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018526091664", + "displayName": "venomeatsheads2", + "bungieGlobalDisplayName": "Xivu’s mushroom stamp", + "bungieGlobalDisplayNameCode": 1246 + }, + "characterClass": "Titan", + "classHash": 3655393761, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 518, + "emblemHash": 707041058 + }, + "characterId": "2305843010153394212", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 594, + "displayValue": "9m 54s" + } + }, + "completionReason": { + "basic": { + "value": 2, + "displayValue": "Failed" + } + }, + "fireteamId": { + "basic": { + "value": -8456771659447240000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 309, + "displayValue": "5m 9s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 236, + "displayValue": "3m 56s" + } + }, + "playerCount": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/5cb593fe1e4dc2913b8f764390d38f2c.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018525602185", + "displayName": "Babydino2008", + "bungieGlobalDisplayName": "savathun dumper", + "bungieGlobalDisplayNameCode": 5847 + }, + "characterClass": "Titan", + "classHash": 3655393761, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 465, + "emblemHash": 2770607178 + }, + "characterId": "2305843010308384088", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 1, + "displayValue": "Yes" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 594, + "displayValue": "9m 54s" + } + }, + "completionReason": { + "basic": { + "value": 2, + "displayValue": "Failed" + } + }, + "fireteamId": { + "basic": { + "value": -8456771659447240000, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 0, + "displayValue": "0m 0s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 594, + "displayValue": "9m 54s" + } + }, + "playerCount": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + } + ], + "teams": [] +} \ No newline at end of file diff --git a/tests/fixtures/pgcr-zero-completions-vault-of-glass.json b/tests/fixtures/pgcr-zero-completions-vault-of-glass.json new file mode 100644 index 0000000..3401080 --- /dev/null +++ b/tests/fixtures/pgcr-zero-completions-vault-of-glass.json @@ -0,0 +1,1683 @@ +{ + "period": "2026-07-26T21:21:21Z", + "startingPhaseIndex": 0, + "activityWasStartedFromBeginning": false, + "activityDifficultyTier": -1, + "activityDetails": { + "referenceId": 3022541210, + "directorActivityHash": 3022541210, + "instanceId": "17091467640", + "mode": 4, + "modes": [ + 7, + 4 + ], + "isPrivate": false, + "membershipType": 3 + }, + "entries": [ + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/19bcc057f9c9fb0bfedcfee2a3c0be16.jpg", + "crossSaveOverride": 2, + "applicableMembershipTypes": [ + 3, + 6, + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018453427950", + "displayName": "nukeguy2019", + "bungieGlobalDisplayName": "Paradox", + "bungieGlobalDisplayNameCode": 5045 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 898834093, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 4183788701 + }, + "characterId": "2305843009260351366", + "values": { + "assists": { + "basic": { + "value": 28, + "displayValue": "28" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 94, + "displayValue": "94" + } + }, + "opponentsDefeated": { + "basic": { + "value": 122, + "displayValue": "122" + } + }, + "efficiency": { + "basic": { + "value": 122, + "displayValue": "122.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 94, + "displayValue": "94.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 108, + "displayValue": "108.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 610, + "displayValue": "10m 10s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -395136662285001150, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 47, + "displayValue": "0m 47s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 510, + "displayValue": "8m 30s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 1111334348, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.5, + "displayValue": "50%" + } + } + } + }, + { + "referenceId": 1715391576, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 27, + "displayValue": "27" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/e348acdfbc0bbd3fe0849df1afe26d35.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 2, + 3, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018431673763", + "displayName": "Vital", + "bungieGlobalDisplayName": "techwood", + "bungieGlobalDisplayNameCode": 1879 + }, + "characterClass": "Warlock", + "classHash": 2271682572, + "raceHash": 2803282938, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 4011836831 + }, + "characterId": "2305843009271037958", + "values": { + "assists": { + "basic": { + "value": 4, + "displayValue": "4" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 86, + "displayValue": "86" + } + }, + "opponentsDefeated": { + "basic": { + "value": 90, + "displayValue": "90" + } + }, + "efficiency": { + "basic": { + "value": 90, + "displayValue": "90.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 86, + "displayValue": "86.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 88, + "displayValue": "88.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 610, + "displayValue": "10m 10s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -395136662285001150, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 4, + "displayValue": "0m 4s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 535, + "displayValue": "8m 55s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3285784871, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 6, + "displayValue": "6" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 4230965989, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.2, + "displayValue": "20%" + } + } + } + }, + { + "referenceId": 3698448090, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 40, + "displayValue": "40" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.25, + "displayValue": "25%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 11, + "displayValue": "11" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 15, + "displayValue": "15" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/e0d1cde8ffe0012424afa6b352b9954d.jpg", + "crossSaveOverride": 0, + "applicableMembershipTypes": [ + 3 + ], + "isPublic": true, + "membershipType": 3, + "membershipId": "4611686018471085014", + "displayName": "Americon", + "bungieGlobalDisplayName": "Americon", + "bungieGlobalDisplayNameCode": 9379 + }, + "characterClass": "Hunter", + "classHash": 671679327, + "raceHash": 2803282938, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 550, + "emblemHash": 3919847960 + }, + "characterId": "2305843009369429351", + "values": { + "assists": { + "basic": { + "value": 17, + "displayValue": "17" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 62, + "displayValue": "62" + } + }, + "opponentsDefeated": { + "basic": { + "value": 79, + "displayValue": "79" + } + }, + "efficiency": { + "basic": { + "value": 79, + "displayValue": "79.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 62, + "displayValue": "62.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 70.5, + "displayValue": "70.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 610, + "displayValue": "10m 10s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -395136662285001150, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 65, + "displayValue": "1m 5s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 513, + "displayValue": "8m 33s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3146657388, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 34, + "displayValue": "34" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.29411764705882354, + "displayValue": "29%" + } + } + } + }, + { + "referenceId": 839786290, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 1111334348, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 10, + "displayValue": "10" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/c362efc1d99ecd70b9df07b3b00623c3.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 2, + 3, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018433920495", + "displayName": "Jachrispyy", + "bungieGlobalDisplayName": "Jachrispy", + "bungieGlobalDisplayNameCode": 4529 + }, + "characterClass": "Titan", + "classHash": 3655393761, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 548, + "emblemHash": 2770607176 + }, + "characterId": "2305843009529435267", + "values": { + "assists": { + "basic": { + "value": 43, + "displayValue": "43" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 143, + "displayValue": "143" + } + }, + "opponentsDefeated": { + "basic": { + "value": 186, + "displayValue": "186" + } + }, + "efficiency": { + "basic": { + "value": 186, + "displayValue": "186.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 143, + "displayValue": "143.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 164.5, + "displayValue": "164.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 610, + "displayValue": "10m 10s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -395136662285001150, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 4, + "displayValue": "0m 4s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 573, + "displayValue": "9m 33s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 42435996, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 104, + "displayValue": "104" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.019230769230769232, + "displayValue": "2%" + } + } + } + }, + { + "referenceId": 1802315656, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 13, + "displayValue": "13" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 2812324400, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/3cb129a210c036bcf13fb77de24aa0fa.jpg", + "crossSaveOverride": 3, + "applicableMembershipTypes": [ + 2, + 5, + 6, + 3 + ], + "isPublic": true, + "membershipType": 3, + "membershipId": "4611686018497674398", + "displayName": "Potato", + "bungieGlobalDisplayName": "Potato", + "bungieGlobalDisplayNameCode": 9715 + }, + "characterClass": "Titan", + "classHash": 3655393761, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 548, + "emblemHash": 3079989875 + }, + "characterId": "2305843009623275804", + "values": { + "assists": { + "basic": { + "value": 9, + "displayValue": "9" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 87, + "displayValue": "87" + } + }, + "opponentsDefeated": { + "basic": { + "value": 96, + "displayValue": "96" + } + }, + "efficiency": { + "basic": { + "value": 96, + "displayValue": "96.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 87, + "displayValue": "87.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 91.5, + "displayValue": "91.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 610, + "displayValue": "10m 10s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -395136662285001150, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 5, + "displayValue": "0m 5s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 573, + "displayValue": "9m 33s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 3549153978, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 68, + "displayValue": "68" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 3285784871, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.2, + "displayValue": "20%" + } + } + } + }, + { + "referenceId": 593808239, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 3, + "displayValue": "3" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.3333333333333333, + "displayValue": "33%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/61f8687b3b68d9c9dc5e734d21f7d24f.jpg", + "crossSaveOverride": 1, + "applicableMembershipTypes": [ + 2, + 3, + 1 + ], + "isPublic": true, + "membershipType": 1, + "membershipId": "4611686018435633288", + "displayName": "blaknite4477", + "bungieGlobalDisplayName": "blaknite4477", + "bungieGlobalDisplayNameCode": 2597 + }, + "characterClass": "Titan", + "classHash": 3655393761, + "raceHash": 898834093, + "genderHash": 3111576190, + "characterLevel": 50, + "lightLevel": 545, + "emblemHash": 3267552999 + }, + "characterId": "2305843009890475407", + "values": { + "assists": { + "basic": { + "value": 13, + "displayValue": "13" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 53, + "displayValue": "53" + } + }, + "opponentsDefeated": { + "basic": { + "value": 66, + "displayValue": "66" + } + }, + "efficiency": { + "basic": { + "value": 66, + "displayValue": "66.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 53, + "displayValue": "53.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 59.5, + "displayValue": "59.50" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 610, + "displayValue": "10m 10s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": -395136662285001150, + "displayValue": "-2147483648" + } + }, + "startSeconds": { + "basic": { + "value": 4, + "displayValue": "0m 4s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 573, + "displayValue": "9m 33s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "weapons": [ + { + "referenceId": 334964261, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 33, + "displayValue": "33" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 12, + "displayValue": "12" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0.36363636363636365, + "displayValue": "36%" + } + } + } + }, + { + "referenceId": 2069224589, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 2, + "displayValue": "2" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + }, + { + "referenceId": 3377522331, + "values": { + "uniqueWeaponKills": { + "basic": { + "value": 5, + "displayValue": "5" + } + }, + "uniqueWeaponPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "uniqueWeaponKillsPrecisionKills": { + "basic": { + "value": 0, + "displayValue": "0%" + } + } + } + } + ], + "values": { + "precisionKills": { + "basic": { + "value": 12, + "displayValue": "12" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 11, + "displayValue": "11" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 1, + "displayValue": "1" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + }, + { + "standing": 0, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "player": { + "destinyUserInfo": { + "iconPath": "/common/destiny2_content/icons/31d09629d4761a859f86d232936a907d.jpg", + "crossSaveOverride": 2, + "applicableMembershipTypes": [ + 3, + 6, + 2 + ], + "isPublic": true, + "membershipType": 2, + "membershipId": "4611686018453427950", + "displayName": "nukeguy2019", + "bungieGlobalDisplayName": "Paradox", + "bungieGlobalDisplayNameCode": 5045 + }, + "characterClass": "Titan", + "classHash": 3655393761, + "raceHash": 2803282938, + "genderHash": 2204441813, + "characterLevel": 50, + "lightLevel": 1582, + "emblemHash": 1918663075 + }, + "characterId": "2305843009986754242", + "values": { + "assists": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "completed": { + "basic": { + "value": 0, + "displayValue": "No" + } + }, + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "opponentsDefeated": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "efficiency": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsRatio": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "killsDeathsAssists": { + "basic": { + "value": 0, + "displayValue": "0.00" + } + }, + "score": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "activityDurationSeconds": { + "basic": { + "value": 610, + "displayValue": "10m 10s" + } + }, + "completionReason": { + "basic": { + "value": 255, + "displayValue": "Unknown" + } + }, + "fireteamId": { + "basic": { + "value": 0, + "displayValue": "" + } + }, + "startSeconds": { + "basic": { + "value": 0, + "displayValue": "0m 0s" + } + }, + "timePlayedSeconds": { + "basic": { + "value": 25, + "displayValue": "0m 25s" + } + }, + "playerCount": { + "basic": { + "value": 7, + "displayValue": "7" + } + }, + "teamScore": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "extended": { + "values": { + "precisionKills": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsGrenade": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsMelee": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsSuper": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "weaponKillsAbility": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + }, + "scoreboardValues": { + "deaths": { + "basic": { + "value": 0, + "displayValue": "0" + } + }, + "kills": { + "basic": { + "value": 0, + "displayValue": "0" + } + } + } + } + } + ], + "teams": [] +} \ No newline at end of file diff --git a/tests/helpers/db.ts b/tests/helpers/db.ts new file mode 100644 index 0000000..5af300e --- /dev/null +++ b/tests/helpers/db.ts @@ -0,0 +1,35 @@ +import type Database from 'better-sqlite3'; +import { getDb } from '@/lib/db'; + +/** + * Access to the per-file test database. + * + * The database path is set by tests/setup/test-db-path.ts before this file's + * imports run, so `getDb()` here opens a throwaway database in a temp directory + * with the production schema already applied — initializeSchema() runs inside + * getDb(), including the ended_at migration guard and the Phase 3 indexes. The + * schema under test is therefore the production schema by construction rather + * than a duplicated copy that can drift. + */ + +export function testDb(): Database.Database { + return getDb(); +} + +/** + * Empties every table, leaving the schema intact. Call in beforeEach so tests + * share one connection but never share rows — reopening the database per test + * would re-run schema init for no benefit. + * + * Order respects the pgcr_players -> pgcrs foreign key. + */ +export function resetTestDb(): void { + const db = getDb(); + db.exec(` + DELETE FROM pgcr_players; + DELETE FROM pgcrs; + DELETE FROM players; + DELETE FROM active_sessions; + DELETE FROM crawler_state; + `); +} diff --git a/tests/helpers/pgcr-builder.ts b/tests/helpers/pgcr-builder.ts new file mode 100644 index 0000000..973aa08 --- /dev/null +++ b/tests/helpers/pgcr-builder.ts @@ -0,0 +1,170 @@ +import type { + DestinyHistoricalStatsValue, + DestinyPostGameCarnageReportData, + DestinyPostGameCarnageReportEntry, +} from '@/lib/bungie/types'; + +/** + * Programmatic PGCR builders. + * + * Fixtures for realism, builders for permutation. When a test needs one specific + * field varied — a duration removed, a single entry's completion flipped — reach + * for a builder. When a test needs to prove we handle what Bungie actually sends, + * reach for a fixture in ../fixtures. + * + * Defaults describe a clean six-player full clear, so every builder call states + * only what makes its case interesting. + */ + +/** Salvation's Edge. Any hash in manifest.ts's RAID_DEFINITIONS works. */ +export const RAID_HASH = 2192826039; + +/** Not present in RAID_DEFINITIONS, so isRaidActivityHash rejects it. */ +export const NON_RAID_HASH = 1; + +function stat(value: number): DestinyHistoricalStatsValue { + return { basic: { value, displayValue: String(value) } }; +} + +export interface EntryOptions { + membershipId?: string; + membershipType?: number; + displayName?: string; + /** Pass `null` to omit the field entirely — the case where Bungie withholds it. */ + bungieGlobalDisplayName?: string | null; + bungieGlobalDisplayNameCode?: number | null; + characterClass?: string; + lightLevel?: number; + completed?: boolean; + kills?: number; + deaths?: number; + assists?: number; + timePlayedSeconds?: number; + /** Per-player join offset. Pass `null` to omit, collapsing Tier 2 to MAX(timePlayed). */ + startSeconds?: number | null; + /** Activity-level duration. Pass `null` to omit, forcing the Tier 2 fallback. */ + activityDurationSeconds?: number | null; +} + +export function buildEntry(options: EntryOptions = {}): DestinyPostGameCarnageReportEntry { + const { + membershipId = '4611686018400000001', + membershipType = 3, + displayName = 'Guardian', + bungieGlobalDisplayName = 'Guardian', + bungieGlobalDisplayNameCode = 1234, + characterClass = 'Warlock', + lightLevel = 2010, + completed = true, + kills = 100, + deaths = 2, + assists = 40, + timePlayedSeconds = 1800, + startSeconds = 0, + activityDurationSeconds = 1800, + } = options; + + const values: Record = { + completed: stat(completed ? 1 : 0), + kills: stat(kills), + deaths: stat(deaths), + assists: stat(assists), + timePlayedSeconds: stat(timePlayedSeconds), + }; + + // Omitted rather than zeroed: the readers distinguish absent from 0, and an + // absent activityDurationSeconds is what drives the Tier 2 fallback. + if (startSeconds !== null) { + values.startSeconds = stat(startSeconds); + } + if (activityDurationSeconds !== null) { + values.activityDurationSeconds = stat(activityDurationSeconds); + } + + return { + standing: 0, + player: { + destinyUserInfo: { + membershipId, + membershipType, + displayName, + ...(bungieGlobalDisplayName !== null ? { bungieGlobalDisplayName } : {}), + ...(bungieGlobalDisplayNameCode !== null ? { bungieGlobalDisplayNameCode } : {}), + }, + characterClass, + characterLevel: 50, + lightLevel, + }, + values, + }; +} + +export interface PGCROptions { + instanceId?: string; + activityHash?: number; + /** Set independently of directorActivityHash to test the referenceId fallback. */ + referenceId?: number; + /** ISO 8601. Bungie reports UTC. */ + period?: string; + activityWasStartedFromBeginning?: boolean; + /** + * Bungie reports this as 0 on every real PGCR, including checkpoint runs, so it + * no longer distinguishes anything. Pass `null` to omit it entirely; pass a + * number only when testing the legacy path. + */ + startingPhaseIndex?: number | null; + entries?: DestinyPostGameCarnageReportEntry[]; +} + +export function buildPGCR(options: PGCROptions = {}): DestinyPostGameCarnageReportData { + const { + instanceId = '17091392013', + activityHash = RAID_HASH, + referenceId = activityHash, + period = '2026-07-26T12:00:00Z', + activityWasStartedFromBeginning = true, + startingPhaseIndex = null, + entries = buildFireteam(), + } = options; + + const pgcr = { + period, + activityWasStartedFromBeginning, + activityDetails: { + referenceId, + directorActivityHash: activityHash, + instanceId, + mode: 4, + modes: [4], + }, + entries, + } as DestinyPostGameCarnageReportData; + + if (startingPhaseIndex !== null) { + pgcr.startingPhaseIndex = startingPhaseIndex; + } + + return pgcr; +} + +/** + * Six distinct players. `completions` sets how many finished, counting from the + * first entry — so `buildFireteam({ completions: 1 })` is the "only one of six + * completed" case. + */ +export function buildFireteam( + options: { size?: number; completions?: number; entry?: EntryOptions } = {} +): DestinyPostGameCarnageReportEntry[] { + const { size = 6, completions = size, entry = {} } = options; + + return Array.from({ length: size }, (_, index) => + buildEntry({ + membershipId: `461168601840000000${index + 1}`, + displayName: `Guardian${index + 1}`, + bungieGlobalDisplayName: `Guardian${index + 1}`, + bungieGlobalDisplayNameCode: 1000 + index, + completed: index < completions, + ...entry, + }) + ); +} diff --git a/tests/helpers/seed.ts b/tests/helpers/seed.ts new file mode 100644 index 0000000..c6c709c --- /dev/null +++ b/tests/helpers/seed.ts @@ -0,0 +1,167 @@ +import { getDb } from '@/lib/db'; +import { insertFullPGCR, upsertPlayer } from '@/lib/db/queries'; +import { getRaidKeyFromHash } from '@/lib/bungie/manifest'; +import { processPGCR } from '@/lib/crawler/pgcr'; +import { readActivityDurationSeconds, readEntryStartSeconds } from '@/lib/bungie/pgcr-stats'; +import type { DestinyPostGameCarnageReportData } from '@/lib/bungie/types'; +import { RAID_HASH } from './pgcr-builder'; + +/** + * Seeds runs through the real ingestion chokepoint. + * + * All four production ingestion sources funnel through `insertFullPGCR`, which + * is where `ended_at` is derived and where `players.last_seen_at` is advanced. + * Seeding with raw INSERTs would skip both and quietly produce rows that could + * never exist in production — so tests would pass against data the app can't + * create. Everything here goes through the same function the crawler calls. + */ + +const HOUR = 3600; + +export interface SeedRunOptions { + instanceId: string; + /** Unix seconds the activity started. */ + period?: number; + /** Members who finished. Each becomes a completed pgcr_players row. */ + completedBy?: string[]; + /** Members present who did not finish. */ + incompleteBy?: string[]; + activityHash?: number; + raidKey?: string; + /** False marks a checkpoint run, which every leaderboard excludes. */ + startedFromBeginning?: boolean; + /** Overrides the run-level completed flag; defaults to "anyone completed". */ + completed?: boolean; + /** Tier 1 duration. Pass null to force the Tier 2 fallback from time played. */ + activityDurationSeconds?: number | null; + timePlayedSeconds?: number; + startSeconds?: number | null; +} + +/** Unix seconds, `hours` in the past. Runs are seeded relative to now because + * every leaderboard query filters on a cutoff derived from Date.now(). */ +export function hoursAgo(hours: number): number { + return Math.floor(Date.now() / 1000) - Math.round(hours * HOUR); +} + +export function seedRun(options: SeedRunOptions): void { + const { + instanceId, + period = hoursAgo(2), + completedBy = [], + incompleteBy = [], + activityHash = RAID_HASH, + raidKey = getRaidKeyFromHash(activityHash), + startedFromBeginning = true, + completed = completedBy.length > 0, + activityDurationSeconds = 1800, + timePlayedSeconds = 1800, + startSeconds = 0, + } = options; + + const members = [ + ...completedBy.map((membershipId) => ({ membershipId, completed: true })), + ...incompleteBy.map((membershipId) => ({ membershipId, completed: false })), + ]; + + insertFullPGCR( + { + instanceId, + activityHash, + raidKey, + period, + // Always 0: Bungie reports startingPhaseIndex as 0 on every run, and the + // writer coerces it with `|| 0` anyway. Checkpoint runs are expressed through + // startedFromBeginning, which is what the leaderboards actually filter on. + startingPhaseIndex: 0, + activityWasStartedFromBeginning: startedFromBeginning, + completed, + playerCount: members.length, + source: 'test', + activityDurationSeconds, + }, + members.map((member) => ({ + instanceId, + membershipId: member.membershipId, + membershipType: 3, + displayName: `Guardian-${member.membershipId}`, + bungieGlobalDisplayName: `Guardian-${member.membershipId}`, + characterClass: 'Warlock', + lightLevel: 2010, + completed: member.completed, + kills: 100, + deaths: 2, + assists: 40, + timePlayedSeconds, + startSeconds, + })) + ); +} + +/** + * Registers a player in the `players` table so the leaderboard's LEFT JOIN finds + * a name. Seeding a run alone does not do this — in production a player is only + * added once crawled — which is exactly why the join is a LEFT one. + */ +export function seedPlayer( + membershipId: string, + bungieGlobalDisplayName?: string, + bungieGlobalDisplayNameCode?: number +): void { + upsertPlayer({ + membershipId, + membershipType: 3, + displayName: bungieGlobalDisplayName ?? `Guardian-${membershipId}`, + bungieGlobalDisplayName, + bungieGlobalDisplayNameCode, + }); +} + +/** + * Ingests a real captured PGCR through the same path the crawler uses. + * + * Mirrors the body of `fetchAndStorePGCR` minus the network call — the mapping + * from Bungie's entry shape to our storage shape is duplicated there rather than + * extracted, so this reproduces it exactly. If that mapping ever changes, this + * must change with it. + */ +export function seedFromFixture(pgcr: DestinyPostGameCarnageReportData, source = 'test'): void { + const processed = processPGCR(pgcr); + + insertFullPGCR( + { + instanceId: processed.instanceId, + activityHash: processed.activityHash, + raidKey: processed.raidKey, + period: processed.period, + startingPhaseIndex: pgcr.startingPhaseIndex || 0, + activityWasStartedFromBeginning: pgcr.activityWasStartedFromBeginning || false, + completed: processed.completed, + playerCount: pgcr.entries.length, + source, + activityDurationSeconds: readActivityDurationSeconds(pgcr.entries), + }, + pgcr.entries.map((entry) => ({ + instanceId: processed.instanceId, + membershipId: entry.player.destinyUserInfo.membershipId, + membershipType: entry.player.destinyUserInfo.membershipType, + displayName: entry.player.destinyUserInfo.displayName, + bungieGlobalDisplayName: entry.player.destinyUserInfo.bungieGlobalDisplayName, + characterClass: entry.player.characterClass || 'Unknown', + lightLevel: entry.player.lightLevel || 0, + completed: entry.values?.completed?.basic?.value === 1, + kills: entry.values?.kills?.basic?.value || 0, + deaths: entry.values?.deaths?.basic?.value || 0, + assists: entry.values?.assists?.basic?.value || 0, + timePlayedSeconds: entry.values?.timePlayedSeconds?.basic?.value || 0, + startSeconds: readEntryStartSeconds(entry), + })) + ); +} + +/** Raw row read, for asserting what the writer actually persisted. */ +export function readPgcrRow(instanceId: string): Record | undefined { + return getDb() + .prepare('SELECT * FROM pgcrs WHERE instance_id = ?') + .get(instanceId) as Record | undefined; +} diff --git a/tests/real-pgcrs.test.ts b/tests/real-pgcrs.test.ts new file mode 100644 index 0000000..db9aef5 --- /dev/null +++ b/tests/real-pgcrs.test.ts @@ -0,0 +1,232 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { processPGCR } from '@/lib/crawler/pgcr'; +import { computeActivityDurationSeconds } from '@/lib/db/queries'; +import { isRaidActivityHash } from '@/lib/bungie/manifest'; +import { readActivityDurationSeconds, readEntryStartSeconds } from '@/lib/bungie/pgcr-stats'; +import type { DestinyPostGameCarnageReportData } from '@/lib/bungie/types'; +import { resetTestDb, testDb } from './helpers/db'; +import { seedFromFixture } from './helpers/seed'; + +import absurdDuration from './fixtures/pgcr-absurd-duration-crotas-end.json'; +import checkpoint from './fixtures/pgcr-checkpoint-root-of-nightmares.json'; +import fullClear from './fixtures/pgcr-fullclear-salvations-edge.json'; +import missingNames from './fixtures/pgcr-missing-bungie-name.json'; +import multiCharacter from './fixtures/pgcr-multi-character-garden.json'; +import nonRaid from './fixtures/pgcr-non-raid.json'; +import partialCompletion from './fixtures/pgcr-partial-completion-last-wish.json'; +import zeroCompletions from './fixtures/pgcr-zero-completions-vault-of-glass.json'; + +/** + * Tests against real captured Bungie responses. + * + * The builder-based tests cover permutations we construct. These cover the + * shapes Bungie actually sends, which repeatedly turn out to be stranger than + * anything we would think to build: nineteen entries in one report, six entries + * belonging to two people, a seven-hour "duration" on an eighteen-minute raid. + * + * See tests/fixtures/README.md for what each file is and how it was captured. + */ + +const as = (fixture: unknown) => fixture as DestinyPostGameCarnageReportData; + +beforeEach(() => { + resetTestDb(); +}); + +describe('a clean full clear', () => { + it('is recognised as a completed raid', () => { + const result = processPGCR(as(fullClear)); + + expect(result.raidKey).toBe('salvations_edge'); + expect(result.completed).toBe(true); + expect(result.players).toHaveLength(6); + }); +}); + +describe('a checkpoint run', () => { + it('is reported by Bungie as not started from the beginning', () => { + expect(as(checkpoint).activityWasStartedFromBeginning).toBe(false); + }); + + it('still carries a zero starting phase index', () => { + // The captured proof that startingPhaseIndex no longer discriminates + // anything: Bungie sends 0 even here, on a confirmed checkpoint run. This + // is why ProcessedPGCR.isFullClear reports true for every run — its + // `startingPhaseIndex === 0` branch always fires. See docs/decisions.md. + expect(as(checkpoint).startingPhaseIndex).toBe(0); + }); + + it('is excluded from the leaderboard once ingested', () => { + seedFromFixture(as(checkpoint)); + + const row = testDb() + .prepare('SELECT activity_was_started_from_beginning AS f FROM pgcrs WHERE instance_id = ?') + .get(as(checkpoint).activityDetails.instanceId) as { f: number }; + + expect(row.f).toBe(0); + }); +}); + +describe('every captured raid agrees on the phase index', () => { + it('reports 0 regardless of how the run was entered', () => { + // Four full clears and three checkpoint runs, all reporting 0. A field that + // takes one value across every observed case cannot be used to tell them + // apart, whatever the type definition implies. + const all = [fullClear, checkpoint, zeroCompletions, partialCompletion, multiCharacter, absurdDuration, missingNames]; + + expect(all.map((f) => as(f).startingPhaseIndex)).toEqual([0, 0, 0, 0, 0, 0, 0]); + }); +}); + +describe('a run nobody completed', () => { + it('is not counted as completed', () => { + expect(processPGCR(as(zeroCompletions)).completed).toBe(false); + }); +}); + +describe('a run where one of two players finished', () => { + it('counts as completed, because any completion counts', () => { + expect(processPGCR(as(partialCompletion)).completed).toBe(true); + }); +}); + +describe('a player who brought several characters', () => { + it('appears once per character in Bungie\'s entries', () => { + const entries = as(multiCharacter).entries; + const distinct = new Set(entries.map((e) => e.player.destinyUserInfo.membershipId)); + + expect(entries).toHaveLength(6); + expect(distinct.size).toBe(2); + }); + + it('collapses to one stored row per player', () => { + // pgcr_players is keyed (instance_id, membership_id) and inserts use + // INSERT OR IGNORE, so the second and third characters are dropped. The + // leaderboard's COUNT(DISTINCT instance_id) would handle duplicates anyway, + // but they never reach it. + seedFromFixture(as(multiCharacter)); + + const row = testDb() + .prepare('SELECT COUNT(*) AS c FROM pgcr_players WHERE instance_id = ?') + .get(as(multiCharacter).activityDetails.instanceId) as { c: number }; + + expect(row.c).toBe(2); + }); + + it('keeps only the first character\'s stats, not the largest or the sum', () => { + // Documented, not endorsed. The first entry for this player reports 981s + // played; their longest character reports 1494s. Nothing reads these + // columns today, so this is latent rather than user-visible. + seedFromFixture(as(multiCharacter)); + + const row = testDb() + .prepare( + 'SELECT time_played_seconds AS t FROM pgcr_players WHERE instance_id = ? AND membership_id = ?' + ) + .get(as(multiCharacter).activityDetails.instanceId, '4611686018462874397') as { t: number }; + + expect(row.t).toBe(981); + }); + + it('still measures the activity across all characters', () => { + // Duration is computed from the in-memory entries, before the dedupe, so + // the dropped rows do not shorten the run. + const entries = as(multiCharacter).entries; + const players = entries.map((e) => ({ + startSeconds: readEntryStartSeconds(e), + timePlayedSeconds: e.values.timePlayedSeconds?.basic?.value ?? 0, + })); + + expect(computeActivityDurationSeconds(null, players)).toBe(2037); + }); +}); + +describe('a run with an absurd reported duration', () => { + it('is taken at face value by the duration tiers', () => { + // Tier 1 trusts Bungie: 27384s (7.6 hours) for a raid where nobody played + // past 1093s. The tiers deliberately do not sanity-check this — the + // future-end-time guard in insertFullPGCR is what catches it, and only + // when the resulting end time lands ahead of the ingest clock. + const entries = as(absurdDuration).entries; + const players = entries.map((e) => ({ + startSeconds: readEntryStartSeconds(e), + timePlayedSeconds: e.values.timePlayedSeconds?.basic?.value ?? 0, + })); + + expect(readActivityDurationSeconds(entries)).toBe(27384); + expect(computeActivityDurationSeconds(27384, players)).toBe(27384); + }); + + it('would derive a far shorter run from per-player time alone', () => { + const entries = as(absurdDuration).entries; + const players = entries.map((e) => ({ + startSeconds: readEntryStartSeconds(e), + timePlayedSeconds: e.values.timePlayedSeconds?.basic?.value ?? 0, + })); + + expect(computeActivityDurationSeconds(null, players)).toBe(1093); + }); +}); + +describe('a report where Bungie withholds every player identity', () => { + it('extracts all nineteen entries without throwing', () => { + // Nineteen entries in one raid report. Neither the count nor the total + // absence of names is something a hand-written fixture would have said. + const result = processPGCR(as(missingNames)); + + expect(result.players).toHaveLength(19); + expect(result.players.every((p) => p.bungieGlobalDisplayName === undefined)).toBe(true); + }); + + it('has no platform display name to fall back to either', () => { + // Worth stating plainly: this is not "the global name is missing so use the + // platform one". Every entry arrives as isPublic: false with membershipType + // 0 and no name field of any kind, so there is no fallback left. The + // downstream display path ends up rendering a raw membership id. + const userInfo = as(missingNames).entries.map((e) => e.player.destinyUserInfo); + + expect(userInfo.every((u) => u.displayName === undefined)).toBe(true); + expect(userInfo.every((u) => u.isPublic === false)).toBe(true); + expect(processPGCR(as(missingNames)).players.every((p) => p.displayName === undefined)).toBe(true); + }); + + it('stores all nineteen rows with null names rather than rejecting them', () => { + // The run is still real and still counts, so dropping it would lose a + // genuine raid. NULL names are the correct outcome here. + seedFromFixture(as(missingNames)); + + const rows = testDb() + .prepare( + 'SELECT COUNT(*) AS c, COUNT(display_name) AS named FROM pgcr_players WHERE instance_id = ?' + ) + .get(as(missingNames).activityDetails.instanceId) as { c: number; named: number }; + + expect(rows.c).toBe(19); + expect(rows.named).toBe(0); + }); + + it('records membershipType 0, which is not a real platform', () => { + // Type "None". These ids cannot be resolved against a platform without a + // LinkedProfiles lookup, which is what scripts/cleanup exists to repair. + seedFromFixture(as(missingNames)); + + const row = testDb() + .prepare('SELECT DISTINCT membership_type AS t FROM pgcr_players WHERE instance_id = ?') + .get(as(missingNames).activityDetails.instanceId) as { t: number }; + + expect(row.t).toBe(0); + }); +}); + +describe('a non-raid activity', () => { + it('is rejected by raid detection', () => { + const hash = + as(nonRaid).activityDetails.directorActivityHash || as(nonRaid).activityDetails.referenceId; + + expect(isRaidActivityHash(hash)).toBe(false); + }); + + it('resolves to no raid key', () => { + expect(processPGCR(as(nonRaid)).raidKey).toBeUndefined(); + }); +}); diff --git a/tests/setup/no-network.test.ts b/tests/setup/no-network.test.ts new file mode 100644 index 0000000..31b31ea --- /dev/null +++ b/tests/setup/no-network.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it, vi } from 'vitest'; + +// Tests the guard in ./no-network.ts. A silently-broken network guard is worse +// than none: the suite would look protected while quietly hitting the real +// Bungie API, burning quota and going flaky against live data. +describe('the global network guard', () => { + it('rejects a fetch that no test has stubbed', async () => { + await expect(fetch('https://stats.bungie.net/Platform/Destiny2/')).rejects.toThrow( + /Blocked an unstubbed network request/ + ); + }); + + it('names the blocked URL so the offending call is findable', async () => { + await expect(fetch('https://www.bungie.net/some/path')).rejects.toThrow( + 'https://www.bungie.net/some/path' + ); + }); + + it('steps aside for a test that stubs fetch deliberately', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('{"ok":true}'))); + + const response = await fetch('https://stats.bungie.net/Platform/Destiny2/'); + + expect(await response.text()).toBe('{"ok":true}'); + }); + + it('is back in force for the test after a stub', async () => { + // Guards against a leaked stub from the previous test: the beforeEach in + // no-network.ts must re-arm the thrower even though vi.stubGlobal was + // called and never explicitly unstubbed. + await expect(fetch('https://stats.bungie.net/Platform/Destiny2/')).rejects.toThrow( + /Blocked an unstubbed network request/ + ); + }); +}); diff --git a/tests/setup/no-network.ts b/tests/setup/no-network.ts new file mode 100644 index 0000000..6e747e2 --- /dev/null +++ b/tests/setup/no-network.ts @@ -0,0 +1,29 @@ +import { beforeEach, vi } from 'vitest'; + +/** + * Global network guard. + * + * Every outbound call in this codebase goes through `fetch` — the Bungie client + * (`src/lib/bungie/client.ts`) and the manifest downloader are the only callers. + * So replacing `fetch` with a thrower is sufficient to catch a test that reaches + * the real internet, which would burn Bungie API quota and make the suite flaky. + * + * A test that legitimately needs `fetch` stubs it with `vi.stubGlobal('fetch', …)`. + * That records this thrower as the original and restores it afterwards, so the + * guard is back in place for the next test without any per-file cleanup. + * + * Scope note: this does not intercept `node:http`/`node:https` directly. Nothing + * in `src/` uses them, and adding an http-module shim would be machinery guarding + * a door nobody walks through. + */ +beforeEach(() => { + vi.stubGlobal('fetch', (input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input instanceof URL ? input.href : String(input); + return Promise.reject( + new Error( + `Blocked an unstubbed network request to ${url}. ` + + `Tests must not touch the real network — stub it with vi.stubGlobal('fetch', …).` + ) + ); + }); +}); diff --git a/tests/setup/test-db-path.ts b/tests/setup/test-db-path.ts new file mode 100644 index 0000000..16ea453 --- /dev/null +++ b/tests/setup/test-db-path.ts @@ -0,0 +1,35 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterAll } from 'vitest'; + +/** + * Points every test file at its own throwaway database directory. + * + * This runs as a setupFile, which Vitest executes *before* the test file's own + * imports. That ordering is the whole point: `DB_PATH` in src/lib/db/index.ts is + * a module-level const evaluated at import time, so if a test file statically + * imported anything that pulls in the db module before this ran, the path would + * resolve against the real data directory. Setting it here means test files can + * use ordinary static imports instead of `await import()` everywhere. + * + * Setting RAID_TRACKER_DB_PATH also relocates DATA_DIR, which derives from its + * dirname (maintenance/state.ts). That isolates maintenance-state.json too — + * necessary because getDb() calls isDbQuiesceActive() on every invocation and + * would otherwise throw DatabaseMaintenanceError for the whole suite if the real + * database happened to be mid-vacuum. + * + * A temp file rather than `:memory:` because SQLite silently downgrades WAL for + * in-memory databases. See docs/adr/0003. + */ + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dff-test-')); + +process.env.RAID_TRACKER_DB_PATH = path.join(dir, 'test.db'); + +// Keep the suite off any real key even if a test reaches code that reads one. +process.env.BUNGIE_API_KEY = 'test-key-not-a-real-credential'; + +afterAll(() => { + fs.rmSync(dir, { recursive: true, force: true }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..cc9142c --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,39 @@ +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; + +// Vitest over Jest: it runs TypeScript and ESM natively with no babel or ts-jest +// layer, and it does not contend with Next.js 16's bundler. Nothing here is +// clever — every setting is spelled out rather than inferred, because this repo +// had no test framework before and the config should be readable cold. +export default defineConfig({ + test: { + // The data pipeline is all Node: SQLite, fetch, timers. No DOM, ever. + environment: 'node', + + // Two homes on purpose. Pure-logic tests sit next to the code they cover + // so they move with it; anything needing a database or fixtures lives + // under tests/ where the helpers are. + include: ['src/**/*.test.ts', 'tests/**/*.test.ts'], + + // Order matters. test-db-path must run before anything imports the db + // module, because DB_PATH is resolved at import time — see the file's + // comment. no-network fails any test that reaches the real internet + // without stubbing fetch, which would burn API quota and go flaky. + setupFiles: ['tests/setup/test-db-path.ts', 'tests/setup/no-network.ts'], + + coverage: { + provider: 'v8', + reporter: ['text'], + // Deliberately no thresholds. Coverage is a diagnostic here, not a gate. + }, + }, + + resolve: { + // Mirrors the `@/*` -> `./src/*` mapping in tsconfig.json. Kept as an + // explicit alias rather than a tsconfig-paths plugin so there is one + // fewer dependency doing something invisible. + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +});