Skip to content

Commit 95df447

Browse files
committed
test(ecosystem): cover manifest validator failures
1 parent ac8f751 commit 95df447

3 files changed

Lines changed: 203 additions & 0 deletions

File tree

.github/workflows/ecosystem-validate.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
- "ecosystem/**"
77
- "recipes/**"
88
- "scripts/validate-ecosystem-manifests.mjs"
9+
- "scripts/test-ecosystem-validator.mjs"
910
- ".github/workflows/ecosystem-validate.yml"
1011
push:
1112
branches:
@@ -14,6 +15,7 @@ on:
1415
- "ecosystem/**"
1516
- "recipes/**"
1617
- "scripts/validate-ecosystem-manifests.mjs"
18+
- "scripts/test-ecosystem-validator.mjs"
1719
- ".github/workflows/ecosystem-validate.yml"
1820

1921
permissions:
@@ -34,3 +36,6 @@ jobs:
3436

3537
- name: Validate manifests
3638
run: node scripts/validate-ecosystem-manifests.mjs
39+
40+
- name: Test manifest validator
41+
run: node scripts/test-ecosystem-validator.mjs

ecosystem/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ node scripts/validate-ecosystem-manifests.mjs recipes
2929

3030
CI runs the same validator on recipe, schema, and validator changes. A manifest fails validation when required metadata is missing, permission declarations are incomplete, examples point at missing docs, or marketplace listing fields are absent.
3131

32+
Run validator regression tests:
33+
34+
```bash
35+
node scripts/test-ecosystem-validator.mjs
36+
```
37+
3238
## Manifest Templates
3339

