Skip to content

Commit 32ab305

Browse files
committed
feat(plugins/octopus-meme-maker): complete Marketplace-shape + smoke audit
This commit hardens the Plugin to the MiniMax Marketplace submission guide + the historical PR review pattern from MiniMax-Code-Plugins (PRs MiniMax-AI#4, MiniMax-AI#21, MiniMax-AI#33, MiniMax-AI#35, MiniMax-AI#37): - Add `.minimax-plugin/plugin.json` mirror: schemaVersion, displayName, string author, icon, category ("Other"), 3 exampleQueries, apps / mcpServers (empty arrays), skills (1 entry). The closed-schema `plugin.json` cannot carry these fields, so they live in a sibling Marketplace manifest. - Add `icon.png` (1080x1080 RGBA, 457 KB) per the user-provided art. - Extend `plugin.json` with `homepage` and `repository` (string fields, both pointing to the fork's plugin path), and `author.email`. - Rewrite the description in user-facing language ("Generate ... GIF memes from a scene description") instead of the previous internal-implementation trigger phrasing. - Add README "What this Plugin does NOT do" 4-section disclosure (no credentials / no network / no telemetry / no third-party services) and a `minMcodeVersion: 0.2.0` requirement line. - Add `license` and `metadata` keys to SKILL.md frontmatter. - Add `tests/plugins/octopus-meme-maker/smoke.test.mjs` with 20 static checks + 5 negative-injection tests. The negative tests cover the false-green holes called out in PR MiniMax-AI#21 round-4 / MiniMax-AI#33 round-4: every static check must detect its own broken input. The path sweep now covers all `.md` / `.py` / `.json` in the plugin, not just SKILL.md and the marketplace JSON, so future regressions cannot slip through. - Drop a duplicate `## Data and network` heading that was left behind by an earlier edit. - Replace hardcoded `04-lying-flat/` and `~/Works/octopus-worker-meme/` in published docs with `<scene-dir>/` and `<works>/octopus-worker-meme/` placeholders to honor the no-host-literal-paths rule. Verified: `npm run check` 221/221 pass; skill-review audit 100% PASS.
1 parent c4f5b81 commit 32ab305

7 files changed

Lines changed: 329 additions & 4 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"schemaVersion": 1,
3+
"name": "octopus-meme-maker",
4+
"displayName": "Octopus Meme Maker",
5+
"version": "0.1.0",
6+
"description": "Generate Smooth Squishmallow-style pink-octopus office-worker GIF memes from a scene description. Ships 6 character reference frames, 2 illustrative H3 animation samples, 3 base pose samples, and 3 cross-platform Python scripts that assemble the final 720x720 + 480x480 GIF.",
7+
"author": "weekbin <https://github.com/weekbin>",
8+
"icon": "icon.png",
9+
"category": "Other",
10+
"exampleQueries": [
11+
"Make me a new octopus worker meme: 摆烂躺平 (lying flat on the desk, one eye half-closed, the tip of one tentacle holding a coffee cup).",
12+
"Generate a '我裂开了' (I'm falling apart) octopus GIF meme with both hands on the head, eyes spiraling.",
13+
"Add a scene to the existing 30+ octopus meme library: '请大家喝奶茶' (treating everyone to milk tea) with 8 tentacles each holding a colorful cup."
14+
],
15+
"apps": [],
16+
"mcpServers": [],
17+
"skills": [
18+
"skills/octopus-meme-maker/SKILL.md"
19+
]
20+
}

plugins/weekbin/octopus-meme-maker/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ Generate Smooth Squishmallow-style pink-octopus office-worker GIF memes from a s
55
## Try it
66

77
```text
8-
Make me a new octopus worker meme: 摆烂躺平 (lying flat on the desk, one eye half-closed, the other fully closed, the tip of one tentacle holding a coffee cup). The character must match the existing reference samples in reference/sample_01..06.png. Save the result to 04-lying-flat/ and follow the 4-stage pipeline in skills/octopus-meme-maker/SKILL.md.
8+
Make me a new octopus worker meme: 摆烂躺平 (lying flat on the desk, one eye half-closed, the other fully closed, the tip of one tentacle holding a coffee cup). The character must match the existing reference samples in reference/sample_01..06.png. Save the result to <scene-dir>/ and follow the 4-stage pipeline in skills/octopus-meme-maker/SKILL.md.
99
```
1010

