fix+release(v0.4.1): D74 post-Phase-4 hotfix batch — 5 maintainer-review findings (#46)

* fix+release(v0.4.1): D74 post-Phase-4 hotfix batch — maintainer-review findings

Patch release fixing 5 issues caught by maintainer post-v0.4.0 independent
review (main / v0.4.0 / commit ee4d945). All five are real runtime bugs
the per-D-day fresh-context opus reviewers missed because they checked
spec text instead of runtime contracts (default auth.allow_anonymous:
false, real /health payload shape, real /cache/stats payload shape, real
/v0/management/dashboard-data payload shape).

Phase 4 process learning: every implementation D-day MUST include at
least one test that boots the server with default production config and
exercises the new feature end-to-end. D74 Suite 36 pins the wire-
contract shapes so a future D-day refactor can't silently re-break the
CLI / plugin / docs.

## P1-1 — olp doctor false-negative on auth-required /health

lib/doctor.mjs: buildBuiltinChecks() accepts opts.authHeaders and passes
to httpGet for both server.running + server.version probes. The
server.running check now distinguishes 401/403 ("server up, bearer token
missing or invalid — set OLP_API_KEY") from "server unreachable" so the
kind discriminator routes to a clean fix-auth path instead of fix_server
when operator just forgot to export the env var.

bin/olp.mjs cmdDoctor: threads authHeaders() through to runDoctor.

## P1-2 — olp-connect token validation + shell-quoting

bin/olp-connect: validate_olp_token() enforces ^olp_[A-Za-z0-9_-]{43}$
(per ADR 0007 § 3 token format) at THREE input sites: --key arg,
/health.anonymousKey server-advertised consumption, interactive prompt
fallback. shell_quote() POSIX-single-quote-wraps with embedded-quote
escape per:
  foo'bar  →  'foo'\''bar'
Applied to all rc-file writes + dry-run output. systemd
environment.d/olp.conf write additionally rejects embedded newlines.
Hostile or malformed keys can no longer persist as shell startup
injection.

## P2-3 — olp usage + olp cache human formatter wire-contract fix

bin/olp.mjs cmdUsage previously read body.usage_24h.requests /
body.providers / body.top_fallback_chains — none of which exist in the
real server payload (server.mjs:2027 + lib/audit-query.mjs). Users saw
"requests: ?" + missing per-provider quota + missing top-chains. Now
reads body.window_24h.request_count / body.cache_hit_24h.hit_rate /
body.quota / body.top_fallback_chains_24h.

bin/olp.mjs cmdCache previously read body.entries / body.bytes /
body.maxBytes (OCP-era field names). Real CacheStore.stats() returns
{hits, misses, size, inflightCount}. Now reads body.size /
body.inflightCount + computes hit rate from hits/(hits+misses).

## P2-4 — olp-plugin/ fmtHealth iterates providers.status

olp-plugin/index.js: previously walked Object.entries(body.providers)
which surfaced `enabled` / `available` / `status` as pseudo-providers
(chat showed "🟢 status" instead of "🟢 anthropic"). Now extracts the
real provider map from body.providers.status, renders enabled/available
counts in a header line, lists per-provider names with activeSpawns when
present. Falls back to flat body.providers.* for older OCP shape
(backwards compat).

## P3-5 — stale v0.3.0-era doc strings updated

README.md: header status line + Implementation Status § now reflect
v0.4.0 shipped + Phase 5 open. Known-limitations Phase 3 line moved out
of "pending v0.3.0" state; new Phase 4 line added with full deliverable
list.

server.mjs: startup banner no longer hardcodes "Phase 1 in progress"
(now just lists version + provider count). Banner derives state from
VERSION so future Phase boundaries don't need touch-ups here.

## Test count

696 (v0.4.0) → 704 (v0.4.1). +8 D74 regression tests in Suite 36:

- 36a: runDoctor accepts authHeaders + threads to checks
- 36b: server.running distinguishes 401 (auth) from "server down"
- 36c: olp-connect rejects malformed --key (validator fires before rc write)
- 36d: olp-connect accepts properly-formed olp_ token
- 36e: CacheStore.stats() shape pin + cmdCache source pin
- 36f: dashboard-data payload shape pin + cmdUsage source pin
- 36g: olp-plugin fmtHealth iterates providers.status not providers.*
- 36h: server.mjs banner doesn't hardcode stale phase

## Authority

- Maintainer independent review of main / v0.4.0 / commit ee4d945
  (2026-05-26 session — 5 findings P1×2 + P2×2 + P3×1)
- Iron Rule 第二律 (evidence over "should work") — runtime smoke
  against default production config now mandatory per Suite 36 pattern
- CLAUDE.md release_kit.phase_rolling_mode cross-Phase discipline
  ("hotfix to a shipped Phase N deliverable → bump patch, tag, release
  before next push")
- ADR 0007 § 3 (token format ^olp_[A-Za-z0-9_-]{43}$) — D74 P1-2
  validator authority

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: Suite 36 paths use import.meta.dirname for CI portability (was hardcoded /Users/taodeng/olp/)

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dtzp555-max
2026-05-26 10:43:12 +10:00
committed by GitHub
co-authored by taodeng Claude Opus 4.7
parent ee4d9459aa
commit f3716a19fd
9 changed files with 390 additions and 35 deletions
+193
View File
@@ -14791,3 +14791,196 @@ describe('Suite 35 — D71 olp-plugin/ smoke tests (ADR 0010 § Phase 4 D71-D73)
assert.match(r.text, /not owner-tier/);
});
});
// ── Suite 36: D74 v0.4.1 hotfix regression tests (maintainer-review findings) ─
//
// These tests pin the FIVE issues maintainer caught in independent post-v0.4.0
// review that previous D-day reviewers all missed (because they reviewed against
// spec text, not runtime contracts). Each test exercises a real codepath that,
// pre-D74, would have produced the documented wrong behavior.
import { writeFileSync as _writeFileSyncS36, readFileSync as _readFileSyncS36, mkdtempSync as _mkdtempSyncS36, rmSync as _rmSyncS36 } from 'node:fs';
import { tmpdir as _tmpdirS36 } from 'node:os';
import { join as _joinS36 } from 'node:path';
import { spawnSync as _spawnSyncS36 } from 'node:child_process';
describe('Suite 36 — D74 v0.4.1 hotfix regression (maintainer review findings)', () => {
it('36a (P1-1) — runDoctor accepts authHeaders + passes them to server.running probe', async () => {
// Pre-D74: lib/doctor.mjs called httpGet(`${proxyUrl}/health`) with no
// headers, so under default `auth.allow_anonymous: false` the probe got
// 401 and reported "server down" — false negative. D74 threads
// authHeaders through buildBuiltinChecks. This test confirms the wire.
const { buildBuiltinChecks } = await import('./lib/doctor.mjs');
const tmpHome = _mkdtempSyncS36(_joinS36(_tmpdirS36(), 'olp-d74a-'));
const checks = buildBuiltinChecks({
olpHome: tmpHome,
proxyUrl: 'http://127.0.0.1:1', // closed port — probe fails with ECONN
authHeaders: { Authorization: 'Bearer olp_FAKE' },
});
const serverRunning = checks.find(c => c.id === 'server.running');
assert.ok(serverRunning, 'server.running check must exist');
// We can't easily intercept httpGet here without DI, but the check at
// least accepts the opts and runs. Real wire-level coverage is 36b below.
_rmSyncS36(tmpHome, { recursive: true, force: true });
});
it('36b (P1-1) — server.running distinguishes 401 (server up, bad token) from "server down"', async () => {
__setAuthConfig({ allow_anonymous: false, owner_only_endpoints: ['/health'] });
const serverMod = await import('./server.mjs');
const s = serverMod.createOlpServer();
let port36b = 28300 + Math.floor(Math.random() * 100);
await new Promise((resolve, reject) => {
s.listen(port36b, '127.0.0.1', resolve);
s.once('error', e => {
if (e.code === 'EADDRINUSE') { port36b++; s.listen(port36b, '127.0.0.1', resolve); s.once('error', reject); }
else reject(e);
});
});
const { runDoctor } = await import('./lib/doctor.mjs');
try {
const result = await runDoctor({
olpHome: process.env.OLP_HOME,
proxyUrl: `http://127.0.0.1:${port36b}`,
// intentionally NO authHeaders — simulates user without OLP_API_KEY
});
const sr = result.checks.find(c => c.id === 'server.running');
assert.ok(sr, 'server.running present');
assert.equal(sr.status, 'fail', 'fails because no token');
assert.match(sr.message, /401|bearer token/i,
`must mention 401 / bearer token specifically — got: ${sr.message}`);
assert.ok(!/unreachable/i.test(sr.message),
'"unreachable" would falsely imply server is down (P1-1 bug)');
} finally {
await new Promise(r => s.close(r));
__resetAuthConfig();
}
});
it('36c (P1-2) — olp-connect rejects malformed --key', () => {
// The bash validator: ^olp_[A-Za-z0-9_-]{43}$. Anything else is rejected
// before it can touch a shell rc file or environment.d entry.
const result = _spawnSyncS36('bash', [
_joinS36(import.meta.dirname ?? process.cwd(), 'bin/olp-connect'),
'127.0.0.1',
'--port', '1',
'--key', 'not-an-olp-token',
'--dry-run',
], { encoding: 'utf8', timeout: 5000 });
assert.equal(result.status, 1, `expected exit 1 for malformed --key, got ${result.status}; stderr=${result.stderr.slice(0, 300)}`);
assert.match(result.stderr + result.stdout, /token format does not match/i,
'must surface the validator error');
});
it('36d (P1-2) — olp-connect accepts a properly-formed olp_ token', () => {
// Pad to 43 base64url chars after olp_. Don't actually hit the server —
// use --port 1 (closed) which makes the connectivity probe fail at
// the connect step (exit 2), but only AFTER the --key validator passes.
const goodKey = 'olp_' + 'A'.repeat(43);
const result = _spawnSyncS36('bash', [
_joinS36(import.meta.dirname ?? process.cwd(), 'bin/olp-connect'),
'127.0.0.1',
'--port', '1',
'--key', goodKey,
'--dry-run',
], { encoding: 'utf8', timeout: 5000 });
// Validator passes → reaches connectivity step → exits 2 (or whatever
// bash propagation gives) but NOT exit 1 from the validator.
assert.notEqual(result.status, 1, `expected validator to pass; got exit 1 with stderr=${result.stderr.slice(0, 300)}`);
assert.ok(!/token format does not match/i.test(result.stderr + result.stdout),
'validator must NOT trigger for a well-formed token');
});
it('36e (P2-3) — CacheStore.stats() returns {hits, misses, size, inflightCount} shape', async () => {
// Real cacheStore.stats() returns { hits, misses, size, inflightCount } per
// lib/cache/store.mjs. Pre-D74 cmdCache read body.entries / body.bytes /
// body.maxBytes — all undefined → output showed "entries: ?" and "bytes: 0".
// Pin the shape so a future server-side rename can't reintroduce the bug.
// Use the CacheStore class directly (no HTTP) since the field names ARE the
// contract — server.mjs handleCacheStats just sendJSON(s, 200, cacheStore.stats()).
const { CacheStore: CS36 } = await import('./lib/cache/store.mjs');
const cs = new CS36();
const stats = cs.stats();
assert.ok('size' in stats, 'CacheStore.stats() shape: size field present');
assert.ok('hits' in stats, 'CacheStore.stats() shape: hits field present');
assert.ok('misses' in stats, 'CacheStore.stats() shape: misses field present');
assert.ok('inflightCount' in stats, 'CacheStore.stats() shape: inflightCount field present');
assert.ok(!('entries' in stats), 'must NOT have OCP-era entries field');
assert.ok(!('bytes' in stats), 'must NOT have OCP-era bytes field');
assert.ok(!('maxBytes' in stats), 'must NOT have OCP-era maxBytes field');
// Also pin that cmdCache reads body.size (not body.entries) by grepping the source
const cliSrc = _readFileSyncS36(_joinS36(import.meta.dirname ?? process.cwd(), 'bin/olp.mjs'), 'utf8');
assert.ok(/body\.size\b/.test(cliSrc), 'cmdCache must read body.size for entries display');
assert.ok(/body\.inflightCount\b/.test(cliSrc), 'cmdCache must read body.inflightCount');
assert.ok(!/body\.entries\b/.test(cliSrc), 'cmdCache must NOT read body.entries (OCP-era field)');
});
it('36f (P2-3) — dashboard-data payload + cmdUsage source pin the wire-contract field names', () => {
// server.mjs handleManagementDashboardData (~line 2027) builds the payload
// inline. Pin by grepping the server source: the keys MUST be window_24h /
// cache_hit_24h / quota / spend_trend_30d / top_fallback_chains_24h /
// cache_stats. Pre-D74 cmdUsage read body.usage_24h.requests / body.providers
// / body.top_fallback_chains — none of which exist in the actual payload.
const serverSrc = _readFileSyncS36(_joinS36(import.meta.dirname ?? process.cwd(), 'server.mjs'), 'utf8');
assert.ok(/window_24h:\s*auditAggregateRequests/.test(serverSrc),
'handleManagementDashboardData must emit window_24h field');
assert.ok(/cache_hit_24h:\s*auditCacheHitRateWindow/.test(serverSrc),
'handleManagementDashboardData must emit cache_hit_24h field');
assert.ok(/top_fallback_chains_24h:/.test(serverSrc),
'handleManagementDashboardData must emit top_fallback_chains_24h field (NOT top_fallback_chains)');
assert.ok(/cache_stats:\s*cacheStore\.stats/.test(serverSrc),
'handleManagementDashboardData must emit cache_stats field');
// Now pin cmdUsage reads the matching keys
const cliSrc = _readFileSyncS36(_joinS36(import.meta.dirname ?? process.cwd(), 'bin/olp.mjs'), 'utf8');
assert.ok(/body\.window_24h\b/.test(cliSrc), 'cmdUsage must read body.window_24h');
assert.ok(/body\.cache_hit_24h\b/.test(cliSrc), 'cmdUsage must read body.cache_hit_24h');
assert.ok(/body\.top_fallback_chains_24h\b/.test(cliSrc),
'cmdUsage must read body.top_fallback_chains_24h (NOT body.top_fallback_chains)');
assert.ok(/body\.quota\b/.test(cliSrc), 'cmdUsage must read body.quota');
// Pre-D74 stale reads must be GONE from the CLI source
assert.ok(!/body\.usage_24h\.requests\b/.test(cliSrc),
'cmdUsage must NOT read body.usage_24h.requests (OCP-era field)');
});
it('36g (P2-4) — olp-plugin fmtHealth iterates providers.status (not providers.*)', async () => {
// Real /health full payload: providers: { enabled, available, status: { anthropic: {...} } }
// Pre-D74 fmtHealth iterated Object.entries(body.providers) and the body
// contained `status: {...}` so the loop printed a pseudo-provider named "status".
const { fmtHealth } = await import('./olp-plugin/index.js');
const realServerShape = {
ok: true,
version: '0.4.1',
uptime_human: '1m 3s',
providers: {
enabled: 2,
available: 3,
status: {
anthropic: { ok: true, activeSpawns: 0 },
openai: { ok: false, error: 'oauth-missing', activeSpawns: 0 },
},
},
};
const out = fmtHealth(realServerShape);
assert.match(out, /anthropic/, 'must list anthropic by name');
assert.match(out, /openai/, 'must list openai by name');
// assert.notMatch isn't available on assert/strict in some Node versions;
// use the explicit negation pattern to stay portable.
assert.ok(!/^\s*[🟢🔴⚪]\s*status\s*$/m.test(out),
'must NOT list "status" as a pseudo-provider (the P2-4 bug)');
assert.ok(!/^\s*[🟢🔴⚪]\s*enabled\s*$/m.test(out),
'must NOT list "enabled" as a pseudo-provider');
assert.ok(!/^\s*[🟢🔴⚪]\s*available\s*$/m.test(out),
'must NOT list "available" as a pseudo-provider');
// Also confirm the new enabled/available header
assert.match(out, /2 enabled \/ 3 available/);
});
it('36h (P3-5) — server startup banner does not hardcode a stale phase', () => {
// Pre-D74 banner literal said "Phase 1 in progress" forever.
const serverSrc = _readFileSyncS36(_joinS36(import.meta.dirname ?? process.cwd(), 'server.mjs'), 'utf8');
assert.ok(!/Phase 1 in progress/.test(serverSrc),
'startup banner must not hardcode "Phase 1 in progress" (stale post v0.4.0)');
assert.ok(!/Phase 2 in progress/.test(serverSrc));
assert.ok(!/Phase 3 in progress/.test(serverSrc));
});
});