From 6e31f7b9d814f3dfd79c9035ad27e82f47684810 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 10:20:47 -0400 Subject: [PATCH 1/7] fix(cli): align commands/ error messages with 4-ingredient strategy Rewrites error messages across packages/cli/src/commands/ to follow the What / Where / Saw vs. wanted / Fix strategy from CLAUDE.md's new Error Messages section. Sources: - scan/cmd-scan-create.mts: 5 throws (ecosystems + 4 numeric flags) - scan/cmd-scan-reach.mts: 4 throws (ecosystems + 3 numeric flags) - scan/cmd-scan-list.mts: 2 throws (--page, --per-page) - organization/cmd-organization-dependencies.mts: 2 throws (--limit, --offset) - audit-log/cmd-audit-log.mts: 2 throws (--page, --per-page) - threat-feed/cmd-threat-feed.mts: 1 throw (--per-page) - fix/cmd-fix.mts: 1 logger.fail (--ecosystems) - ask/cmd-ask.mts: 1 InputError (missing QUERY) - login/cmd-login.mts: 1 InputError (non-interactive TTY) - wrapper/add-socket-wrapper.mts: 1 throw (fs.appendFile failure) - wrapper/postinstall-wrapper.mts: 1 throw (nested wrapper setup) - manifest/convert-gradle-to-maven.mts: 1 throw (spawn returned no output) - fix/coana-fix.mts: 1 throw (coana returned non-array JSON) - config/handle-config-set.mts: 1 throw (missing VALUE) Tests updated to match new substrings (regex patterns for easier future evolution). All throws previously typed as plain Error that represent user input validation have been converted to InputError, following the codebase convention. Follows strategy landed in #1254. --- packages/cli/src/commands/ask/cmd-ask.mts | 2 +- .../src/commands/audit-log/cmd-audit-log.mts | 9 ++++++-- .../src/commands/config/handle-config-set.mts | 5 ++++- packages/cli/src/commands/fix/cmd-fix.mts | 2 +- packages/cli/src/commands/fix/coana-fix.mts | 4 +++- packages/cli/src/commands/login/cmd-login.mts | 2 +- .../manifest/convert-gradle-to-maven.mts | 4 +++- .../cmd-organization-dependencies.mts | 9 ++++++-- .../cli/src/commands/scan/cmd-scan-create.mts | 21 +++++++++++-------- .../cli/src/commands/scan/cmd-scan-list.mts | 9 ++++++-- .../cli/src/commands/scan/cmd-scan-reach.mts | 17 ++++++++------- .../commands/threat-feed/cmd-threat-feed.mts | 5 ++++- .../commands/wrapper/add-socket-wrapper.mts | 7 ++++++- .../commands/wrapper/postinstall-wrapper.mts | 6 +++--- .../cli/test/integration/cli/cmd-ask.test.mts | 2 +- .../integration/cli/cmd-scan-create.test.mts | 4 ++-- .../cli/cmd-scan-reach-dry-run.test.mts | 4 ++-- .../test/unit/commands/ask/cmd-ask.test.mts | 2 +- .../commands/audit-log/cmd-audit-log.test.mts | 8 +++---- .../test/unit/commands/fix/cmd-fix.test.mts | 2 +- .../unit/commands/login/cmd-login.test.mts | 2 +- .../cmd-organization-dependencies.test.mts | 16 ++++++++++---- .../commands/scan/cmd-scan-create.test.mts | 8 +++---- .../commands/scan/cmd-scan-reach.test.mts | 8 +++---- .../wrapper/add-socket-wrapper.test.mts | 2 +- .../wrapper/postinstall-wrapper.test.mts | 8 ++++++- 26 files changed, 108 insertions(+), 60 deletions(-) diff --git a/packages/cli/src/commands/ask/cmd-ask.mts b/packages/cli/src/commands/ask/cmd-ask.mts index 67194fbff..a5ede7672 100644 --- a/packages/cli/src/commands/ask/cmd-ask.mts +++ b/packages/cli/src/commands/ask/cmd-ask.mts @@ -83,7 +83,7 @@ async function run( if (!query) { throw new InputError( - 'Please provide a question.\n\nExample: socket ask "scan for vulnerabilities"', + 'socket ask requires a QUERY positional argument; pass a question like `socket ask "scan for vulnerabilities"`', ) } diff --git a/packages/cli/src/commands/audit-log/cmd-audit-log.mts b/packages/cli/src/commands/audit-log/cmd-audit-log.mts index 6d9d3cd7c..a5b1fea11 100644 --- a/packages/cli/src/commands/audit-log/cmd-audit-log.mts +++ b/packages/cli/src/commands/audit-log/cmd-audit-log.mts @@ -1,6 +1,7 @@ import { handleAuditLog } from './handle-audit-log.mts' import { FLAG_JSON, FLAG_MARKDOWN } from '../../constants/cli.mts' import { outputDryRunFetch } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { V1_MIGRATION_GUIDE_URL } from '../../constants/socket.mjs' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' @@ -191,10 +192,14 @@ async function run( } if (Number.isNaN(validatedPage) || validatedPage < 0) { - throw new Error(`Invalid value for --page: ${page}`) + throw new InputError( + `--page must be a non-negative integer (saw: "${page}"); pass a number like --page=1`, + ) } if (Number.isNaN(validatedPerPage) || validatedPerPage < 0) { - throw new Error(`Invalid value for --per-page: ${perPage}`) + throw new InputError( + `--per-page must be a non-negative integer (saw: "${perPage}"); pass a number like --per-page=30`, + ) } await handleAuditLog({ diff --git a/packages/cli/src/commands/config/handle-config-set.mts b/packages/cli/src/commands/config/handle-config-set.mts index 3349cb016..2eded70a7 100644 --- a/packages/cli/src/commands/config/handle-config-set.mts +++ b/packages/cli/src/commands/config/handle-config-set.mts @@ -2,6 +2,7 @@ import { debug, debugDir } from '@socketsecurity/lib/debug' import { outputConfigSet } from './output-config-set.mts' import { updateConfigValue } from '../../utils/config.mts' +import { InputError } from '../../utils/error/errors.mts' import type { OutputKind } from '../../types.mts' import type { LocalConfig } from '../../utils/config.mts' @@ -16,7 +17,9 @@ export async function handleConfigSet({ outputKind: OutputKind }) { if (value === undefined) { - throw new Error('Value is required for config set') + throw new InputError( + `socket config set ${key} requires a VALUE argument; pass the value as the second positional (e.g. \`socket config set ${key} my-value\`)`, + ) } debug(`Setting config ${key} = ${value}`) diff --git a/packages/cli/src/commands/fix/cmd-fix.mts b/packages/cli/src/commands/fix/cmd-fix.mts index bd1ebb033..9d862a2e0 100644 --- a/packages/cli/src/commands/fix/cmd-fix.mts +++ b/packages/cli/src/commands/fix/cmd-fix.mts @@ -371,7 +371,7 @@ async function run( for (const ecosystem of ecosystemsRaw) { if (!validEcosystemChoices.includes(ecosystem)) { logger.fail( - `Invalid ecosystem: "${ecosystem}". Valid values are: ${joinAnd(validEcosystemChoices)}`, + `--ecosystems must be one of: ${joinAnd(validEcosystemChoices)} (saw: "${ecosystem}"); pass a supported ecosystem like --ecosystems=${validEcosystemChoices[0]}`, ) process.exitCode = 1 return diff --git a/packages/cli/src/commands/fix/coana-fix.mts b/packages/cli/src/commands/fix/coana-fix.mts index 2f28c4833..613b0a576 100644 --- a/packages/cli/src/commands/fix/coana-fix.mts +++ b/packages/cli/src/commands/fix/coana-fix.mts @@ -340,7 +340,9 @@ export async function coanaFix( if (ghsaIdsRaw && ghsaIdsRaw.trim()) { const parsed = JSON.parse(ghsaIdsRaw) if (!Array.isArray(parsed)) { - throw new Error('Expected array of GHSA IDs from coana output') + throw new Error( + `coana find-vulnerabilities returned non-array JSON on last line (got: ${typeof parsed}); expected an array of GHSA ID strings`, + ) } discoveredIds.push(...parsed) } diff --git a/packages/cli/src/commands/login/cmd-login.mts b/packages/cli/src/commands/login/cmd-login.mts index 3b13d2c36..6ddaaf934 100644 --- a/packages/cli/src/commands/login/cmd-login.mts +++ b/packages/cli/src/commands/login/cmd-login.mts @@ -97,7 +97,7 @@ async function run( if (!isInteractive()) { throw new InputError( - 'Cannot prompt for credentials in a non-interactive shell. Use SOCKET_CLI_API_TOKEN environment variable instead', + 'socket login needs an interactive TTY to prompt for credentials (stdin/stdout is not a TTY); set SOCKET_CLI_API_TOKEN in the environment instead', ) } diff --git a/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts b/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts index 319b41feb..2d4577a46 100644 --- a/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts +++ b/packages/cli/src/commands/manifest/convert-gradle-to-maven.mts @@ -178,7 +178,9 @@ async function execGradleWithSpinner( }) if (!output) { - throw new Error(`Failed to execute gradle: ${bin}`) + throw new Error( + `spawn returned no output for gradle (bin: ${bin}); check that the gradlew wrapper is executable and re-run with --verbose`, + ) } pass = true diff --git a/packages/cli/src/commands/organization/cmd-organization-dependencies.mts b/packages/cli/src/commands/organization/cmd-organization-dependencies.mts index d75704baf..53568f58b 100644 --- a/packages/cli/src/commands/organization/cmd-organization-dependencies.mts +++ b/packages/cli/src/commands/organization/cmd-organization-dependencies.mts @@ -1,6 +1,7 @@ import { handleDependencies } from './handle-dependencies.mts' import { FLAG_JSON, FLAG_MARKDOWN } from '../../constants/cli.mts' import { outputDryRunFetch } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { @@ -115,10 +116,14 @@ async function run( } if (Number.isNaN(validatedLimit) || validatedLimit < 0) { - throw new Error(`Invalid value for --limit: ${limit}`) + throw new InputError( + `--limit must be a non-negative integer (saw: "${limit}"); pass a number like --limit=50`, + ) } if (Number.isNaN(validatedOffset) || validatedOffset < 0) { - throw new Error(`Invalid value for --offset: ${offset}`) + throw new InputError( + `--offset must be a non-negative integer (saw: "${offset}"); pass a number like --offset=0`, + ) } await handleDependencies({ diff --git a/packages/cli/src/commands/scan/cmd-scan-create.mts b/packages/cli/src/commands/scan/cmd-scan-create.mts index fc33239e1..437b7fd7e 100644 --- a/packages/cli/src/commands/scan/cmd-scan-create.mts +++ b/packages/cli/src/commands/scan/cmd-scan-create.mts @@ -13,6 +13,7 @@ import { suggestTarget } from './suggest_target.mts' import { validateReachabilityTarget } from './validate-reachability-target.mts' import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { outputDryRunUpload } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mts' import { getEcosystemChoicesForMeow } from '../../utils/ecosystem/types.mts' @@ -441,8 +442,8 @@ async function run( const validEcosystems = getEcosystemChoicesForMeow() for (const ecosystem of reachEcosystemsRaw) { if (!validEcosystems.includes(ecosystem)) { - throw new Error( - `Invalid ecosystem: "${ecosystem}". Valid values are: ${joinAnd(validEcosystems)}`, + throw new InputError( + `--reach-ecosystems must be one of: ${joinAnd(validEcosystems)} (saw: "${ecosystem}"); pass a supported ecosystem like --reach-ecosystems=${validEcosystems[0]}`, ) } reachEcosystems.push(ecosystem as PURL_Type) @@ -727,7 +728,9 @@ async function run( // Validate numeric flag conversions. const validatedPullRequest = Number(pullRequest) if (pullRequest !== undefined && Number.isNaN(validatedPullRequest)) { - throw new Error(`Invalid number value for --pull-request: ${pullRequest}`) + throw new InputError( + `--pull-request must be a non-negative integer (saw: "${pullRequest}"); pass a number like --pull-request=42`, + ) } const validatedReachAnalysisMemoryLimit = Number(reachAnalysisMemoryLimit) @@ -735,8 +738,8 @@ async function run( reachAnalysisMemoryLimit !== undefined && Number.isNaN(validatedReachAnalysisMemoryLimit) ) { - throw new Error( - `Invalid number value for --reach-analysis-memory-limit: ${reachAnalysisMemoryLimit}`, + throw new InputError( + `--reach-analysis-memory-limit must be a number of megabytes (saw: "${reachAnalysisMemoryLimit}"); pass an integer like --reach-analysis-memory-limit=4096`, ) } @@ -745,8 +748,8 @@ async function run( reachAnalysisTimeout !== undefined && Number.isNaN(validatedReachAnalysisTimeout) ) { - throw new Error( - `Invalid number value for --reach-analysis-timeout: ${reachAnalysisTimeout}`, + throw new InputError( + `--reach-analysis-timeout must be a number of seconds (saw: "${reachAnalysisTimeout}"); pass an integer like --reach-analysis-timeout=300`, ) } @@ -755,8 +758,8 @@ async function run( reachConcurrency !== undefined && Number.isNaN(validatedReachConcurrency) ) { - throw new Error( - `Invalid number value for --reach-concurrency: ${reachConcurrency}`, + throw new InputError( + `--reach-concurrency must be a positive integer (saw: "${reachConcurrency}"); pass a number like --reach-concurrency=4`, ) } diff --git a/packages/cli/src/commands/scan/cmd-scan-list.mts b/packages/cli/src/commands/scan/cmd-scan-list.mts index 596bb51db..6147cc719 100644 --- a/packages/cli/src/commands/scan/cmd-scan-list.mts +++ b/packages/cli/src/commands/scan/cmd-scan-list.mts @@ -1,5 +1,6 @@ import { handleListScans } from './handle-list-scans.mts' import { outputDryRunFetch } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { V1_MIGRATION_GUIDE_URL } from '../../constants/socket.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' @@ -201,10 +202,14 @@ async function run( } if (Number.isNaN(validatedPage) || validatedPage < 1) { - throw new Error(`Invalid value for --page: ${cli.flags['page']}`) + throw new InputError( + `--page must be a positive integer (saw: "${cli.flags['page']}"); pass a number like --page=1`, + ) } if (Number.isNaN(validatedPerPage) || validatedPerPage < 1) { - throw new Error(`Invalid value for --per-page: ${cli.flags['perPage']}`) + throw new InputError( + `--per-page must be a positive integer (saw: "${cli.flags['perPage']}"); pass a number like --per-page=30`, + ) } await handleListScans({ diff --git a/packages/cli/src/commands/scan/cmd-scan-reach.mts b/packages/cli/src/commands/scan/cmd-scan-reach.mts index fc94b80e1..7c88c3f58 100644 --- a/packages/cli/src/commands/scan/cmd-scan-reach.mts +++ b/packages/cli/src/commands/scan/cmd-scan-reach.mts @@ -7,6 +7,7 @@ import { reachabilityFlags } from './reachability-flags.mts' import { suggestTarget } from './suggest_target.mts' import { validateReachabilityTarget } from './validate-reachability-target.mts' import { outputDryRunExecute } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mts' import { getEcosystemChoicesForMeow } from '../../utils/ecosystem/types.mts' @@ -171,8 +172,8 @@ async function run( const validEcosystems = getEcosystemChoicesForMeow() for (const ecosystem of reachEcosystemsRaw) { if (!validEcosystems.includes(ecosystem)) { - throw new Error( - `Invalid ecosystem: "${ecosystem}". Valid values are: ${joinAnd(validEcosystems)}`, + throw new InputError( + `--reach-ecosystems must be one of: ${joinAnd(validEcosystems)} (saw: "${ecosystem}"); pass a supported ecosystem like --reach-ecosystems=${validEcosystems[0]}`, ) } reachEcosystems.push(ecosystem as PURL_Type) @@ -277,8 +278,8 @@ async function run( reachAnalysisMemoryLimit !== undefined && Number.isNaN(validatedReachAnalysisMemoryLimit) ) { - throw new Error( - `Invalid number value for --reach-analysis-memory-limit: ${reachAnalysisMemoryLimit}`, + throw new InputError( + `--reach-analysis-memory-limit must be a number of megabytes (saw: "${reachAnalysisMemoryLimit}"); pass an integer like --reach-analysis-memory-limit=4096`, ) } @@ -287,8 +288,8 @@ async function run( reachAnalysisTimeout !== undefined && Number.isNaN(validatedReachAnalysisTimeout) ) { - throw new Error( - `Invalid number value for --reach-analysis-timeout: ${reachAnalysisTimeout}`, + throw new InputError( + `--reach-analysis-timeout must be a number of seconds (saw: "${reachAnalysisTimeout}"); pass an integer like --reach-analysis-timeout=300`, ) } @@ -297,8 +298,8 @@ async function run( reachConcurrency !== undefined && Number.isNaN(validatedReachConcurrency) ) { - throw new Error( - `Invalid number value for --reach-concurrency: ${reachConcurrency}`, + throw new InputError( + `--reach-concurrency must be a positive integer (saw: "${reachConcurrency}"); pass a number like --reach-concurrency=4`, ) } diff --git a/packages/cli/src/commands/threat-feed/cmd-threat-feed.mts b/packages/cli/src/commands/threat-feed/cmd-threat-feed.mts index 92861924f..c743838f0 100644 --- a/packages/cli/src/commands/threat-feed/cmd-threat-feed.mts +++ b/packages/cli/src/commands/threat-feed/cmd-threat-feed.mts @@ -4,6 +4,7 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger' import { handleThreatFeed } from './handle-threat-feed.mts' import { outputDryRunFetch } from '../../utils/dry-run/output.mts' +import { InputError } from '../../utils/error/errors.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { @@ -288,7 +289,9 @@ async function run( return } if (Number.isNaN(validatedPerPage) || validatedPerPage < 1) { - throw new Error(`Invalid value for --per-page: ${cli.flags['perPage']}`) + throw new InputError( + `--per-page must be a positive integer (saw: "${cli.flags['perPage']}"); pass a number like --per-page=30`, + ) } await handleThreatFeed({ diff --git a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts index fad9ce57b..3e52fc462 100644 --- a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts +++ b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts @@ -1,6 +1,9 @@ import { promises as fs } from 'node:fs' import { getDefaultLogger } from '@socketsecurity/lib/logger' + +import { InputError } from '../../utils/error/errors.mts' + const logger = getDefaultLogger() export async function addSocketWrapper(file: string): Promise { @@ -10,7 +13,9 @@ export async function addSocketWrapper(file: string): Promise { 'alias npm="socket npm"\nalias npx="socket npx"\n', ) } catch (e) { - throw new Error(`There was an error setting up the alias: ${e}`) + throw new InputError( + `failed to append socket aliases to ${file} (${(e as Error).message}); check that the file exists and is writable`, + ) } logger.success( `The alias was added to ${file}. Running 'npm install' will now be wrapped in Socket's "safe npm" 🎉`, diff --git a/packages/cli/src/commands/wrapper/postinstall-wrapper.mts b/packages/cli/src/commands/wrapper/postinstall-wrapper.mts index 48b3c77ad..27790447d 100644 --- a/packages/cli/src/commands/wrapper/postinstall-wrapper.mts +++ b/packages/cli/src/commands/wrapper/postinstall-wrapper.mts @@ -8,7 +8,7 @@ import { addSocketWrapper } from './add-socket-wrapper.mts' import { checkSocketWrapperSetup } from './check-socket-wrapper-setup.mts' import { getBashRcPath, getZshRcPath } from '../../constants/paths.mts' import { getBashrcDetails } from '../../utils/cli/completion.mts' -import { getErrorCause } from '../../utils/error/errors.mjs' +import { getErrorCause, InputError } from '../../utils/error/errors.mjs' import { updateInstalledTabCompletionScript } from '../install/setup-tab-completion.mts' const logger = getDefaultLogger() @@ -85,8 +85,8 @@ async function setupSocketWrapper(query: string): Promise { await addSocketWrapper(zshRcPath) } } catch (e) { - throw new Error( - `There was an issue setting up the alias: ${getErrorCause(e)}`, + throw new InputError( + `failed to add socket aliases to ${bashRcPath} / ${zshRcPath} (${getErrorCause(e)}); check that your shell rc files exist and are writable`, ) } } diff --git a/packages/cli/test/integration/cli/cmd-ask.test.mts b/packages/cli/test/integration/cli/cmd-ask.test.mts index d5b5dab58..9f94fca1a 100644 --- a/packages/cli/test/integration/cli/cmd-ask.test.mts +++ b/packages/cli/test/integration/cli/cmd-ask.test.mts @@ -62,7 +62,7 @@ describe('socket ask', async () => { 'should error when no query provided', async cmd => { const { code, stdout } = await spawnSocketCli(binCliPath, cmd) - expect(stdout).toContain('Please provide a question') + expect(stdout).toContain('requires a QUERY positional argument') expect(code, 'should exit with non-zero code').not.toBe(0) }, ) diff --git a/packages/cli/test/integration/cli/cmd-scan-create.test.mts b/packages/cli/test/integration/cli/cmd-scan-create.test.mts index ead3b40cb..4c069467a 100644 --- a/packages/cli/test/integration/cli/cmd-scan-create.test.mts +++ b/packages/cli/test/integration/cli/cmd-scan-create.test.mts @@ -541,7 +541,7 @@ describe('socket scan create', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('Invalid ecosystem: "invalid-ecosystem"') + expect(output).toContain('(saw: "invalid-ecosystem")') expect( code, 'should exit with non-zero code when invalid ecosystem is provided', @@ -661,7 +661,7 @@ describe('socket scan create', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('Invalid ecosystem: "invalid"') + expect(output).toContain('(saw: "invalid")') expect( code, 'should exit with non-zero code when invalid ecosystem provided', diff --git a/packages/cli/test/integration/cli/cmd-scan-reach-dry-run.test.mts b/packages/cli/test/integration/cli/cmd-scan-reach-dry-run.test.mts index 9e49b8719..f242f90a2 100644 --- a/packages/cli/test/integration/cli/cmd-scan-reach-dry-run.test.mts +++ b/packages/cli/test/integration/cli/cmd-scan-reach-dry-run.test.mts @@ -244,7 +244,7 @@ describe('socket scan reach - dry-run tests', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('Invalid ecosystem: "invalid-ecosystem"') + expect(output).toContain('(saw: "invalid-ecosystem")') expect(code, 'should exit with non-zero code').not.toBe(0) }, ) @@ -445,7 +445,7 @@ describe('socket scan reach - dry-run tests', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('Invalid ecosystem: "invalid1"') + expect(output).toContain('(saw: "invalid1")') expect(code, 'should exit with non-zero code').not.toBe(0) }, ) diff --git a/packages/cli/test/unit/commands/ask/cmd-ask.test.mts b/packages/cli/test/unit/commands/ask/cmd-ask.test.mts index 71f83c5c3..e1a74701f 100644 --- a/packages/cli/test/unit/commands/ask/cmd-ask.test.mts +++ b/packages/cli/test/unit/commands/ask/cmd-ask.test.mts @@ -50,7 +50,7 @@ describe('cmd-ask', () => { it('should throw InputError when no query provided', async () => { await expect(cmdAsk.run([], importMeta, context)).rejects.toThrow( - 'Please provide a question', + /socket ask requires a QUERY positional argument/, ) }) diff --git a/packages/cli/test/unit/commands/audit-log/cmd-audit-log.test.mts b/packages/cli/test/unit/commands/audit-log/cmd-audit-log.test.mts index 8e87c48f9..3b3f31845 100644 --- a/packages/cli/test/unit/commands/audit-log/cmd-audit-log.test.mts +++ b/packages/cli/test/unit/commands/audit-log/cmd-audit-log.test.mts @@ -296,7 +296,7 @@ describe('cmd-audit-log', () => { await expect( cmdAuditLog.run(['--page', 'invalid'], importMeta, context), - ).rejects.toThrow('Invalid value for --page') + ).rejects.toThrow(/--page must be a non-negative integer/) }) it('should validate per-page is numeric', async () => { @@ -304,7 +304,7 @@ describe('cmd-audit-log', () => { await expect( cmdAuditLog.run(['--per-page', 'invalid'], importMeta, context), - ).rejects.toThrow('Invalid value for --per-page') + ).rejects.toThrow(/--per-page must be a non-negative integer/) }) it('should reject negative page numbers', async () => { @@ -312,7 +312,7 @@ describe('cmd-audit-log', () => { await expect( cmdAuditLog.run(['--page', '-1'], importMeta, context), - ).rejects.toThrow('Invalid value for --page') + ).rejects.toThrow(/--page must be a non-negative integer/) }) it('should reject negative per-page numbers', async () => { @@ -320,7 +320,7 @@ describe('cmd-audit-log', () => { await expect( cmdAuditLog.run(['--per-page', '-1'], importMeta, context), - ).rejects.toThrow('Invalid value for --per-page') + ).rejects.toThrow(/--per-page must be a non-negative integer/) }) it('should accept zero as page number', async () => { diff --git a/packages/cli/test/unit/commands/fix/cmd-fix.test.mts b/packages/cli/test/unit/commands/fix/cmd-fix.test.mts index 53935f125..c50539e70 100644 --- a/packages/cli/test/unit/commands/fix/cmd-fix.test.mts +++ b/packages/cli/test/unit/commands/fix/cmd-fix.test.mts @@ -196,7 +196,7 @@ describe('cmd-fix', () => { expect(process.exitCode).toBe(1) expect(mockHandleFix).not.toHaveBeenCalled() expect(mockLogger.fail).toHaveBeenCalledWith( - expect.stringContaining('Invalid ecosystem'), + expect.stringContaining('--ecosystems must be one of'), ) }) diff --git a/packages/cli/test/unit/commands/login/cmd-login.test.mts b/packages/cli/test/unit/commands/login/cmd-login.test.mts index af68c2bba..106dfa71d 100644 --- a/packages/cli/test/unit/commands/login/cmd-login.test.mts +++ b/packages/cli/test/unit/commands/login/cmd-login.test.mts @@ -115,7 +115,7 @@ describe('cmd-login', () => { mockIsInteractive.mockReturnValue(false) await expect(cmdLogin.run([], importMeta, context)).rejects.toThrow( - 'Cannot prompt for credentials in a non-interactive shell', + /socket login needs an interactive TTY/, ) expect(mockAttemptLogin).not.toHaveBeenCalled() }) diff --git a/packages/cli/test/unit/commands/organization/cmd-organization-dependencies.test.mts b/packages/cli/test/unit/commands/organization/cmd-organization-dependencies.test.mts index 1e56f41f5..eff3a38b8 100644 --- a/packages/cli/test/unit/commands/organization/cmd-organization-dependencies.test.mts +++ b/packages/cli/test/unit/commands/organization/cmd-organization-dependencies.test.mts @@ -197,7 +197,9 @@ describe('cmd-organization-dependencies', () => { await expect( cmdOrganizationDependencies.run(['--limit', '-1'], importMeta, context), - ).rejects.toThrow('Invalid value for --limit: -1') + ).rejects.toThrow( + /--limit must be a non-negative integer \(saw: "-1"\)/, + ) expect(mockHandleDependencies).not.toHaveBeenCalled() }) @@ -211,7 +213,9 @@ describe('cmd-organization-dependencies', () => { importMeta, context, ), - ).rejects.toThrow('Invalid value for --offset: -1') + ).rejects.toThrow( + /--offset must be a non-negative integer \(saw: "-1"\)/, + ) expect(mockHandleDependencies).not.toHaveBeenCalled() }) @@ -225,7 +229,9 @@ describe('cmd-organization-dependencies', () => { importMeta, context, ), - ).rejects.toThrow('Invalid value for --limit: invalid') + ).rejects.toThrow( + /--limit must be a non-negative integer \(saw: "invalid"\)/, + ) expect(mockHandleDependencies).not.toHaveBeenCalled() }) @@ -239,7 +245,9 @@ describe('cmd-organization-dependencies', () => { importMeta, context, ), - ).rejects.toThrow('Invalid value for --offset: invalid') + ).rejects.toThrow( + /--offset must be a non-negative integer \(saw: "invalid"\)/, + ) expect(mockHandleDependencies).not.toHaveBeenCalled() }) diff --git a/packages/cli/test/unit/commands/scan/cmd-scan-create.test.mts b/packages/cli/test/unit/commands/scan/cmd-scan-create.test.mts index f3a7e7ec2..7fe41f8ff 100644 --- a/packages/cli/test/unit/commands/scan/cmd-scan-create.test.mts +++ b/packages/cli/test/unit/commands/scan/cmd-scan-create.test.mts @@ -763,7 +763,7 @@ describe('cmd-scan-create', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid ecosystem/) + ).rejects.toThrow(/--reach-ecosystems must be one of/) }) it('should pass --commit-hash flag to handleCreateNewScan', async () => { @@ -878,7 +878,7 @@ describe('cmd-scan-create', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-analysis-memory-limit/) + ).rejects.toThrow(/--reach-analysis-memory-limit must be a number of megabytes/) }) it('should validate --reach-analysis-timeout is a number', async () => { @@ -898,7 +898,7 @@ describe('cmd-scan-create', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-analysis-timeout/) + ).rejects.toThrow(/--reach-analysis-timeout must be a number of seconds/) }) it('should validate --reach-concurrency is a number', async () => { @@ -918,7 +918,7 @@ describe('cmd-scan-create', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-concurrency/) + ).rejects.toThrow(/--reach-concurrency must be a positive integer/) }) }) diff --git a/packages/cli/test/unit/commands/scan/cmd-scan-reach.test.mts b/packages/cli/test/unit/commands/scan/cmd-scan-reach.test.mts index 0a496c74b..a9ee81b4d 100644 --- a/packages/cli/test/unit/commands/scan/cmd-scan-reach.test.mts +++ b/packages/cli/test/unit/commands/scan/cmd-scan-reach.test.mts @@ -288,7 +288,7 @@ describe('cmd-scan-reach', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid ecosystem/) + ).rejects.toThrow(/--reach-ecosystems must be one of/) }) it('should support --json output mode', async () => { @@ -544,7 +544,7 @@ describe('cmd-scan-reach', () => { context, ), ).rejects.toThrow( - /Invalid number value for --reach-analysis-memory-limit/, + /--reach-analysis-memory-limit must be a number of megabytes/, ) }) @@ -557,7 +557,7 @@ describe('cmd-scan-reach', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-analysis-timeout/) + ).rejects.toThrow(/--reach-analysis-timeout must be a number of seconds/) }) it('should validate invalid numeric values for concurrency', async () => { @@ -569,7 +569,7 @@ describe('cmd-scan-reach', () => { importMeta, context, ), - ).rejects.toThrow(/Invalid number value for --reach-concurrency/) + ).rejects.toThrow(/--reach-concurrency must be a positive integer/) }) it('should default to current directory if no target specified', async () => { diff --git a/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts b/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts index 051bc12fa..1c0ed6dae 100644 --- a/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts +++ b/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts @@ -78,7 +78,7 @@ describe('addSocketWrapper', () => { mockAppendFile.mockRejectedValue(error) await expect(addSocketWrapper('/etc/protected-file')).rejects.toThrow( - 'There was an error setting up the alias', + /failed to append socket aliases to \/etc\/protected-file/, ) expect(fs.promises.appendFile).toHaveBeenCalledWith( diff --git a/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts b/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts index 2b5b89f4b..3c233077d 100644 --- a/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts +++ b/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts @@ -61,6 +61,12 @@ vi.mock('../../../../src/commands/install/setup-tab-completion.mts', () => ({ })) vi.mock('../../../../src/utils/error/errors.mts', () => ({ getErrorCause: vi.fn(e => e?.message || String(e)), + InputError: class InputError extends Error { + constructor(message: string) { + super(message) + this.name = 'InputError' + } + }, })) describe('postinstallWrapper', () => { @@ -196,7 +202,7 @@ describe('postinstallWrapper', () => { }) await expect(postinstallWrapper()).rejects.toThrow( - 'There was an issue setting up the alias: Permission denied', + /failed to add socket aliases to .* \(Permission denied\)/, ) }) From f24a8bea910b5894771ffda2b519949593573b98 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 11:51:17 -0400 Subject: [PATCH 2/7] chore(cli): harden (e as Error) casts to safe stringify Switch `(e as Error).message` to `e instanceof Error ? e.message : String(e)` so that when a non-Error value is thrown (strings, objects, null) the error message stays informative instead of becoming 'undefined'. Same fix as applied to #1260 (iocraft.mts) after Cursor bugbot flagged the pattern on that PR. --- packages/cli/src/commands/wrapper/add-socket-wrapper.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts index 3e52fc462..be7f8e6f3 100644 --- a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts +++ b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts @@ -14,7 +14,7 @@ export async function addSocketWrapper(file: string): Promise { ) } catch (e) { throw new InputError( - `failed to append socket aliases to ${file} (${(e as Error).message}); check that the file exists and is writable`, + `failed to append socket aliases to ${file} (${e instanceof Error ? e.message : String(e)}); check that the file exists and is writable`, ) } logger.success( From 949a9012800c1189b1e06c5dda82a9b6819afad5 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 12:11:51 -0400 Subject: [PATCH 3/7] chore(cli): use getErrorCause helper in add-socket-wrapper Switch the inline `e instanceof Error ? e.message : String(e)` to getErrorCause(e), matching the fleet helper pattern. Same behavior for Error throws; non-Error throws now produce 'Unknown error' instead of '[object Object]'. --- packages/cli/src/commands/wrapper/add-socket-wrapper.mts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts index be7f8e6f3..36f42dc75 100644 --- a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts +++ b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts @@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs' import { getDefaultLogger } from '@socketsecurity/lib/logger' -import { InputError } from '../../utils/error/errors.mts' +import { getErrorCause, InputError } from '../../utils/error/errors.mts' const logger = getDefaultLogger() @@ -14,7 +14,7 @@ export async function addSocketWrapper(file: string): Promise { ) } catch (e) { throw new InputError( - `failed to append socket aliases to ${file} (${e instanceof Error ? e.message : String(e)}); check that the file exists and is writable`, + `failed to append socket aliases to ${file} (${getErrorCause(e)}); check that the file exists and is writable`, ) } logger.success( From 769170f3241ff2f1b7afd8d38ddb7a1a2d23bc89 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 20:26:23 -0400 Subject: [PATCH 4/7] fix(cli): wrap wrapper fs.appendFile failures as FileSystemError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor bugbot flagged both add-socket-wrapper.mts and postinstall-wrapper.mts for rendering I/O errors with the "Invalid input" title and "Check command syntax with --help" recovery hint (InputError's display mapping). fs.appendFile failures are permission / disk / path problems — FileSystemError renders them as "File system error" with contextual recovery (check file permissions, disk space, etc.). Pass the file path (where available) and the ErrnoException code through so FileSystemError can surface EACCES/ENOSPC/ENOENT-specific recovery text. Reported on PR #1255. --- packages/cli/src/commands/wrapper/add-socket-wrapper.mts | 6 ++++-- packages/cli/src/commands/wrapper/postinstall-wrapper.mts | 6 ++++-- pnpm-lock.yaml | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts index 36f42dc75..73ce186d6 100644 --- a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts +++ b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts @@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs' import { getDefaultLogger } from '@socketsecurity/lib/logger' -import { getErrorCause, InputError } from '../../utils/error/errors.mts' +import { FileSystemError, getErrorCause } from '../../utils/error/errors.mts' const logger = getDefaultLogger() @@ -13,8 +13,10 @@ export async function addSocketWrapper(file: string): Promise { 'alias npm="socket npm"\nalias npx="socket npx"\n', ) } catch (e) { - throw new InputError( + throw new FileSystemError( `failed to append socket aliases to ${file} (${getErrorCause(e)}); check that the file exists and is writable`, + file, + (e as NodeJS.ErrnoException)?.code, ) } logger.success( diff --git a/packages/cli/src/commands/wrapper/postinstall-wrapper.mts b/packages/cli/src/commands/wrapper/postinstall-wrapper.mts index 27790447d..6ee329fac 100644 --- a/packages/cli/src/commands/wrapper/postinstall-wrapper.mts +++ b/packages/cli/src/commands/wrapper/postinstall-wrapper.mts @@ -8,7 +8,7 @@ import { addSocketWrapper } from './add-socket-wrapper.mts' import { checkSocketWrapperSetup } from './check-socket-wrapper-setup.mts' import { getBashRcPath, getZshRcPath } from '../../constants/paths.mts' import { getBashrcDetails } from '../../utils/cli/completion.mts' -import { getErrorCause, InputError } from '../../utils/error/errors.mjs' +import { FileSystemError, getErrorCause } from '../../utils/error/errors.mjs' import { updateInstalledTabCompletionScript } from '../install/setup-tab-completion.mts' const logger = getDefaultLogger() @@ -85,8 +85,10 @@ async function setupSocketWrapper(query: string): Promise { await addSocketWrapper(zshRcPath) } } catch (e) { - throw new InputError( + throw new FileSystemError( `failed to add socket aliases to ${bashRcPath} / ${zshRcPath} (${getErrorCause(e)}); check that your shell rc files exist and are writable`, + undefined, + (e as NodeJS.ErrnoException)?.code, ) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adc83df24..158a84812 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2151,6 +2151,7 @@ packages: '@socketaddon/iocraft@file:packages/package-builder/build/dev/out/socketaddon-iocraft': resolution: {directory: packages/package-builder/build/dev/out/socketaddon-iocraft, type: directory} + engines: {node: '>=18'} '@socketregistry/es-set-tostringtag@1.0.10': resolution: {integrity: sha512-btXmvw1JpA8WtSoXx9mTapo9NAyIDKRRzK84i48d8zc0X09M6ORfobVnHbgwhXf7CFhkRzhYrHG9dqbI9vpELQ==} From e321c0d22a23bc67fd3c2e4420cc01e5461ec770 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 20:29:51 -0400 Subject: [PATCH 5/7] test(cli): update postinstall-wrapper mock to export FileSystemError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit swapped InputError → FileSystemError in postinstall-wrapper.mts but left the test's vi.mock factory still exporting InputError, so the source couldn't resolve its FileSystemError import under vitest: [vitest] No "FileSystemError" export is defined on the ".../src/utils/error/errors.mts" mock Replace the mock's InputError stub with a FileSystemError stub that matches the real class's shape (path, code, recovery) so the source code resolves correctly. The existing regex assertion on the error message is unaffected. --- .../commands/wrapper/postinstall-wrapper.test.mts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts b/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts index 3c233077d..ccc7865fb 100644 --- a/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts +++ b/packages/cli/test/unit/commands/wrapper/postinstall-wrapper.test.mts @@ -61,10 +61,19 @@ vi.mock('../../../../src/commands/install/setup-tab-completion.mts', () => ({ })) vi.mock('../../../../src/utils/error/errors.mts', () => ({ getErrorCause: vi.fn(e => e?.message || String(e)), - InputError: class InputError extends Error { - constructor(message: string) { + FileSystemError: class FileSystemError extends Error { + public readonly path?: string | undefined + public readonly code?: string | undefined + public readonly recovery: string[] = [] + constructor( + message: string, + path?: string | undefined, + code?: string | undefined, + ) { super(message) - this.name = 'InputError' + this.name = 'FileSystemError' + this.path = path + this.code = code } }, })) From 2269113bcdaca10cad13f4cca7fea9b5d0c81851 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 21:23:22 -0400 Subject: [PATCH 6/7] fix(cli): address bugbot re-review findings on #1255 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues flagged on the fresh review of HEAD: - FileSystemError duplicates the filepath: the message embeds ${file} AND the constructor receives `file` as the path argument. Since display.formatErrorForDisplay appends \`(\${error.path})\` automatically, this surfaced the path twice. Drop the \${file} interpolation from the message — the path arg alone is enough. - Scan command numeric flag errors overstated validation: \`--pull-request\` claimed "non-negative integer" and \`--reach-concurrency\` claimed "positive integer," but the checks only rejected NaN. Negatives and floats passed through silently. Tighten the validation to match what the error messages promise — Number.isInteger + range check at every call site (cmd-scan-create.mts for both flags, cmd-scan-reach.mts for reach-concurrency). The third flagged item (FileSystemError vs InputError in the wrapper commands) was cursor re-flagging a finding already addressed in commit 769170f32 — the current code already uses FileSystemError. No action. --- packages/cli/src/commands/scan/cmd-scan-create.mts | 11 +++++++++-- packages/cli/src/commands/scan/cmd-scan-reach.mts | 4 +++- .../cli/src/commands/wrapper/add-socket-wrapper.mts | 5 ++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/scan/cmd-scan-create.mts b/packages/cli/src/commands/scan/cmd-scan-create.mts index 437b7fd7e..43fffeb85 100644 --- a/packages/cli/src/commands/scan/cmd-scan-create.mts +++ b/packages/cli/src/commands/scan/cmd-scan-create.mts @@ -727,7 +727,12 @@ async function run( // Validate numeric flag conversions. const validatedPullRequest = Number(pullRequest) - if (pullRequest !== undefined && Number.isNaN(validatedPullRequest)) { + if ( + pullRequest !== undefined && + (Number.isNaN(validatedPullRequest) || + !Number.isInteger(validatedPullRequest) || + validatedPullRequest < 0) + ) { throw new InputError( `--pull-request must be a non-negative integer (saw: "${pullRequest}"); pass a number like --pull-request=42`, ) @@ -756,7 +761,9 @@ async function run( const validatedReachConcurrency = Number(reachConcurrency) if ( reachConcurrency !== undefined && - Number.isNaN(validatedReachConcurrency) + (Number.isNaN(validatedReachConcurrency) || + !Number.isInteger(validatedReachConcurrency) || + validatedReachConcurrency <= 0) ) { throw new InputError( `--reach-concurrency must be a positive integer (saw: "${reachConcurrency}"); pass a number like --reach-concurrency=4`, diff --git a/packages/cli/src/commands/scan/cmd-scan-reach.mts b/packages/cli/src/commands/scan/cmd-scan-reach.mts index 7c88c3f58..8722a7769 100644 --- a/packages/cli/src/commands/scan/cmd-scan-reach.mts +++ b/packages/cli/src/commands/scan/cmd-scan-reach.mts @@ -296,7 +296,9 @@ async function run( const validatedReachConcurrency = Number(reachConcurrency) if ( reachConcurrency !== undefined && - Number.isNaN(validatedReachConcurrency) + (Number.isNaN(validatedReachConcurrency) || + !Number.isInteger(validatedReachConcurrency) || + validatedReachConcurrency <= 0) ) { throw new InputError( `--reach-concurrency must be a positive integer (saw: "${reachConcurrency}"); pass a number like --reach-concurrency=4`, diff --git a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts index 73ce186d6..adc744dd9 100644 --- a/packages/cli/src/commands/wrapper/add-socket-wrapper.mts +++ b/packages/cli/src/commands/wrapper/add-socket-wrapper.mts @@ -13,8 +13,11 @@ export async function addSocketWrapper(file: string): Promise { 'alias npm="socket npm"\nalias npx="socket npx"\n', ) } catch (e) { + // Don't include `file` in the message: display.formatErrorForDisplay + // appends `(${error.path})` automatically when FileSystemError carries + // a path, so embedding it here would show the filename twice. throw new FileSystemError( - `failed to append socket aliases to ${file} (${getErrorCause(e)}); check that the file exists and is writable`, + `failed to append socket aliases (${getErrorCause(e)}); check that the file exists and is writable`, file, (e as NodeJS.ErrnoException)?.code, ) From 1f7533f7fe898e8f5000f525a7f54016aa83418b Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 22 Apr 2026 23:53:56 -0400 Subject: [PATCH 7/7] test(cli): update add-socket-wrapper regex for new FileSystemError shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous commit (2269113bc) dropped the \${file} interpolation from FileSystemError's message to avoid display.formatErrorForDisplay double-printing the path (which it appends from error.path). The test regex still matched the pre-change format — /failed to append socket aliases to \/etc\/protected-file/ — and wouldn't have matched the new message. Assert on the new shape instead: - regex matches `failed to append socket aliases (Permission denied)` - toMatchObject pins name='FileSystemError' and path='/etc/protected-file' Drive-by: the existing regex had \./etc\/protected-file/ but the message never contained that substring after the FileSystemError swap — so the assertion was silently wrong. Flagged by cursor on #1255. --- .../commands/wrapper/add-socket-wrapper.test.mts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts b/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts index 1c0ed6dae..edd6de3ee 100644 --- a/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts +++ b/packages/cli/test/unit/commands/wrapper/add-socket-wrapper.test.mts @@ -77,9 +77,19 @@ describe('addSocketWrapper', () => { mockAppendFile.mockRejectedValue(error) + // The FileSystemError wraps the cause in the message; the path is + // stored on the `.path` property (not embedded in the message) to + // avoid display.formatErrorForDisplay double-printing it. Assert on + // the message shape + the path property separately. await expect(addSocketWrapper('/etc/protected-file')).rejects.toThrow( - /failed to append socket aliases to \/etc\/protected-file/, + /failed to append socket aliases \(Permission denied\)/, ) + await expect( + addSocketWrapper('/etc/protected-file'), + ).rejects.toMatchObject({ + name: 'FileSystemError', + path: '/etc/protected-file', + }) expect(fs.promises.appendFile).toHaveBeenCalledWith( '/etc/protected-file',