feat+test+docs: D64-D67 — olp Node CLI + doctor framework + per-provider doctor checks + ADR 0002 Amendment 7 (#42)

* feat+test+docs: D64+D65+D66+D67 — olp Node CLI + olp doctor framework + per-provider doctor checks + ADR 0002 Amendment 7

Second substantive Phase 4 implementation. 4 D-days bundled per Iron Rule
11 IDR — CLI dispatches to doctor; doctor calls into provider plugins via
the new contract method; ADR amendment authorizes the contract change.
Single PR is the minimum reviewable unit for "does plugin amendment +
plugin impl + doctor consumer line up?"

## D64 — bin/olp.mjs Node CLI scaffold

Operator surface for OLP. Node not bash (per ADR 0010 § Notes — bash with
python3 JSON parsing is a known fragile point; OLP standardizes on Node).

Subcommands:
- status / health / usage / models / cache — HTTP calls to existing endpoints
- providers — local: cross-references models-registry.json + config.json
- chain show [<model>] — local: prints routing.chains from ~/.olp/config.json
- logs [N] [--level X] — reads ~/.olp/logs/audit.ndjson via audit-query
- restart — launchctl (macOS) / systemctl --user (Linux), best-effort
- keys ... — delegates to bin/olp-keys.mjs runCli (no logic duplicated)
- doctor [--check <id|category>] [--json] — D65 framework
- help / --help / -h

Token / URL resolution:
- OLP_PROXY_URL env → OLP_PORT env → http://127.0.0.1:4567 (D60 default)
- OLP_API_KEY env → OLP_OWNER_TOKEN env (filesystem manifest tokens are
  one-way SHA-256 per ADR 0007 § 5 — not recoverable; CLI surfaces
  helpful 401 message pointing at olp-keys keygen)

Output:
- Default: human-readable ANSI-colored text (no chalk dep, auto-suppressed
  under --json)
- --json: raw JSON for scripting
- Exit codes: 0=ok / 1=usage / 2=network|HTTP / 3=auth

No npm deps. Built-ins only.

Installed via package.json bin entry so `npx olp <subcommand>` works.

## D65 — lib/doctor.mjs framework

Ports OCP scripts/doctor.mjs (the bedrock of AI-driven self-repair per the
OCP audit's #2 inheritance candidate). Machine-readable next_action so a
Claude Code / Cursor / etc. agent can self-repair OLP.

Check shape:
  { id, category, async run(): { status: 'ok'|'fail'|'warn', message, evidence? } }

Built-in checks: server.running, server.version, config.exists,
config.providers_enabled, config.chains_configured, auth.owner_key_exists,
system.node_version. Per-provider checks collected dynamically via
provider.doctorChecks() per D67.

--json output:
  { checks: [...], kind: noop|update|fix_oauth|fix_config|fresh_install|
    fix_server|fix_provider, next_action: { ai_executable: [],
    human_required: [], verify: 'olp doctor' }, summary }

--check <id-or-category> for tight repair-loop fast paths.

## D66 — Per-provider doctorChecks() implementations

Each shipped plugin contributes its own checks (lives in plugin file so the
provider's maintainer updates it naturally):

- anthropic.mjs: cli_available (claude --version) + oauth_token_present
  (~/.claude/.credentials.json OR ANTHROPIC_OAUTH_TOKEN env)
- codex.mjs: cli_available (codex --version) + auth_present
  (~/.codex/config.json)
- mistral.mjs: cli_available (vibe --version) + api_key_present
  (MISTRAL_API_KEY env OR ~/.vibe/.env)

Each fail returns evidence.fix_commands (for ai_executable[]) or
evidence.human_required (e.g., 'run: claude auth login').

## D67 — ADR 0002 Amendment 7

New amendment adds OPTIONAL provider.doctorChecks(): DoctorCheck[] to the
Provider contract. Backwards compatible — plugins without doctorChecks()
contribute no provider checks (default behavior). Validator extended in
lib/providers/base.mjs validateProvider.

## Test count

636 → 658 (+22 tests across Suites 32, 33).

- Suite 32 — bin/olp.mjs CLI scaffold (10 tests): parseArgv, USAGE,
  unknown-subcommand, providers local + --json, chain show, status via
  ephemeral server with owner token, ECONNREFUSED → exit 2,
  resolveBearerToken precedence
- Suite 33 — lib/doctor.mjs framework (12 tests): all kind branches
  (noop / fresh_install / fix_server / fix_oauth / fix_provider),
  collectProviderChecks reads doctorChecks(), throwing plugin captured,
  --check filter, built-in checks against temp HOME, anthropic plugin
  probe set, resolveProxyUrl precedence, deriveKind/deriveNextAction units

## Scope discipline

server.mjs UNTOUCHED. All HTTP subcommands consume EXISTING endpoints.
No new endpoints. No /health.anonymousKey. No olp-connect. No Telegram
plugin. No IDE docs bundle. No CHANGELOG / package.json version bump
(Phase 4 close handles versioning; only package.json bin entries updated).

## Known limitations (flagged for reviewer)

- olp restart not unit-tested (would require mocking child_process.spawn
  in invasive way; manual smoke-test only at this D-day)
- olp logs --level filtering matches optional level field if present in
  audit-event objects; appendAuditEvent already populates it where
  meaningful — no schema change needed in this bundle
- olp usage panel shape inferred from lib/audit-query.mjs exports; if
  /v0/management/dashboard-data wire shape differs in subtle ways,
  formatter degrades to '?' but --json always works

## Authority

- ADR 0010 § Phase 4 D-day plan D64-D67 line
- ADR 0002 Amendment 7 (this commit — new amendment)
- OCP ocp bash wrapper /Users/taodeng/ocp/ocp (subcommand reference,
  translated to Node)
- OCP scripts/doctor.mjs /Users/taodeng/ocp/scripts/doctor.mjs
  (framework reference)
- 2026-05-26 brainstorm (Top 5 OCP inheritance candidates, item 2:
  olp doctor machine-readable next_action)

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

* fix: D64-D67 reviewer P2 fold-in — shell-quote ai_executable paths + launchctl kickstart caveat

Reviewer APPROVE — 0 P0/P1, 2 P2 hardening notes folded in.

P2-1 — Shell-quote interpolated paths in fix_commands.

lib/doctor.mjs config.exists fix_commands previously interpolated
${olpHome} / ${configPath} unquoted into the printf template. A
malicious OLP_HOME env value containing shell metacharacters could
inject commands into the suggested-fix string an AI agent (or human)
pastes back into a terminal.

Added _shellQuote(s) helper (POSIX single-quote-wrap with escape for
embedded single quotes per POSIX shell rules). Risk surface is narrow
at family scale (operator local env, single-user proxy), but hardening
cost is one helper.

P2-2 — Document launchctl kickstart -k env-stale pitfall.

cmdRestart header now carries an explicit caveat that `launchctl
kickstart -k` does NOT re-read the plist EnvironmentVariables block —
launchd uses cached env from the most recent bootstrap. This is a
known OCP institutional lesson (PIT INDEX in cc-rules MEMORY.md). The
comment documents the bootout/bootstrap dance for env reloads and
notes that the Phase 4 installer (post-D73) will expose `olp restart
--full` for the safer reload path.

658/658 tests still pass; the _shellQuote change is invisible to
existing tests because the test fixtures use safe paths.

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

---------

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 08:48:20 +10:00
committed by GitHub
co-authored by taodeng Claude Opus 4.7
parent e6701ff698
commit e69e908dae
9 changed files with 1982 additions and 1 deletions
+464
View File
@@ -13591,3 +13591,467 @@ describe('Suite 31 — D63 /v0/management/status (ADR 0010 § Phase 4 D61-D63)',
`total_requests expected >= 1, got ${body.stats.total_requests}`);
});
});
// ── Suite 32: D64 bin/olp.mjs CLI scaffold (ADR 0010 § Phase 4 D64-D67) ───
//
// Smoke tests for the operator CLI. The CLI is invoked in-process via its
// exported runCli() so we get exit codes without spawning a subprocess. The
// process.env is mutated per-test (saved + restored in before/after) so the
// runtime resolution paths (OLP_PROXY_URL / OLP_PORT / OLP_API_KEY / OLP_HOME)
// behave deterministically.
//
// The CLI sources are:
// bin/olp.mjs — runCli(argv, { out, err, useColor }) → exit code
// lib/doctor.mjs — runDoctor() consumed by `olp doctor`
//
// These tests do NOT spawn a real OLP server for the HTTP subcommands; instead
// each HTTP test starts an ephemeral createOlpServer() listening on port 0 and
// points the CLI at that port via --proxy-url. The owner_token used is created
// per-test via createKey({ owner_tier: 'owner' }).
import { runCli as runOlpCli, parseArgv as parseOlpArgv, resolveBearerToken as olpResolveBearerToken } from './bin/olp.mjs';
describe('Suite 32 — D64 bin/olp.mjs CLI scaffold (ADR 0010 § Phase 4 D64-D67)', () => {
const SAVED_ENV_32 = {
OLP_HOME: process.env.OLP_HOME,
OLP_PROXY_URL: process.env.OLP_PROXY_URL,
OLP_PORT: process.env.OLP_PORT,
OLP_API_KEY: process.env.OLP_API_KEY,
OLP_OWNER_TOKEN: process.env.OLP_OWNER_TOKEN,
};
let TMP32;
before(() => {
TMP32 = mkdtempSync(pathJoin(tmpdir(), 'olp-test-32-'));
process.env.OLP_HOME = TMP32;
// Clear conflicting env vars for deterministic resolution
delete process.env.OLP_PROXY_URL;
delete process.env.OLP_PORT;
delete process.env.OLP_API_KEY;
delete process.env.OLP_OWNER_TOKEN;
});
after(() => {
for (const [k, v] of Object.entries(SAVED_ENV_32)) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
rmSync(TMP32, { recursive: true, force: true });
});
// Helper: capture stdout / stderr buffers via IO writers
function makeCapture() {
let out = '', err = '';
return {
out: s => { out += s; },
err: s => { err += s; },
get stdout() { return out; },
get stderr() { return err; },
};
}
it('32a — parseArgv: --flag=value + --flag value + bare --flag', () => {
const a = parseOlpArgv(['--json', '--port=4567', '--name', 'foo', 'positional']);
assert.equal(a.flags.json, true);
assert.equal(a.flags.port, '4567');
assert.equal(a.flags.name, 'foo');
assert.deepEqual(a.positional, ['positional']);
});
it('32b — `olp` (no args) prints USAGE and returns exit code 1', async () => {
const cap = makeCapture();
const code = await runOlpCli([], { out: cap.out, err: cap.err, useColor: false });
assert.equal(code, 1, 'no-args should return 1');
assert.match(cap.stdout, /OLP operator CLI/);
assert.match(cap.stdout, /Subcommands:/);
});
it('32c — `olp help` prints USAGE and returns exit code 0', async () => {
const cap = makeCapture();
const code = await runOlpCli(['help'], { out: cap.out, err: cap.err, useColor: false });
assert.equal(code, 0);
assert.match(cap.stdout, /OLP operator CLI/);
});
it('32d — `olp unknown` prints error + USAGE on stderr, returns exit code 1', async () => {
const cap = makeCapture();
const code = await runOlpCli(['frobnicate'], { out: cap.out, err: cap.err, useColor: false });
assert.equal(code, 1);
assert.match(cap.stderr, /unknown subcommand "frobnicate"/);
assert.match(cap.stderr, /OLP operator CLI/);
});
it('32e — `olp providers` (local, no HTTP) → lists registry entries', async () => {
const cap = makeCapture();
const code = await runOlpCli(['providers'], { out: cap.out, err: cap.err, useColor: false });
assert.equal(code, 0);
// All three shipped plugin keys must appear
assert.match(cap.stdout, /anthropic/);
assert.match(cap.stdout, /openai/);
assert.match(cap.stdout, /mistral/);
// No providers enabled in the empty TMP32 config → all show disabled
assert.match(cap.stdout, /disabled/);
});
it('32f — `olp providers --json` emits parseable JSON with providers[]', async () => {
const cap = makeCapture();
const code = await runOlpCli(['providers', '--json'], { out: cap.out, err: cap.err, useColor: false });
assert.equal(code, 0);
const body = JSON.parse(cap.stdout);
assert.ok(Array.isArray(body.providers));
assert.ok(body.providers.length >= 3);
const names = body.providers.map(p => p.name);
assert.ok(names.includes('anthropic'));
assert.ok(names.includes('openai'));
assert.ok(names.includes('mistral'));
});
it('32g — `olp chain show` with no chains in config prints "no chains configured"', async () => {
const cap = makeCapture();
const code = await runOlpCli(['chain', 'show'], { out: cap.out, err: cap.err, useColor: false });
assert.equal(code, 0);
assert.match(cap.stdout, /no chains configured/);
});
it('32h — `olp status` against an ephemeral OLP server with owner token → 200', async () => {
// Spin up an ephemeral server bound to a random port; CLI talks to it.
__setProvidersEnabled({});
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
const mod = await import('./server.mjs');
mod.__clearCache();
mod.__clearRecentErrors();
mod.__resetRequestCounters();
const server32 = mod.createOlpServer();
await new Promise((resolve, reject) => {
server32.listen(0, '127.0.0.1', resolve);
server32.once('error', reject);
});
const port32 = server32.address().port;
try {
// Create owner key + use it via OLP_API_KEY env (resolveBearerToken reads it first)
const { plaintext_token } = createKey({
name: '32h-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP32,
});
const SAVED_TOK = process.env.OLP_API_KEY;
process.env.OLP_API_KEY = plaintext_token;
try {
const cap = makeCapture();
const code = await runOlpCli(
['status', `--proxy-url=http://127.0.0.1:${port32}`, '--json'],
{ out: cap.out, err: cap.err, useColor: false },
);
assert.equal(code, 0, `status JSON exit non-zero; stderr=${cap.stderr}`);
const body = JSON.parse(cap.stdout);
assert.equal(body.ok, true);
assert.ok(typeof body.version === 'string');
assert.ok(typeof body.uptime_ms === 'number');
} finally {
if (SAVED_TOK === undefined) delete process.env.OLP_API_KEY;
else process.env.OLP_API_KEY = SAVED_TOK;
}
} finally {
await new Promise(r => server32.close(r));
__resetProvidersEnabled();
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
}
});
it('32i — `olp status` against a non-existent port → exit 2 ECONNREFUSED', async () => {
// Pick a random high port unlikely to be in use; the CLI must detect
// ECONNREFUSED and surface the helpful hint.
const cap = makeCapture();
const code = await runOlpCli(
['status', '--proxy-url=http://127.0.0.1:1'], // port 1 → ECONNREFUSED on macOS/Linux
{ out: cap.out, err: cap.err, useColor: false },
);
assert.equal(code, 2, `expected ECONNREFUSED → exit 2, got ${code}; stderr=${cap.stderr}`);
assert.match(cap.stderr, /unreachable|ECONNREFUSED/);
});
it('32j — resolveBearerToken precedence: OLP_API_KEY > OLP_OWNER_TOKEN > null', () => {
const SAVED_API = process.env.OLP_API_KEY;
const SAVED_OWN = process.env.OLP_OWNER_TOKEN;
try {
// 1. Neither set → null
delete process.env.OLP_API_KEY;
delete process.env.OLP_OWNER_TOKEN;
assert.equal(olpResolveBearerToken(), null);
// 2. Only OLP_OWNER_TOKEN → that wins
process.env.OLP_OWNER_TOKEN = 'owner-tok-32j';
assert.equal(olpResolveBearerToken(), 'owner-tok-32j');
// 3. OLP_API_KEY also set → API key wins
process.env.OLP_API_KEY = 'api-key-32j';
assert.equal(olpResolveBearerToken(), 'api-key-32j');
} finally {
if (SAVED_API === undefined) delete process.env.OLP_API_KEY;
else process.env.OLP_API_KEY = SAVED_API;
if (SAVED_OWN === undefined) delete process.env.OLP_OWNER_TOKEN;
else process.env.OLP_OWNER_TOKEN = SAVED_OWN;
}
});
});
// ── Suite 33: D65 lib/doctor.mjs framework (ADR 0010 § Phase 4 D64-D67) ───
//
// Framework tests for `olp doctor`. Uses runDoctor()'s opts.injectChecks
// hook to drive every kind branch (noop / fresh_install / fix_server /
// fix_oauth / fix_provider / fix_config) deterministically without depending
// on real filesystem or network state.
import {
runDoctor as runOlpDoctor,
buildBuiltinChecks as buildOlpBuiltinChecks,
collectProviderChecks as collectOlpProviderChecks,
deriveKind as deriveOlpKind,
deriveNextAction as deriveOlpNextAction,
resolveProxyUrl as resolveOlpProxyUrl,
DOCTOR_SCHEMA_VERSION,
} from './lib/doctor.mjs';
describe('Suite 33 — D65 lib/doctor.mjs framework (ADR 0010 § Phase 4 D64-D67)', () => {
// Helper: build a fake check with deterministic status + optional evidence
function fakeCheck(id, category, status, message = 'fake', evidence) {
return {
id,
category,
async run() {
return evidence ? { status, message, evidence } : { status, message };
},
};
}
it('33a — all-ok checks → kind=noop, next_action.ai_executable=[]', async () => {
const result = await runOlpDoctor({
injectChecks: [
fakeCheck('system.node_version', 'system', 'ok', 'Node 20'),
fakeCheck('server.running', 'server', 'ok', 'live'),
fakeCheck('auth.owner_key_exists', 'auth', 'ok', 'present'),
],
});
assert.equal(result.schema_version, DOCTOR_SCHEMA_VERSION);
assert.equal(result.fail_count, 0);
assert.equal(result.kind, 'noop');
assert.deepEqual(result.next_action.ai_executable, []);
assert.deepEqual(result.next_action.human_required, []);
assert.equal(result.next_action.verify, 'already healthy');
assert.match(result.summary, /all 3 checks ok/);
});
it('33b — server.running FAIL → kind=fix_server + ai_executable propagates', async () => {
const result = await runOlpDoctor({
injectChecks: [
fakeCheck('server.running', 'server', 'fail', 'unreachable', {
fix_commands: ['olp restart'],
}),
fakeCheck('auth.owner_key_exists', 'auth', 'ok', 'present'),
],
});
assert.equal(result.fail_count, 1);
assert.equal(result.kind, 'fix_server');
assert.deepEqual(result.next_action.ai_executable, ['olp restart']);
assert.equal(result.next_action.verify, 'olp doctor');
});
it('33c — auth.owner_key_exists FAIL (no server fail) → kind=fix_oauth', async () => {
const result = await runOlpDoctor({
injectChecks: [
fakeCheck('server.running', 'server', 'ok', 'live'),
fakeCheck('auth.owner_key_exists', 'auth', 'fail', 'no owner', {
fix_commands: ['npx olp-keys keygen --owner'],
}),
],
});
assert.equal(result.kind, 'fix_oauth');
assert.deepEqual(result.next_action.ai_executable, ['npx olp-keys keygen --owner']);
});
it('33d — config.exists FAIL beats everything else → kind=fresh_install', async () => {
const result = await runOlpDoctor({
injectChecks: [
fakeCheck('config.exists', 'config', 'fail', 'missing', {
fix_commands: ['mkdir -p ~/.olp'],
}),
fakeCheck('server.running', 'server', 'fail', 'unreachable', {
fix_commands: ['olp restart'],
}),
fakeCheck('auth.owner_key_exists', 'auth', 'fail', 'no owner'),
],
});
assert.equal(result.kind, 'fresh_install');
// ai_executable concatenates all FAIL evidence.fix_commands
assert.ok(result.next_action.ai_executable.includes('mkdir -p ~/.olp'));
assert.ok(result.next_action.ai_executable.includes('olp restart'));
});
it('33e — provider-category FAIL only → kind=fix_provider, human_steps propagate', async () => {
const result = await runOlpDoctor({
injectChecks: [
fakeCheck('server.running', 'server', 'ok', 'live'),
fakeCheck('auth.owner_key_exists', 'auth', 'ok', 'present'),
fakeCheck('anthropic.oauth_token_present', 'provider', 'fail', 'missing token', {
human_steps: ['run: claude (browser OAuth)'],
}),
],
});
assert.equal(result.kind, 'fix_provider');
assert.deepEqual(result.next_action.ai_executable, []);
assert.deepEqual(result.next_action.human_required, ['run: claude (browser OAuth)']);
});
it('33f — collectProviderChecks reads plugin doctorChecks() (ADR 0002 Amendment 7)', () => {
// Synthetic plugin map that includes one plugin WITH doctorChecks() + one
// plugin WITHOUT (default behaviour — back-compat — contributes nothing).
const providers = new Map();
providers.set('alpha', {
name: 'alpha',
doctorChecks() {
return [
{ id: 'alpha.cli_available', category: 'provider', async run() { return { status: 'ok', message: 'alpha bin ok' }; } },
{ id: 'alpha.auth_present', category: 'provider', async run() { return { status: 'fail', message: 'alpha missing' }; } },
];
},
});
providers.set('legacy', {
name: 'legacy',
// No doctorChecks() — plugin pre-amendment 7; collectProviderChecks ignores it
});
const checks = collectOlpProviderChecks({ providersOverride: providers });
const ids = checks.map(c => c.id).sort();
assert.deepEqual(ids, ['alpha.auth_present', 'alpha.cli_available']);
// Categories normalized to 'provider'
assert.ok(checks.every(c => c.category === 'provider'));
});
it('33g — plugin doctorChecks() that throws is captured (does not crash framework)', async () => {
const providers = new Map();
providers.set('bad', {
name: 'bad',
doctorChecks() { throw new Error('boom from bad plugin'); },
});
const checks = collectOlpProviderChecks({ providersOverride: providers });
assert.equal(checks.length, 1);
assert.equal(checks[0].id, 'bad.doctor_checks_threw');
// Run it — should resolve to fail without re-throwing
const r = await checks[0].run();
assert.equal(r.status, 'fail');
assert.match(r.message, /boom from bad plugin/);
});
it('33h — --check filter restricts to matching id/category/prefix', async () => {
// Filter by category
const r1 = await runOlpDoctor({
injectChecks: [
fakeCheck('a.x', 'provider', 'ok'),
fakeCheck('b.y', 'system', 'ok'),
],
checkFilter: 'system',
});
assert.equal(r1.checks.length, 1);
assert.equal(r1.checks[0].id, 'b.y');
// Filter by id prefix
const r2 = await runOlpDoctor({
injectChecks: [
fakeCheck('anthropic.cli_available', 'provider', 'ok'),
fakeCheck('anthropic.oauth_token_present', 'provider', 'ok'),
fakeCheck('openai.cli_available', 'provider', 'ok'),
],
checkFilter: 'anthropic',
});
assert.equal(r2.checks.length, 2);
assert.ok(r2.checks.every(c => c.id.startsWith('anthropic.')));
// Filter by exact id
const r3 = await runOlpDoctor({
injectChecks: [
fakeCheck('anthropic.cli_available', 'provider', 'ok'),
fakeCheck('anthropic.oauth_token_present', 'provider', 'ok'),
],
checkFilter: 'anthropic.cli_available',
});
assert.equal(r3.checks.length, 1);
assert.equal(r3.checks[0].id, 'anthropic.cli_available');
});
it('33i — built-in checks run against a temp OLP_HOME (no config → fresh_install)', async () => {
// skipNetwork: true so server.* checks don't run against the localhost port.
const tmpEmpty = mkdtempSync(pathJoin(tmpdir(), 'olp-doc-33i-'));
try {
const result = await runOlpDoctor({
olpHome: tmpEmpty,
skipNetwork: true,
// Don't load real provider plugins for this run; pass an empty Map.
providersOverride: new Map(),
});
assert.equal(result.schema_version, DOCTOR_SCHEMA_VERSION);
// config.exists must FAIL (no ~/.olp/config.json in tmpEmpty)
const configCheck = result.checks.find(c => c.id === 'config.exists');
assert.ok(configCheck, 'config.exists check should run');
assert.equal(configCheck.status, 'fail');
// kind must be fresh_install per the precedence rule
assert.equal(result.kind, 'fresh_install');
// ai_executable must include the mkdir + printf seed bootstrap
assert.ok(result.next_action.ai_executable.some(c => c.startsWith('mkdir -p')));
} finally {
rmSync(tmpEmpty, { recursive: true, force: true });
}
});
it('33j — anthropic plugin doctorChecks() returns the documented probe set', () => {
// Plugin contract: ADR 0002 Amendment 7. anthropic.mjs ships
// anthropic.cli_available + anthropic.oauth_token_present.
const checks = anthropic.doctorChecks();
assert.ok(Array.isArray(checks));
const ids = checks.map(c => c.id).sort();
assert.deepEqual(ids, ['anthropic.cli_available', 'anthropic.oauth_token_present']);
assert.ok(checks.every(c => c.category === 'provider'));
assert.ok(checks.every(c => typeof c.run === 'function'));
});
it('33k — resolveProxyUrl: explicit > env OLP_PROXY_URL > OLP_PORT > default', () => {
const SAVED_URL = process.env.OLP_PROXY_URL;
const SAVED_PORT = process.env.OLP_PORT;
try {
delete process.env.OLP_PROXY_URL;
delete process.env.OLP_PORT;
// 1. default = http://127.0.0.1:4567
assert.equal(resolveOlpProxyUrl(), 'http://127.0.0.1:4567');
// 2. OLP_PORT env
process.env.OLP_PORT = '9999';
assert.equal(resolveOlpProxyUrl(), 'http://127.0.0.1:9999');
// 3. OLP_PROXY_URL wins over OLP_PORT
process.env.OLP_PROXY_URL = 'https://example.com:8443';
assert.equal(resolveOlpProxyUrl(), 'https://example.com:8443');
// 4. Explicit opts wins over everything
assert.equal(resolveOlpProxyUrl({ proxyUrl: 'http://override:1234' }), 'http://override:1234');
} finally {
if (SAVED_URL === undefined) delete process.env.OLP_PROXY_URL;
else process.env.OLP_PROXY_URL = SAVED_URL;
if (SAVED_PORT === undefined) delete process.env.OLP_PORT;
else process.env.OLP_PORT = SAVED_PORT;
}
});
it('33l — deriveKind / deriveNextAction pure-function shape', () => {
// deriveKind unit
assert.equal(deriveOlpKind([{ status: 'ok', category: 'server', id: 'a' }]), 'noop');
assert.equal(deriveOlpKind([
{ status: 'fail', category: 'server', id: 's.r' },
{ status: 'fail', category: 'auth', id: 'a.o' },
]), 'fix_server'); // server beats auth
// deriveNextAction aggregates evidence
const na = deriveOlpNextAction([
{ status: 'fail', id: 'x', category: 'provider', evidence: { fix_commands: ['cmd1'], human_steps: ['step1'] } },
{ status: 'fail', id: 'y', category: 'provider', evidence: { fix_commands: ['cmd2'] } },
{ status: 'ok', id: 'z', category: 'provider' },
], 'fix_provider');
assert.deepEqual(na.ai_executable, ['cmd1', 'cmd2']);
assert.deepEqual(na.human_required, ['step1']);
assert.equal(na.verify, 'olp doctor');
});
});