664 lines
21 KiB
TypeScript
664 lines
21 KiB
TypeScript
import { describe, it, expect, afterEach } from 'vitest';
|
|
import { computeDoctorExitCode, runChecks, type CheckResult, type DoctorContext } from './doctor.js';
|
|
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
|
|
describe('doctor checks', () => {
|
|
const testDir = join(tmpdir(), 'flynn-test-doctor');
|
|
|
|
afterEach(() => {
|
|
try { rmSync(testDir, { recursive: true }); } catch {}
|
|
});
|
|
|
|
it('computeDoctorExitCode returns 0 with warnings in non-strict mode', () => {
|
|
const results: CheckResult[] = [
|
|
{ status: 'pass', label: 'a' },
|
|
{ status: 'warn', label: 'b' },
|
|
];
|
|
expect(computeDoctorExitCode(results, false)).toBe(0);
|
|
});
|
|
|
|
it('computeDoctorExitCode returns 1 with warnings in strict mode', () => {
|
|
const results: CheckResult[] = [
|
|
{ status: 'pass', label: 'a' },
|
|
{ status: 'warn', label: 'b' },
|
|
];
|
|
expect(computeDoctorExitCode(results, true)).toBe(1);
|
|
});
|
|
|
|
it('reports PASS when config file exists and is valid', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'config.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const configExists = results.find(r => r.label.includes('Config file'));
|
|
expect(configExists?.status).toBe('pass');
|
|
|
|
const configParses = results.find(r => r.label.includes('parses'));
|
|
expect(configParses?.status).toBe('pass');
|
|
|
|
const configValidates = results.find(r => r.label.includes('validates'));
|
|
expect(configValidates?.status).toBe('pass');
|
|
});
|
|
|
|
it('reports FAIL when config file does not exist', async () => {
|
|
const ctx: DoctorContext = { configPath: '/nonexistent/config.yaml', dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const configExists = results.find(r => r.label.includes('Config file'));
|
|
expect(configExists?.status).toBe('fail');
|
|
});
|
|
|
|
it('reports FAIL on invalid YAML', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'bad.yaml');
|
|
writeFileSync(configPath, '{{{{bad yaml');
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const configParses = results.find(r => r.label.includes('parses'));
|
|
expect(configParses?.status).toBe('fail');
|
|
});
|
|
|
|
it('reports FAIL on schema validation failure', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'invalid.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: ""
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const configValidates = results.find(r => r.label.includes('validates'));
|
|
expect(configValidates?.status).toBe('fail');
|
|
});
|
|
|
|
it('warns when deprecated server.tailscale_only key is present', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'config.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
server:
|
|
tailscale_only: true
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const deprecated = results.find(r => r.label.includes('deprecated keys'));
|
|
expect(deprecated?.status).toBe('warn');
|
|
});
|
|
|
|
it('reports PASS for writable data directory', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'config.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const dataDir = results.find(r => r.label.includes('Data directory'));
|
|
expect(dataDir?.status).toBe('pass');
|
|
});
|
|
|
|
it('reports PASS for accessible session DB', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'config.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const sessionDb = results.find(r => r.label.includes('Session DB'));
|
|
expect(sessionDb?.status).toBe('pass');
|
|
});
|
|
|
|
it('reports SKIP for Gmail when not enabled', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'config.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const gmailCheck = results.find(r => r.label.includes('Gmail'));
|
|
expect(gmailCheck?.status).toBe('skip');
|
|
});
|
|
|
|
it('reports WARN for Gmail when token missing', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'config.yaml');
|
|
const credsPath = join(testDir, 'gmail-creds.json');
|
|
writeFileSync(credsPath, '{}');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
automation:
|
|
gmail:
|
|
enabled: true
|
|
credentials_file: "${credsPath}"
|
|
token_file: "${join(testDir, 'nonexistent-token.json')}"
|
|
output:
|
|
channel: telegram
|
|
peer: "123"
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const gmailCheck = results.find(r => r.label.includes('Gmail'));
|
|
expect(gmailCheck?.status).toBe('warn');
|
|
expect(gmailCheck?.detail).toContain('flynn gmail-auth');
|
|
});
|
|
|
|
it('reports PASS for Gmail when enabled (poll only)', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'config.yaml');
|
|
const credsPath = join(testDir, 'gmail-creds.json');
|
|
const tokenPath = join(testDir, 'gmail-token.json');
|
|
writeFileSync(credsPath, JSON.stringify({ installed: { project_id: 'test-project' } }));
|
|
writeFileSync(tokenPath, JSON.stringify({ refresh_token: 'x' }));
|
|
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
automation:
|
|
gmail:
|
|
enabled: true
|
|
credentials_file: "${credsPath}"
|
|
token_file: "${tokenPath}"
|
|
output:
|
|
channel: telegram
|
|
peer: "123"
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const gmailCheck = results.find(r => r.label.includes('Gmail configured'));
|
|
expect(gmailCheck?.status).toBe('pass');
|
|
expect(gmailCheck?.detail).toContain('poll');
|
|
});
|
|
|
|
it('reports WARN for Gmail when pubsub_topic shorthand used without project_id', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'config.yaml');
|
|
const credsPath = join(testDir, 'gmail-creds.json');
|
|
const tokenPath = join(testDir, 'gmail-token.json');
|
|
writeFileSync(credsPath, '{}');
|
|
writeFileSync(tokenPath, JSON.stringify({ refresh_token: 'x' }));
|
|
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
automation:
|
|
gmail:
|
|
enabled: true
|
|
credentials_file: "${credsPath}"
|
|
token_file: "${tokenPath}"
|
|
pubsub_topic: gmail-push
|
|
output:
|
|
channel: telegram
|
|
peer: "123"
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const gmailCheck = results.find(r => r.label.includes('Gmail configured'));
|
|
expect(gmailCheck?.status).toBe('warn');
|
|
expect(gmailCheck?.detail).toContain('pubsub_topic shorthand');
|
|
});
|
|
|
|
it('skips downstream checks when config is invalid', async () => {
|
|
const ctx: DoctorContext = { configPath: '/nonexistent/config.yaml', dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const modelCheck = results.find(r => r.label.includes('Model connectivity'));
|
|
expect(modelCheck?.status).toBe('skip');
|
|
|
|
const telegramCheck = results.find(r => r.label.includes('Telegram'));
|
|
expect(telegramCheck?.status).toBe('skip');
|
|
});
|
|
|
|
it('reports WARN for skills when configured directories are missing', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'skills-missing.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
skills:
|
|
bundled_dir: "${join(testDir, 'missing-bundled')}"
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const skillsCheck = results.find(r => r.label.includes('Skills loaded'));
|
|
expect(skillsCheck?.status).toBe('warn');
|
|
expect(skillsCheck?.detail).toContain('missing dirs');
|
|
});
|
|
|
|
it('reports PASS for skills when configured directory exists', async () => {
|
|
const bundledDir = join(testDir, 'bundled');
|
|
const sampleSkillDir = join(bundledDir, 'sample');
|
|
mkdirSync(sampleSkillDir, { recursive: true });
|
|
writeFileSync(join(sampleSkillDir, 'SKILL.md'), '# Sample skill');
|
|
|
|
const configPath = join(testDir, 'skills-pass.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
skills:
|
|
bundled_dir: "${bundledDir}"
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const skillsCheck = results.find(r => r.label.includes('Skills loaded'));
|
|
expect(skillsCheck?.status).toBe('pass');
|
|
expect(skillsCheck?.detail).toContain('1 skill(s), 1 available, 0 unavailable');
|
|
});
|
|
|
|
it('reports WARN for skills registry when unconfigured', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'registry-unconfigured.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
const registryCheck = results.find(r => r.label === 'Skills registry');
|
|
expect(registryCheck?.status).toBe('warn');
|
|
expect(registryCheck?.detail).toContain('unconfigured');
|
|
});
|
|
|
|
it('reports SKIP for MinIO ingest extractors when MinIO is disabled', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'minio-disabled.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
backup:
|
|
minio:
|
|
enabled: false
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const minioCheck = results.find(r => r.label.includes('MinIO ingest extractors'));
|
|
expect(minioCheck?.status).toBe('skip');
|
|
});
|
|
|
|
it('reports MinIO ingest extractor status when MinIO is enabled', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'minio-enabled.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
backup:
|
|
minio:
|
|
enabled: true
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
|
|
const minioCheck = results.find(r => r.label.includes('MinIO ingest extractors'));
|
|
expect(minioCheck).toBeDefined();
|
|
expect(['pass', 'warn']).toContain(minioCheck?.status);
|
|
expect(minioCheck?.detail).toContain('pdf:');
|
|
expect(minioCheck?.detail).toContain('docx:');
|
|
});
|
|
|
|
it('reports PASS for skills registry when source is parsable', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const registryPath = join(testDir, 'registry.json');
|
|
writeFileSync(registryPath, JSON.stringify({ skills: [] }), 'utf-8');
|
|
|
|
const configPath = join(testDir, 'registry-pass.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
skills:
|
|
registry_source: "${registryPath}"
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
const registryCheck = results.find(r => r.label === 'Skills registry');
|
|
expect(registryCheck?.status).toBe('pass');
|
|
expect(registryCheck?.detail).toContain('loaded 0 entries');
|
|
});
|
|
|
|
it('reports FAIL for skills registry when source cannot be loaded', async () => {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'registry-fail.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
skills:
|
|
registry_source: "${join(testDir, 'missing-registry.json')}"
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
const registryCheck = results.find(r => r.label === 'Skills registry');
|
|
expect(registryCheck?.status).toBe('fail');
|
|
});
|
|
|
|
it('reports FAIL when OpenAI auth_mode=api_key has no available key sources', async () => {
|
|
const originalHome = process.env.HOME;
|
|
const originalKey = process.env.OPENAI_API_KEY;
|
|
delete process.env.OPENAI_API_KEY;
|
|
|
|
const homeDir = join(tmpdir(), `flynn-test-doctor-home-${Date.now()}`);
|
|
process.env.HOME = homeDir;
|
|
|
|
try {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'openai-missing.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: openai
|
|
model: gpt-4o
|
|
auth_mode: api_key
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
|
|
expect(modelCheck?.status).toBe('fail');
|
|
expect(modelCheck?.detail).toContain('OPENAI_API_KEY');
|
|
expect(modelCheck?.detail).toContain('flynn openai-key');
|
|
} finally {
|
|
process.env.HOME = originalHome;
|
|
if (originalKey) {
|
|
process.env.OPENAI_API_KEY = originalKey;
|
|
} else {
|
|
delete process.env.OPENAI_API_KEY;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('loads env vars from FLYNN_ENV_FILE before env-var checks', async () => {
|
|
const originalEnvFile = process.env.FLYNN_ENV_FILE;
|
|
const originalOpenAIKey = process.env.OPENAI_API_KEY;
|
|
delete process.env.OPENAI_API_KEY;
|
|
|
|
try {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const envPath = join(testDir, 'cloud.env');
|
|
writeFileSync(envPath, 'OPENAI_API_KEY=sk-test-from-env-file\n');
|
|
process.env.FLYNN_ENV_FILE = envPath;
|
|
|
|
const configPath = join(testDir, 'openai-env-file.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: openai
|
|
model: gpt-5.2
|
|
auth_mode: api_key
|
|
api_key: \${OPENAI_API_KEY}
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
const envCheck = results.find((r) => r.label.includes('Env vars resolved')) as CheckResult | undefined;
|
|
const modelCheck = results.find((r) => r.label.includes('Model connectivity')) as CheckResult | undefined;
|
|
|
|
expect(envCheck?.status).toBe('pass');
|
|
expect(modelCheck?.status).toBe('pass');
|
|
expect(modelCheck?.detail).toContain('api_key=config+env');
|
|
} finally {
|
|
if (originalEnvFile !== undefined) {
|
|
process.env.FLYNN_ENV_FILE = originalEnvFile;
|
|
} else {
|
|
delete process.env.FLYNN_ENV_FILE;
|
|
}
|
|
if (originalOpenAIKey !== undefined) {
|
|
process.env.OPENAI_API_KEY = originalOpenAIKey;
|
|
} else {
|
|
delete process.env.OPENAI_API_KEY;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('reports WARN when Vercel AI Gateway has no available API key sources', async () => {
|
|
const originalKey = process.env.AI_GATEWAY_API_KEY;
|
|
delete process.env.AI_GATEWAY_API_KEY;
|
|
|
|
try {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'vercel-missing.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: vercel
|
|
model: openai/gpt-4.1
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
|
|
expect(modelCheck?.status).toBe('warn');
|
|
expect(modelCheck?.detail).toContain('AI_GATEWAY_API_KEY');
|
|
} finally {
|
|
if (originalKey) {
|
|
process.env.AI_GATEWAY_API_KEY = originalKey;
|
|
} else {
|
|
delete process.env.AI_GATEWAY_API_KEY;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('reports WARN when MiniMax has no available API key sources', async () => {
|
|
const originalKey = process.env.MINIMAX_API_KEY;
|
|
delete process.env.MINIMAX_API_KEY;
|
|
|
|
try {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'minimax-missing.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: minimax
|
|
model: MiniMax-M1
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
|
|
expect(modelCheck?.status).toBe('warn');
|
|
expect(modelCheck?.detail).toContain('MINIMAX_API_KEY');
|
|
} finally {
|
|
if (originalKey) {
|
|
process.env.MINIMAX_API_KEY = originalKey;
|
|
} else {
|
|
delete process.env.MINIMAX_API_KEY;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('reports WARN when Moonshot has no available API key sources', async () => {
|
|
const originalKey = process.env.MOONSHOT_API_KEY;
|
|
delete process.env.MOONSHOT_API_KEY;
|
|
|
|
try {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'moonshot-missing.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: moonshot
|
|
model: moonshot-v1-8k
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
|
|
expect(modelCheck?.status).toBe('warn');
|
|
expect(modelCheck?.detail).toContain('MOONSHOT_API_KEY');
|
|
} finally {
|
|
if (originalKey) {
|
|
process.env.MOONSHOT_API_KEY = originalKey;
|
|
} else {
|
|
delete process.env.MOONSHOT_API_KEY;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('reports FAIL when Anthropic auth_mode=oauth has no available auth token sources', async () => {
|
|
const originalHome = process.env.HOME;
|
|
const originalToken = process.env.ANTHROPIC_AUTH_TOKEN;
|
|
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
|
|
|
const homeDir = join(tmpdir(), `flynn-test-doctor-home-${Date.now()}-2`);
|
|
process.env.HOME = homeDir;
|
|
|
|
try {
|
|
mkdirSync(testDir, { recursive: true });
|
|
const configPath = join(testDir, 'anthropic-missing-token.yaml');
|
|
writeFileSync(configPath, `
|
|
telegram:
|
|
bot_token: "test-token"
|
|
allowed_chat_ids: [123]
|
|
models:
|
|
default:
|
|
provider: anthropic
|
|
model: claude-sonnet
|
|
auth_mode: oauth
|
|
`);
|
|
|
|
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
|
const results = await runChecks(ctx);
|
|
const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
|
|
expect(modelCheck?.status).toBe('fail');
|
|
expect(modelCheck?.detail).toContain('ANTHROPIC_AUTH_TOKEN');
|
|
expect(modelCheck?.detail).toContain('flynn anthropic-auth --token');
|
|
} finally {
|
|
process.env.HOME = originalHome;
|
|
if (originalToken) {
|
|
process.env.ANTHROPIC_AUTH_TOKEN = originalToken;
|
|
} else {
|
|
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
|
}
|
|
}
|
|
});
|
|
});
|