openvibers/openvibe.report Commits
Validate the personal AI usage answer against report.ai-usage-me-result@2 (#37)
Changes
@@@ -205,7 +205,7 @@ payloads with money or personal data are not stored. `/internal/` is loopback-onverifies 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 -->@@@ -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",@@@ -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",@@@ -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"@@@ -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();
@@ -205,7 +205,7 @@ payloads with money or personal data are not stored. `/internal/` is loopback-on
@@ -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).
verifies the Events v2 HMAC. See [SECURITY.md](SECURITY.md).
<!-- versions:start -->
<!-- versions:start -->
-- openvibe-contracts: v0.260.0
+- openvibe-contracts: v0.263.0
- openvibe-sdk: v0.78.0
- openvibe-sdk: v0.78.0
- openvibe-shared: v4.67.0
- openvibe-shared: v4.67.0
<!-- versions:end -->
<!-- versions:end -->
@@ -8,11 +8,11 @@
@@ -8,11 +8,11 @@
"runtime": "node 22, express 4, PostgreSQL (PGlite in development), no sessions",
"runtime": "node 22, express 4, PostgreSQL (PGlite in development), no sessions",
"port": "5080",
"port": "5080",
"domain": "openvibe.report",
"domain": "openvibe.report",
- "contracts": "openvibe-contracts v0.260.0",
+ "contracts": "openvibe-contracts v0.263.0",
"sdk": "openvibe-sdk v0.78.0",
"sdk": "openvibe-sdk v0.78.0",
"shared": "openvibe-shared v4.67.0",
"shared": "openvibe-shared v4.67.0",
"packages": [
"packages": [
- "openvibe-contracts v0.260.0",
+ "openvibe-contracts v0.263.0",
"openvibe-sdk v0.78.0",
"openvibe-sdk v0.78.0",
"openvibe-shared v4.67.0",
"openvibe-shared v4.67.0",
"pg v8.23.1",
"pg v8.23.1",
@@ -13,7 +13,7 @@
@@ -13,7 +13,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.67.0",
"pg": "^8.23.1"
"pg": "^8.23.1"
@@ -1056,9 +1056,9 @@
@@ -1056,9 +1056,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"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",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.67.0",
"pg": "^8.23.1"
"pg": "^8.23.1"
@@ -9,6 +9,7 @@
@@ -9,6 +9,7 @@
*/
*/
const assert = require('assert');
const assert = require('assert');
const crypto = require('crypto');
const crypto = require('crypto');
+const contracts = require('openvibe-contracts');
const { createSsoClient } = require('openvibe-sdk/sso');
const { createSsoClient } = require('openvibe-sdk/sso');
const { boot, check, done } = require('./helpers/boot');
const { boot, check, done } = require('./helpers/boot');
const ledger = require('../server/analytics/ai-usage');
const ledger = require('../server/analytics/ai-usage');
@@ -39,6 +40,12 @@ const sso = createSsoClient({
@@ -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'];
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 () => {
(async () => {
const t = await boot({ db: true, now: () => NOW, sso });
const t = await boot({ db: true, now: () => NOW, sso });
const store = t.ctx.store;
const store = t.ctx.store;
@@ -122,6 +129,24 @@ const MEASURES = ['calls', 'input_tokens', 'output_tokens', 'cached_tokens', 'ca
@@ -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 () => {
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']) {
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();
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();