Skip to content

Commit 43faca6

Browse files
committed
test(skills): add unit and CLI tests for the sync CLI
Guard the CLI entrypoint so the module is importable, export the pure helpers, and add node:test suites (no new deps) covering repo inference, .skills.local parsing, frontmatter, maturity/domain filtering, arg parsing, skill collection, and CLI commands. Wire `test` to `node --test`.
1 parent 977b8f8 commit 43faca6

4 files changed

Lines changed: 398 additions & 28 deletions

File tree

bin/metamask-skills.mjs

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { spawnSync } from 'node:child_process';
33
import { mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
44
import os from 'node:os';
55
import path from 'node:path';
6-
import { fileURLToPath } from 'node:url';
6+
import { fileURLToPath, pathToFileURL } from 'node:url';
77

88
const __dirname = path.dirname(fileURLToPath(import.meta.url));
99
const PACKAGE_ROOT = path.resolve(__dirname, '..');
@@ -737,31 +737,61 @@ function postinstall(args) {
737737
}
738738
}
739739

740-
const [command, ...args] = process.argv.slice(2);
741-
if (!command || command === '-h' || command === '--help') {
742-
usage(0);
743-
}
744-
745-
let exitCode;
746-
try {
747-
if (command === 'list') {
748-
exitCode = listSkills(args);
749-
} else if (command === 'search') {
750-
exitCode = searchSkills(args);
751-
} else if (command === 'describe') {
752-
exitCode = describeSkill(args);
753-
} else if (command === 'sync') {
754-
exitCode = sync(args);
755-
} else if (command === 'postinstall') {
756-
exitCode = postinstall(args);
757-
} else if (command === 'install') {
758-
exitCode = install(args);
759-
} else {
760-
process.stderr.write(`Unknown command: ${command}\n\n`);
761-
usage(1);
740+
export {
741+
bodyAfterFrontmatter,
742+
collectSkills,
743+
domainMatches,
744+
expandHome,
745+
filterSkills,
746+
findSkill,
747+
formatSkillTable,
748+
getConfigValue,
749+
hasArg,
750+
isTruthy,
751+
maturityMatches,
752+
parseDiscoveryArgs,
753+
parseFrontmatter,
754+
parseGlobalArgs,
755+
parseSkillsLocal,
756+
repoNameFromGitHubUrl,
757+
repoOverlays,
758+
hasSkillsSource,
759+
shouldSkipPostinstall,
760+
splitList,
761+
stripInlineComment,
762+
unquote,
763+
};
764+
765+
const invokedDirectly =
766+
Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
767+
768+
if (invokedDirectly) {
769+
const [command, ...args] = process.argv.slice(2);
770+
if (!command || command === '-h' || command === '--help') {
771+
usage(0);
772+
}
773+
774+
let exitCode;
775+
try {
776+
if (command === 'list') {
777+
exitCode = listSkills(args);
778+
} else if (command === 'search') {
779+
exitCode = searchSkills(args);
780+
} else if (command === 'describe') {
781+
exitCode = describeSkill(args);
782+
} else if (command === 'sync') {
783+
exitCode = sync(args);
784+
} else if (command === 'postinstall') {
785+
exitCode = postinstall(args);
786+
} else if (command === 'install') {
787+
exitCode = install(args);
788+
} else {
789+
process.stderr.write(`Unknown command: ${command}\n\n`);
790+
usage(1);
791+
}
792+
} catch (error) {
793+
process.stderr.write(`metamask-skills: ${error instanceof Error ? error.message : String(error)}\n`);
794+
exitCode = 1;
762795
}
763-
} catch (error) {
764-
process.stderr.write(`metamask-skills: ${error instanceof Error ? error.message : String(error)}\n`);
765-
exitCode = 1;
796+
process.exit(exitCode);
766797
}
767-
process.exit(exitCode);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"scripts": {
1919
"smoke": "node bin/metamask-skills.mjs --help",
2020
"build": "node bin/metamask-skills.mjs --help",
21-
"test": "node bin/metamask-skills.mjs --help",
21+
"test": "node --test test/*.test.mjs",
2222
"pack:dry-run": "npm pack --dry-run",
2323
"changelog:validate": "bash scripts/validate-changelog.sh",
2424
"create-release-branch": "create-release-branch --formatter oxfmt"

test/cli.test.mjs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import assert from 'node:assert/strict';
2+
import { spawnSync } from 'node:child_process';
3+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4+
import os from 'node:os';
5+
import path from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
import { after, before, describe, test } from 'node:test';
8+
9+
const BIN = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'metamask-skills.mjs');
10+
11+
function runCli(args, env = {}) {
12+
return spawnSync(process.execPath, [BIN, ...args], {
13+
encoding: 'utf8',
14+
maxBuffer: 16 * 1024 * 1024,
15+
// Neutralize any source-dir env vars from the host shell so tests are deterministic.
16+
env: { ...process.env, METAMASK_SKILLS_DIR: '', CONSENSYS_SKILLS_DIR: '', ...env },
17+
});
18+
}
19+
20+
describe('CLI entrypoint', () => {
21+
test('--help prints usage and exits 0', () => {
22+
const result = runCli(['--help']);
23+
assert.equal(result.status, 0);
24+
assert.match(result.stdout, /MetaMask skills CLI/u);
25+
assert.match(result.stdout, /metamask-skills sync/u);
26+
});
27+
28+
test('no command prints usage and exits 0', () => {
29+
const result = runCli([]);
30+
assert.equal(result.status, 0);
31+
assert.match(result.stdout, /Usage:/u);
32+
});
33+
34+
test('unknown command exits 1 with error', () => {
35+
const result = runCli(['frobnicate']);
36+
assert.equal(result.status, 1);
37+
assert.match(result.stderr, /Unknown command: frobnicate/u);
38+
});
39+
});
40+
41+
describe('CLI list against a fixture source', () => {
42+
let root;
43+
let source;
44+
45+
before(() => {
46+
root = mkdtempSync(path.join(os.tmpdir(), 'mms-cli-'));
47+
source = path.join(root, 'skills-source');
48+
const skillDir = path.join(source, 'domains', 'testing', 'skills', 'unit-testing');
49+
mkdirSync(path.join(skillDir, 'repos'), { recursive: true });
50+
mkdirSync(path.join(source, 'tools'), { recursive: true });
51+
writeFileSync(
52+
path.join(skillDir, 'skill.md'),
53+
['---', 'name: unit-testing', 'description: Write unit tests', 'maturity: stable', '---', 'Body.'].join('\n'),
54+
);
55+
});
56+
57+
after(() => {
58+
rmSync(root, { recursive: true, force: true });
59+
});
60+
61+
test('list --json emits valid JSON scoped to the configured source', () => {
62+
const result = runCli(['list', '--json', '--target', root, '--repo', 'core'], {
63+
METAMASK_SKILLS_DIR: source,
64+
});
65+
assert.equal(result.status, 0);
66+
const parsed = JSON.parse(result.stdout);
67+
assert.equal(parsed.repo, 'core');
68+
assert.deepEqual(parsed.sources, [source]);
69+
assert.deepEqual(parsed.skills.map((s) => s.id), ['testing/unit-testing']);
70+
});
71+
72+
test('search narrows results and exits 0', () => {
73+
const result = runCli(['search', 'unit', '--json', '--target', root], {
74+
METAMASK_SKILLS_DIR: source,
75+
});
76+
assert.equal(result.status, 0);
77+
const parsed = JSON.parse(result.stdout);
78+
assert.equal(parsed.skills.length, 1);
79+
assert.equal(parsed.query, 'unit');
80+
});
81+
});

0 commit comments

Comments
 (0)