mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
## F4 — bin/olp.mjs + olp-plugin/index.js migrate to quota_v2 Codex post-v0.5.0 review Q4: both `olp usage` CLI and `/olp usage` plugin handler read legacy `body.quota` shape, which never has `percent_used` or meaningful `available` data → display always fell through to "no quota api" even when anthropic live quota was visible on the dashboard (D81/D82). Authority: codex review Q4 (PR #58); ADR 0008 Amendment 2 (quota_v2 shape: { provider, status, utilization, reset, representative_claim, fallback_percentage, overage, failure?, failure_kind?, backoff_until? }). ### bin/olp.mjs cmdUsage - When `body.quota_v2` present (non-empty array, server v0.5.0+): render per-provider rows with status badge (live/stale/unreachable/unavailable), 5h + 7d utilization % (color-coded: green <50% / yellow 50-80% / red ≥80%), reset countdowns, binding claim, ⚠ stale / ❌ unreachable annotations. - When `body.quota_v2` absent: fall through to existing legacy `body.quota` rendering (preserves backwards compat with pre-v0.5.0 servers). - Added `formatResetCountdown(epochSeconds)` — 5-range formatter (past / <1h / <24h / <7d / ≥7d), ported from dashboard.html D82. Exported for tests. Kept in bin/olp.mjs (not lib/) per spec guidance. - Added `formatAgo(diffMs)` — small helper for stale-row age display. ### olp-plugin/index.js fmtUsage() - Same quota_v2 migration. Plain-text output (no ANSI), one line per provider. Legacy body.quota fallback preserved. - Added `pluginFormatResetCountdown(epochSeconds)` — intentionally duplicated from bin/olp.mjs (olp-plugin ships as a separate package and must not import from bin/). Exported for tests. - Also fixed fmtUsage to read `w.request_count` (dashboard-data shape) in addition to legacy `w.requests` (OCP-era shape) for completeness. ### Backwards compat - Pre-v0.5.0 server returns body.quota only → both surfaces use legacy display (unchanged behaviour). - v0.5.0+ server returns both quota and quota_v2 → both surfaces prefer quota_v2. - Legacy code paths kept (5-10 lines each, not deleted). ## v1.x roadmap #7 — AUTH_MISSING tuple path test coverage — CLOSED The dedicated test was already shipped at D56 (test-features.mjs line 6255: 'engine: AUTH_MISSING terminates chain, fallbackDetail tuple records trigger_type:"auth_missing" (D56, v1.x roadmap #7)'). This commit closes the roadmap entry with a date stamp and PR reference. Authority: docs/v1x-roadmap.md § "#7 — AUTH_MISSING tuple path test coverage (D40 follow-up)". ## Tests Suite 40 (9 new tests — 40a through 40i): 40a — cmdUsage parses quota_v2 live rows (mock server) 40b — cmdUsage falls back to legacy body.quota when quota_v2 absent 40c — olp-plugin fmtUsage parses quota_v2 live row 40d — olp-plugin fmtUsage falls back to legacy body.quota 40e — formatResetCountdown covers all 5 time ranges 40f — pluginFormatResetCountdown covers past/<1h/<24h/<7d/≥7d 40g — cmdUsage renders quota_v2 stale row with ⚠ stale note 40h — cmdUsage renders quota_v2 unreachable row with ❌ indicator 40i — cmdUsage renders quota_v2 unavailable rows correctly 759 → 768 tests, 0 fail. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16900,3 +16900,366 @@ describe('Suite 39 — D83 dashboard rendering smoke tests (Phase 5, ADR 0012 D8
|
||||
'39h: dashboard HTML must contain legacy quota fallback path (graceful-degradation proof)');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Suite 40: F4 CLI/plugin quota_v2 migration + v1.x #7 AUTH_MISSING test pin ─
|
||||
//
|
||||
// Codex post-v0.5.0 review Q4: bin/olp.mjs cmdUsage and olp-plugin/index.js
|
||||
// fmtUsage both read legacy body.quota (never has percent_used or available data),
|
||||
// so the display always fell through to "no quota api" even when anthropic's live
|
||||
// quota was visible on the dashboard. F4 adds consumption of body.quota_v2 (server
|
||||
// v0.5.0+ shape per ADR 0008 Amendment 2) with graceful fallback to body.quota on
|
||||
// older servers.
|
||||
//
|
||||
// v1.x roadmap #7 was already closed at D56 (test at line 6255); this suite
|
||||
// references that test by description. The roadmap entry closure (marking ✅ CLOSED)
|
||||
// happens in docs/v1x-roadmap.md (committed alongside these tests).
|
||||
//
|
||||
// Tests:
|
||||
// 40a — cmdUsage renders quota_v2 live rows (mock server)
|
||||
// 40b — cmdUsage falls back to legacy body.quota when quota_v2 absent
|
||||
// 40c — olp-plugin fmtUsage parses quota_v2 live row
|
||||
// 40d — olp-plugin fmtUsage falls back to legacy body.quota when quota_v2 absent
|
||||
// 40e — formatResetCountdown past / <1h / <24h / <7d / ≥7d ranges
|
||||
// 40f — olp-plugin pluginFormatResetCountdown parallel coverage
|
||||
// 40g — cmdUsage renders quota_v2 stale row with ⚠ stale note
|
||||
// 40h — cmdUsage renders quota_v2 unreachable row with ❌ indicator
|
||||
// 40i — cmdUsage renders quota_v2 unavailable row with reason
|
||||
|
||||
import { runCli as runOlpCli40, formatResetCountdown as olpFormatResetCountdown } from './bin/olp.mjs';
|
||||
import { fmtUsage as plugFmtUsage40, pluginFormatResetCountdown } from './olp-plugin/index.js';
|
||||
|
||||
// ── Helper: tiny HTTP server that responds to one request with a static JSON body ──
|
||||
import { createServer as createHttpServerS40 } from 'node:http';
|
||||
|
||||
function makeJsonServer40(responseBody) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const srv = createHttpServerS40((req, res) => {
|
||||
const body = typeof responseBody === 'function' ? responseBody(req) : responseBody;
|
||||
const raw = JSON.stringify(body);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(raw) });
|
||||
res.end(raw);
|
||||
});
|
||||
srv.listen(0, '127.0.0.1', () => {
|
||||
const port = srv.address().port;
|
||||
resolve({
|
||||
port,
|
||||
url: `http://127.0.0.1:${port}`,
|
||||
close: () => new Promise(r => srv.close(r)),
|
||||
});
|
||||
});
|
||||
srv.once('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ── quota_v2 fixture shapes ──
|
||||
const QUOTA_V2_LIVE = [
|
||||
{
|
||||
provider: 'anthropic',
|
||||
status: 'live',
|
||||
schema_version: '2026-05-26',
|
||||
last_fresh_at: Date.now() - 60000,
|
||||
utilization: { '5h': 0.36, '7d': 0.34 },
|
||||
reset: { '5h': Math.floor((Date.now() + 2 * 3600 * 1000) / 1000), '7d': Math.floor((Date.now() + 5 * 24 * 3600 * 1000) / 1000) },
|
||||
representative_claim: 'five_hour',
|
||||
fallback_percentage: 0.02,
|
||||
raw_available: null,
|
||||
},
|
||||
{
|
||||
provider: 'openai',
|
||||
status: 'unavailable',
|
||||
reason: 'no public quota api',
|
||||
},
|
||||
{
|
||||
provider: 'mistral',
|
||||
status: 'unavailable',
|
||||
reason: 'no public quota api',
|
||||
},
|
||||
];
|
||||
|
||||
const QUOTA_V2_STALE = [
|
||||
{
|
||||
provider: 'anthropic',
|
||||
status: 'stale',
|
||||
last_fresh_at: Date.now() - 6 * 60000,
|
||||
utilization: { '5h': 0.60, '7d': 0.20 },
|
||||
reset: { '5h': Math.floor((Date.now() + 3600 * 1000) / 1000), '7d': Math.floor((Date.now() + 4 * 24 * 3600 * 1000) / 1000) },
|
||||
representative_claim: 'five_hour',
|
||||
failure: { kind: 'rate_limited', message: 'too many requests' },
|
||||
},
|
||||
];
|
||||
|
||||
const QUOTA_V2_UNREACHABLE = [
|
||||
{
|
||||
provider: 'anthropic',
|
||||
status: 'unreachable',
|
||||
failure: { kind: 'auth_failed', message: 're-run `claude setup-token`' },
|
||||
},
|
||||
];
|
||||
|
||||
const LEGACY_QUOTA_BODY = {
|
||||
window_24h: {},
|
||||
cache_hit_24h: {},
|
||||
quota: [
|
||||
{ provider: 'anthropic', available: null },
|
||||
{ provider: 'openai', available: null },
|
||||
],
|
||||
};
|
||||
|
||||
describe('Suite 40 — F4 CLI/plugin quota_v2 migration + v1.x #7 AUTH_MISSING test pin', () => {
|
||||
|
||||
// ── 40a — cmdUsage renders quota_v2 live rows ────────────────────────────
|
||||
|
||||
it('40a — cmdUsage parses quota_v2 live rows (mock server → asserts key strings present)', async () => {
|
||||
const dashData = {
|
||||
window_24h: { request_count: 12, status_2xx: 12, status_4xx: 0, status_5xx: 0 },
|
||||
cache_hit_24h: { hit_rate: 0.5, hit: 6, miss: 6 },
|
||||
quota: [],
|
||||
quota_v2: QUOTA_V2_LIVE,
|
||||
top_fallback_chains_24h: [],
|
||||
cache_stats: { hits: 6, misses: 6, size: 4, inflightCount: 0 },
|
||||
};
|
||||
const srv = await makeJsonServer40(dashData);
|
||||
try {
|
||||
// Clear auth env so resolveBearerToken returns null (anonymous allowed)
|
||||
const savedKey = process.env.OLP_API_KEY;
|
||||
const savedOwner = process.env.OLP_OWNER_TOKEN;
|
||||
delete process.env.OLP_API_KEY;
|
||||
delete process.env.OLP_OWNER_TOKEN;
|
||||
let out = '', err = '';
|
||||
try {
|
||||
const code = await runOlpCli40(
|
||||
['usage', `--proxy-url=${srv.url}`],
|
||||
{ out: s => { out += s; }, err: s => { err += s; }, useColor: false },
|
||||
);
|
||||
assert.equal(code, 0, `cmdUsage exit non-zero; stderr=${err}`);
|
||||
} finally {
|
||||
if (savedKey === undefined) delete process.env.OLP_API_KEY; else process.env.OLP_API_KEY = savedKey;
|
||||
if (savedOwner === undefined) delete process.env.OLP_OWNER_TOKEN; else process.env.OLP_OWNER_TOKEN = savedOwner;
|
||||
}
|
||||
// Must display quota_v2 section heading
|
||||
assert.match(out, /Per-provider quota.*live/i, '40a: header must mention live quota');
|
||||
// Must show anthropic provider name
|
||||
assert.match(out, /ANTHROPIC/, '40a: ANTHROPIC row must appear');
|
||||
// Must show percentage data (36% or 34% from utilization shape)
|
||||
assert.match(out, /36%|34%/, '40a: live utilization must appear');
|
||||
// Must show reset countdown strings
|
||||
assert.match(out, /resets in|resets /i, '40a: reset countdown must appear');
|
||||
// Legacy "no quota api" fallthrough must NOT appear
|
||||
assert.ok(!out.includes('no quota api'), '40a: "no quota api" must not appear when quota_v2 is present');
|
||||
} finally {
|
||||
await srv.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ── 40b — cmdUsage falls back to legacy body.quota when quota_v2 absent ──
|
||||
|
||||
it('40b — cmdUsage falls back to legacy body.quota when quota_v2 absent (older server)', async () => {
|
||||
const srv = await makeJsonServer40(LEGACY_QUOTA_BODY);
|
||||
try {
|
||||
const savedKey = process.env.OLP_API_KEY;
|
||||
const savedOwner = process.env.OLP_OWNER_TOKEN;
|
||||
delete process.env.OLP_API_KEY;
|
||||
delete process.env.OLP_OWNER_TOKEN;
|
||||
let out = '', err = '';
|
||||
try {
|
||||
const code = await runOlpCli40(
|
||||
['usage', `--proxy-url=${srv.url}`],
|
||||
{ out: s => { out += s; }, err: s => { err += s; }, useColor: false },
|
||||
);
|
||||
assert.equal(code, 0, `cmdUsage fallback exit non-zero; stderr=${err}`);
|
||||
} finally {
|
||||
if (savedKey === undefined) delete process.env.OLP_API_KEY; else process.env.OLP_API_KEY = savedKey;
|
||||
if (savedOwner === undefined) delete process.env.OLP_OWNER_TOKEN; else process.env.OLP_OWNER_TOKEN = savedOwner;
|
||||
}
|
||||
// Legacy path shows "no quota api" for providers with null available
|
||||
assert.match(out, /no quota api/, '40b: legacy path must show "no quota api" fallthrough');
|
||||
} finally {
|
||||
await srv.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ── 40c — olp-plugin fmtUsage parses quota_v2 live row ──────────────────
|
||||
|
||||
it('40c — olp-plugin fmtUsage parses quota_v2 live row (mock body)', () => {
|
||||
const body = {
|
||||
window_24h: { request_count: 5 },
|
||||
cache_hit_24h: { hit_rate: 0.4 },
|
||||
quota: [],
|
||||
quota_v2: QUOTA_V2_LIVE,
|
||||
top_fallback_chains_24h: [],
|
||||
};
|
||||
const out = plugFmtUsage40(body);
|
||||
// Section heading
|
||||
assert.match(out, /Per-provider quota.*live/i, '40c: section heading must indicate live quota');
|
||||
// Anthropic row present
|
||||
assert.match(out, /ANTHROPIC/, '40c: ANTHROPIC row must appear');
|
||||
// live status
|
||||
assert.match(out, /live/, '40c: status "live" must appear for anthropic');
|
||||
// Utilization percentage
|
||||
assert.match(out, /36%/, '40c: 5h utilization 36% must appear');
|
||||
// Reset countdown present
|
||||
assert.match(out, /resets in|resets /i, '40c: reset countdown must appear');
|
||||
// openai/mistral show unavailable
|
||||
assert.match(out, /OPENAI.*unavailable|OPENAI\s+unavailable/, '40c: OPENAI must show unavailable');
|
||||
// Legacy bar() fallthrough must NOT fire
|
||||
assert.ok(!out.includes('█'), '40c: legacy bar() must not appear when quota_v2 present');
|
||||
});
|
||||
|
||||
// ── 40d — olp-plugin fmtUsage falls back to legacy body.quota ────────────
|
||||
|
||||
it('40d — olp-plugin fmtUsage falls back to legacy body.quota when quota_v2 absent', () => {
|
||||
const body = {
|
||||
quota: [
|
||||
{ provider: 'anthropic', percent_used: 45 },
|
||||
{ provider: 'openai', percent_used: null },
|
||||
],
|
||||
};
|
||||
const out = plugFmtUsage40(body);
|
||||
assert.match(out, /Per-provider quota/, '40d: section heading must appear');
|
||||
// Legacy bar() fires for percent_used=45
|
||||
assert.match(out, /anthropic.*45%|45%.*anthropic/i, '40d: anthropic 45% must appear');
|
||||
assert.match(out, /no quota api/, '40d: "no quota api" must appear for provider with null percent_used');
|
||||
});
|
||||
|
||||
// ── 40e — formatResetCountdown ranges ────────────────────────────────────
|
||||
|
||||
it('40e — formatResetCountdown covers all 5 time ranges', () => {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
// Past: diffMs <= 0
|
||||
assert.equal(olpFormatResetCountdown(nowSec - 5), 'resetting now', '40e: past → resetting now');
|
||||
// < 1h: exactly 30 minutes ahead by feeding epoch seconds directly (no ms conversion drift)
|
||||
const t30m = nowSec + 30 * 60;
|
||||
const r30m = olpFormatResetCountdown(t30m);
|
||||
assert.match(r30m, /resets in \d+m/, '40e: ~30min → resets in Xm format');
|
||||
assert.ok(r30m.includes('resets in'), '40e: 30min must use < 1h branch');
|
||||
// < 24h: 3 hours + 15 minutes from now (uses hours+minutes branch)
|
||||
const t3h15m = nowSec + (3 * 60 + 15) * 60;
|
||||
const r3h15m = olpFormatResetCountdown(t3h15m);
|
||||
assert.match(r3h15m, /resets in \d+h/, '40e: 3h15m → resets in Xh format');
|
||||
// < 7d: 2 days from now → weekday format
|
||||
const t2d = nowSec + 2 * 24 * 3600;
|
||||
const r2d = olpFormatResetCountdown(t2d);
|
||||
assert.match(r2d, /resets (Mon|Tue|Wed|Thu|Fri|Sat|Sun)/, '40e: 2 days → weekday format');
|
||||
// >= 7d: 10 days from now → month+day format
|
||||
const t10d = nowSec + 10 * 24 * 3600;
|
||||
const r10d = olpFormatResetCountdown(t10d);
|
||||
assert.match(r10d, /resets (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/, '40e: 10 days → month format');
|
||||
// null input
|
||||
assert.equal(olpFormatResetCountdown(null), '—', '40e: null → —');
|
||||
});
|
||||
|
||||
// ── 40f — plugin formatResetCountdown parallel coverage ─────────────────
|
||||
|
||||
it('40f — olp-plugin pluginFormatResetCountdown covers past / <1h / <24h / <7d / ≥7d', () => {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
assert.equal(pluginFormatResetCountdown(nowSec - 1), 'resetting now', '40f: past → resetting now');
|
||||
// ~45 minutes ahead → < 1h branch
|
||||
const t45m = nowSec + 45 * 60;
|
||||
const r45m = pluginFormatResetCountdown(t45m);
|
||||
assert.match(r45m, /resets in \d+m/, '40f: ~45min → resets in Xm format');
|
||||
// ~2h ahead (well within <24h range — may read as 1h 59m due to sub-second drift)
|
||||
const t2h = nowSec + 2 * 3600;
|
||||
const r2h = pluginFormatResetCountdown(t2h);
|
||||
assert.match(r2h, /resets in \d+h/, '40f: ~2h → resets in Xh format');
|
||||
assert.equal(pluginFormatResetCountdown(null), '—', '40f: null → —');
|
||||
});
|
||||
|
||||
// ── 40g — cmdUsage renders quota_v2 stale row ────────────────────────────
|
||||
|
||||
it('40g — cmdUsage renders quota_v2 stale row with ⚠ stale note', async () => {
|
||||
const dashData = {
|
||||
window_24h: {},
|
||||
cache_hit_24h: {},
|
||||
quota: [],
|
||||
quota_v2: QUOTA_V2_STALE,
|
||||
top_fallback_chains_24h: [],
|
||||
};
|
||||
const srv = await makeJsonServer40(dashData);
|
||||
try {
|
||||
const savedKey = process.env.OLP_API_KEY;
|
||||
const savedOwner = process.env.OLP_OWNER_TOKEN;
|
||||
delete process.env.OLP_API_KEY;
|
||||
delete process.env.OLP_OWNER_TOKEN;
|
||||
let out = '', err = '';
|
||||
try {
|
||||
await runOlpCli40(
|
||||
['usage', `--proxy-url=${srv.url}`],
|
||||
{ out: s => { out += s; }, err: s => { err += s; }, useColor: false },
|
||||
);
|
||||
} finally {
|
||||
if (savedKey === undefined) delete process.env.OLP_API_KEY; else process.env.OLP_API_KEY = savedKey;
|
||||
if (savedOwner === undefined) delete process.env.OLP_OWNER_TOKEN; else process.env.OLP_OWNER_TOKEN = savedOwner;
|
||||
}
|
||||
assert.match(out, /stale/, '40g: stale status must appear in output');
|
||||
} finally {
|
||||
await srv.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ── 40h — cmdUsage renders quota_v2 unreachable row ─────────────────────
|
||||
|
||||
it('40h — cmdUsage renders quota_v2 unreachable row with ❌ indicator', async () => {
|
||||
const dashData = {
|
||||
window_24h: {},
|
||||
cache_hit_24h: {},
|
||||
quota: [],
|
||||
quota_v2: QUOTA_V2_UNREACHABLE,
|
||||
top_fallback_chains_24h: [],
|
||||
};
|
||||
const srv = await makeJsonServer40(dashData);
|
||||
try {
|
||||
const savedKey = process.env.OLP_API_KEY;
|
||||
const savedOwner = process.env.OLP_OWNER_TOKEN;
|
||||
delete process.env.OLP_API_KEY;
|
||||
delete process.env.OLP_OWNER_TOKEN;
|
||||
let out = '', err = '';
|
||||
try {
|
||||
await runOlpCli40(
|
||||
['usage', `--proxy-url=${srv.url}`],
|
||||
{ out: s => { out += s; }, err: s => { err += s; }, useColor: false },
|
||||
);
|
||||
} finally {
|
||||
if (savedKey === undefined) delete process.env.OLP_API_KEY; else process.env.OLP_API_KEY = savedKey;
|
||||
if (savedOwner === undefined) delete process.env.OLP_OWNER_TOKEN; else process.env.OLP_OWNER_TOKEN = savedOwner;
|
||||
}
|
||||
assert.match(out, /❌|no cached data/, '40h: unreachable row must show ❌ or "no cached data"');
|
||||
assert.match(out, /auth_failed/, '40h: failure kind must appear in output');
|
||||
} finally {
|
||||
await srv.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ── 40i — cmdUsage renders quota_v2 unavailable row ─────────────────────
|
||||
|
||||
it('40i — cmdUsage renders quota_v2 unavailable rows (openai / mistral) correctly', async () => {
|
||||
const dashData = {
|
||||
window_24h: {},
|
||||
cache_hit_24h: {},
|
||||
quota: [],
|
||||
quota_v2: QUOTA_V2_LIVE, // includes openai + mistral as unavailable
|
||||
top_fallback_chains_24h: [],
|
||||
};
|
||||
const srv = await makeJsonServer40(dashData);
|
||||
try {
|
||||
const savedKey = process.env.OLP_API_KEY;
|
||||
const savedOwner = process.env.OLP_OWNER_TOKEN;
|
||||
delete process.env.OLP_API_KEY;
|
||||
delete process.env.OLP_OWNER_TOKEN;
|
||||
let out = '', err = '';
|
||||
try {
|
||||
await runOlpCli40(
|
||||
['usage', `--proxy-url=${srv.url}`],
|
||||
{ out: s => { out += s; }, err: s => { err += s; }, useColor: false },
|
||||
);
|
||||
} finally {
|
||||
if (savedKey === undefined) delete process.env.OLP_API_KEY; else process.env.OLP_API_KEY = savedKey;
|
||||
if (savedOwner === undefined) delete process.env.OLP_OWNER_TOKEN; else process.env.OLP_OWNER_TOKEN = savedOwner;
|
||||
}
|
||||
// openai + mistral are unavailable in the fixture
|
||||
assert.match(out, /OPENAI/, '40i: OPENAI row must appear');
|
||||
assert.match(out, /unavailable/, '40i: "unavailable" must appear for providers with no quota api');
|
||||
} finally {
|
||||
await srv.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user