Skip to content

Commit 4e5ab69

Browse files
committed
Scope the component tests per component, not per plugin
Three assertions generalised from the docs skill to the whole plugin, which holds only while docs is the only thing here. It is the first component, not the last. The scoped-MCP-name test required *every* server in .mcp.json to be named by skills/docs/SKILL.md, so adding a second server for some later skill would have failed the docs skill for not knowing about it. Replaced by two tests that scale: every declared server must be reached by *some* component, and any component reading MCP resources must name a server to read them from. The pmndrs-only link check now applies to the docs skill alone, where it means something — a lookup skill that sends you off-site is a lookup it cannot vouch for. Other skills keep the https requirement. Verified by simulating the roadmap: a second skill with its own bundled server passes clean, while dead server config, an unnamed server and a plain http link each still fail the test that guards them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnvYiz7aGhnBTK2tzExHis
1 parent 82a2486 commit 4e5ab69

2 files changed

Lines changed: 45 additions & 20 deletions

File tree

test/manifests.test.mjs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import test from 'node:test'
22
import assert from 'node:assert/strict'
33
import { existsSync, readFileSync } from 'node:fs'
44
import { join } from 'node:path'
5-
import { readJson, root, agents, skills } from './helpers.mjs'
5+
import { readJson, root, agents, skills, asList } from './helpers.mjs'
66

77
const plugin = readJson('.claude-plugin', 'plugin.json')
88
const marketplace = readJson('.claude-plugin', 'marketplace.json')
@@ -51,6 +51,38 @@ test('README lists every component file the plugin ships', () => {
5151
}
5252
})
5353

54+
const scope = (s) => s.replace(/[^A-Za-z0-9_-]/g, '_')
55+
const components = () => [...skills(), ...agents()]
56+
57+
test('every bundled MCP server is reached by some component', () => {
58+
// Which component is deliberately not pinned: a server added for a future
59+
// skill has no business being documented by an existing one. What matters is
60+
// that nothing in .mcp.json is dead config.
61+
for (const key of Object.keys(mcp.mcpServers ?? {})) {
62+
const scoped = `plugin:${plugin.name}:${key}`
63+
const prefix = `mcp__plugin_${scope(plugin.name)}_${scope(key)}__`
64+
const users = components().filter(
65+
(c) => c.body.includes(scoped) || asList(c.fields.tools).some((t) => t.startsWith(prefix)),
66+
)
67+
assert.ok(users.length > 0, `server "${key}" is declared but no skill or agent uses it`)
68+
}
69+
})
70+
71+
test('components that read MCP resources name the server they read from', () => {
72+
// ReadMcpResourceTool takes a configured server name. Bundled in a plugin,
73+
// that name is plugin:<plugin>:<server> — the bare key from .mcp.json is not
74+
// a name anything answers to.
75+
const scoped = Object.keys(mcp.mcpServers ?? {}).map((key) => `plugin:${plugin.name}:${key}`)
76+
for (const component of components()) {
77+
const uses = component.body.includes('ReadMcpResourceTool') || asList(component.fields.tools).includes('ReadMcpResourceTool')
78+
if (!uses) continue
79+
assert.ok(
80+
scoped.some((name) => component.body.includes(name)),
81+
`${component.path} reads MCP resources without naming a server; expected one of ${scoped.join(', ')}`,
82+
)
83+
}
84+
})
85+
5486
test('every marketplace source resolves on disk', () => {
5587
for (const entry of marketplace.plugins) {
5688
if (typeof entry.source !== 'string' || !entry.source.startsWith('.')) continue

test/skills.test.mjs

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import test from 'node:test'
22
import assert from 'node:assert/strict'
3-
import { readJson, skills, agents } from './helpers.mjs'
3+
import { skills, agents } from './helpers.mjs'
44

5-
const plugin = readJson('.claude-plugin', 'plugin.json')
6-
const mcp = readJson('.mcp.json')
75
const all = skills()
86

97
test('the plugin ships at least one skill', () => {
@@ -22,21 +20,26 @@ for (const skill of all) {
2220
assert.ok(fields.description?.length > 60, 'the description is the trigger')
2321
})
2422

25-
test(`${path}: documentation links point at docs.pmnd.rs over https`, () => {
23+
test(`${path}: links are https`, () => {
2624
for (const url of body.match(/https?:\/\/[^\s)`>]+/g) ?? []) {
2725
assert.match(url, /^https:/, `${url} must be https`)
28-
assert.ok(
29-
/^https:\/\/(docs\.pmnd\.rs|github\.com\/pmndrs)/.test(url),
30-
`${url} points outside the pmndrs docs and repos`,
31-
)
3226
}
3327
})
3428
}
3529

3630
const docs = all.find((s) => s.fields.name === 'docs')
3731

32+
test('the docs skill only sends readers to pmndrs docs and repos', () => {
33+
// Specific to this skill, not a house rule: its whole job is to replace a
34+
// remembered API with the published one, so an off-site link is a lookup
35+
// that went somewhere it cannot vouch for.
36+
for (const url of docs.body.match(/https:\/\/[^\s)`>]+/g) ?? []) {
37+
assert.match(url, /^https:\/\/(docs\.pmnd\.rs|github\.com\/pmndrs)/, `${url} points outside pmndrs`)
38+
}
39+
})
40+
3841
test('the docs skill states which libraries are served, and when that was checked', () => {
39-
assert.ok(docs, 'skills/docs/SKILL.md is the plugin’s reason to exist')
42+
assert.ok(docs, 'skills/docs/SKILL.md is where this plugin started')
4043

4144
const rows = [...docs.body.matchAll(/^\|\s*`([a-z0-9-]+)`\s*\|\s*(\d+)\s*\|/gm)]
4245
assert.ok(rows.length >= 3, 'the coverage table should list the served libraries with page counts')
@@ -59,13 +62,3 @@ test('the docs skill tells Claude when to delegate, and to an agent that exists'
5962
const referenced = names.filter((name) => docs.body.includes(name))
6063
assert.ok(referenced.length > 0, `SKILL.md should route wide lookups to one of: ${names.join(', ')}`)
6164
})
62-
63-
test('the docs skill uses the scoped MCP server name', () => {
64-
for (const key of Object.keys(mcp.mcpServers ?? {})) {
65-
const scoped = `plugin:${plugin.name}:${key}`
66-
assert.ok(
67-
docs.body.includes(scoped),
68-
`SKILL.md must name the server as ${scoped}; the bare key "${key}" does not resolve once installed`,
69-
)
70-
}
71-
})

0 commit comments

Comments
 (0)