131 lines
4.7 KiB
TypeScript
131 lines
4.7 KiB
TypeScript
|
|
/**
|
||
|
|
* Smoke tests: Project.llmProvider as Llm reference (Phase 4).
|
||
|
|
*
|
||
|
|
* Verifies the describe-project warning behavior against live mcpd:
|
||
|
|
* 1. Project with `--llm <existing>` → no warning.
|
||
|
|
* 2. Project with `--llm <nonexistent>` → describe flags the orphan.
|
||
|
|
* 3. Project with `--llm none` → explicit disable, no warning.
|
||
|
|
*/
|
||
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||
|
|
import http from 'node:http';
|
||
|
|
import https from 'node:https';
|
||
|
|
import { execSync } from 'node:child_process';
|
||
|
|
|
||
|
|
const MCPD_URL = process.env.MCPD_URL ?? 'https://mcpctl.ad.itaz.eu';
|
||
|
|
const SUFFIX = Date.now().toString(36);
|
||
|
|
const LLM_NAME = `smoke-proj-llm-${SUFFIX}`;
|
||
|
|
const PROJ_OK = `smoke-proj-ok-${SUFFIX}`;
|
||
|
|
const PROJ_ORPHAN = `smoke-proj-orphan-${SUFFIX}`;
|
||
|
|
const PROJ_NONE = `smoke-proj-none-${SUFFIX}`;
|
||
|
|
|
||
|
|
interface CliResult { code: number; stdout: string; stderr: string }
|
||
|
|
|
||
|
|
function run(args: string): CliResult {
|
||
|
|
try {
|
||
|
|
const stdout = execSync(`mcpctl --direct ${args}`, {
|
||
|
|
encoding: 'utf-8',
|
||
|
|
timeout: 30_000,
|
||
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||
|
|
});
|
||
|
|
return { code: 0, stdout: stdout.trim(), stderr: '' };
|
||
|
|
} catch (err) {
|
||
|
|
const e = err as { status?: number; stdout?: Buffer | string; stderr?: Buffer | string };
|
||
|
|
return {
|
||
|
|
code: e.status ?? 1,
|
||
|
|
stdout: e.stdout ? (typeof e.stdout === 'string' ? e.stdout : e.stdout.toString('utf-8')) : '',
|
||
|
|
stderr: e.stderr ? (typeof e.stderr === 'string' ? e.stderr : e.stderr.toString('utf-8')) : '',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function healthz(url: string, timeoutMs = 5000): Promise<boolean> {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
const parsed = new URL(`${url.replace(/\/$/, '')}/healthz`);
|
||
|
|
const driver = parsed.protocol === 'https:' ? https : http;
|
||
|
|
const req = driver.get(
|
||
|
|
{
|
||
|
|
hostname: parsed.hostname,
|
||
|
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
||
|
|
path: parsed.pathname,
|
||
|
|
timeout: timeoutMs,
|
||
|
|
},
|
||
|
|
(res) => { resolve((res.statusCode ?? 500) < 500); res.resume(); },
|
||
|
|
);
|
||
|
|
req.on('error', () => resolve(false));
|
||
|
|
req.on('timeout', () => { req.destroy(); resolve(false); });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
let mcpdUp = false;
|
||
|
|
|
||
|
|
describe('project-llm-ref smoke', () => {
|
||
|
|
beforeAll(async () => {
|
||
|
|
mcpdUp = await healthz(MCPD_URL);
|
||
|
|
if (!mcpdUp) {
|
||
|
|
// eslint-disable-next-line no-console
|
||
|
|
console.warn(`\n ○ project-llm-ref smoke: skipped — ${MCPD_URL}/healthz unreachable.\n`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// Fixture: an Llm we can point projects at.
|
||
|
|
run(`delete llm ${LLM_NAME}`);
|
||
|
|
const createLlm = run([
|
||
|
|
`create llm ${LLM_NAME}`,
|
||
|
|
'--type openai',
|
||
|
|
'--model gpt-4o-mini',
|
||
|
|
'--tier fast',
|
||
|
|
'--url http://127.0.0.1:1',
|
||
|
|
].join(' '));
|
||
|
|
if (createLlm.code !== 0) {
|
||
|
|
// eslint-disable-next-line no-console
|
||
|
|
console.warn(` ○ could not create fixture Llm: ${createLlm.stderr || createLlm.stdout}`);
|
||
|
|
}
|
||
|
|
}, 30_000);
|
||
|
|
|
||
|
|
afterAll(() => {
|
||
|
|
if (!mcpdUp) return;
|
||
|
|
run(`delete project ${PROJ_OK} --force`);
|
||
|
|
run(`delete project ${PROJ_ORPHAN} --force`);
|
||
|
|
run(`delete project ${PROJ_NONE} --force`);
|
||
|
|
run(`delete llm ${LLM_NAME}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('project with --llm pointing at a registered Llm describes without warning', () => {
|
||
|
|
if (!mcpdUp) return;
|
||
|
|
run(`delete project ${PROJ_OK} --force`);
|
||
|
|
const created = run(`create project ${PROJ_OK} --llm ${LLM_NAME}`);
|
||
|
|
expect(created.code, created.stderr || created.stdout).toBe(0);
|
||
|
|
|
||
|
|
const described = run(`describe project ${PROJ_OK}`);
|
||
|
|
expect(described.code).toBe(0);
|
||
|
|
expect(described.stdout).toContain('LLM:');
|
||
|
|
expect(described.stdout).toContain(LLM_NAME);
|
||
|
|
expect(described.stdout).not.toContain('warning:');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('project with --llm naming an unregistered Llm shows the warning line', () => {
|
||
|
|
if (!mcpdUp) return;
|
||
|
|
run(`delete project ${PROJ_ORPHAN} --force`);
|
||
|
|
const created = run(`create project ${PROJ_ORPHAN} --llm claude-ghost-${SUFFIX}`);
|
||
|
|
expect(created.code, created.stderr || created.stdout).toBe(0);
|
||
|
|
|
||
|
|
const described = run(`describe project ${PROJ_ORPHAN}`);
|
||
|
|
expect(described.code).toBe(0);
|
||
|
|
expect(described.stdout).toContain(`claude-ghost-${SUFFIX}`);
|
||
|
|
expect(described.stdout).toContain('warning:');
|
||
|
|
expect(described.stdout).toContain('registry default');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('project with --llm none treats it as an explicit disable (no warning)', () => {
|
||
|
|
if (!mcpdUp) return;
|
||
|
|
run(`delete project ${PROJ_NONE} --force`);
|
||
|
|
const created = run(`create project ${PROJ_NONE} --llm none`);
|
||
|
|
expect(created.code).toBe(0);
|
||
|
|
|
||
|
|
const described = run(`describe project ${PROJ_NONE}`);
|
||
|
|
expect(described.code).toBe(0);
|
||
|
|
expect(described.stdout).toContain('LLM:');
|
||
|
|
expect(described.stdout).toContain('none');
|
||
|
|
expect(described.stdout).not.toContain('warning:');
|
||
|
|
});
|
||
|
|
});
|