diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd2338..9831ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,16 @@ All notable changes to OLP land here. Per `CLAUDE.md` release_kit overlay, this ## Unreleased -(empty — Phase 4 entries land here once Phase 4 opens) +### D56 — v1.x cleanup batch #1: AUTH_MISSING tuple test + `/health` activeSpawns (post-Phase-3) + +Post-Phase-3 cleanup batch. Resolves two small v1.x-roadmap deferrals into one D-day. No new user-facing feature; pins existing behaviour into tests + finally wires the ADR-documented `activeSpawns` field on `/health`. + +- **AUTH_MISSING tuple test** (v1.x roadmap #7 / D45 reviewer P3 deferral). New engine-level test in Suite D40 asserts that an `AUTH_MISSING` hop produces a `fallbackDetail` tuple with `trigger_type: 'auth_missing'` AND that the engine does NOT advance past the AUTH_MISSING hop (per ADR 0004 § Decision — `HARD_TRIGGER_CODES[AUTH_MISSING]=false`). Pre-D56 the behaviour was implicit through other engine-path tests; this commit makes it explicit so a future refactor that moves the tuple-push past the auth_missing branch fails this test directly. +- **`/health` activeSpawns integration** (v1.x roadmap #4 / ADR 0002 Amendment 6 forward note). `handleHealth` now surfaces `providers.status..activeSpawns` (sourced from D38 `getActiveSpawnCount(name)`). The field is set BEFORE `healthCheck()` is awaited so it remains accessible even when `healthCheck()` throws (cheap in-memory counter read). New Suite 21c-extra test pins the field presence + non-negative value for every enabled provider. With no requests in flight: 0; under saturation: equals `hints.maxConcurrent`. +- **Test count:** 601 → 603 (+2 D56 tests). +- **Authority:** `docs/v1x-roadmap.md` #4 + #7; ADR 0002 Amendment 6 (concurrency observability forward note); ADR 0004 Amendment 5 (X-OLP-Fallback-Detail tuple shape the new test asserts). + + ## v0.3.0 — 2026-05-25 diff --git a/server.mjs b/server.mjs index 304fff9..a71c570 100644 --- a/server.mjs +++ b/server.mjs @@ -577,10 +577,17 @@ async function handleHealth(req, res) { const available = listAllProviderNames().length; const providerStatuses = {}; for (const [name, provider] of loadedProviders) { + // D56 / v1.x roadmap #4 (ADR 0002 Amendment 6 forward note): surface + // per-provider active spawn count for capacity-planning observability. + // D38 shipped getActiveSpawnCount; this is the /health integration. + // The field is set BEFORE healthCheck() in case healthCheck throws — + // activeSpawns is cheap (in-memory counter read) and useful even + // when healthCheck fails. + const activeSpawns = getActiveSpawnCount(name); try { - providerStatuses[name] = await provider.healthCheck(); + providerStatuses[name] = { ...(await provider.healthCheck()), activeSpawns }; } catch (e) { - providerStatuses[name] = { ok: false, error: e.message }; + providerStatuses[name] = { ok: false, error: e.message, activeSpawns }; } } sendJSON(res, 200, { diff --git a/test-features.mjs b/test-features.mjs index 5f61140..4831c8d 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -6247,6 +6247,30 @@ describe('D40 — X-OLP-Fallback-Detail header (issue #7)', () => { assert.equal(result.fallbackDetail[0].code, 'SPAWN_FAILED'); }); + it('engine: AUTH_MISSING terminates chain, fallbackDetail tuple records trigger_type:"auth_missing" (D56, v1.x roadmap #7)', async () => { + // v1.x roadmap #7 (D40 follow-up): explicit pin that the AUTH_MISSING + // path produces a fallbackDetail tuple with trigger_type:"auth_missing" + // BEFORE the engine's early-return at engine.mjs:486. Was implicit via + // other engine-path tests; D56 makes it explicit so any future refactor + // that moves the tuple-push past the auth_missing branch fails this. + const err = new ProviderError('No OAuth token found', 'AUTH_MISSING'); + const chain = [ + { provider: 'anthropic', model: 'claude-sonnet-4-6' }, + { provider: 'openai', model: 'gpt-5.5' }, // present to verify AUTH_MISSING does NOT advance + ]; + const hopFn = async () => { throw err; }; + const result = await executeWithFallback(chain, makeIR({ model: 'claude-sonnet-4-6' }), hopFn); + // AUTH_MISSING is HARD_TRIGGER_CODES[AUTH_MISSING]=false (engine.mjs L52); + // chain stops at hop 0 instead of advancing to openai. + assert.equal(result.chunks, null, 'AUTH_MISSING terminates chain'); + assert.equal(result.fallbackHops, 0, 'AUTH_MISSING does NOT advance — stays at hop 0'); + assert.equal(result.fallbackDetail.length, 1, 'fallbackDetail has exactly 1 tuple (the AUTH_MISSING hop)'); + assert.equal(result.fallbackDetail[0].code, 'AUTH_MISSING'); + assert.equal(result.fallbackDetail[0].trigger_type, 'auth_missing'); + assert.equal(result.fallbackDetail[0].provider, 'anthropic'); + assert.equal(result.fallbackDetail[0].hop, 0); + }); + it('engine: non-ProviderError exception → tuple code is "UNKNOWN"', async () => { const err = new Error('Something unexpected'); // no .code field const chain = [{ provider: 'anthropic', model: 'claude-sonnet-4-6' }]; @@ -10522,6 +10546,29 @@ describe('Suite 21 — D46 owner-vs-guest gating (ADR 0007 §§ 7.1, 7.2)', () = assert.ok('status' in body.providers, 'owner /health providers.status must be present'); }); + it('21c-extra: owner /health each provider status carries activeSpawns field (D56, v1.x #4 / ADR 0002 Amendment 6)', async () => { + // ADR 0002 Amendment 6 forward note: when surfaced on /health, the + // per-provider concurrency counter lives at providers.status.. + // activeSpawns. D56 wires it. With no requests in flight, the value + // is 0; under saturation it equals hints.maxConcurrent. + const { plaintext_token } = createKey({ name: '21c-extra', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP }); + const r = await fetch({ + port, method: 'GET', path: '/health', + headers: { Authorization: `Bearer ${plaintext_token}` }, + }); + assert.equal(r.status, 200); + const body = JSON.parse(r.body); + // At least one provider must be enabled in the fixture + const statusEntries = Object.entries(body.providers.status); + assert.ok(statusEntries.length >= 1, 'fixture has at least one enabled provider'); + for (const [name, status] of statusEntries) { + assert.ok('activeSpawns' in status, + `providers.status.${name}.activeSpawns MUST be present (ADR 0002 Amendment 6)`); + assert.equal(typeof status.activeSpawns, 'number'); + assert.ok(status.activeSpawns >= 0, 'activeSpawns >= 0'); + } + }); + it('21d: owner_only_endpoints config opt-out — empty list → guest gets full payload', async () => { __setAuthConfig({ allow_anonymous: true,