Files
gbrain/test/ai/anthropic-key.test.ts
T
5911072aec v0.42.4.0 fix: think --model fails loud — slash-form ids + never persist empty synthesis (#1698) (#1736)
* fix: think --model fails loud on unresolvable model; never persist empty synthesis (#1698)

Slash-form model ids (anthropic/claude-sonnet-4-6) silently degraded to the
no-LLM stub and wrote empty synthesis pages with exit 0 (reporter saw 200).
Three compounding defects, fixed:

- normalizeModelId (src/core/model-id.ts): one shared provider:model normalizer
  replacing 4 colon-only inlines; slash→colon, bare→default, malformed
  leading-separator (:foo) returned unchanged so resolveRecipe throws loud.
- validateModelId + probeChatModel (gateway.ts): shared id-validity + key probe;
  runThink hard-errors on an explicit --model it can't run (no silent degrade).
- synthesisOk + persist-skip (think/index.ts): empty/malformed/empty-JSON
  synthesis is never persisted; --save with no synthesis exits 1.
- auto-think (cycle/auto-think.ts): empty synthesis no longer counts complete or
  advances the cooldown (the autonomous third caller of persistSynthesis).
- hasAnthropicKey consolidated into src/core/ai/anthropic-key.ts (3 copies → 1).

MCP think op sets modelExplicit; saved_slug '' maps to null.

* chore: bump version and changelog (v0.42.4.0)

#1698 think --model fail-loud wave. Also files the P3 follow-up TODO for the
provider-symmetric early gate (D1 accept-as-is).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:54:13 -07:00

65 lines
2.2 KiB
TypeScript

/**
* #1698 — shared `hasAnthropicKey` (consolidated from 3 private copies).
*
* Hermetic: every case isolates env + GBRAIN_HOME via `withEnv` (R1) so the
* dev machine's real ~/.gbrain/config.json never leaks into the "neither" case.
*/
import { describe, test, expect, afterEach } from 'bun:test';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { withEnv } from '../helpers/with-env.ts';
import { hasAnthropicKey } from '../../src/core/ai/anthropic-key.ts';
const tmpDirs: string[] = [];
function freshHome(withConfig?: Record<string, unknown>): string {
const home = mkdtempSync(join(tmpdir(), 'gbrain-akey-'));
tmpDirs.push(home);
if (withConfig) {
const dir = join(home, '.gbrain');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'config.json'), JSON.stringify(withConfig), 'utf8');
}
return home;
}
afterEach(() => {
while (tmpDirs.length) {
const d = tmpDirs.pop()!;
try { rmSync(d, { recursive: true, force: true }); } catch { /* best effort */ }
}
});
describe('hasAnthropicKey', () => {
test('env ANTHROPIC_API_KEY set → true (no config read needed)', async () => {
const home = freshHome(); // empty home so config can't accidentally satisfy it
await withEnv(
{ ANTHROPIC_API_KEY: 'sk-test', GBRAIN_HOME: home, DATABASE_URL: undefined, GBRAIN_DATABASE_URL: undefined },
async () => {
expect(hasAnthropicKey()).toBe(true);
},
);
});
test('gbrain config anthropic_api_key set (no env) → true', async () => {
const home = freshHome({ anthropic_api_key: 'sk-from-config' });
await withEnv(
{ ANTHROPIC_API_KEY: undefined, GBRAIN_HOME: home, DATABASE_URL: undefined, GBRAIN_DATABASE_URL: undefined },
async () => {
expect(hasAnthropicKey()).toBe(true);
},
);
});
test('neither env nor config → false', async () => {
const home = freshHome(); // no config.json written
await withEnv(
{ ANTHROPIC_API_KEY: undefined, GBRAIN_HOME: home, DATABASE_URL: undefined, GBRAIN_DATABASE_URL: undefined },
async () => {
expect(hasAnthropicKey()).toBe(false);
},
);
});
});