vpp-ai-platform/packages/services/test/helpers.ts
Thomas Bayes 1cc21e0dc6
Some checks are pending
ci / typescript (push) Waiting to run
ci / python (push) Waiting to run
ci / evals (push) Blocked by required conditions
M3: Mastra runtime, safety chain, two agents, Case Desk v1
- packages/domain: safety-chain objects (ValidationResult, SimulationResult,
  EnvelopeMatch, StaleDenial, ExecutionReceipt, LineageRef/BidProposalDraft,
  RouterDecision, BidExportFile) + fixtures on both sides.
- packages/services: proposal digest; PolicyEngine + hubei-spot-bidding pack
  (digest-valid, bid-format, price-limits, quantity-non-negative,
  ledger-consistency, lineage-integrity, originator-permission — each with
  pass/fail tests); EnvelopeService; AuthorityService (fresh check, permits,
  revoke, gateway validate); FileExportGateway (idempotent receipts);
  Memory/File EventBus; LineageRecorder + P2 assembler; RevenueScenario
  simulator; CaseDeskService; FsRepository; skill HTTP client moved here.
- packages/runtime: createRuntime (LibSQL storage, per-runtime workflow
  factories), proposal-lifecycle (rule check → simulation → envelope gate
  with suspend/resume → fresh check + permit → release), day-ahead-situation,
  day-ahead-bid, TriggerService (scheduled/event/manual), LlmPort
  (Mastra/Scripted/Null), Case Desk HTTP API, dev entry point.
- Tests: all eight docs/01 invariants, docs/07 06:00→08:30 end to end with
  LLM down, restart survival of a suspended approval, permit expiry and
  revocation, replay of a released proposal, trigger scheduling. 141 TS +
  60 Python tests.
- Known gaps: ledger not yet persisted (replayed on restart); STALE ends the
  run instead of looping to rule check; synthetic data stands in for
  historical replay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoYoGYzHkFyv3ALenkRPhA
2026-09-02 06:55:55 -04:00

100 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Approval, Curve96, Envelope, ForecastBundle, Proposal } from '@vpp/domain'
import { AuthorityService } from '../src/authority.js'
import { MemoryEventBus } from '../src/events.js'
import { LedgerService } from '../src/ledger.js'
import { LineageRecorder, assembleBidProposal, withLineage } from '../src/lineage.js'
import { HUBEI_BID_PACK_PLACEHOLDER, PolicyEngine, hubeiSpotBiddingPack } from '../src/policy.js'
import { MemorySnapshotStore } from '../src/snapshot.js'
export const NOW = '2026-03-14T08:00:00Z'
export const DATE = '2026-03-15'
export const PACK_VERSION = '2026.03'
export const curve = (v: string | string[], date = DATE): Curve96 => ({
interval_minutes: 15,
date,
values: Array.isArray(v) ? v : Array.from({ length: 96 }, () => v),
})
export const priceForecast = (p50 = '425.50'): ForecastBundle => ({
id: 'fc-price',
kind: 'PRICE',
market_date: DATE,
unit: 'yuan_per_mwh',
quantiles: { p10: curve('360.00'), p50: curve(p50), p90: curve('490.00') },
model: { name: 'price-forecast', version: '1.0.0' },
features_snapshot_ref: 'a'.repeat(64),
generated_at: NOW,
})
/** A consistent little world: ledger with a monthly contract, a recorded MILP output, a proposal built from refs. */
export function world(opts: { qty?: string; price?: string; agent?: string } = {}) {
const snapshots = new MemorySnapshotStore()
const ledger = new LedgerService({ daMonthlyDeviationBand: '0.05', clock: () => NOW })
ledger.append({
id: 'contract-2026-03', timescale: 'MONTHLY', period: '2026-03', kind: 'CONTRACT',
energy_mwh: '37200.0', curve: null, source_ref: 'contract-2026-03-001', expected_version: 0,
}) // 1200 MWh/day → band [1140, 1260]
const engine = new PolicyEngine([hubeiSpotBiddingPack(PACK_VERSION, HUBEI_BID_PACK_PLACEHOLDER)])
const lineage = new LineageRecorder()
const events = new MemoryEventBus(() => NOW)
const authority = new AuthorityService(undefined, () => 'permit-001')
let n = 0
const milp = withLineage(lineage, snapshots, { tool: 'bid-optimization-milp', version: '1.0.0', idGen: () => `tc-${++n}` }, (_input: unknown) => ({
market_date: DATE,
prices_yuan_per_mwh: curve(opts.price ?? '0.00'),
quantities_mwh: curve(opts.qty ?? '12.5'), // 12.5 × 96 = 1200 MWh
expected_revenue_yuan: '510600.00',
}))
const forecastTool = withLineage(lineage, snapshots, { tool: 'price-forecast', version: '1.0.0', idGen: () => `tc-${++n}` }, (_i: unknown) => priceForecast())
const proposal = lineage.run(() => {
forecastTool({ kind: 'PRICE' })
const { call } = milp({ market_date: DATE })
return assembleBidProposal(
{
market_date: DATE,
prices_ref: { tool_call_id: call.tool_call_id, path: 'prices_yuan_per_mwh' },
quantities_ref: { tool_call_id: call.tool_call_id, path: 'quantities_mwh' },
expected_revenue_ref: { tool_call_id: call.tool_call_id, path: 'expected_revenue_yuan' },
rationale: 'test',
},
{
id: 'prop-001',
originator: { agent: opts.agent ?? 'trading-agent', trigger: 'SCHEDULED', task_id: 'task-001' },
lineage: lineage.current(),
ledger_version: ledger.read().version,
policy_pack_version: PACK_VERSION,
snapshots,
now: NOW,
},
)
})
const ruleCtx = () => ({ ledger: ledger.read(), ledgerService: ledger, snapshots, now: NOW })
return { snapshots, ledger, engine, lineage, events, authority, proposal, ruleCtx }
}
export const envelope = (bounds: Record<string, string>, over: Partial<Envelope> = {}): Envelope => ({
id: 'env-bid-001',
scope: { proposal_type: 'BID', timescales: ['DAY_AHEAD'], resource_set: 'pool-hubei-01' },
bounds,
validity: { from: '2026-03-01T00:00:00Z', to: '2026-03-31T23:59:59Z' },
approval: { level: 'L1', approved_by: ['user-ops-lead'] },
escalation: { max_consecutive_deviations: 3, deviation_threshold_pct: '10.0' },
status: 'ACTIVE',
...over,
})
export const approvalFor = (p: Proposal, ledgerVersion: number, over: Partial<Approval> = {}): Approval => ({
id: 'appr-001',
proposal_digest: p.digest,
decision: 'APPROVE',
approver: { id: 'user-trader-01', role: 'senior-trader' },
scope: { effect_type: 'BID', limits: {} },
validity: { from: NOW, to: '2026-03-15T09:00:00Z' },
evidence_versions: { policy_pack_version: PACK_VERSION, ledger_version: ledgerVersion, data_snapshot_refs: [] },
decided_at: NOW,
...over,
})