mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
feat(cli): rig plugin validate subcommand + --runtime/--source filter validation (slice 3.4 Checkpoint C)
Per IMPL-PRD §4.2 + HG-4.4 (validate) AND closes velocity-guard 3.4.A-repair
pre-close punch (--runtime/--source CLI-side validation).
============ rig plugin validate <path> (HG-4.4) ============
Local file inspection — does NOT require daemon. Validates:
- Manifest dir presence: at least one of .claude-plugin/plugin.json or
.codex-plugin/plugin.json must exist
- Manifest required fields: name (string) + version (string); description
(string) is REQUIRED on Codex (per Codex spec) and recommended-not-
required on Claude
- Skill frontmatter for each skills/<id>/SKILL.md: name (non-empty) +
description (non-empty, ≤1024 chars per agentskills.io spec)
- Path existence + isDirectory checks
Output:
- Default (pretty): 'Plugin at <path>: valid' OR error list with exit 1
- --json: { valid: boolean, errors: string[] }; exit 1 when invalid
8 tests:
- Valid dual-manifest + skill passes (exit 0)
- No manifest dirs at all FAILS
- Codex manifest missing description FAILS (Codex spec requires)
- Manifest missing name FAILS
- Skill missing frontmatter name FAILS
- Skill description >1024 chars FAILS (agentskills.io limit)
- --json output returns { valid, errors } shape
- Non-existent path FAILS with clear error
============ --runtime / --source filter validation (closes pre-close punch) ============
CLI-side validation per velocity-guard 3.4.A-repair carried concern: typos
like '--source rig-cwd' (a daemon-side parseSourceFilter rejection that
silently returns ALL plugins) now fail loud with clear error naming
allowed values.
VALID_RUNTIMES = ['claude', 'codex'] (mirror packages/daemon/src/routes/
plugins.ts L25-30 parseRuntimeFilter)
VALID_SOURCES = ['vendored', 'claude-cache', 'codex-cache'] (mirror L37-40
parseSourceFilter)
3 tests:
- rejects invalid --runtime value with clear error + non-zero exit
- rejects invalid --source value (rig-cwd typo case) with clear error
- accepts valid combos (regression for normal pass-through)
============ Verification ============
- plugin.test.ts: 22/22 pass (was 11; +8 validate + +3 filter validation)
- CLI typecheck: clean
- Full CLI suite: 897 pass + 4 fail (pre-existing DaemonClient/Preflight;
NOT my regressions; same 4 fail since pre-batch-2-merge state)
============ Slice 3.4 status ============
All 4 subcommands shipped + tested + integrated:
- list (HG-4.1) ✓
- show (HG-4.2) ✓
- used-by (HG-4.3) ✓
- validate (HG-4.4) ✓
- All ship --json output (HG-4.5 building-agent-software) ✓
Ready for slice-close ACK + walk packet authoring + final hand-off DM
to orch-lead in next commit.
NO new SC-29 declaration this slice (read-only CLI consumer + local
file validation; no ConfigStore additions, no new HTTP routes, no DB
migrations). Cumulative ledger stays at #1-4 + project-observability
#5/#6 + #8 (3.3) + #9 (3.2).
Per IMPL-PRD §4 plugin-primitive Phase 3a slice 3.4 + per banked
feedback_tdd_scope.
This commit is contained in:
1 parent
120a56ec9a
commit
489577b503
2 files changed
+408
-4
No files matched your search
@@ -12,9 +12,9 @@
|
||||
// flags: --json
|
||||
// used-by <id> — agents referencing this plugin
|
||||
// flags: --json
|
||||
//
|
||||
// Pending in later checkpoints of slice 3.4:
|
||||
// validate <path> — local file inspection (3.4.C)
|
||||
// validate <path> — local file inspection of a plugin source tree
|
||||
// (manifest shape + skill frontmatter per agentskills.io)
|
||||
// flags: --json
|
||||
//
|
||||
// list/show consume slice 3.3 daemon HTTP routes (GET /api/plugins[/...]).
|
||||
//
|
||||
@@ -45,11 +45,20 @@
|
||||
// tightly scoped to what the daemon actually supports.
|
||||
|
||||
import { Command } from "commander";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { DaemonClient } from "../client.js";
|
||||
import { getDaemonStatus, getDaemonUrl } from "../daemon-lifecycle.js";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import type { StatusDeps } from "./status.js";
|
||||
|
||||
// Filter-value enums per slice-3.3 daemon route validation
|
||||
// (packages/daemon/src/routes/plugins.ts L37-58). CLI-side validation
|
||||
// gives operators clear errors instead of silent pass-through where the
|
||||
// daemon ignores unknown values.
|
||||
const VALID_RUNTIMES = ["claude", "codex"] as const;
|
||||
const VALID_SOURCES = ["vendored", "claude-cache", "codex-cache"] as const;
|
||||
|
||||
// ============================================================
|
||||
// Wire shapes — must mirror PluginDiscoveryService verbatim
|
||||
// (packages/daemon/src/domain/plugin-discovery-service.ts L41-132)
|
||||
@@ -140,6 +149,16 @@ export function pluginCommand(depsOverride?: StatusDeps): Command {
|
||||
.option("--json", "JSON output")
|
||||
.action(async (opts: { runtime?: string; source?: string; json?: boolean }) => {
|
||||
try {
|
||||
// CLI-side filter-value validation per pre-close punch from velocity-guard
|
||||
// 3.4.A repair verdict: typos like --source rig-cwd should fail loud,
|
||||
// not silently be ignored by the daemon route.
|
||||
if (opts.runtime && !(VALID_RUNTIMES as readonly string[]).includes(opts.runtime)) {
|
||||
throw new Error(`Invalid --runtime value "${opts.runtime}". Valid: ${VALID_RUNTIMES.join(" | ")} (omit flag for all)`);
|
||||
}
|
||||
if (opts.source && !(VALID_SOURCES as readonly string[]).includes(opts.source)) {
|
||||
throw new Error(`Invalid --source value "${opts.source}". Valid: ${VALID_SOURCES.join(" | ")}`);
|
||||
}
|
||||
|
||||
const client = await getClient();
|
||||
const params: string[] = [];
|
||||
if (opts.runtime) params.push(`runtime=${encodeURIComponent(opts.runtime)}`);
|
||||
@@ -298,5 +317,145 @@ export function pluginCommand(depsOverride?: StatusDeps): Command {
|
||||
}
|
||||
});
|
||||
|
||||
// -- rig plugin validate <path> --
|
||||
// Local file inspection — no daemon dependency; useful when authoring
|
||||
// a plugin in a feature branch where the daemon may not yet have
|
||||
// discovered it. Validates manifest shape (per Claude/Codex specs)
|
||||
// + skill frontmatter (per agentskills.io: name + description ≤1024 chars).
|
||||
cmd.command("validate")
|
||||
.argument("<path>", "Plugin source directory to validate")
|
||||
.description("Validate plugin manifest + skill frontmatter against agentskills.io spec")
|
||||
.option("--json", "JSON output ({ valid: boolean, errors: string[] })")
|
||||
.action((path: string, opts: { json?: boolean }) => {
|
||||
const errors = validatePluginTree(path);
|
||||
const valid = errors.length === 0;
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify({ valid, errors }, null, 2));
|
||||
process.exitCode = valid ? undefined : 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
console.log(`Plugin at ${path}: valid`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`Plugin at ${path}: INVALID (${errors.length} error${errors.length === 1 ? "" : "s"})`);
|
||||
for (const err of errors) {
|
||||
console.error(` - ${err}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// validate plumbing — local file inspection
|
||||
// ============================================================
|
||||
|
||||
function validatePluginTree(pluginPath: string): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!existsSync(pluginPath)) {
|
||||
errors.push(`Plugin path does not exist: ${pluginPath}`);
|
||||
return errors;
|
||||
}
|
||||
if (!statSync(pluginPath).isDirectory()) {
|
||||
errors.push(`Plugin path is not a directory: ${pluginPath}`);
|
||||
return errors;
|
||||
}
|
||||
|
||||
const claudeManifestPath = join(pluginPath, ".claude-plugin", "plugin.json");
|
||||
const codexManifestPath = join(pluginPath, ".codex-plugin", "plugin.json");
|
||||
const hasClaude = existsSync(claudeManifestPath);
|
||||
const hasCodex = existsSync(codexManifestPath);
|
||||
|
||||
if (!hasClaude && !hasCodex) {
|
||||
errors.push("No plugin manifest found: expected .claude-plugin/plugin.json and/or .codex-plugin/plugin.json");
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (hasClaude) {
|
||||
errors.push(...validateManifest(claudeManifestPath, "claude"));
|
||||
}
|
||||
if (hasCodex) {
|
||||
errors.push(...validateManifest(codexManifestPath, "codex"));
|
||||
}
|
||||
|
||||
// Validate skill frontmatter
|
||||
const skillsDir = join(pluginPath, "skills");
|
||||
if (existsSync(skillsDir) && statSync(skillsDir).isDirectory()) {
|
||||
for (const skillId of readdirSync(skillsDir)) {
|
||||
const skillMdPath = join(skillsDir, skillId, "SKILL.md");
|
||||
if (existsSync(skillMdPath)) {
|
||||
errors.push(...validateSkillFrontmatter(skillMdPath, skillId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateManifest(manifestPath: string, runtime: "claude" | "codex"): string[] {
|
||||
const errors: string[] = [];
|
||||
let raw: Record<string, unknown>;
|
||||
try {
|
||||
raw = JSON.parse(readFileSync(manifestPath, "utf-8")) as Record<string, unknown>;
|
||||
} catch (err) {
|
||||
errors.push(`${runtime} manifest parse error at ${manifestPath}: ${(err as Error).message}`);
|
||||
return errors;
|
||||
}
|
||||
if (!raw["name"] || typeof raw["name"] !== "string") {
|
||||
errors.push(`${runtime} manifest missing required field: name (must be non-empty string)`);
|
||||
}
|
||||
if (!raw["version"] || typeof raw["version"] !== "string") {
|
||||
errors.push(`${runtime} manifest missing required field: version (must be non-empty string)`);
|
||||
}
|
||||
// Codex spec: description is REQUIRED. Claude spec: description is recommended but
|
||||
// not a hard requirement; we treat missing description as a warning by NOT erroring
|
||||
// on Claude-only-missing-description while erroring on Codex-missing-description.
|
||||
if (runtime === "codex" && (!raw["description"] || typeof raw["description"] !== "string")) {
|
||||
errors.push(`codex manifest missing required field: description (Codex spec requires it)`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateSkillFrontmatter(skillPath: string, skillId: string): string[] {
|
||||
const errors: string[] = [];
|
||||
let content: string;
|
||||
try {
|
||||
content = readFileSync(skillPath, "utf-8");
|
||||
} catch (err) {
|
||||
errors.push(`skill "${skillId}": failed to read SKILL.md: ${(err as Error).message}`);
|
||||
return errors;
|
||||
}
|
||||
// Frontmatter: file MUST start with `---\n` and contain a closing `---` line
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!fmMatch) {
|
||||
errors.push(`skill "${skillId}": SKILL.md missing frontmatter (must open with --- ... ---)`);
|
||||
return errors;
|
||||
}
|
||||
const fmBody = fmMatch[1] ?? "";
|
||||
|
||||
// Required: name (non-empty single-line string)
|
||||
const nameMatch = fmBody.match(/^name:\s*(.+)$/m);
|
||||
if (!nameMatch || !nameMatch[1]?.trim()) {
|
||||
errors.push(`skill "${skillId}": frontmatter missing required field: name`);
|
||||
}
|
||||
|
||||
// Required: description (non-empty; supports multi-line YAML block scalar)
|
||||
// Match either inline `description: ...` or block-scalar `description: |` / `description: >`
|
||||
const descBlockMatch = fmBody.match(/^description:\s*(?:>|\|)?\s*\n?([\s\S]*?)(?=\n\w[\w-]*:|\n*$)/m);
|
||||
if (!descBlockMatch || !descBlockMatch[1]?.trim()) {
|
||||
errors.push(`skill "${skillId}": frontmatter missing required field: description`);
|
||||
} else {
|
||||
const descText = descBlockMatch[1].trim();
|
||||
if (descText.length > 1024) {
|
||||
errors.push(`skill "${skillId}": description length ${descText.length} chars exceeds agentskills.io limit of 1024`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -12,8 +12,11 @@
|
||||
// Test pattern follows context-pack.test.ts (slice 3.4 mirror) — in-memory
|
||||
// http.createServer daemon mock + Commander.parseAsync via captureLogs.
|
||||
|
||||
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from "vitest";
|
||||
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
|
||||
import http from "node:http";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { Command } from "commander";
|
||||
import { pluginCommand } from "../src/commands/plugin.js";
|
||||
import { DaemonClient } from "../src/client.js";
|
||||
@@ -394,4 +397,246 @@ describe("rig plugin CLI (slice 3.4)", () => {
|
||||
expect(parsed).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// rig plugin list — filter-value validation (pre-close punch from
|
||||
// velocity-guard 3.4.A repair verdict)
|
||||
//
|
||||
// CLI-side validation so typos like --source rig-cwd don't look
|
||||
// filtered while the daemon silently returns all plugins.
|
||||
// ============================================================
|
||||
|
||||
describe("rig plugin list — filter validation", () => {
|
||||
it("rejects invalid --runtime value with clear error + non-zero exit", async () => {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand(runningDeps(port)));
|
||||
|
||||
const { errLogs, exitCode } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "list", "--runtime", "bogus"]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
const err = errLogs.join("\n");
|
||||
expect(err).toMatch(/--runtime/);
|
||||
expect(err).toMatch(/bogus/);
|
||||
// Should name the allowed values so operator can correct
|
||||
expect(err.toLowerCase()).toMatch(/claude|codex/);
|
||||
});
|
||||
|
||||
it("rejects invalid --source value (rig-cwd typo case) with clear error + non-zero exit", async () => {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand(runningDeps(port)));
|
||||
|
||||
const { errLogs, exitCode } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "list", "--source", "rig-cwd"]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
const err = errLogs.join("\n");
|
||||
expect(err).toMatch(/--source/);
|
||||
expect(err).toMatch(/rig-cwd/);
|
||||
// Should name the allowed values
|
||||
expect(err).toMatch(/vendored|claude-cache|codex-cache/);
|
||||
});
|
||||
|
||||
it("accepts valid --runtime + --source values (regression: still passes through correctly)", async () => {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand(runningDeps(port)));
|
||||
|
||||
const { exitCode } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "list", "--runtime", "claude", "--source", "vendored", "--json"]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBeUndefined();
|
||||
expect(lastQuery.runtime).toBe("claude");
|
||||
expect(lastQuery.source).toBe("vendored");
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// rig plugin validate <path> (HG-4.4)
|
||||
//
|
||||
// Local file inspection — does NOT require daemon. Validates:
|
||||
// - .claude-plugin/plugin.json + .codex-plugin/plugin.json shape
|
||||
// (name + version required; description required on Codex)
|
||||
// - At least one manifest dir is present
|
||||
// - Each skills/<id>/SKILL.md has frontmatter with name (non-empty)
|
||||
// + description (non-empty, ≤1024 chars per agentskills.io)
|
||||
//
|
||||
// Output: error list; exit code 0 when valid, 1 when any errors.
|
||||
// ============================================================
|
||||
|
||||
describe("rig plugin validate <path>", () => {
|
||||
let tmpRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpRoot = mkdtempSync(join(tmpdir(), "plugin-validate-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tmpRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeManifest(relativePath: string, contents: Record<string, unknown>) {
|
||||
const fullPath = join(tmpRoot, relativePath);
|
||||
mkdirSync(dirname(fullPath), { recursive: true });
|
||||
writeFileSync(fullPath, JSON.stringify(contents, null, 2));
|
||||
}
|
||||
|
||||
function writeSkill(skillId: string, frontmatter: string, body = "# body\n") {
|
||||
const skillPath = join(tmpRoot, "skills", skillId, "SKILL.md");
|
||||
mkdirSync(dirname(skillPath), { recursive: true });
|
||||
writeFileSync(skillPath, `---\n${frontmatter}\n---\n\n${body}`);
|
||||
}
|
||||
|
||||
it("valid dual-manifest plugin tree passes validation (exit 0)", async () => {
|
||||
writeManifest(".claude-plugin/plugin.json", {
|
||||
name: "test-plugin", version: "1.0.0", description: "Test",
|
||||
});
|
||||
writeManifest(".codex-plugin/plugin.json", {
|
||||
name: "test-plugin", version: "1.0.0", description: "Test",
|
||||
});
|
||||
writeSkill("foo", "name: foo\ndescription: A foo skill");
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand());
|
||||
|
||||
const { exitCode, logs } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "validate", tmpRoot]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBeUndefined();
|
||||
expect(logs.join("\n").toLowerCase()).toMatch(/valid|ok|pass/);
|
||||
});
|
||||
|
||||
it("plugin with no manifest dirs at all FAILS validation", async () => {
|
||||
writeSkill("foo", "name: foo\ndescription: A foo skill");
|
||||
// No .claude-plugin/ + no .codex-plugin/
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand());
|
||||
|
||||
const { exitCode, errLogs } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "validate", tmpRoot]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(errLogs.join("\n").toLowerCase()).toMatch(/manifest|plugin\.json/);
|
||||
});
|
||||
|
||||
it("Codex manifest missing description FAILS (Codex requires description)", async () => {
|
||||
writeManifest(".codex-plugin/plugin.json", {
|
||||
name: "test-plugin", version: "1.0.0",
|
||||
// description MISSING
|
||||
});
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand());
|
||||
|
||||
const { exitCode, errLogs } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "validate", tmpRoot]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(errLogs.join("\n").toLowerCase()).toMatch(/codex.*description|description.*codex/);
|
||||
});
|
||||
|
||||
it("manifest missing required name FAILS", async () => {
|
||||
writeManifest(".claude-plugin/plugin.json", {
|
||||
version: "1.0.0", description: "Test",
|
||||
// name MISSING
|
||||
});
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand());
|
||||
|
||||
const { exitCode, errLogs } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "validate", tmpRoot]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(errLogs.join("\n").toLowerCase()).toMatch(/name/);
|
||||
});
|
||||
|
||||
it("skill missing frontmatter name FAILS", async () => {
|
||||
writeManifest(".claude-plugin/plugin.json", {
|
||||
name: "test-plugin", version: "1.0.0",
|
||||
});
|
||||
writeSkill("bad-skill", "description: a description but no name");
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand());
|
||||
|
||||
const { exitCode, errLogs } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "validate", tmpRoot]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
const err = errLogs.join("\n").toLowerCase();
|
||||
expect(err).toMatch(/bad-skill/);
|
||||
expect(err).toMatch(/name/);
|
||||
});
|
||||
|
||||
it("skill frontmatter description >1024 chars FAILS (agentskills.io limit)", async () => {
|
||||
writeManifest(".claude-plugin/plugin.json", {
|
||||
name: "test-plugin", version: "1.0.0",
|
||||
});
|
||||
const longDescription = "x".repeat(1100);
|
||||
writeSkill("verbose", `name: verbose\ndescription: ${longDescription}`);
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand());
|
||||
|
||||
const { exitCode, errLogs } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "validate", tmpRoot]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
const err = errLogs.join("\n").toLowerCase();
|
||||
expect(err).toMatch(/verbose/);
|
||||
expect(err).toMatch(/1024|description.*length|too long/);
|
||||
});
|
||||
|
||||
it("--json output returns structured { valid, errors } shape", async () => {
|
||||
writeManifest(".claude-plugin/plugin.json", {
|
||||
name: "test-plugin", version: "1.0.0",
|
||||
});
|
||||
writeSkill("foo", "name: foo\ndescription: A foo skill");
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand());
|
||||
|
||||
const { exitCode, logs } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "validate", tmpRoot, "--json"]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBeUndefined();
|
||||
const parsed = JSON.parse(logs.join("\n")) as { valid: boolean; errors: string[] };
|
||||
expect(parsed.valid).toBe(true);
|
||||
expect(parsed.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("non-existent path FAILS with clear error", async () => {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.addCommand(pluginCommand());
|
||||
|
||||
const { exitCode, errLogs } = await captureLogs(async () => {
|
||||
await program.parseAsync(["node", "rig", "plugin", "validate", join(tmpRoot, "nonexistent")]);
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(errLogs.join("\n").toLowerCase()).toMatch(/not found|does not exist|enoent/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user