3440
- [`templates/skill-manifest.template.json`](templates/skill-manifest.template.json)
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env node
2+
3+
import assert from "node:assert/strict";
4+
import { spawnSync } from "node:child_process";
5+
import fs from "node:fs";
6+
import os from "node:os";
7+
import path from "node:path";
8+
import process from "node:process";
9+
10+
const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
11+
const VALIDATOR = path.join(REPO_ROOT, "scripts/validate-ecosystem-manifests.mjs");
12+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "qveris-manifest-validator-"));
13+
14+
const tests = [
15+
["accepts valid recipe manifest", testValidManifest],
16+
["rejects missing required recipe permission", testMissingInspectPermission],
17+
["rejects missing docs path", testMissingDocsPath],
18+
["rejects invalid marketplace integration method", testInvalidIntegrationMethod],
19+
["rejects unsupported schema version", testUnsupportedSchemaVersion],
20+
["rejects missing example path", testMissingExamplePath],
21+
];
22+
23+
try {
24+
for (const [name, testFn] of tests) {
25+
testFn();
26+
console.log(`ok ${name}`);
27+
}
28+
console.log(`\n${tests.length} validator regression test(s) passed.`);
29+
} finally {
30+
fs.rmSync(tmpRoot, { recursive: true, force: true });
31+
}
32+
33+
function testValidManifest() {
34+
const manifestPath = writeFixture("valid");
35+
const result = runValidator(manifestPath);
36+
assertEqualStatus(result, 0);
37+
assert.match(result.stdout, /Validated 1 ecosystem manifest\(s\)\./);
38+
}
39+
40+
function testMissingInspectPermission() {
41+
const manifestPath = writeFixture("missing-inspect", (manifest) => {
42+
manifest.permissions.qveris = manifest.permissions.qveris.filter(
43+
(permission) => permission.scope !== "capability.inspect",
44+
);
45+
});
46+
const result = runValidator(manifestPath);
47+
assertEqualStatus(result, 1);
48+
assert.match(result.stderr, /recipe manifests must declare capability\.inspect/);
49+
}
50+
51+
function testMissingDocsPath() {
52+
const manifestPath = writeFixture("missing-docs", (manifest) => {
53+
manifest.docs.readme = "MISSING.md";
54+
});
55+
const result = runValidator(manifestPath);
56+
assertEqualStatus(result, 1);
57+
assert.match(result.stderr, /docs\.readme path does not exist: MISSING\.md/);
58+
}
59+
60+
function testInvalidIntegrationMethod() {
61+
const manifestPath = writeFixture("invalid-method", (manifest) => {
62+
manifest.marketplace.integration_methods = ["cli", "spreadsheet"];
63+
});
64+
const result = runValidator(manifestPath);
65+
assertEqualStatus(result, 1);
66+
assert.match(result.stderr, /marketplace\.integration_methods has unsupported value: spreadsheet/);
67+
}
68+
69+
function testUnsupportedSchemaVersion() {
70+
const manifestPath = writeFixture("bad-schema-version", (manifest) => {
71+
manifest.schema_version = "2026-01-01";
72+
});
73+
const result = runValidator(manifestPath);
74+
assertEqualStatus(result, 1);
75+
assert.match(result.stderr, /schema_version must be 2026-05-13/);
76+
}
77+
78+
function testMissingExamplePath() {
79+
const manifestPath = writeFixture("missing-example", (manifest) => {
80+
manifest.examples[0].path = "missing-example.md";
81+
});
82+
const result = runValidator(manifestPath);
83+
assertEqualStatus(result, 1);
84+
assert.match(result.stderr, /examples\[0\]\.path does not exist: missing-example\.md/);
85+
}
86+
87+
function runValidator(manifestPath) {
88+
return spawnSync(process.execPath, [VALIDATOR, manifestPath], {
89+
cwd: REPO_ROOT,
90+
encoding: "utf8",
91+
});
92+
}
93+
94+
function writeFixture(name, mutate = () => {}) {
95+
const dir = path.join(tmpRoot, name);
96+
fs.mkdirSync(dir, { recursive: true });
97+
fs.writeFileSync(path.join(dir, "README.md"), `# ${name}\n`, "utf8");
98+
99+
const manifest = validManifest();
100+
manifest.id = `qveris.recipe.${name}`;
101+
manifest.marketplace.listing_slug = name;
102+
mutate(manifest);
103+
104+
const manifestPath = path.join(dir, "qveris.manifest.json");
105+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
106+
return manifestPath;
107+
}
108+
109+
function assertEqualStatus(result, expected) {
110+
assert.equal(
111+
result.status,
112+
expected,
113+
[
114+
`Expected exit status ${expected}, got ${result.status}`,
115+
`stdout:\n${result.stdout}`,
116+
`stderr:\n${result.stderr}`,
117+
].join("\n\n"),
118+
);
119+
}
120+
121+
function validManifest() {
122+
return {
123+
"$schema": "../../ecosystem/manifest.schema.json",
124+
schema_version: "2026-05-13",
125+
id: "qveris.recipe.valid",
126+
kind: "recipe",
127+
name: "Valid Recipe",
128+
summary: "A valid recipe manifest used by the validator regression tests.",
129+
description:
130+
"This manifest exercises the validator success path with complete permissions, docs, examples, compatibility, and marketplace metadata.",
131+
version: "0.1.0",
132+
status: "beta",
133+
owner: {
134+
name: "QVeris",
135+
url: "https://qveris.ai",
136+
},
137+
tags: ["test", "recipe"],
138+
categories: ["automation"],
139+
permissions: {
140+
qveris: [
141+
{
142+
scope: "capability.discover",
143+
reason: "Find external capabilities for this recipe.",
144+
required: true,
145+
},
146+
{
147+
scope: "capability.inspect",
148+
reason: "Inspect candidate capability metadata before execution.",
149+
required: true,
150+
},
151+
],
152+
network: [
153+
{
154+
host: "qveris.ai",
155+
reason: "Call QVeris API endpoints.",
156+
required: true,
157+
},
158+
],
159+
secrets: [
160+
{
161+
scope: "QVERIS_API_KEY",
162+
reason: "Authenticate QVeris API requests.",
163+
required: true,
164+
},
165+
],
166+
},
167+
compatibility: {
168+
cli: ">=0.5.0",
169+
python_sdk: ">=0.1.0",
170+
},
171+
marketplace: {
172+
listing_slug: "valid",
173+
headline: "Valid manifest fixture for validator regression coverage.",
174+
audience: "Maintainers who need confidence in ecosystem manifest validation.",
175+
use_cases: ["Validate complete recipe metadata", "Catch permission and listing mistakes"],
176+
integration_methods: ["cli", "python-sdk"],
177+
primary_cta: "Run test",
178+
docs_url: "recipes/valid/README.md",
179+
},
180+
docs: {
181+
readme: "README.md",
182+
},
183+
examples: [
184+
{
185+
name: "CLI example",
186+
path: "README.md",
187+
language: "shell",
188+
command: "qveris discover \"example capability\" --limit 3",
189+
},
190+
],
191+
};
192+
}

0 commit comments

Comments
 (0)