80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
|
|
/**
|
||
|
|
* The shipped `templates/*.yaml` are seeded into mcpd and are what `mcpctl
|
||
|
|
* create server --from-template` builds from, so drift there ships broken
|
||
|
|
* servers. The unifi-network template had drifted on every field that
|
||
|
|
* mattered — python runtime for an npm package, an env contract
|
||
|
|
* (UNIFI_HOST/USERNAME/PASSWORD) the package doesn't read, and a comment
|
||
|
|
* disabling its health check for a reason that had stopped being true — and
|
||
|
|
* nothing caught it because no test ever read the files.
|
||
|
|
*/
|
||
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import { readdirSync, readFileSync } from 'node:fs';
|
||
|
|
import { join } from 'node:path';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
import yaml from 'js-yaml';
|
||
|
|
import { CreateTemplateSchema } from '../src/validation/template.schema.js';
|
||
|
|
|
||
|
|
const TEMPLATES_DIR = fileURLToPath(new URL('../../../templates', import.meta.url));
|
||
|
|
|
||
|
|
const files = readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
|
||
|
|
|
||
|
|
interface RawTemplate {
|
||
|
|
name?: string;
|
||
|
|
runtime?: string;
|
||
|
|
packageName?: string;
|
||
|
|
dockerImage?: string;
|
||
|
|
externalUrl?: string;
|
||
|
|
healthCheck?: { tool?: string };
|
||
|
|
env?: Array<{ name?: string }>;
|
||
|
|
}
|
||
|
|
|
||
|
|
function load(file: string): RawTemplate {
|
||
|
|
return yaml.load(readFileSync(join(TEMPLATES_DIR, file), 'utf-8')) as RawTemplate;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('shipped templates', () => {
|
||
|
|
it('ships at least one template', () => {
|
||
|
|
expect(files.length).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.each(files)('%s validates against CreateTemplateSchema', (file) => {
|
||
|
|
const parsed = CreateTemplateSchema.safeParse(load(file));
|
||
|
|
expect(parsed.success ? null : parsed.error.issues).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it.each(files)('%s declares a runner the orchestrator knows', (file) => {
|
||
|
|
const tpl = load(file);
|
||
|
|
// `runtime` only means anything for package-based servers, and only
|
||
|
|
// 'node' (npx) and 'python' (uvx) are wired in buildRuntimeSpawnCmd.
|
||
|
|
if (tpl.runtime !== undefined) {
|
||
|
|
expect(['node', 'python']).toContain(tpl.runtime);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it.each(files)('%s says how to actually run the server', (file) => {
|
||
|
|
const tpl = load(file);
|
||
|
|
const runnable = tpl.packageName !== undefined
|
||
|
|
|| tpl.dockerImage !== undefined
|
||
|
|
|| tpl.externalUrl !== undefined;
|
||
|
|
expect(runnable, `${file} has no packageName, dockerImage, or externalUrl`).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.each(files)('%s names a readiness probe tool, not a bare liveness probe', (file) => {
|
||
|
|
const tpl = load(file);
|
||
|
|
// Without a `tool`, an instance from this template can only ever report
|
||
|
|
// `live` — nothing would ever check its upstream. See docs/reliability.md.
|
||
|
|
expect(tpl.healthCheck?.tool, `${file} has no healthCheck.tool`).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it.each(files)('%s declares uniquely-named env entries', (file) => {
|
||
|
|
const names = (load(file).env ?? []).map((e) => e.name);
|
||
|
|
expect(new Set(names).size).toBe(names.length);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('has no template for a retired server', () => {
|
||
|
|
// node-red was retired 2026-08-09: it answered on neither its Tailscale
|
||
|
|
// nor its LAN address and had no deployment anywhere.
|
||
|
|
expect(files).not.toContain('node-red.yaml');
|
||
|
|
});
|
||
|
|
});
|