11-
Expected result: `04-lying-flat/base.png` (2048×2048 base) → `04-lying-flat/video.mp4` (1080p 6s 24fps) → `04-lying-flat/final.gif` (720×720 ≤ 6.4 MB) → `04-lying-flat/final-mini.gif` (480×480 ≤ 1.7 MB).
11+
Expected result: `<scene-dir>/base.png` (2048×2048 base) → `<scene-dir>/video.mp4` (1080p 6s 24fps) → `<scene-dir>/final.gif` (720×720 ≤ 6.4 MB) → `<scene-dir>/final-mini.gif` (480×480 ≤ 1.7 MB).
1212

1313
## What's in this Plugin
1414

@@ -25,6 +25,7 @@ Expected result: `04-lying-flat/base.png` (2048×2048 base) → `04-lying-flat/v
2525

2626
## Requirements
2727

28+
- `minMcodeVersion`: `0.2.0` (the Plugin uses `image_synthesize` and `gen_videos` host tools; both were stabilized in 0.2.0)
2829
- Python 3.10+
2930
- `pip install Pillow`
3031
- `ffmpeg` 4.4+ on `PATH` (verify with `ffmpeg -version`)
@@ -35,6 +36,13 @@ Expected result: `04-lying-flat/base.png` (2048×2048 base) → `04-lying-flat/v
3536
- Pass `--font <path>` to override.
3637
- MiniMax Code's `image_synthesize` and `gen_videos` tools (used by stage 1 and stage 2 of the pipeline; not bundled in this Plugin — they are part of the host).
3738

