fix(templates): make the shipped templates match reality
Some checks failed
CI/CD / lint (pull_request) Successful in 1m12s
CI/CD / test (pull_request) Successful in 1m25s
CI/CD / typecheck (pull_request) Successful in 2m50s
CI/CD / smoke (pull_request) Failing after 1m57s
CI/CD / build (pull_request) Successful in 4m49s
CI/CD / publish (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Successful in 1m12s
CI/CD / test (pull_request) Successful in 1m25s
CI/CD / typecheck (pull_request) Successful in 2m50s
CI/CD / smoke (pull_request) Failing after 1m57s
CI/CD / build (pull_request) Successful in 4m49s
CI/CD / publish (pull_request) Has been skipped
The templates are what `create server --from-template` builds from and what mcpd seeds on start, so drift there ships broken servers. Nothing ever read these files in a test, and they had rotted badly. - grafana: GRAFANA_URL now defaults to the in-cluster ClusterIP and the description spells out why the public hostname is wrong — reaching a co-located Grafana over its ingress hairpins through the per-host Envoy L7 policy, which drops the caller's identity and returns a bare `Access denied` 403 with a perfectly valid token. That cost a day of looking at the token. - unifi-network: was wrong on every field that mattered. `runtime: python` for an npm package, an env contract (UNIFI_HOST/USERNAME/PASSWORD) the package doesn't read, and no probe. Now UNIFI_TARGETS with the classic-vs-unifi_os distinction and the :8443 egress caveat written down. - docmost, gitea: both carried "health check disabled" comments citing a limitation of the old docker-exec probe, which readiness-via-proxy removed. Both probes verified against the live servers. gitea uses search_repos, not get_me, because get_me needs a `read:user` scope a repo-scoped token lacks. - filesystem: packageName was `@anthropic/filesystem-mcp`, which 404s on npm — the template could never have installed. Points at the real package. - terraform: deleted. `@anthropic/terraform-mcp` 404s too and there is no npm-published replacement to point it at. - node-red: deleted, the service is gone. Two supporting fixes: - The seeder declared no `runtime` field and never wrote the column, so a template asking for the python runner silently seeded as null and got node. - A new templates test reads every shipped file: schema-valid, a runner the orchestrator knows, some way to actually start, unique env names, and a readiness probe (without one an instance can only ever report `live`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114dg56YmVacyqhp5fitcTb
This commit is contained in:
79
src/mcpd/tests/templates.test.ts
Normal file
79
src/mcpd/tests/templates.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user