diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb5a5ea..8d79a051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to `omnidash` are documented here. This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v1.1.6 (2026-10-08) + +### Features +- The Overview shows Baseline spend: the served `counterfactual_usd` of metering-summary.v1's all-time + row, the figure `onex metering --json` reports, beside the baseline model and pricing manifest it is + priced at. With no baseline resolved it shows Baseline unresolved, never $0 (OMN-20752) + ## v1.1.5 (2026-10-08) ### Fixes diff --git a/package-lock.json b/package-lock.json index d24ee0a6..a58b5b6d 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omnidash", - "version": "1.1.5", + "version": "1.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omnidash", - "version": "1.1.5", + "version": "1.1.6", "license": "MIT", "dependencies": { "@fontsource/ibm-plex-mono": "^5.2.7", diff --git a/package.json b/package.json index c63b173d..8735f543 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omnidash", - "version": "1.1.5", + "version": "1.1.6", "description": "Composable widget dashboard for real-time observability — Vite + React with per-widget 2D and 3D variants.", "main": "index.js", "homepage": "https://github.com/OmniNode-ai/omnidash#readme", diff --git a/src/pages/LocalDashboardPage.omn20752.test.tsx b/src/pages/LocalDashboardPage.omn20752.test.tsx new file mode 100644 index 00000000..0b7b9a02 --- /dev/null +++ b/src/pages/LocalDashboardPage.omn20752.test.tsx @@ -0,0 +1,106 @@ +// @vitest-environment jsdom +// OMN-20752: the Overview shows Baseline spend, the served counterfactual_usd of metering-summary.v1's all-time row -- +// the figure `onex metering --json` reports as counterfactual_usd -- and a typed state, never $0, when no baseline +// resolved. +// +// Failure modes, each with a test below: +// B1 a served all-time row with a known counterfactual renders that amount on a Baseline spend card; +// B2 a row whose baseline_state is unresolved renders Baseline unresolved, never $0, whether the row leaves +// counterfactual_usd null or serves it as 0; +// B3 the card names the same row's baseline model and pricing manifest, and says the figure is modelled; +// B4 no served row at all is a typed not-measured state, never a number; +// B5 only the all-time row is read: a day row's counterfactual is not the headline (the control for B1). +import { act, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const METERING = 'onex.snapshot.projection.metering-summary.v1'; + +const harness = vi.hoisted(() => ({ + reachable: new Set(), + answer: (_topic: string): Promise => Promise.resolve([]), +})); + +vi.mock('@/data-source', () => ({ + createSnapshotSource: () => ({ + async *readAll() { yield []; }, + readSnapshot: async (topic: string) => { + const rows = await harness.answer(topic); + return { rows, rowCount: rows.length, dataFreshness: 'fresh', latestEventAt: null, readAt: new Date().toISOString() }; + }, + }), +})); +vi.mock('@/data-source/data-source-override', () => ({ resolveEffectiveDataSource: () => ({ mode: 'http' }) })); +vi.mock('@/data-source/exposure-census', () => ({ + fetchExposureCensus: async () => ({ + rows: [...harness.reachable].map((topic) => ({ topic, reachability: 'reachable' })), + }), +})); + +import { LocalDashboardPage } from './LocalDashboardPage'; + +// The all-time row the OMN-20223 walk read on 2026-10-08: 23 runs, `onex metering --json` counterfactual_usd 0.015292. +const allRow = (fields: Record) => ({ + window_kind: 'all', window_start: '', as_of: '2026-10-08T18:40:44Z', baseline_model: 'claude-sonnet-5-5', + pricing_manifest_version: '3', baseline_state: 'resolved', runs_total: 23, runs_measured: 23, + runs_unknown_tokens: 0, runs_unknown_spend: 0, tokens_in: 5756, tokens_out: 378, spend_usd: 0, + counterfactual_usd: 0.015292, savings_usd: 0.015292, savings_per_measured_run_usd: 0.000665, ...fields, +}); + +async function baselineCard(rows: unknown[]): Promise { + harness.reachable = new Set([METERING]); + harness.answer = (topic) => Promise.resolve(topic === METERING ? rows : []); + render(); + await act(async () => { await vi.advanceTimersByTimeAsync(0); }); + const card = screen.getByRole('heading', { name: 'Baseline spend' }).closest('article'); + if (card === null) throw new Error('no Baseline spend card on the Overview'); + return card as HTMLElement; +} + +const figure = (card: HTMLElement) => card.querySelector('.local-dashboard-metric')?.textContent ?? ''; + +describe('OMN-20752 Baseline spend on the Overview', () => { + beforeEach(() => { + vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval', 'setTimeout', 'clearTimeout', 'Date'] }); + vi.setSystemTime(new Date('2026-10-08T18:45:00Z')); + }); + afterEach(() => { vi.useRealTimers(); }); + + it('B1: renders the served counterfactual_usd of the all-time row', async () => { + const card = await baselineCard([allRow({})]); + expect(figure(card)).toBe('$0.0153'); + }); + + it('B1: a different served counterfactual renders that amount, not a constant', async () => { + const card = await baselineCard([allRow({ counterfactual_usd: 12.5, savings_usd: 12.5 })]); + expect(figure(card)).toBe('$12.5000'); + }); + + for (const counterfactual of [null, 0]) { + it(`B2: an unresolved baseline is Baseline unresolved, never $0 (counterfactual_usd ${String(counterfactual)})`, async () => { + const card = await baselineCard([allRow({ + baseline_state: 'unresolved', baseline_model: null, counterfactual_usd: counterfactual, savings_usd: null, + })]); + expect(figure(card)).toBe('Baseline unresolved'); + expect(card.textContent ?? '').not.toMatch(/\$\s*0/); + }); + } + + it("B3: names the row's baseline model and pricing manifest, and says the figure is modelled", async () => { + const card = await baselineCard([allRow({})]); + expect(card.textContent).toContain('Baseline claude-sonnet-5-5'); + expect(card.textContent).toContain('pricing manifest v3'); + expect(card.textContent).toContain('Modelled'); + }); + + it('B4: no served row is a typed not-measured state, never a number', async () => { + const card = await baselineCard([]); + expect(figure(card)).toMatch(/^Not measured/); + expect(figure(card)).not.toMatch(/\d/); + }); + + it("B5: a day row's counterfactual is not the headline", async () => { + const day = { ...allRow({ counterfactual_usd: 99 }), window_kind: 'day', window_start: '2026-10-08' }; + const card = await baselineCard([day, allRow({})]); + expect(figure(card)).toBe('$0.0153'); + }); +}); diff --git a/src/pages/local/overview.contracts.yaml b/src/pages/local/overview.contracts.yaml index 8178f14a..ff99e86c 100644 --- a/src/pages/local/overview.contracts.yaml +++ b/src/pages/local/overview.contracts.yaml @@ -271,6 +271,36 @@ "no-data" ] }, + { + "component_id": "overview-baseline-spend", + "component_kind": "metric_card", + "title": "Baseline spend", + "contract_version": { + "major": 1, + "minor": 0, + "patch": 0 + }, + "data_bindings": [ + { + "binding_id": "overview-baseline-spend-metering", + "projection_topic": "onex.snapshot.projection.metering-summary.v1", + "ordering_authority_field": "as_of", + "ordering_direction": "descending", + "required_fields": [ + "window_kind", + "as_of", + "baseline_model", + "pricing_manifest_version", + "baseline_state", + "counterfactual_usd" + ] + } + ], + "supported_empty_state_reasons": [ + "missing-field", + "no-data" + ] + }, { "component_id": "overview-quality", "component_kind": "table", diff --git a/src/pages/local/overview.page.yaml b/src/pages/local/overview.page.yaml index fe883641..b638e0bb 100644 --- a/src/pages/local/overview.page.yaml +++ b/src/pages/local/overview.page.yaml @@ -277,6 +277,24 @@ "precision": 6 } }, + { + "widget_id": "6f1d2c8e-4b7a-5e39-9c0d-20752a1c0c01", + "title": "Baseline spend", + "row": 8, + "col": 8, + "width": 4, + "height": 2, + "data_source": "overview-baseline-spend", + "config": { + "config_kind": "metric_card", + "widget_type": "metric_card", + "metric_key": "counterfactual_usd", + "label": "Baseline spend", + "unit": "USD", + "format": "currency", + "precision": 4 + } + }, { "widget_id": "d4da9a48-f797-4b1d-b622-41f028a67e04", "title": "Quality", diff --git a/src/pages/local/pages.test.ts b/src/pages/local/pages.test.ts index a7128d9c..15f42b93 100644 --- a/src/pages/local/pages.test.ts +++ b/src/pages/local/pages.test.ts @@ -298,6 +298,8 @@ describe('local pages against the captured lab catalogue', () => { 'spend_usd', 'savings_usd', 'runs_measured', 'tokens_in_and_out', // OMN-20009: the served run-locally share and the served saving per measured run. 'by_tier.local_call_share', 'savings_per_measured_run_usd', + // OMN-20752: Baseline spend, the same all-time row's served counterfactual_usd. + 'counterfactual_usd', // OMN-20226: compression and cache hit rate, each a served metering-summary.v1 column, Not measured until produced. 'compression_ratio', 'cache_hit_rate', ]);