Task 14: Port StructuredModel mit Anthropic-Adapter (@anthropic-ai/sdk 0.131.0 exakt, Lieferkette geprüft), fester Endpunkt https://api.anthropic.com (Umgebungs-Header werden entfernt), feste Systemregeln ohne Datum/Volk/Person, Transkript als Daten in Tags, strukturierte Ausgabe per JSON-Schema mit effort low, serverseitiger Fallback per VOICE_SERVER_FALLBACK abschaltbar. Jeder Vorschlag wird geprüft: bekanntes Feld, Wert in den Formulargrenzen, Zitat steht wörtlich im Gesagten (Dezimalzahl als ein Wort), Korrektur gewinnt. Netzwächter: nur die Worker-Verdrahtung erreicht den Adapter (transitiv geprüft). Task 15: Worker-Schleife voice-extractions (LISTEN + 15-s-Abfrage, ein Lauf zugleich, höchstens 20 Notizen je Lauf), Modellaufruf außerhalb jeder Transaktion mit 240-s-Obergrenze, Ergebnis versiegelt mit Ereignis nur bei gehaltenem Claim; ohne Schlüssel EXTRACT_DISABLED ohne Versuch, Tageslimit zählt jeden bezahlten Aufruf über das neue Journal VoiceExtractionCall (nur IDs, Modell, Tokens, Ergebnis; 90 Tage, nicht im Backup). Nur der Worker-Container erhält ANTHROPIC_API_KEY aus dem Secret apicore-anthropic (optional); API- und Job-Rollen warnen, wenn der Schlüssel bei ihnen gesetzt ist. Gates: Backend lint, typecheck, 4589 Unit-Tests, build; e2e 104 Suites (nur der Wetter-Test gegen die echte API schwankt); Manifest-Policy 55. Refs #27 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
import { Logger } from '@nestjs/common';
|
|
import { anthropicApiKey, assertAnthropicKey } from './anthropic-config';
|
|
|
|
const KEY = 'sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789';
|
|
|
|
function fakeLogger() {
|
|
return { warn: jest.fn(), error: jest.fn() } satisfies Pick<
|
|
Logger,
|
|
'warn' | 'error'
|
|
>;
|
|
}
|
|
|
|
describe('anthropicApiKey', () => {
|
|
it('returns a well-formed key, trimmed', () => {
|
|
expect(anthropicApiKey({ ANTHROPIC_API_KEY: ` ${KEY} ` })).toBe(KEY);
|
|
});
|
|
|
|
it.each([
|
|
['unset', {}],
|
|
['empty', { ANTHROPIC_API_KEY: ' ' }],
|
|
['a placeholder', { ANTHROPIC_API_KEY: 'change-me-anthropic-key' }],
|
|
['the wrong format', { ANTHROPIC_API_KEY: 'sk-ant-short' }],
|
|
['dummy', { ANTHROPIC_API_KEY: 'dummy' }],
|
|
])('returns null for %s', (_label, env) => {
|
|
expect(anthropicApiKey(env)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('assertAnthropicKey', () => {
|
|
it('assertAnthropicKey never throws and disables on a placeholder', () => {
|
|
for (const value of ['change-me-anthropic-key', 'dummy', '', 'sk-ant-x']) {
|
|
const logger = fakeLogger();
|
|
expect(() =>
|
|
assertAnthropicKey(
|
|
'worker',
|
|
{ NODE_ENV: 'production', ANTHROPIC_API_KEY: value },
|
|
logger,
|
|
),
|
|
).not.toThrow();
|
|
}
|
|
const logger = fakeLogger();
|
|
assertAnthropicKey(
|
|
'worker',
|
|
{ NODE_ENV: 'production', ANTHROPIC_API_KEY: 'change-me-x' },
|
|
logger,
|
|
);
|
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
|
expect(anthropicApiKey({ ANTHROPIC_API_KEY: 'change-me-x' })).toBeNull();
|
|
});
|
|
|
|
it('logs the variable name once, never the value', () => {
|
|
const logger = fakeLogger();
|
|
assertAnthropicKey(
|
|
'worker',
|
|
{ ANTHROPIC_API_KEY: 'sk-ant-short-secret' },
|
|
logger,
|
|
);
|
|
const output = JSON.stringify([
|
|
logger.warn.mock.calls,
|
|
logger.error.mock.calls,
|
|
]);
|
|
expect(output).toContain('ANTHROPIC_API_KEY');
|
|
expect(output).not.toContain('short-secret');
|
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('api and job roles do not check the key format and stay silent without a key', () => {
|
|
for (const role of ['api', 'job'] as const) {
|
|
const logger = fakeLogger();
|
|
assertAnthropicKey(role, {}, logger);
|
|
assertAnthropicKey(role, { ANTHROPIC_API_KEY: ' ' }, logger);
|
|
expect(logger.error).not.toHaveBeenCalled();
|
|
expect(logger.warn).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it('warns once, by name, when an api or job role holds the key', () => {
|
|
for (const role of ['api', 'job'] as const) {
|
|
const logger = fakeLogger();
|
|
assertAnthropicKey(role, { ANTHROPIC_API_KEY: KEY }, logger);
|
|
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
expect(logger.error).not.toHaveBeenCalled();
|
|
const text = String((logger.warn.mock.calls[0] as unknown[])[0]);
|
|
expect(text).toContain(`ANTHROPIC_API_KEY is set for role ${role}`);
|
|
expect(text).toContain('apicore-anthropic');
|
|
expect(text).not.toContain(KEY);
|
|
}
|
|
});
|
|
|
|
it('accepts a good key silently in the worker and in all-in-one mode', () => {
|
|
for (const role of ['worker', 'all'] as const) {
|
|
const logger = fakeLogger();
|
|
assertAnthropicKey(role, { ANTHROPIC_API_KEY: KEY }, logger);
|
|
expect(logger.error).not.toHaveBeenCalled();
|
|
expect(logger.warn).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it('warns that extraction stays off when the worker has no key', () => {
|
|
const logger = fakeLogger();
|
|
assertAnthropicKey('worker', {}, logger);
|
|
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
expect(logger.error).not.toHaveBeenCalled();
|
|
});
|
|
});
|