feat(3255): add --json-errors structured error mode to gsd-tools (#3304)
* test(3255): add red/green tests for --json-errors structured error mode
Ten tests covering the --json-errors mode contract:
- Unknown command → sdk_unknown_command
- Dotted unknown command → sdk_unknown_command
- Missing --pick value → usage
- Config key not found → config_key_not_found
- Unknown subcommand → sdk_unknown_command
- GSD_JSON_ERRORS=1 env var activation
- Successful command unaffected
- Stable error shape ({ok, reason, message})
- Single error line per invocation
- Unknown flag → usage
All assertions use JSON.parse on stderr captures, never .includes() on
text (#2974 / CONTRIBUTING.md "Prohibited: Raw Text Matching" rule).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(3255): add typed ERROR_REASON codes and GSD_JSON_ERRORS env var support
- Destructure ERROR_REASON from core in gsd-tools.cjs
- Add GSD_JSON_ERRORS=1 env var as alternative to --json-errors CLI flag
- Pass ERROR_REASON.SDK_UNKNOWN_COMMAND to unknown top-level command default path
- Pass ERROR_REASON.SDK_UNKNOWN_COMMAND to unknown intel subcommand path
- Pass ERROR_REASON.USAGE to --pick missing value error path
- Pass ERROR_REASON.USAGE to --version flag rejection path
All ten tests in feat-3255-json-errors-mode.test.cjs pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(3255): add json-errors taxonomy doc, changeset, and CHANGELOG entry
- docs/json-errors.md: full error code taxonomy, wire format spec, and
test-authoring guidelines for the --json-errors mode
- .changeset/gentle-tigers-roar.md: changeset fragment (pr will be updated
after PR is opened)
- CHANGELOG.md: Unreleased → Added entry for the new structured error mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: update changeset PR number to 3304
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(gsd-tools): document --json-errors in usage/help text (#3255)
Add [--json-errors] to the TOP_LEVEL_USAGE synopsis line and introduce a
"Global flags:" section describing all four global flags (--raw, --pick,
--cwd, --ws) plus --json-errors with its GSD_JSON_ERRORS=1 env-var
alternative, so operators can discover the flag via `gsd-tools --help`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: drop redundant CHANGELOG.md edit (use .changeset/ fragment per CONTRIBUTING.md)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/gentle-tigers-roar.md
Normal file
5
.changeset/gentle-tigers-roar.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 3304
|
||||
---
|
||||
gsd-tools --json-errors mode: all error paths now emit structured JSON ({ok, reason, message}) when invoked with --json-errors or GSD_JSON_ERRORS=1 — tests can assert on typed reason codes instead of grepping stderr text
|
||||
123
docs/json-errors.md
Normal file
123
docs/json-errors.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# JSON Error Mode — `gsd-tools` Structured Errors
|
||||
|
||||
## Overview
|
||||
|
||||
`gsd-tools` supports a **JSON error mode** that emits all errors as structured
|
||||
JSON objects on stderr instead of free-form text. This is the recommended
|
||||
surface for tests and tooling that need to assert on error types without
|
||||
grepping raw text (see `CONTRIBUTING.md` — "Prohibited: Raw Text Matching on
|
||||
Test Outputs").
|
||||
|
||||
## Activating
|
||||
|
||||
Either flag or env var activates the mode:
|
||||
|
||||
```bash
|
||||
# Flag (preferred in test code):
|
||||
node gsd-tools.cjs --json-errors <command> [args]
|
||||
|
||||
# Env var (preferred for shell wrappers and CI):
|
||||
GSD_JSON_ERRORS=1 node gsd-tools.cjs <command> [args]
|
||||
```
|
||||
|
||||
## Wire format
|
||||
|
||||
On any error, exactly one JSON line is written to **stderr** and the process
|
||||
exits with code 1:
|
||||
|
||||
```json
|
||||
{ "ok": false, "reason": "<error_code>", "message": "<human text>" }
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `ok` | `false` | Always `false` for error objects. |
|
||||
| `reason` | string | Typed reason code from the taxonomy below. |
|
||||
| `message` | string | Human-readable description (may change; do not assert on it). |
|
||||
|
||||
## Error code taxonomy
|
||||
|
||||
Codes are frozen constants in `get-shit-done/bin/lib/core.cjs` under
|
||||
`ERROR_REASON`. Tests must assert on `reason` values (stable), not `message`
|
||||
text (unstable).
|
||||
|
||||
### Dispatch errors (gsd-tools routing layer)
|
||||
|
||||
| Code | When emitted |
|
||||
|------|-------------|
|
||||
| `sdk_unknown_command` | Unknown top-level command (`gsd-tools bogus-cmd`) |
|
||||
| `sdk_unknown_command` | Unknown dotted command (`gsd-tools foo.bar` where `foo` is not a known command) |
|
||||
| `sdk_unknown_command` | Unknown subcommand within a domain (e.g. `gsd-tools intel bogus-sub`) |
|
||||
| `sdk_missing_arg` | Required argument omitted by an SDK-level guard |
|
||||
| `sdk_fail_fast` | SDK fail-fast policy triggered |
|
||||
|
||||
### Usage / flag errors
|
||||
|
||||
| Code | When emitted |
|
||||
|------|-------------|
|
||||
| `usage` | `--pick` flag used without a following value |
|
||||
| `usage` | Version flag (`--version`, `-v`) which gsd-tools never accepts |
|
||||
| `usage` | Top-level no-args invocation (usage text) |
|
||||
|
||||
### Config errors (`config-get`, `config-set`, `config-ensure-section`)
|
||||
|
||||
| Code | When emitted |
|
||||
|------|-------------|
|
||||
| `config_key_not_found` | `config-get` for a key that is absent from the config file |
|
||||
| `config_no_file` | Config operation when `.planning/config.json` does not exist |
|
||||
| `config_parse_failed` | Config file exists but is not valid JSON |
|
||||
| `config_invalid_key` | `config-set` for a key outside the allowed whitelist |
|
||||
|
||||
### Phase / workflow errors
|
||||
|
||||
| Code | When emitted |
|
||||
|------|-------------|
|
||||
| `phase_not_found` | Phase directory lookup returns no match |
|
||||
| `summary_no_planning` | Summary operation when no `.planning/` directory exists |
|
||||
|
||||
### Graphify errors
|
||||
|
||||
| Code | When emitted |
|
||||
|------|-------------|
|
||||
| `graphify_no_graph` | Graphify query or diff when no graph has been built |
|
||||
| `graphify_invalid_query` | Graphify query with a malformed query string |
|
||||
|
||||
### Hook / security errors
|
||||
|
||||
| Code | When emitted |
|
||||
|------|-------------|
|
||||
| `hooks_opt_out` | Hooks are disabled via opt-out config |
|
||||
| `security_scan_failed` | Security scan produced a finding that blocks the operation |
|
||||
|
||||
### Fallback
|
||||
|
||||
| Code | When emitted |
|
||||
|------|-------------|
|
||||
| `unknown` | All other errors without a specific reason code assigned |
|
||||
|
||||
## Writing tests
|
||||
|
||||
Always parse stderr with `JSON.parse` and assert on typed fields. Never use
|
||||
`.includes()`, `.match()`, or regex on the raw error string.
|
||||
|
||||
```js
|
||||
// CORRECT: parse then assert on typed field
|
||||
const result = runGsdTools(['--json-errors', 'bogus-command'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
const err = JSON.parse(result.error);
|
||||
assert.strictEqual(err.ok, false);
|
||||
assert.strictEqual(err.reason, 'sdk_unknown_command');
|
||||
|
||||
// WRONG: text matching (banned by lint-no-source-grep policy)
|
||||
// assert.ok(result.error.includes('Unknown command'));
|
||||
```
|
||||
|
||||
## Adding a new error code
|
||||
|
||||
1. Add the constant to `ERROR_REASON` in
|
||||
`get-shit-done/bin/lib/core.cjs` (snake\_case, prefixed by subsystem).
|
||||
2. Pass it as the second argument to `error()` at the call site.
|
||||
3. Add a row to this document.
|
||||
4. Add a test asserting the new `reason` code via `JSON.parse`.
|
||||
@@ -172,7 +172,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const core = require('./lib/core.cjs');
|
||||
const { error, findProjectRoot } = core;
|
||||
const { error, findProjectRoot, ERROR_REASON } = core;
|
||||
const { getActiveWorkstream } = require('./lib/planning-workspace.cjs');
|
||||
const { resolveActiveWorkstream, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs');
|
||||
const state = require('./lib/state.cjs');
|
||||
@@ -304,6 +304,8 @@ async function main() {
|
||||
if (jsonErrorsIdx !== -1) {
|
||||
core.setJsonErrorMode(true);
|
||||
args.splice(jsonErrorsIdx, 1);
|
||||
} else if (process.env.GSD_JSON_ERRORS === '1') {
|
||||
core.setJsonErrorMode(true);
|
||||
}
|
||||
|
||||
// --pick <name>: extract a single field from JSON output (replaces jq dependency).
|
||||
@@ -313,7 +315,7 @@ async function main() {
|
||||
let pickField = null;
|
||||
if (pickIdx !== -1) {
|
||||
pickField = args[pickIdx + 1];
|
||||
if (!pickField || pickField.startsWith('--')) error('Missing value for --pick');
|
||||
if (!pickField || pickField.startsWith('--')) error('Missing value for --pick', ERROR_REASON.USAGE);
|
||||
args.splice(pickIdx, 2);
|
||||
}
|
||||
|
||||
@@ -359,7 +361,7 @@ async function main() {
|
||||
// supported by the dispatcher so `--help` is actually useful for
|
||||
// discovery; previously it was a partial subset that didn't include
|
||||
// phase / roadmap / milestone / progress / etc.
|
||||
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>]\n' +
|
||||
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
||||
'Commands: agent-skills, audit-open, audit-uat, check-commit, commit, commit-to-subrepo, ' +
|
||||
'config-ensure-section, config-get, config-new-project, config-path, config-set, ' +
|
||||
'current-timestamp, detect-custom-files, docs-init, extract-messages, find-phase, ' +
|
||||
@@ -368,6 +370,12 @@ async function main() {
|
||||
'learnings, list-todos, milestone, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
||||
'profile-sample, progress, requirements, resolve-model, roadmap, scaffold, state, ' +
|
||||
'template, validate, verify, verify-path-exists, verify-summary, workstream\n\n' +
|
||||
'Global flags:\n' +
|
||||
' --raw Emit raw output without post-processing\n' +
|
||||
' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
|
||||
' --cwd <path> Override working directory for project-root resolution\n' +
|
||||
' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
|
||||
' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n\n' +
|
||||
'For command-specific argument requirements, invoke the command without args ' +
|
||||
'(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
|
||||
|
||||
@@ -396,7 +404,7 @@ async function main() {
|
||||
const NEVER_VALID_FLAGS = new Set(['--version', '-v']);
|
||||
for (const arg of args) {
|
||||
if (NEVER_VALID_FLAGS.has(arg)) {
|
||||
error(`Unknown flag: ${arg}\ngsd-tools does not accept version flags. Run "gsd-tools" with no arguments for usage.`);
|
||||
error(`Unknown flag: ${arg}\ngsd-tools does not accept version flags. Run "gsd-tools" with no arguments for usage.`, ERROR_REASON.USAGE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1011,7 +1019,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
const planningDir = path.join(cwd, '.planning');
|
||||
core.output(intel.intelUpdate(planningDir), raw);
|
||||
} else {
|
||||
error('Unknown intel subcommand. Available: query, status, update, diff, snapshot, patch-meta, validate, extract-exports');
|
||||
error('Unknown intel subcommand. Available: query, status, update, diff, snapshot, patch-meta, validate, extract-exports', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1192,7 +1200,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
const rest = originalCommand.slice(dotIdx + 1);
|
||||
suggestion = ` — did you mean: "${head} ${rest}"?`;
|
||||
}
|
||||
error(`Unknown command: ${command}${suggestion}`);
|
||||
error(`Unknown command: ${command}${suggestion}`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
209
tests/feat-3255-json-errors-mode.test.cjs
Normal file
209
tests/feat-3255-json-errors-mode.test.cjs
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Tests for the --json-errors mode added in #3255.
|
||||
*
|
||||
* When gsd-tools is invoked with --json-errors, all error() calls emit a
|
||||
* structured JSON object to stderr:
|
||||
*
|
||||
* { ok: false, reason: "<error_code>", message: "<human text>" }
|
||||
*
|
||||
* This lets tests assert on typed reason codes instead of grepping free-form
|
||||
* stderr text. All assertions below parse the captured stderr via JSON.parse
|
||||
* and inspect typed fields — never result.error.includes() (#2974 / k001).
|
||||
*
|
||||
* Covered error paths (representative set, each exercises a different branch):
|
||||
* 1. Unknown top-level command → reason: "sdk_unknown_command"
|
||||
* 2. Unknown dotted command → reason: "sdk_unknown_command"
|
||||
* 3. Missing required argument → reason: "usage" (--pick without value)
|
||||
* 4. Config key not found → reason: "config_key_not_found"
|
||||
* 5. Unknown subcommand → reason: "sdk_unknown_command"
|
||||
* 6. GSD_JSON_ERRORS=1 env var → same structured output without --flag
|
||||
* 7. Successful command unaffected
|
||||
* 8. Error object shape is stable ({ok, reason, message})
|
||||
* 9. Single error line per invocation
|
||||
* 10. Unknown flag → reason: "usage"
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
// Helper: run gsd-tools with --json-errors and parse the structured stderr.
|
||||
// Returns the parsed object, or throws if stderr is not valid JSON.
|
||||
function runJsonErrors(args, tmpDir, env = {}) {
|
||||
const allArgs = ['--json-errors', ...args];
|
||||
const result = runGsdTools(allArgs, tmpDir, env);
|
||||
// Must have failed
|
||||
assert.strictEqual(result.success, false,
|
||||
`Expected failure with --json-errors for args: ${args.join(' ')}\nstdout: ${result.output}\nstderr: ${result.error}`);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(result.error);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`--json-errors must emit valid JSON on stderr.\n` +
|
||||
`Args: ${args.join(' ')}\n` +
|
||||
`stderr: ${result.error}\n` +
|
||||
`parse error: ${e.message}`
|
||||
);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
describe('feat #3255: --json-errors mode emits structured error objects', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
// ── 1. Unknown top-level command ─────────────────────────────────────────
|
||||
|
||||
test('unknown top-level command emits { ok: false, reason: "sdk_unknown_command" }', () => {
|
||||
const parsed = runJsonErrors(['totally-unknown-command-xyzzy'], tmpDir);
|
||||
|
||||
assert.strictEqual(parsed.ok, false,
|
||||
'error object must have ok: false');
|
||||
assert.strictEqual(parsed.reason, 'sdk_unknown_command',
|
||||
`reason must be "sdk_unknown_command", got: ${parsed.reason}`);
|
||||
assert.ok(typeof parsed.message === 'string' && parsed.message.length > 0,
|
||||
'message must be a non-empty string');
|
||||
});
|
||||
|
||||
// ── 2. Unknown dotted command ────────────────────────────────────────────
|
||||
|
||||
test('unknown dotted command (foo.bar) emits { ok: false, reason: "sdk_unknown_command" }', () => {
|
||||
const parsed = runJsonErrors(['foo.bar'], tmpDir);
|
||||
|
||||
assert.strictEqual(parsed.ok, false,
|
||||
'error object must have ok: false');
|
||||
assert.strictEqual(parsed.reason, 'sdk_unknown_command',
|
||||
`dotted unknown command reason must be "sdk_unknown_command", got: ${parsed.reason}`);
|
||||
assert.ok(typeof parsed.message === 'string' && parsed.message.length > 0,
|
||||
'message must be a non-empty string');
|
||||
});
|
||||
|
||||
// ── 3. Missing --pick value ───────────────────────────────────────────────
|
||||
|
||||
test('--pick without value emits { ok: false, reason: "usage" }', () => {
|
||||
const parsed = runJsonErrors(['generate-slug', 'test-text', '--pick'], tmpDir);
|
||||
|
||||
assert.strictEqual(parsed.ok, false,
|
||||
'error object must have ok: false');
|
||||
assert.strictEqual(parsed.reason, 'usage',
|
||||
`missing --pick value reason must be "usage", got: ${parsed.reason}`);
|
||||
assert.ok(typeof parsed.message === 'string' && parsed.message.length > 0,
|
||||
'message must be a non-empty string');
|
||||
});
|
||||
|
||||
// ── 4. Config key not found ───────────────────────────────────────────────
|
||||
|
||||
test('config-get for absent key emits { ok: false, reason: "config_key_not_found" }', () => {
|
||||
// Initialise config.json first so we reach the "key not found" branch
|
||||
// rather than the "no config.json" branch.
|
||||
runGsdTools(['config-ensure-section'], tmpDir);
|
||||
|
||||
const parsed = runJsonErrors(['config-get', 'nonexistent_config_key_xyzzy'], tmpDir);
|
||||
|
||||
assert.strictEqual(parsed.ok, false,
|
||||
'error object must have ok: false');
|
||||
assert.strictEqual(parsed.reason, 'config_key_not_found',
|
||||
`reason must be "config_key_not_found", got: ${parsed.reason}`);
|
||||
assert.ok(typeof parsed.message === 'string' && parsed.message.length > 0,
|
||||
'message must be a non-empty string');
|
||||
});
|
||||
|
||||
// ── 5. Unknown subcommand within a domain ────────────────────────────────
|
||||
|
||||
test('unknown intel subcommand emits { ok: false, reason: "sdk_unknown_command" }', () => {
|
||||
const parsed = runJsonErrors(['intel', 'bogus-subcommand-xyzzy'], tmpDir);
|
||||
|
||||
assert.strictEqual(parsed.ok, false,
|
||||
'error object must have ok: false');
|
||||
assert.strictEqual(parsed.reason, 'sdk_unknown_command',
|
||||
`unknown subcommand reason must be "sdk_unknown_command", got: ${parsed.reason}`);
|
||||
assert.ok(typeof parsed.message === 'string' && parsed.message.length > 0,
|
||||
'message must be a non-empty string');
|
||||
});
|
||||
|
||||
// ── 6. GSD_JSON_ERRORS=1 env var activates structured mode ───────────────
|
||||
|
||||
test('GSD_JSON_ERRORS=1 env var produces same structured error as --json-errors flag', () => {
|
||||
// Run with env var instead of --json-errors flag
|
||||
const result = runGsdTools(
|
||||
['totally-unknown-command-xyzzy'],
|
||||
tmpDir,
|
||||
{ GSD_JSON_ERRORS: '1' }
|
||||
);
|
||||
assert.strictEqual(result.success, false,
|
||||
'command must fail');
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(result.error);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`GSD_JSON_ERRORS=1 must emit valid JSON on stderr.\n` +
|
||||
`stderr: ${result.error}\n` +
|
||||
`parse error: ${e.message}`
|
||||
);
|
||||
}
|
||||
assert.strictEqual(parsed.ok, false,
|
||||
'error object must have ok: false');
|
||||
assert.strictEqual(parsed.reason, 'sdk_unknown_command',
|
||||
`reason must be "sdk_unknown_command", got: ${parsed.reason}`);
|
||||
});
|
||||
|
||||
// ── 7. Successful commands are unaffected by --json-errors ───────────────
|
||||
|
||||
test('successful command with --json-errors flag still succeeds normally', () => {
|
||||
const result = runGsdTools(
|
||||
['--json-errors', 'generate-slug', 'hello-world'],
|
||||
tmpDir
|
||||
);
|
||||
assert.strictEqual(result.success, true,
|
||||
`Successful command must not be broken by --json-errors flag.\nstderr: ${result.error}`);
|
||||
assert.ok(result.output.length > 0,
|
||||
'stdout must be non-empty for successful generate-slug');
|
||||
});
|
||||
|
||||
// ── 8. Error object shape is stable (no extra top-level keys) ────────────
|
||||
|
||||
test('error object contains exactly {ok, reason, message} — no extra keys', () => {
|
||||
const parsed = runJsonErrors(['totally-unknown-command-xyzzy'], tmpDir);
|
||||
|
||||
const keys = Object.keys(parsed).sort();
|
||||
assert.deepStrictEqual(keys, ['message', 'ok', 'reason'],
|
||||
`error object must have exactly {ok, reason, message}. Got keys: ${keys.join(', ')}`);
|
||||
});
|
||||
|
||||
// ── 9. Multiple errors in one session: only the first error is emitted ───
|
||||
|
||||
test('only one error JSON line is emitted per invocation (process exits on first error)', () => {
|
||||
const result = runGsdTools(
|
||||
['--json-errors', 'totally-unknown-command-xyzzy'],
|
||||
tmpDir
|
||||
);
|
||||
assert.strictEqual(result.success, false, 'must fail');
|
||||
const lines = result.error.trim().split('\n').filter(l => l.length > 0);
|
||||
assert.strictEqual(lines.length, 1,
|
||||
`stderr must contain exactly one JSON line, got ${lines.length}:\n${result.error}`);
|
||||
// Also verify the single line is valid JSON
|
||||
const parsed = JSON.parse(lines[0]);
|
||||
assert.strictEqual(parsed.ok, false);
|
||||
});
|
||||
|
||||
// ── 10. Unknown flag emits { ok: false, reason: "usage" } ────────────────
|
||||
|
||||
test('unknown version flag emits { ok: false, reason: "usage" }', () => {
|
||||
const parsed = runJsonErrors(['--version', 'generate-slug', 'x'], tmpDir);
|
||||
|
||||
assert.strictEqual(parsed.ok, false, 'error object must have ok: false');
|
||||
assert.strictEqual(parsed.reason, 'usage',
|
||||
`--version flag reason must be "usage", got: ${parsed.reason}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user