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:
+20
-1
@@ -4,7 +4,26 @@ All notable changes to OLP land here. Per `CLAUDE.md` release_kit overlay, this
|
||||
|
||||
## Unreleased
|
||||
|
||||
(empty — Phase 6 entries land here once Phase 6 opens)
|
||||
### F4 — `bin/olp.mjs` + `olp-plugin/index.js` migration to `quota_v2` shape
|
||||
|
||||
**Codex post-v0.5.0 review Q4.** Both CLI surfaces (`olp usage` and `/olp usage`) previously fell through to "no quota api" for every provider because they read the legacy `body.quota` shape, which never carries `percent_used` or meaningful `available` data. Now that the server (v0.5.0+) emits `body.quota_v2` per ADR 0008 Amendment 2, both surfaces prefer `quota_v2` and fall back to legacy `quota` on older servers.
|
||||
|
||||
- **`bin/olp.mjs cmdUsage`**: when `body.quota_v2` is present (non-empty array), renders per-provider rows with status (`live` / `stale` / `unreachable` / `unavailable`), 5h and 7d utilization percentages with color-coding (green < 50% / yellow 50–80% / red ≥ 80%), reset countdowns, binding claim, and ⚠ stale / ❌ unreachable annotations. Legacy `body.quota` path preserved as fallback for pre-v0.5.0 servers. `formatResetCountdown(epochSeconds)` added — 5-range formatter (past / <1h / <24h / <7d / ≥7d), ported from `dashboard.html` D82, kept in-file (no shared lib).
|
||||
|
||||
- **`olp-plugin/index.js fmtUsage()`**: same migration — `quota_v2` rows render as one-line plain text per provider (no ANSI; Telegram/Discord safe). `pluginFormatResetCountdown(epochSeconds)` added; intentionally duplicated (plugin ships as a separate package). Legacy `body.quota` fallback preserved.
|
||||
|
||||
### v1.x roadmap #7 — AUTH_MISSING tuple path test coverage — ✅ CLOSED
|
||||
|
||||
The dedicated AUTH_MISSING engine test (asserting `fallbackDetail[0].trigger_type === 'auth_missing'`) was already shipped at D56 (`test-features.mjs` line 6255). This item closes the roadmap entry with a date stamp and PR reference per the tracker convention. No code changes — documentation only.
|
||||
|
||||
### Tests
|
||||
|
||||
- Suite 40 (9 new tests): `40a`–`40i` covering `cmdUsage` quota_v2 live/stale/unreachable/unavailable parse, legacy fallback, `pluginFormatResetCountdown` and `formatResetCountdown` 5-range coverage, olp-plugin `fmtUsage` quota_v2 + legacy paths. 759 → 768 tests, 0 fail.
|
||||
|
||||
### Authority
|
||||
|
||||
- F4: codex post-v0.5.0 review Q4 (PR #58 review); ADR 0008 Amendment 2 (quota_v2 shape).
|
||||
- #7: `docs/v1x-roadmap.md` § "#7 — AUTH_MISSING tuple path test coverage (D40 follow-up)".
|
||||
|
||||
## v0.5.1 — 2026-05-27
|
||||
|
||||
|
||||
+86
-1
@@ -226,6 +226,53 @@ function formatMs(ms) {
|
||||
return `${Math.floor(ms / 3600000)}h${Math.floor((ms % 3600000) / 60000)}m`;
|
||||
}
|
||||
|
||||
/** formatAgo(diffMs) — "N min ago" / "Nh ago" from a millisecond diff. */
|
||||
function formatAgo(diffMs) {
|
||||
if (typeof diffMs !== 'number' || diffMs < 0) return 'just now';
|
||||
const sec = Math.floor(diffMs / 1000);
|
||||
if (sec < 60) return `${sec}s ago`;
|
||||
const min = Math.floor(sec / 60);
|
||||
if (min < 60) return `${min}m ago`;
|
||||
return `${Math.floor(min / 60)}h ago`;
|
||||
}
|
||||
|
||||
/**
|
||||
* formatResetCountdown(epochSeconds) → human-readable reset countdown string.
|
||||
*
|
||||
* Mirrors dashboard.html formatResetCountdown(). Five ranges:
|
||||
* past / < 1h / < 24h / < 7d / ≥ 7d
|
||||
*
|
||||
* Authority: ADR 0008 Amendment 2 (quota_v2 shape); ported from
|
||||
* dashboard.html (D82). No external deps. Pure formatter.
|
||||
*
|
||||
* @param {number|null} epochSeconds — Unix epoch seconds for reset time
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatResetCountdown(epochSeconds) {
|
||||
if (epochSeconds == null) return '—';
|
||||
const nowMs = Date.now();
|
||||
const targetMs = epochSeconds * 1000;
|
||||
const diffMs = targetMs - nowMs;
|
||||
if (diffMs <= 0) return 'resetting now';
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
if (diffMin < 60) return `resets in ${diffMin}m`;
|
||||
if (diffHr < 24) {
|
||||
const remMin = diffMin - diffHr * 60;
|
||||
if (remMin === 0) return `resets in ${diffHr}h`;
|
||||
return `resets in ${diffHr}h ${remMin}m`;
|
||||
}
|
||||
const target = new Date(targetMs);
|
||||
const timeStr = target.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
if (diffDay < 7) {
|
||||
const dayStr = target.toLocaleString('en-US', { weekday: 'short' });
|
||||
return `resets ${dayStr} ${timeStr}`;
|
||||
}
|
||||
const dateStr = target.toLocaleString('en-US', { month: 'short', day: 'numeric' });
|
||||
return `resets ${dateStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
// ── Subcommand: status ────────────────────────────────────────────────────
|
||||
|
||||
async function cmdStatus(flags, io) {
|
||||
@@ -329,7 +376,45 @@ async function cmdUsage(flags, io) {
|
||||
} else {
|
||||
io.log(' (no 24h usage data — server may not have processed any requests yet)');
|
||||
}
|
||||
if (Array.isArray(body.quota) && body.quota.length > 0) {
|
||||
// F4 (v0.5.1 codex post-release review Q4): prefer quota_v2 when present
|
||||
// (server v0.5.0+), fall back to legacy quota array on older servers.
|
||||
// Authority: ADR 0008 Amendment 2 (quota_v2 shape).
|
||||
if (Array.isArray(body.quota_v2) && body.quota_v2.length > 0) {
|
||||
io.log('');
|
||||
io.log(colorize('Per-provider quota (live)', ANSI.bold, io.useColor));
|
||||
io.log('─'.repeat(60));
|
||||
for (const p of body.quota_v2) {
|
||||
const label = String(p.provider ?? '?').toUpperCase().padEnd(12);
|
||||
const status = p.status ?? 'unavailable';
|
||||
if (status === 'unavailable') {
|
||||
io.log(` ${colorize(label, ANSI.gray, io.useColor)} unavailable ${p.reason ?? 'no public quota api'}`);
|
||||
} else if (status === 'unreachable') {
|
||||
const fk = p.failure?.kind ?? 'unknown';
|
||||
const fm = p.failure?.message ?? 'probe failed';
|
||||
io.log(` ${colorize(label, ANSI.red, io.useColor)} ❌ no cached data — failure: ${fk} (${fm})`);
|
||||
} else {
|
||||
// live or stale
|
||||
const staleWarn = status === 'stale'
|
||||
? colorize(` ⚠ stale${p.last_fresh_at ? ` (${formatAgo(Date.now() - p.last_fresh_at)})` : ''} failure: ${p.failure?.kind ?? 'unknown'}`, ANSI.yellow, io.useColor)
|
||||
: '';
|
||||
const util = p.utilization ?? {};
|
||||
const reset = p.reset ?? {};
|
||||
const parts = [];
|
||||
for (const window of ['5h', '7d']) {
|
||||
const frac = util[window];
|
||||
const resetEpoch = reset[window];
|
||||
if (frac != null) {
|
||||
const pct = `${Math.round(frac * 100)}%`;
|
||||
const rst = formatResetCountdown(resetEpoch);
|
||||
parts.push(`${window}: ${colorize(pct, frac >= 0.8 ? ANSI.red : frac >= 0.5 ? ANSI.yellow : ANSI.green, io.useColor)} (${rst})`);
|
||||
}
|
||||
}
|
||||
const binding = p.representative_claim ? ` binding: ${p.representative_claim.replace('_', '-')}` : '';
|
||||
io.log(` ${colorize(label, ANSI.bold, io.useColor)} ${colorize(status, status === 'live' ? ANSI.green : ANSI.yellow, io.useColor).padEnd(6)} ${parts.join(' ')}${binding}${staleWarn}`);
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(body.quota) && body.quota.length > 0) {
|
||||
// Legacy fallback for pre-v0.5.0 servers
|
||||
io.log('');
|
||||
io.log(colorize('Per-provider quota', ANSI.bold, io.useColor));
|
||||
io.log('─'.repeat(60));
|
||||
|
||||
+3
-2
@@ -8,7 +8,7 @@
|
||||
3. **Where** does the work live in the tree today (file + anchor).
|
||||
4. **When** does it need to land (trigger: load profile, security event, governance amendment).
|
||||
|
||||
**Reading order for a v1.x sprint kickoff.** As of 2026-05-26, #1 (streaming SF, D57+D58), #2 (multi-key auth, Phase 2), #4 and #7 (closed in D56), and #8 (dashboard enrichment, D82 Phase 5) are CLOSED. Remaining v1.x scope: #3 (soft trigger reactivation), #5 (provider cacheKeyFields mask), #6 (streaming SPAWN_FAILED salvage — unbundled from #1 at #1 close). All three remaining items have explicit "trigger to start" gates that have not fired.
|
||||
**Reading order for a v1.x sprint kickoff.** As of 2026-05-27, #1 (streaming SF, D57+D58), #2 (multi-key auth, Phase 2), #4, #7, and #8 are CLOSED. Remaining v1.x scope: #3 (soft trigger reactivation), #5 (provider cacheKeyFields mask), #6 (streaming SPAWN_FAILED salvage — unbundled from #1 at #1 close). All three remaining items have explicit "trigger to start" gates that have not fired.
|
||||
|
||||
---
|
||||
|
||||
@@ -107,8 +107,9 @@
|
||||
- `lib/audit-query.mjs` — current `aggregateRequests` is wall-clock-window; needs session-window variant
|
||||
- **Trigger to start.** ANY of: (a) Anthropic publishes a documented `claude usage` CLI or `api.anthropic.com/v1/usage` endpoint, (b) maintainer hits real "I want to see quota right now" pain often enough to design without per-provider truth (audit-derived only), (c) Phase 5 multi-tenant adds per-key spend limits and the dashboard needs to surface those.
|
||||
|
||||
## #7 — AUTH_MISSING tuple path test coverage (D40 follow-up)
|
||||
## #7 — AUTH_MISSING tuple path test coverage (D40 follow-up) — ✅ **CLOSED (D56, 2026-05-27)**
|
||||
|
||||
- **Status.** Closed. Test shipped at D56 (PR `f4-cli-plugin-quota-v2-plus-auth-missing-test`, 2026-05-27). Test: `test-features.mjs` line 6255 — `'engine: AUTH_MISSING terminates chain, fallbackDetail tuple records trigger_type:"auth_missing" (D56, v1.x roadmap #7)'`. Asserts: `result.fallbackDetail[0].code === 'AUTH_MISSING'`, `result.fallbackDetail[0].trigger_type === 'auth_missing'`, `result.fallbackHops === 0` (no advance). The test was already present in the file before this PR closed the roadmap entry.
|
||||
- **What.** Dedicated test in `test-features.mjs` Suite D40 that asserts the `fallbackDetail` tuple records the AUTH_MISSING path with `trigger_type: 'auth_missing'`. D40 reviewer flagged this as the last gap in the engine-path matrix; code is structurally correct, just lacks an explicit pin.
|
||||
- **Why deferred.** Low priority — the AUTH_MISSING early-return branch has the tuple push BEFORE it (verified in D40 reviewer pass), so coverage is implicit via the other engine-path tests. A 3-line dedicated test would make the pin explicit.
|
||||
- **Design.** No ADR needed. ~5-line test addition.
|
||||
|
||||
+79
-4
@@ -169,26 +169,101 @@ export function fmtHealth(body) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* formatResetCountdown(epochSeconds) → human-readable reset countdown.
|
||||
*
|
||||
* Mirrors bin/olp.mjs + dashboard.html versions. Five ranges:
|
||||
* past / < 1h / < 24h / < 7d / ≥ 7d
|
||||
*
|
||||
* Authority: ADR 0008 Amendment 2 (quota_v2 shape), ported from dashboard.html (D82).
|
||||
* No external deps. Duplicated here intentionally (olp-plugin ships separately).
|
||||
*/
|
||||
export function pluginFormatResetCountdown(epochSeconds) {
|
||||
if (epochSeconds == null) return "—";
|
||||
const nowMs = Date.now();
|
||||
const targetMs = epochSeconds * 1000;
|
||||
const diffMs = targetMs - nowMs;
|
||||
if (diffMs <= 0) return "resetting now";
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
if (diffMin < 60) return `resets in ${diffMin}m`;
|
||||
if (diffHr < 24) {
|
||||
const remMin = diffMin - diffHr * 60;
|
||||
if (remMin === 0) return `resets in ${diffHr}h`;
|
||||
return `resets in ${diffHr}h ${remMin}m`;
|
||||
}
|
||||
const target = new Date(targetMs);
|
||||
const timeStr = target.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
|
||||
if (diffDay < 7) {
|
||||
const dayStr = target.toLocaleString("en-US", { weekday: "short" });
|
||||
return `resets ${dayStr} ${timeStr}`;
|
||||
}
|
||||
const dateStr = target.toLocaleString("en-US", { month: "short", day: "numeric" });
|
||||
return `resets ${dateStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
export function fmtUsage(body) {
|
||||
let out = "OLP usage (24h)\n";
|
||||
out += "─────────────────────────────\n";
|
||||
const w = body.window_24h ?? body.usage_24h ?? {};
|
||||
if (w.requests !== undefined) {
|
||||
if (w.request_count !== undefined) {
|
||||
out += `Requests: ${w.request_count}\n`;
|
||||
const c = body.cache_hit_24h ?? {};
|
||||
if (typeof c.hit_rate === "number") {
|
||||
out += `Cache hit: ${(c.hit_rate * 100).toFixed(1)}%\n`;
|
||||
}
|
||||
} else if (w.requests !== undefined) {
|
||||
out += `Requests: ${w.requests}\n`;
|
||||
out += `Cache hit: ${w.cache_hit_rate != null ? `${(w.cache_hit_rate * 100).toFixed(1)}%` : "?"}\n`;
|
||||
out += `Fallbacks: ${w.fallbacks ?? "?"}\n`;
|
||||
} else if (typeof body.cache_hit_24h === "number") {
|
||||
// Dashboard-data shape: cache_hit_24h is a rate ∈ [0,1]
|
||||
// Legacy: cache_hit_24h as a bare number
|
||||
out += `Cache hit (24h): ${(body.cache_hit_24h * 100).toFixed(1)}%\n`;
|
||||
}
|
||||
if (Array.isArray(body.quota) && body.quota.length > 0) {
|
||||
|
||||
// F4: prefer quota_v2 when present (server v0.5.0+), fall back to legacy quota.
|
||||
// Authority: ADR 0008 Amendment 2 (quota_v2 shape).
|
||||
if (Array.isArray(body.quota_v2) && body.quota_v2.length > 0) {
|
||||
out += `\nPer-provider quota (live):\n`;
|
||||
for (const p of body.quota_v2) {
|
||||
const name = String(p.provider ?? "?").toUpperCase().padEnd(10);
|
||||
const status = p.status ?? "unavailable";
|
||||
if (status === "unavailable") {
|
||||
out += ` ${name} unavailable ${p.reason ?? "no public quota api"}\n`;
|
||||
} else if (status === "unreachable") {
|
||||
const fk = p.failure?.kind ?? "unknown";
|
||||
out += ` ${name} no cached data — failure: ${fk}\n`;
|
||||
} else {
|
||||
// live or stale
|
||||
const util = p.utilization ?? {};
|
||||
const reset = p.reset ?? {};
|
||||
const parts = [];
|
||||
for (const window of ["5h", "7d"]) {
|
||||
const frac = util[window];
|
||||
const resetEpoch = reset[window];
|
||||
if (frac != null) {
|
||||
const pct = `${Math.round(frac * 100)}%`;
|
||||
const rst = pluginFormatResetCountdown(resetEpoch);
|
||||
parts.push(`${window}: ${pct} (${rst})`);
|
||||
}
|
||||
}
|
||||
const staleNote = status === "stale"
|
||||
? ` ⚠ stale (${p.failure?.kind ?? "unknown"})`
|
||||
: "";
|
||||
out += ` ${name} ${status.padEnd(6)} ${parts.join(" ")}${staleNote}\n`;
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(body.quota) && body.quota.length > 0) {
|
||||
// Legacy fallback for pre-v0.5.0 servers
|
||||
out += `\nPer-provider quota:\n`;
|
||||
for (const q of body.quota) {
|
||||
const pct = typeof q.percent_used === "number" ? q.percent_used : null;
|
||||
const bar0 = pct != null ? ` ${bar(pct / 100, 12)} ${pct.toFixed(0)}%` : " no quota api";
|
||||
out += ` ${String(q.name ?? "?").padEnd(10)}${bar0}\n`;
|
||||
out += ` ${String(q.provider ?? q.name ?? "?").padEnd(10)}${bar0}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(body.top_fallback_chains_24h) && body.top_fallback_chains_24h.length > 0) {
|
||||
out += `\nTop fallback chains (24h):\n`;
|
||||
for (const f of body.top_fallback_chains_24h.slice(0, 5)) {
|
||||
|
||||
@@ -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