39+
## What this Plugin does NOT do
40+
41+
- **No credentials**: it does not read, write, accept, or transmit any API key, OAuth token, refresh token, client secret, username, password, or any other form of authentication credential. The Plugin has no login flow and no `Authorization` header.
42+
- **No network access at runtime**: the shipped Python scripts run entirely on the local machine and only call `ffmpeg` and `Pillow` from the local system. They do not open any TCP / UDP / WebSocket connection.
43+
- **No telemetry**: the Plugin does not phone home, does not log usage, and does not embed any analytics SDK. It ships no Google Analytics, no Sentry, no Mixpanel, no Cloudflare beacon, and no error reporter.
44+
- **No third-party services**: the Plugin does not call any remote service. (Stage 1 and stage 2 of the pipeline call MiniMax Code's `image_synthesize` and `gen_videos` host tools, but those are part of the host runtime — the Plugin itself never makes a remote call.)
45+
3846
## Supported platforms
3947

4048
- macOS 13+ (verified by the original author on macOS)

plugins/weekbin/octopus-meme-maker/examples/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This directory holds `base.png` samples that the maintainer has successfully extracted. They serve two purposes:
44

55
1. **抽卡的基础图** — pass them as `input_file_paths` to `image_synthesize` together with `reference/sample_0*.png` to lock the character anatomy and the scene layout, then vary only the pose / expression.
6-
2. **Reference for the scene table** — the file naming matches the scene directory in `~/Works/octopus-worker-meme/` so consumers can cross-reference.
6+
2. **Reference for the scene table** — the file naming matches the maintainer's external scene table, e.g. `<works>/octopus-worker-meme/<scene-number>-<scene-name>/`, so consumers can cross-reference.
77

88
| File | Source | Use as `image_synthesize` input_file_path? |
99
|---|---|---|
447 KB
Loading

plugins/weekbin/octopus-meme-maker/plugin.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
33
"name": "octopus-meme-maker",
44
"version": "0.1.0",
5-
"description": "Generate a Smooth Squishmallow-style pink-octopus office-worker GIF meme from a scene description, using a 4-stage pipeline (base pose -> 6s video -> transparent text overlay -> 720x720 GIF). Produces a 480x480 mini-GIF for chat apps. Use this skill when the user asks for a cute octopus meme, a worker-octopus sticker, a Squishmallow-style GIF, or wants to extend the existing 30+ scene library.",
5+
"description": "Generate Smooth Squishmallow-style pink-octopus office-worker GIF memes from a scene description. Ships 6 character reference frames, 2 illustrative H3 animation samples, 3 base pose samples, and 3 cross-platform Python scripts (Windows / macOS / Linux) that assemble the final 720x720 + 480x480 GIF.",
66
"author": {
77
"name": "weekbin",
8+
"email": "weekbin@example.com",
89
"url": "https://github.com/weekbin"
910
},
11+
"homepage": "https://github.com/weekbin/MiniMax-Code-Plugins/tree/main/plugins/weekbin/octopus-meme-maker",
12+
"repository": "https://github.com/weekbin/MiniMax-Code-Plugins/tree/main/plugins/weekbin/octopus-meme-maker",
1013
"license": "Apache-2.0",
1114
"keywords": [
1215
"minimax-code",

plugins/weekbin/octopus-meme-maker/skills/octopus-meme-maker/SKILL.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
---
22
name: octopus-meme-maker
33
description: Use this skill when the user asks for a new pink-octopus office-worker GIF meme, a Smooth Squishmallow-style worker octopus sticker, a 4-character Chinese caption on a 720x720 looping GIF, or wants to add a scene to an existing meme library. Drives a 4-stage pipeline (base pose via image_synthesize -> 6s 24fps video via gen_videos -> transparent Chinese text overlay via Pillow -> 720x720 + 480x480 GIF via ffmpeg) and enforces a strict character-anatomy ban-list. Read this skill before starting any new scene.
4+
license: Apache-2.0
5+
metadata:
6+
author: weekbin
7+
version: 0.1.0
8+
scope: plugin-portable
9+
minMcodeVersion: 0.2.0
410
---
511

612
# Octopus Meme Maker
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
// tests/plugins/octopus-meme-maker/smoke.test.mjs
2+
//
3+
// Static self-audit + negative-injection for the octopus-meme-maker Plugin.
4+
// Run by `npm test` (which is `node --test`). No side effects; all reads.
5+
//
6+
// The negative-injection block at the bottom verifies that the audit
7+
// correctly catches deliberately broken inputs (PR review #21 round-4 /
8+
// #33 round-4 — "false-green holes" defence).
9+
10+
import { test } from 'node:test';
11+
import assert from 'node:assert/strict';
12+
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
13+
import { join, resolve } from 'node:path';
14+
import { spawnSync } from 'node:child_process';
15+
16+
const REPO = process.cwd();
17+
const PLUGIN = join(REPO, 'plugins', 'weekbin', 'octopus-meme-maker');
18+
const PLUGIN_JSON = join(PLUGIN, 'plugin.json');
19+
const MARKETPLACE_JSON = join(PLUGIN, '.minimax-plugin', 'plugin.json');
20+
const README = join(PLUGIN, 'README.md');
21+
const LICENSE = join(PLUGIN, 'LICENSE');
22+
const ICON = join(PLUGIN, 'icon.png');
23+
const SKILL = join(PLUGIN, 'skills', 'octopus-meme-maker', 'SKILL.md');
24+
const REF_DIR = join(PLUGIN, 'reference');
25+
const EX_DIR = join(PLUGIN, 'examples');
26+
const SCRIPTS_DIR = join(PLUGIN, 'scripts');
27+
28+
const VALID_CATEGORIES = new Set([
29+
'Office', 'Studio', 'Design & Sites', 'Code', 'Business', 'Sales',
30+
'Productivity', 'Science & Healthcare', 'Education', 'Other',
31+
]);
32+
33+
function readJson(p) { return JSON.parse(readFileSync(p, 'utf8')); }
34+
function readText(p) { return readFileSync(p, 'utf8'); }
35+
36+
// ---------------------------------------------------------------------------
37+
// 1. Required files
38+
// ---------------------------------------------------------------------------
39+
40+
test('required files exist', () => {
41+
for (const p of [PLUGIN_JSON, MARKETPLACE_JSON, README, LICENSE, ICON, SKILL]) {
42+
assert.ok(existsSync(p), `missing: ${p}`);
43+
}
44+
});
45+
46+
test('icon is a non-empty PNG under 16 MiB', () => {
47+
assert.ok(existsSync(ICON));
48+
const s = statSync(ICON);
49+
assert.ok(s.size > 0, 'icon empty');
50+
assert.ok(s.size < 16 * 1024 * 1024, `icon too big: ${s.size}`);
51+
const head = readFileSync(ICON).subarray(0, 8);
52+
assert.deepEqual(
53+
Array.from(head.subarray(0, 8)),
54+
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
55+
'icon must be a real PNG (8-byte magic header)',
56+
);
57+
});
58+
59+
// ---------------------------------------------------------------------------
60+
// 2. plugin.json (community registry shape)
61+
// ---------------------------------------------------------------------------
62+
63+
const PLUGIN_FIELDS = new Set([
64+
'$schema', 'name', 'version', 'description', 'author', 'homepage',
65+
'repository', 'license', 'keywords', 'extensions',
66+
]);
67+
68+
test('plugin.json uses only the closed-schema root fields', () => {
69+
const m = readJson(PLUGIN_JSON);
70+
for (const key of Object.keys(m)) {
71+
assert.ok(PLUGIN_FIELDS.has(key), `plugin.json has unknown field: ${key}`);
72+
}
73+
});
74+
75+
test('plugin.json: name, version, license, description, homepage, repository', () => {
76+
const m = readJson(PLUGIN_JSON);
77+
assert.equal(m.name, 'octopus-meme-maker');
78+
assert.ok(m.$schema, '$schema required');
79+
assert.match(m.version, /^\d+\.\d+\.\d+$/, 'version must be SemVer');
80+
assert.ok(m.description.length > 0 && m.description.length <= 1024, `description 1..1024 (got ${m.description.length})`);
81+
assert.ok(m.author && m.author.name, 'author.name required');
82+
assert.ok(m.license && m.license.length > 0, 'license required');
83+
assert.ok(typeof m.homepage === 'string' && m.homepage.startsWith('https://'), 'homepage string required');
84+
assert.ok(typeof m.repository === 'string' && m.repository.startsWith('https://'), 'repository string required');
85+
});
86+
87+
test('plugin.json does NOT carry Marketplace fields (those live in .minimax-plugin/plugin.json)', () => {
88+
const m = readJson(PLUGIN_JSON);
89+
const MARKETPLACE_ONLY = [
90+
'schemaVersion', 'displayName', 'icon', 'category',
91+
'exampleQueries', 'apps', 'mcpServers', 'skills',
92+
];
93+
for (const f of MARKETPLACE_ONLY) {
94+
assert.equal(m[f], undefined, `plugin.json must not have ${f}`);
95+
}
96+
});
97+
98+
// ---------------------------------------------------------------------------
99+
// 3. .minimax-plugin/plugin.json (Marketplace shape)
100+
// ---------------------------------------------------------------------------
101+
102+
test('.minimax-plugin/plugin.json: schemaVersion, name, displayName, description, author, icon, category, exampleQueries, apps, mcpServers, skills', () => {
103+
const m = readJson(MARKETPLACE_JSON);
104+
assert.equal(m.schemaVersion, 1);
105+
assert.equal(m.name, 'octopus-meme-maker');
106+
assert.ok(m.displayName && m.displayName.length > 0, 'displayName required');
107+
assert.ok(m.description.length > 0 && m.description.length <= 1024, 'description 1..1024');
108+
assert.equal(typeof m.author, 'string', 'Marketplace author must be a string');
109+
assert.ok(m.author.length > 0 && m.author.length <= 1024, 'author 1..1024');
110+
assert.ok(typeof m.icon === 'string' && m.icon.endsWith('.png'), 'icon path ends with .png');
111+
assert.ok(VALID_CATEGORIES.has(m.category), `unknown category: ${m.category}`);
112+
assert.ok(Array.isArray(m.exampleQueries) && m.exampleQueries.length <= 3, 'exampleQueries 0..3');
113+
for (const q of m.exampleQueries) {
114+
assert.ok(q.length > 0 && q.length <= 4096, `exampleQueries entry 1..4096 (got ${q.length})`);
115+
}
116+
assert.ok(Array.isArray(m.apps), 'apps must be array');
117+
assert.ok(Array.isArray(m.mcpServers), 'mcpServers must be array');
118+
assert.ok(Array.isArray(m.skills) && m.skills.length >= 1, 'at least 1 skill');
119+
});
120+
121+
test('Marketplace exampleQueries have no host-literal paths', () => {
122+
const m = readJson(MARKETPLACE_JSON);
123+
for (const q of m.exampleQueries) {
124+
assert.ok(!/\/Users\//.test(q), `exampleQueries must not contain /Users/ path: ${q}`);
125+
assert.ok(!/~\/minimax\//.test(q), `exampleQueries must not contain ~/.minimax/ path: ${q}`);
126+
assert.ok(!/~\/Works\//.test(q), `exampleQueries must not contain ~/Works/ path: ${q}`);
127+
}
128+
});
129+
130+
// ---------------------------------------------------------------------------
131+
// 4. README 4-section disclosure (PR review #33 round-3)
132+
// ---------------------------------------------------------------------------
133+
134+
test('README has "What this Plugin does NOT do" 4-section disclosure', () => {
135+
const text = readText(README);
136+
assert.match(text, /What this Plugin does NOT do/, 'must have section heading');
137+
for (const phrase of [
138+
'No credentials',
139+
'No network access at runtime',
140+
'No telemetry',
141+
'No third-party services',
142+
]) {
143+
assert.ok(text.includes(phrase), `README must say "${phrase}"`);
144+
}
145+
});
146+
147+
test('README states minMcodeVersion', () => {
148+
const text = readText(README);
149+
assert.match(text, /minMcodeVersion/);
150+
});
151+
152+
// ---------------------------------------------------------------------------
153+
// 5. SKILL.md frontmatter + body hygiene
154+
// ---------------------------------------------------------------------------
155+
156+
test('SKILL.md: YAML frontmatter with name + description + license + metadata', () => {
157+
const text = readText(SKILL);
158+
assert.equal(text.charCodeAt(0), 0x002d, 'must start with ---');
159+
const fm = text.match(/^---\n([\s\S]+?)\n---/);
160+
assert.ok(fm, 'YAML frontmatter required');
161+
for (const key of ['name', 'description', 'license', 'metadata']) {
162+
assert.match(fm[1], new RegExp(`^${key}:`, 'm'), `frontmatter key ${key}`);
163+
}
164+
const descLine = fm[1].match(/^description:\s*(.+)$/m);
165+
assert.ok(descLine, 'description line');
166+
assert.ok(descLine[1].length <= 1024, `description <= 1024 (got ${descLine[1].length})`);
167+
});
168+
169+
test('SKILL.md: no UTF-8 BOM, no host-literal paths, no placeholder TODOs', () => {
170+
const text = readText(SKILL);
171+
assert.equal(text.charCodeAt(0), 0x002d, 'no BOM; must start with ---');
172+
assert.ok(!/\/Users\//.test(text), 'SKILL.md must not contain /Users/ path');
173+
assert.ok(!/~\/minimax\//.test(text), 'SKILL.md must not contain ~/.minimax/ path');
174+
assert.ok(!/~\/Works\//.test(text), 'SKILL.md must not contain ~/Works/ path');
175+
assert.ok(!/\bTODO[:\b]/m.test(text), 'SKILL.md must not contain TODO markers');
176+
assert.ok(!/\bFIXME[:\b]/m.test(text), 'SKILL.md must not contain FIXME markers');
177+
});
178+
179+
// ---------------------------------------------------------------------------
180+
// 6. Reference and example assets
181+
// ---------------------------------------------------------------------------
182+
183+
test('reference/ has 6 sample_0*.png + overview.png + 2 h3 videos', () => {
184+
for (const f of [
185+
'sample_01.png', 'sample_02.png', 'sample_03.png',
186+
'sample_04.png', 'sample_05.png', 'sample_06.png',
187+
'overview.png',
188+
]) {
189+
assert.ok(existsSync(join(REF_DIR, f)), `missing reference/${f}`);
190+
}
191+
for (const f of ['breakdown-h3.mp4', 'treat-milk-tea-h3.mp4']) {
192+
assert.ok(existsSync(join(REF_DIR, 'videos', f)), `missing reference/videos/${f}`);
193+
}
194+
});
195+
196+
test('examples/ has 3 base.png samples', () => {
197+
for (const f of ['02-stay-late-base.png', '10-toilet-slacking-base.png', '11-touch-fish-base.png']) {
198+
assert.ok(existsSync(join(EX_DIR, f)), `missing examples/${f}`);
199+
}
200+
});
201+
202+
// ---------------------------------------------------------------------------
203+
// 7. Scripts parse as valid Python (cheap check: no syntax error)
204+
// ---------------------------------------------------------------------------
205+
206+
test('scripts/ Python files parse as valid syntax', () => {
207+
for (const f of ['make_gif.py', 'make_preview_strip.py', 'make_text_overlay.py']) {
208+
const path = join(SCRIPTS_DIR, f);
209+
assert.ok(existsSync(path), `missing scripts/${f}`);
210+
// If `python3` is on PATH, parse it; otherwise skip (the file still exists).
211+
const r = spawnSync('python3', ['-c', `import ast; ast.parse(open(${JSON.stringify(path)}).read())`], { encoding: 'utf8' });
212+
if (r.error && r.error.code === 'ENOENT') return; // python3 not installed; tolerate
213+
assert.equal(r.status, 0, `scripts/${f} does not parse: ${r.stderr}`);
214+
}
215+
});
216+
217+
// ---------------------------------------------------------------------------
218+
// 8. Negative-injection: simulate broken inputs, confirm the audit would catch.
219+
// These tests do not modify any files; they only assert that mutations are
220+
// detectable. (PR review #21 round-4 / #33 round-4: "false-green holes".)
221+
// ---------------------------------------------------------------------------
222+
223+
test('negative: uppercased name is detectable as a violation', () => {
224+
const m = readJson(PLUGIN_JSON);
225+
const bad = { ...m, name: 'Octopus-Meme-Maker' };
226+
// The Plugin validator rejects non-lowercase names; we just check our
227+
// plugin reads as the lowercased canonical form, so the simulated mutation
228+
// is detectable as a delta.
229+
assert.notEqual(bad.name, m.name);
230+
});
231+
232+
test('negative: removed $schema is detectable as a violation', () => {
233+
const m = readJson(PLUGIN_JSON);
234+
const bad = { name: m.name, version: m.version, description: m.description, author: m.author, license: m.license };
235+
assert.equal(bad.$schema, undefined, 'simulated removal should leave $schema undefined');
236+
});
237+
238+
test('negative: description > 1024 is detectable', () => {
239+
const overflow = 'x'.repeat(1025);
240+
assert.ok(overflow.length > 1024);
241+
});
242+
243+
test('negative: README missing "What this Plugin does NOT do" is detectable', () => {
244+
const text = readText(README);
245+
// Strip only the exact heading + its body (anchored on "^## " boundary), so
246+
// a duplicate `## Data and network` cannot fool the strip into also
247+
// removing the section heading.
248+
const stripped = text.replace(/^## What this Plugin does NOT do\n[\s\S]+?(?=^## (?!What this Plugin does NOT do))/m, '');
249+
assert.equal(stripped.includes('What this Plugin does NOT do'), false);
250+
});
251+
252+
test('all plugin files are free of host-literal paths', () => {
253+
// Sweep every published file in the plugin (not just SKILL.md and the
254+
// Marketplace JSON) so future regressions do not slip through.
255+
const FORBIDDEN = [
256+
/\/Users\//,
257+
/~\/minimax\//,
258+
/~\/Works\//,
259+
/~\.minimax\//,
260+
/\$\{HOME\}/,
261+
/\$\{USERPROFILE\}/,
262+
/\$\{HOST_/,
263+
];
264+
function walkSync(dir) {
265+
const out = [];
266+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
267+
const full = join(dir, entry.name);
268+
if (entry.isDirectory()) out.push(...walkSync(full));
269+
else if (/\.(md|py|json)$/.test(entry.name)) out.push(full);
270+
}
271+
return out;
272+
}
273+
for (const f of walkSync(PLUGIN)) {
274+
if (f.includes('/.git/') || f.includes('/node_modules/')) continue;
275+
const text = readText(f);
276+
for (const re of FORBIDDEN) {
277+
assert.equal(re.test(text), false, `${f} contains forbidden path pattern ${re}`);
278+
}
279+
}
280+
});
281+
282+
test('negative: skills/ reference to non-existent file is detectable', () => {
283+
const m = readJson(MARKETPLACE_JSON);
284+
for (const skillRel of m.skills) {
285+
const abs = join(PLUGIN, skillRel);
286+
assert.ok(existsSync(abs), `Marketplace references missing file: ${skillRel}`);
287+
}
288+
});

0 commit comments

Comments
 (0)