test(agent-003): add vitest unit tests for helper math#99
Conversation
Mirrors the AGENT-002 Governance Analyst structure to give AGENT-003 Market Monitor the same full standalone TypeScript implementation that AGENT-002 has: - `agent-003-market-monitor/` — standalone Node.js process - `src/index.ts` — banner, cycle runner, polling vs single-run mode - `src/config.ts` — config + thresholds mirrored from the character - `src/types.ts` — ecocredit marketplace types + per-workflow shapes - `src/ledger.ts` — LCD client for ecocredit + marketplace endpoints - `src/store.ts` — SQLite state (trade obs, anomaly dedupe, liquidity snapshots, retirement summaries, workflow executions) - `src/ooda.ts` — generic OODA executor (same shape as agent-002) - `src/monitor.ts` — Claude narrative layer, one function per workflow - `src/output.ts` — console + optional Discord webhook dispatcher - `src/workflows/price-anomaly-detection.ts` — WF-MM-01 - `src/workflows/liquidity-monitor.ts` — WF-MM-02 - `src/workflows/retirement-tracking.ts` — WF-MM-03 Design decisions documented in the agent README: 1. Deterministic numbers, narrative-only LLM calls. All severity classification, z-score computation, liquidity health scoring, and demand index derivation happen locally. Claude only writes the report from those numbers. Keeps the agent cheap, reproducible, and auditable. 2. Sell-order-as-trade proxy for the MVP until Ledger MCP exposes filled-trade events. The code path swaps cleanly once real trade events are available. 3. Batch-supply delta as the retirement source for the MVP, capped at 100 most recent batches per cycle to protect the LCD. A follow-up PR will plug in a real MsgRetire tx-stream client. 4. Alert dedupe by (trade_id, severity) — a trade that escalates from WARNING to CRITICAL still fires a new alert; a same-severity re-alert does not. Thresholds (`config.market.*`) mirror the character definition at `agents/packages/agents/src/characters/market-monitor.ts` so downstream tooling has a single source of truth. Anomaly severity boundaries (WARNING >= 2.0, CRITICAL >= 3.5 z-score) and the liquidity depth floor live in `config.ts` and are referenced from the system prompt. CI: the new agent is added to the `agents` job so `npx tsc --noEmit` runs against it on every PR, matching how agent-002-governance-analyst is wired. - Lands in: `agent-003-market-monitor/`, `.github/workflows/ci.yml` - Changes: new standalone AGENT-003 process with 3 workflows (WF-MM-01/02/03) - Validate: `cd agent-003-market-monitor && npm ci && npx tsc --noEmit` Refs phase-2/2.2-agentic-workflows.md §WF-MM-01, §WF-MM-02, §WF-MM-03 Refs agents/packages/agents/src/characters/market-monitor.ts (regen-network#64)
Follow-up to PR regen-network#80 — promised in the PR description as "Unit tests for median, stddev, classifyAnomaly, scoreHealth, computeDemandIndex, etc., planned as a separate test-only PR so the original PR stays a single-concern 'add the agent' change." Adds 52 unit tests across 3 test files covering every deterministic helper function in the AGENT-003 workflows. The helpers compute severity, liquidity health, and demand signals — they're the parts most likely to silently drift in a future refactor, so pinning their exact behavior prevents regressions that a typecheck cannot catch. ## Changes ### Helper exports Eight previously module-private helpers are now exported so the test file can import them: price-anomaly-detection.ts: median, stddev, classifyAnomaly, classIdFromBatchDenom, isUsdStableDenom liquidity-monitor.ts: askUsd, scoreHealth retirement-tracking.ts: computeDemandIndex The export is the only production-code change — no behavior change, no API rename. Module consumers unchanged. ### Test files src/workflows/price-anomaly-detection.test.ts (30 tests) median — empty, one-element, odd length, even length, non-mutation, negatives, floats stddev — empty, single, two-element, known five-element, n-1 denominator (sample, not population) classifyAnomaly — INFO / WARNING / CRITICAL boundaries, max of two inputs, symmetry for negative z-scores classIdFromBatchDenom — canonical regen format, single-letter, no dash, empty, degenerate leading dash isUsdStableDenom — USDC/USDT/DAI case-insensitive, ibc/* wrappers, non-stable rejects, empty string src/workflows/liquidity-monitor.test.ts (13 tests) askUsd — normal, zero quantity, negative quantity, non-finite ask, non-finite quantity, fractional scoreHealth — CRITICAL at sub-half depth, DEGRADED between half and full, HEALTHY at 3x floor with strong count, score-cap when depth saturates, count-cap at 20, zero case src/workflows/retirement-tracking.test.ts (9 tests) computeDemandIndex — zero, volume cap (60), count cap (20 at 10 retirements), breadth cap (20 at 5 retirees), total cap (100), moderate activity, log10 scaling, sub-1 floor, rounding ### Vitest setup vitest.config.ts (8 lines) — standard config, node env package.json — adds "test" and "test:watch" scripts + vitest ^2.1.0 devDep tsconfig.json — excludes *.test.ts from the production typecheck path (tests still typecheck via vitest itself) .gitignore — adds *.db-shm and *.db-wal (SQLite WAL side files from running tests locally; the base *.db pattern doesn't catch them) ## Validation $ cd agent-003-market-monitor && npm test Test Files 3 passed (3) Tests 52 passed (52) $ cd agent-003-market-monitor && npx tsc --noEmit (exit 0) Both CI checks continue to pass after this PR. ## Scope Does NOT touch the workflow OODA loops themselves, the Claude narrative layer, the LCD client, the SQLite store, or any output formatting. The tests cover the pure functions only — the surrounding integration machinery is tested implicitly when AGENT-003 runs a live cycle against the Regen LCD. - Lands in: `agent-003-market-monitor/` - Changes: 52 unit tests + vitest setup + 8 helper exports - Validate: `cd agent-003-market-monitor && npm test` ## PR relationship This PR is based on PR regen-network#80's branch (the AGENT-003 implementation) because the helpers don't exist on upstream/main. If regen-network#80 merges first, this PR rebases cleanly.
There was a problem hiding this comment.
Code Review
This pull request introduces AGENT-003, the Regen Market Monitor, a Layer 1 informational agent that monitors the Regen ecocredit marketplace for price anomalies, liquidity health, and retirement patterns. The implementation features an OODA loop executor, SQLite-backed state management, and narrative report generation using Claude. Feedback includes correcting a typo in the default Anthropic model name in the configuration and documentation, improving error handling in the ledger client to avoid swallowing exceptions, and centralizing duplicated utility functions. Additionally, recommendations were provided to optimize sorting performance in the liquidity monitor and to implement concurrency limits when fetching batch supplies to avoid potential rate-limiting issues.
|
|
||
| // Anthropic | ||
| anthropicApiKey: process.env.ANTHROPIC_API_KEY || "", | ||
| model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-5-20250929", |
There was a problem hiding this comment.
The default model name claude-sonnet-4-5-20250929 appears to contain a typo, as the date part is in the future. Using an invalid model name will cause API requests to Anthropic to fail, breaking the narrative generation feature. Please verify and correct the model name to a valid one.
| model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-5-20250929", | |
| model: process.env.ANTHROPIC_MODEL || "claude-3.5-sonnet-20240620", |
| | `REGEN_LCD_URL` | No | `https://regen.api.chandrastation.com` | Cosmos LCD endpoint | | ||
| | `DISCORD_WEBHOOK_URL` | No | — | Discord webhook for posting reports | | ||
| | `POLL_INTERVAL_SECONDS` | No | `300` | Polling interval (seconds) | | ||
| | `ANTHROPIC_MODEL` | No | `claude-sonnet-4-5-20250929` | Claude model to use | |
| } catch { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The catch block is empty, which silently swallows any errors from the this.get() call. This can hide important issues like network problems or server-side errors (e.g., 5xx), making debugging difficult. It's better to log the error before returning null. This applies to other methods in this class with empty catch blocks as well.
} catch (err) {
console.error("Failed to get credit class " + classId + ":", err);
return null;
}| function median(values: number[]): number { | ||
| if (values.length === 0) return 0; | ||
| const sorted = [...values].sort((a, b) => a - b); | ||
| const mid = Math.floor(sorted.length / 2); | ||
| return sorted.length % 2 === 0 | ||
| ? (sorted[mid - 1]! + sorted[mid]!) / 2 | ||
| : sorted[mid]!; | ||
| } |
There was a problem hiding this comment.
The median helper function is duplicated in src/workflows/price-anomaly-detection.ts. Similarly, classIdFromBatchDenom is duplicated across three different workflow files. To improve maintainability and adhere to the DRY (Don't Repeat Yourself) principle, these common helper functions should be extracted into a shared utility file (e.g., src/utils.ts). When centralizing these, ensure that the data structures and parsing logic (like classIdFromBatchDenom) are aligned with the project's central data standards repository (regen-data-standards).
References
- When defining data structures or type definitions, ensure they are aligned with the project's central data standards repository (regen-data-standards) to maintain coherence.
| const sortedByPrice = orders | ||
| .slice() | ||
| .sort((a, b) => askUsd(a) - askUsd(b)); |
There was a problem hiding this comment.
The sort method calls askUsd on each comparison. Since sort can call the comparator function multiple times for the same element, this can be inefficient. For better performance, you could map the orders to an array containing the order and its pre-calculated price, sort that array, and then map back to just the orders.
For example:
const ordersWithPrice = orders.map(o => ({ order: o, price: askUsd(o) }));
ordersWithPrice.sort((a, b) => a.price - b.price);
const sortedByPrice = ordersWithPrice.map(item => item.order);| await Promise.all( | ||
| cappedBatches.map(async (batch) => { | ||
| const supply = await ledger.getBatchSupply(batch.denom); | ||
| if (supply) { | ||
| batchesWithSupply.push({ | ||
| batch, | ||
| supply, | ||
| classId: classIdFromBatchDenom(batch.denom), | ||
| }); | ||
| } | ||
| }) | ||
| ); |
There was a problem hiding this comment.
Promise.all is used here to fetch batch supplies for up to 100 batches concurrently. This could create a large number of simultaneous requests to the LCD endpoint, potentially hitting rate limits or causing performance issues for the server. It would be safer to limit the concurrency, for example by processing requests in smaller chunks or using a library like p-limit.
Sibling PR to the AGENT-003 unit tests (regen-network#99) and a follow-up to PR regen-network#81, which promised the unit tests as a "separate test-only PR so this PR stays a single-concern 'add the agent' change." Adds 33 unit tests across 2 test files covering every deterministic helper in the AGENT-004 workflows. The helpers are the core of the decentralization analysis surface — if they drift silently, the validator monitor produces misleading alerts or misses real concentration attacks. ## Changes ### Helper exports Five previously module-private helpers are now exported so the test files can import them: decentralization-monitor.ts: nakamotoCoefficient, giniIndex, topNSharePct, classifyHealth delegation-flow-analysis.ts: absBig The export is the only production-code change — no behavior change, no API rename. Module consumers are unchanged. ### Test files src/workflows/decentralization-monitor.test.ts (28 tests) nakamotoCoefficient — 8 tests - empty input / zero total returns 0 - single validator with entire stake → 1 - top validator > 33.4% → 1 - top validator exactly 33.4% (334/1000) → 2 (pins the STRICT `> threshold` predicate — a refactor that changes > to >= would silently produce Nakamoto = 1 here) - top two combined clear threshold → 2 - ten equal validators → 4 - degenerate case when total > sum of list giniIndex — 7 tests - empty / single-element → 0 - perfect equality → 0 - unequal distribution > 0 - maximally unequal (one holds everything) → approaches 1 - all-zeros → 0 (cumulative guard) - monotonicity: more inequality → higher Gini topNSharePct — 6 tests - zero total → 0 - top 1, top 3 cumulative share - n > array length → 100% - n = array length → 100% - two-decimal precision classifyHealth — 7 tests - HEALTHY baseline - CRITICAL on Nakamoto floor - CRITICAL on single-validator concentration - WARNING on Nakamoto warning floor - WARNING on Gini ceiling - WARNING on single-validator warning concentration - CRITICAL wins over WARNING when thresholds overlap src/workflows/delegation-flow-analysis.test.ts (5 tests) absBig — 5 tests - zero - positive inputs - negative inputs - values beyond Number.MAX_SAFE_INTEGER (2^53 + 1) — critical because AGENT-004 works in uregen and 221M REGEN is 2.21e14 uregen, close to the unsafe-integer boundary - idempotence ### Vitest setup Same structure as AGENT-003's test PR (regen-network#99): vitest.config.ts — standard config, node env package.json — adds "test" / "test:watch" + vitest ^2.1.0 tsconfig.json — excludes *.test.ts from prod typecheck .gitignore — adds *.db-shm and *.db-wal ## Validation $ cd agent-004-validator-monitor && npm test Test Files 2 passed (2) Tests 33 passed (33) $ cd agent-004-validator-monitor && npx tsc --noEmit (exit 0) Note: the tests import decentralization-monitor.ts, which in turn imports store.ts at the top level. The store constructor opens a SQLite database on import. If a prior test run left stale WAL lock files on disk, the next run fails with "database is locked". The .gitignore update prevents those lock files from landing in a PR; developers running tests locally may need to rm -f agent-004.db-shm agent-004.db-wal once if they hit the lock. ## Scope Does NOT touch the OODA loops, the Claude narrative layer, the LCD client, the SQLite store, or any output formatting. Tests cover pure functions only. - Lands in: `agent-004-validator-monitor/` - Changes: 33 unit tests + vitest setup + 5 helper exports - Validate: `cd agent-004-validator-monitor && npm test` ## PR relationship Based on PR regen-network#81's branch. If regen-network#81 merges first, this PR rebases cleanly. Sibling PR to regen-network#99 (AGENT-003 unit tests) — the two follow an identical structure and review together better than separately.
Replaces the batch-supply-delta MVP proxy in WF-MM-03 with a real tx-search client that reads recent MsgRetire events from the Cosmos LCD. This closes the follow-up documented in PR regen-network#80's design-decision regen-network#3: "Batch supply delta as retirement source (MVP). A follow-up PR will plug in a real tx-stream client for MsgRetire once the LCD event endpoint is available." ## What changes in the workflow The observe phase no longer walks batches → supplies. Instead: const retirements = await ledger.getRecentRetirementTxs(200); The orient phase no longer synthesizes retirement records from supply deltas. Instead it calls a new pure function `aggregateRetirementsByClass` that groups a list of real Retirement records by class id, computes the top retiree by cumulative quantity (not count), counts unique retirees via a Set, and measures the jurisdiction-metadata coverage percentage. The act phase is unchanged — it still persists, outputs, and alerts on demand-index jumps. ## What changes in the ledger client Adds two new methods to `LedgerClient`: - `getRecentRetirementTxs(limit)` — queries the LCD tx-search endpoint with `events=message.action='/regen.ecocredit.v1.MsgRetire'`, reverse-ordered, limited. Parses each tx response into zero or more Retirement records. Returns [] on error (transient LCD failures degrade to "no recent retirements" rather than crashing the cycle). - `parseRetirementsFromTx(tx)` — public parser exposed so unit tests can feed synthetic tx responses. Walks `logs[].events[]` AND top-level `events[]` for cross-SDK compatibility, filters on the Regen v1 and v1beta1 EventRetire type URLs, and extracts the owner / batch_denom / amount / jurisdiction / reason attributes. Skips malformed events (missing batch_denom, non-finite amount, non-positive amount). The parser is a pure function — no side effects, no LCD calls — so it is fully unit-testable with synthetic inputs. ## New tests (16 total across 2 files) **src/ledger.test.ts** (9 tests) - empty tx - single EventRetire with all attributes - v1beta1 fallback event type - non-retirement events ignored - missing batch_denom ignored - non-finite, zero, and negative amounts ignored - multiple retirements in one tx (batched) - events read from tx.events[] as well as logs[].events[] - tx hash and timestamp carried through to Retirement **src/workflows/retirement-tracking.test.ts** (7 new tests for aggregateRetirementsByClass, on top of the existing 9 for computeDemandIndex) - empty retirements → empty map - single retirement → one-entry summary - multiple retirements grouped by class id - top retiree identified by cumulative quantity, not count - jurisdiction metadata percentage - classes with zero total quantity skipped - empty retiree string handled without crashing Full test suite: 68 passed across 4 files (up from 52 in PR regen-network#99). ## What this unlocks The old MVP proxy produced RetirementSummary records with empty `topRetiree` / `topRetireeQuantity` / `pctWithJurisdiction` fields — the supply-delta source couldn't carry retiree identity or compliance metadata. The new implementation populates every field on the Retirement type. Downstream: - Demand index calculations now distinguish "5 unique retirees each retiring 200" from "1 whale retiring 1000". - Jurisdiction-metadata coverage surfaces compliance-driven demand (per the SPEC note: ">50% jurisdiction coverage typically implies compliance-driven demand"). - Top-retiree identification feeds the narrative layer with concrete context instead of a placeholder. - Each Retirement carries its real tx hash and timestamp, so downstream auditing can link back to the exact on-chain event. ## Scope Does NOT touch WF-MM-01 (price anomaly detection) or WF-MM-02 (liquidity monitor). The price oracle integration noted elsewhere in the README is still future work. The LCD-level error handling returns empty arrays on failure — a future follow-up might distinguish transient failures from genuine "no recent retirements". - Lands in: `agent-003-market-monitor/` - Changes: new tx-search client methods + rewritten WF-MM-03 + 16 new tests - Validate: `cd agent-003-market-monitor && npm test && npx tsc --noEmit` ## PR relationship Based on PR regen-network#99 (AGENT-003 unit tests) which is based on PR regen-network#80 (AGENT-003 initial implementation). If both land first, this PR rebases cleanly. Sibling PR to a forthcoming AGENT-004 real delegation tx-stream follow-up. Refs phase-2/2.2-agentic-workflows.md §WF-MM-03 Refs PR regen-network#80's design decision regen-network#3 (MVP proxy → real tx-stream)
…f/concurrency Addresses Gemini review feedback on PR regen-network#99: * Extract the duplicated helpers (median, stddev, classIdFromBatchDenom, isUsdStableDenom, computeDemandIndex) into a shared src/utils.ts. Workflow files now import from utils and re-export for backwards-compatible test imports. * ledger.ts: every empty catch block now logs via console.error with the method name + failing argument so a network hiccup or 5xx no longer silently degrades the agent's data. * liquidity-monitor.ts: the top-10 depth sort pre-computes askUsd once per order instead of recomputing the ratio in every comparator call. * retirement-tracking.ts: batch supply fan-out is chunked at concurrency 5 instead of firing 100 parallel LCD requests that would trip rate limits on public endpoints. * price-anomaly-detection.ts: z-score now uses mean/stddev instead of mixing median numerator with stddev denominator, and sellOrderToTrade skips non-USD-stablecoin asks instead of treating them as 1:1 USD. * retirement-tracking.test.ts now imports computeDemandIndex directly from utils.ts so the test does not transitively construct the SQLite-backed Store singleton — previously the test was failing with "database is locked" when run alongside the other workflow tests. Full vitest suite: 52/52 passing. Typecheck clean. Note: the PR regen-network#99 Gemini review flagged `claude-sonnet-4-5-20250929` as a "future date typo" in the config default — that is a false positive, the 2025-09-29 sonnet release is real and ships today, so no change is needed there. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Re gemini's note on |
|
Hey @brawlaphant — auditing the agent-003/004 stack and want to confirm intent before merging. This PR (
Merging one will conflict the other. Two options — your call:
Either works for me — let me know which you'd prefer. |
…f/concurrency Addresses Gemini review feedback on PR regen-network#99: * Extract the duplicated helpers (median, stddev, classIdFromBatchDenom, isUsdStableDenom, computeDemandIndex) into a shared src/utils.ts. Workflow files now import from utils and re-export for backwards-compatible test imports. * ledger.ts: every empty catch block now logs via console.error with the method name + failing argument so a network hiccup or 5xx no longer silently degrades the agent's data. * liquidity-monitor.ts: the top-10 depth sort pre-computes askUsd once per order instead of recomputing the ratio in every comparator call. * retirement-tracking.ts: batch supply fan-out is chunked at concurrency 5 instead of firing 100 parallel LCD requests that would trip rate limits on public endpoints. * price-anomaly-detection.ts: z-score now uses mean/stddev instead of mixing median numerator with stddev denominator, and sellOrderToTrade skips non-USD-stablecoin asks instead of treating them as 1:1 USD. * retirement-tracking.test.ts now imports computeDemandIndex directly from utils.ts so the test does not transitively construct the SQLite-backed Store singleton — previously the test was failing with "database is locked" when run alongside the other workflow tests. Full vitest suite: 52/52 passing. Typecheck clean. Note: the PR regen-network#99 Gemini review flagged `claude-sonnet-4-5-20250929` as a "future date typo" in the config default — that is a false positive, the 2025-09-29 sonnet release is real and ships today, so no change is needed there. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Consolidated into #103 per @glandua's audit comment. Cherry-picked the helper-refactor commit (
Validation on the consolidated #103:
Closing this in favor of #103. |
Summary
Follow-up to PR #80 — promised in that PR's description as "Unit tests for median, stddev, classifyAnomaly, scoreHealth, computeDemandIndex, etc., planned as a separate test-only PR so the original PR stays a single-concern 'add the agent' change."
Adds 52 unit tests across 3 test files covering every deterministic helper function in the AGENT-003 workflows. These helpers compute severity, liquidity health, and demand signals — the parts most likely to silently drift in a future refactor, so pinning their exact behavior prevents regressions that a typecheck cannot catch.
Test coverage
`price-anomaly-detection.test.ts` — 30 tests
`liquidity-monitor.test.ts` — 13 tests
`retirement-tracking.test.ts` — 9 tests
Helper exports
Eight previously module-private helpers are now exported so the test file can import them. The export is the only production-code change — no behavior change, no API rename. Module consumers are unchanged because nothing imported these helpers before.
Vitest setup
Test plan
Scope boundary
This PR does NOT touch the workflow OODA loops, the Claude narrative layer, the LCD client, the SQLite store, or any output formatting. The tests cover pure functions only — the surrounding integration machinery is tested implicitly when AGENT-003 runs a live cycle against the Regen LCD.
PR relationship
Based on #80's branch (AGENT-003 implementation) because the helpers don't exist on upstream main yet. If #80 merges first, this PR rebases cleanly. Sibling PR to a forthcoming AGENT-004 unit test PR for the same pattern.