openvibers/openvibe.report Commits

Validate the personal AI usage answer against report.ai-usage-me-result@2 (#37)

OP OpenVibers committed 13 days ago · f869e5b5d5 · 5 files, +33 −8

The route test now validates real answers (default, paid_by, empty, rollups, raw+rollups and an org_key payer) against the published schema instead of only checking fields by hand. Pin openvibe-contracts v0.263.0, which carries the v2 contract.

Changes

ddiff --git a/README.md b/README.md
iindex 47f29a5..27321c5 100644
---- a/README.md
++++ b/README.md
@@@ -205,7 +205,7 @@ payloads with money or personal data are not stored. `/internal/` is loopback-on
verifies the Events v2 HMAC. See [SECURITY.md](SECURITY.md).
<!-- versions:start -->
--- openvibe-contracts: v0.260.0
++- openvibe-contracts: v0.263.0
- openvibe-sdk: v0.78.0
- openvibe-shared: v4.67.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex af1071f..56d114f 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -8,11 +8,11 @@
"runtime": "node 22, express 4, PostgreSQL (PGlite in development), no sessions",
"port": "5080",
"domain": "openvibe.report",
-- "contracts": "openvibe-contracts v0.260.0",
++ "contracts": "openvibe-contracts v0.263.0",
"sdk": "openvibe-sdk v0.78.0",
"shared": "openvibe-shared v4.67.0",
"packages": [
-- "openvibe-contracts v0.260.0",
++ "openvibe-contracts v0.263.0",
"openvibe-sdk v0.78.0",
"openvibe-shared v4.67.0",
"pg v8.23.1",
ddiff --git a/package-lock.json b/package-lock.json
iindex 5346e49..73f2d4e 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -13,7 +13,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.260.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.263.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.78.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.67.0",
"pg": "^8.23.1"
@@@ -1056,9 +1056,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.260.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.260.0",
-- "integrity": "sha512-gI0o3SGEj6kxOuFR/kWgA+YrGZ5xHthi5h/Zt6Y02Rtj0D+PlHz/bQTAvoQriwO6qX+JtbcKFI9RCQqgr/THcg==",
++ "version": "0.263.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.263.0",
++ "integrity": "sha512-mzVXLXIzgJc8VuhTZNDJak+iHm547nisRZZNfOp6V4arn8oePlM/S1BU1enaikboD/RkAgRjcITkIHLSyRq0VQ==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 52bad73..d3f5a6f 100644
---- a/package.json
++++ b/package.json
@@@ -19,7 +19,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.260.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.263.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.78.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.67.0",
"pg": "^8.23.1"
ddiff --git a/test/ai-usage-me.test.js b/test/ai-usage-me.test.js
iindex 395467b..6e192e2 100644
---- a/test/ai-usage-me.test.js
++++ b/test/ai-usage-me.test.js
@@@ -9,6 +9,7 @@
*/
const assert = require('assert');
const crypto = require('crypto');
++const contracts = require('openvibe-contracts');
const { createSsoClient } = require('openvibe-sdk/sso');
const { boot, check, done } = require('./helpers/boot');
const ledger = require('../server/analytics/ai-usage');
@@@ -39,6 +40,12 @@ const sso = createSsoClient({
const MEASURES = ['calls', 'input_tokens', 'output_tokens', 'cached_tokens', 'cache_write_tokens', 'cost_usd', 'fee_usd', 'charged_usd', 'saved_usd'];
++/** The route's answer must match the published contract, not only this test's idea of it. */
++function assertContract(body, label) {
++ const r = contracts.validate('report.ai-usage-me-result@2', body);
++ assert.ok(r.valid, `${label} does not match report.ai-usage-me-result@2: ${JSON.stringify(r.errors)}`);
++}
++
(async () => {
const t = await boot({ db: true, now: () => NOW, sso });
const store = t.ctx.store;
@@@ -122,6 +129,24 @@ const MEASURES = ['calls', 'input_tokens', 'output_tokens', 'cached_tokens', 'ca
}
});
++ await check('a real answer matches report.ai-usage-me-result@2', async () => {
++ const headers = { cookie: `ov_token=${alice}` };
++ for (const [label, query] of [
++ ['default', ''],
++ ['paid_by', '?group=paid_by&from=2026-09-01&to=2026-10-31'],
++ ['empty', '?from=2026-01-01&to=2026-01-02'],
++ ['rollups', '?group=surface&from=2026-01-01&to=2026-06-30'],
++ ['straddle', '?group=provider&from=2026-01-01&to=2026-10-31'],
++ ]) {
++ const r = await t.get('/api/v1/me/ai/usage' + query, { headers });
++ assert.strictEqual(r.status, 200, label + ': ' + r.text);
++ assertContract(r.json(), label);
++ }
++ const bobAnswer = await t.get('/api/v1/me/ai/usage', { headers: { cookie: `ov_token=${bob}` } });
++ assert.strictEqual(bobAnswer.status, 200, bobAnswer.text);
++ assertContract(bobAnswer.json(), 'bob (org_key)');
++ });
++
await check('the totals and by_paid_by add up to the rows', async () => {
for (const group of ['day', 'week', 'month', 'provider', 'model', 'surface', 'paid_by', 'week']) {
const body = (await t.get(`/api/v1/me/ai/usage?group=${group}&from=2026-09-01&to=2026-10-31`, { headers: { cookie: `ov_token=${alice}` } })).json();