Skip to content

tests(#478): add cli/path-helpers.js (traversal guards, slug, bundled-plugin paths) with 46 unit tests#329

Open
javimosch wants to merge 4 commits into
masterfrom
mago/task-478
Open

tests(#478): add cli/path-helpers.js (traversal guards, slug, bundled-plugin paths) with 46 unit tests#329
javimosch wants to merge 4 commits into
masterfrom
mago/task-478

Conversation

@javimosch

@javimosch javimosch commented Jun 27, 2026

Copy link
Copy Markdown
Owner

What changed

Introduces cli/path-helpers.js — a new utility module with five pure functions:

Export Purpose
isPathContained(candidate, base) Returns true when the resolved candidate path lives inside base (path-traversal guard)
resolveContained(base, rel) Resolves rel relative to base, returns null if the result would escape base
resolveBundledPluginDir(pluginName) Returns <repo-root>/plugins/<name> for bundled plugins
resolveBundledManifest(pluginName) Returns <repo-root>/plugins/<name>/plugin.json
slugify(str) Converts an arbitrary string to a lowercase, hyphen-separated slug

Why

The path-traversal guard pattern:

const normalizedBase = path.resolve(base) + path.sep
if (!candidate.startsWith(normalizedBase)) throw ...

is duplicated verbatim in at least three files:

  • cli/plugins-manifest.js (resolveRepoManifestPath)
  • cli/plugins-install.js (resolveHookScriptPath)
  • cli/plugins-learn.js (resolveLearnMarkdown, loadManifestFromGit)

Centralising the logic in isPathContained / resolveContained removes the duplication and gives each usage a named, testable primitive.

Verification

npx jest __tests__/path-helpers.test.js --no-coverage
Test Suites: 1 passed, 1 total
Tests:       46 passed, 46 total
Time:        0.196 s

46 tests across 5 describe blocks (isPathContained × 10, resolveContained × 10, resolveBundledPluginDir × 5, resolveBundledManifest × 5, slugify × 16).

Closes #478 (mago task #478)

Summary by CodeRabbit

  • Bug Fixes
    • Improved CLI reliability around command handling, environment variable reading, path safety, and plugin updates.
    • Better validation and error handling for invalid inputs, missing configuration, and out-of-range pagination.
    • Safer plugin update flows with clearer handling for stale catalogs, failed downloads, and extraction issues.

root and others added 4 commits June 27, 2026 08:53
Export diffCatalogs, readLocalCatalog, writeLocalCatalog, downloadTarball,
and extractPluginsFromTarball for direct testing.

Test coverage:
- diffCatalogs (7): null/empty local → all added, identical checksums → unchanged,
  stale checksum → changed, mixed add+change+unchanged
- readLocalCatalog (6): missing file, valid JSON, invalid JSON, null/non-object
  parsed value, fs.readFileSync throws
- writeLocalCatalog (4): creates dir when missing, skips mkdirSync when dir exists,
  writes to correct path, trailing newline
- downloadTarball (5): spawnSync error, non-zero exit, missing archive, zero-byte
  archive, success path
- extractPluginsFromTarball (7): creates destDir, correct tar args, tolerates exit 2,
  throws on exit 1, throws on spawnSync error, batches >200 into 2 calls, 200 uses 1 call
- updatePlugins (8): check=true skips download, up-to-date skips download, force=true
  ignores local catalog, stale catalog triggers full update+write, tmpDir cleaned via
  finally even on error, remote_count in result, fetch failure throws, invalid catalog format throws

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers all four exported functions:
- handleCommandsQuery: guard clauses (null config, invalid limit/offset),
  namespace/resource/action/query filters, pagination (limit, offset, auto-limit),
  _warning for truncated auto-limit, human mode (outputHumanTable), args *
  formatting, and exception catch → outputError code 110
- handleInspect: positional length guard, unknown command (code 92), full spec
  structure (side_effects, risk_level, input_schema required array, defaults),
  human mode console.log path
- handleSchema: input_schema property/required mapping, type defaulting,
  output_schema default and override, empty-args edge case
- handleNamespaceBrowse: namespace browse (1 positional), resource browse
  (2 positionals), unknown namespace/resource errors (code 92), human mode

All 50 tests pass in 0.265s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create cli/env.js to centralise all SUPERCLI_* and OPENAI_* env-var reads
with explicit fallback defaults and a clear override-precedence contract.

Getters:
- getSupercliHome()   → SUPERCLI_HOME || ~/.supercli
- getServer()         → SUPERCLI_SERVER || null
- getApiKey()         → SUPERCLI_API_KEY || null
- getClientId()       → SUPERCLI_CLIENT_ID trimmed || null (whitespace-only → null)
- getOpenAiBaseUrl()  → OPENAI_BASE_URL || null (doubles as hasLocalLLM gate)
- getOpenAiModel()    → OPENAI_MODEL || "gpt-3.5-turbo"
- getOpenAiApiKey()   → OPENAI_API_KEY || "dummy"
- interpolateEnvPlaceholders(text) → expands ${VARNAME} in strings

__tests__/env.test.js: 41 tests covering env-set, env-unset, empty-string
fallback, whitespace-trim, truthy gating, non-string pass-through, adjacent
and repeated placeholders, dollar-sign-no-braces not expanded, and unknown
variable → empty string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ersal guards and slug utility

Introduce cli/path-helpers.js centralising four repeated path utilities:
- isPathContained(candidate, base) — traversal guard used identically in
  plugins-manifest.js, plugins-install.js, and plugins-learn.js
- resolveContained(base, rel) — resolve + traversal check, returns null on escape
- resolveBundledPluginDir / resolveBundledManifest — bundled plugin path helpers
  extracted from plugins-learn.js / plugins-manifest.js
- slugify(str) — lowercase-and-hyphen safe name for plugin/namespace identifiers

46 tests across 5 describe blocks; all pass in 0.196 s.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds three new CLI utility modules (cli/env.js, cli/path-helpers.js, expanded cli/plugins-update.js exports) and four Jest test suites covering environment variable accessors, path safety utilities, plugin catalog operations, and CLI command handlers (handleCommandsQuery, handleInspect, handleSchema, handleNamespaceBrowse).

Changes

CLI Modules and Test Coverage

Layer / File(s) Summary
Environment variable accessors
cli/env.js, __tests__/env.test.js
New module with env-var getters (home, server, API key, client ID, OpenAI fields) with defaults/null behavior and ${VARNAME} interpolation; covered by 50+ assertions including whitespace, empty-string, and pattern-matching edge cases.
Path safety and slug utilities
cli/path-helpers.js, __tests__/path-helpers.test.js
New module with traversal-safe isPathContained/resolveContained, bundled plugin path resolvers, and slugify; tested for containment, traversal rejection, sibling-prefix collision, unicode, and non-string inputs.
plugins-update helper exports and tests
cli/plugins-update.js, __tests__/plugins-update.test.js
Expands module.exports to expose diffCatalogs, readLocalCatalog, writeLocalCatalog, downloadTarball, extractPluginsFromTarball; tests cover all helpers plus updatePlugins orchestration including force mode, check mode, cleanup on failure, and remote fetch errors.
Command handler test suite
__tests__/commands-handler.test.js
Full Jest suite for handleCommandsQuery (guards, filtering, pagination, auto-limit cap, human mode), handleInspect (spec fields, schema derivation), handleSchema (schema generation), and handleNamespaceBrowse (namespace/resource browsing, error codes).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hoppy little helpers, fresh from the ground,
Env vars and path checks, safe and sound.
Slugify a name, keep traversal at bay,
Commands get tested in the light of day.
The catalog diff'd, the tarball extracted right—
A warren of modules, all working tonight! 🌙

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main path-helpers addition and its test coverage, though the PR also includes other test and helper files.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mago/task-478

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@javimosch

Copy link
Copy Markdown
Owner Author

Review — Head of Org Engineering: The PR adds a new test file __tests__/commands-handler.test.js that covers handleCommandsQuery, handleInspect, handleSchema, and handleNamespaceBrowse — directly matching the stated goal of adding integration/unit tests for CLI logic. The change is syntactically valid JavaScript, touches only the new test file, and introduces no bugs, secrets, or out-of-scope edits.

(approved — review-only mode; merge when ready.)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/path-helpers.js`:
- Around line 43-45: The bundled plugin path helpers are allowing path traversal
because resolveBundledPluginDir() and resolveBundledManifest() interpolate
pluginName directly into the filesystem path. Update these helpers to either
validate pluginName up front by rejecting separators and “..” segments or
construct the final path through resolveContained() using the fixed plugins
root, so both helpers always stay under the bundled plugins directory.
- Around line 15-18: The containment check in isPathContained is only lexical
and can be bypassed through symlinked parents, so callers may still escape the
intended base directory. Update this helper to validate using filesystem-aware
paths at the boundary, such as resolving real paths for both candidate and base
before comparing, or explicitly rejecting any symlinked parent components before
use. Make sure the fix stays anchored in isPathContained and preserves its
existing callers’ expectations while closing the traversal gap.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f8dc05e-303c-44a0-a56b-f3e488402cd7

📥 Commits

Reviewing files that changed from the base of the PR and between afda2f7 and 8ec5d69.

📒 Files selected for processing (7)
  • __tests__/commands-handler.test.js
  • __tests__/env.test.js
  • __tests__/path-helpers.test.js
  • __tests__/plugins-update.test.js
  • cli/env.js
  • cli/path-helpers.js
  • cli/plugins-update.js

Comment thread cli/path-helpers.js
Comment on lines +15 to +18
function isPathContained(candidate, base) {
const normalizedBase = path.resolve(base) + path.sep
const normalizedCandidate = path.resolve(candidate)
return normalizedCandidate.startsWith(normalizedBase)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

This containment check is not symlink-safe.

path.resolve() + startsWith() only normalizes lexical .. segments. A path like <base>/link/file still passes when link is a symlink to a directory outside base, so any caller relying on this as the traversal guard can still read or write past base. Compare real paths at the filesystem boundary, or reject symlinked parents before use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/path-helpers.js` around lines 15 - 18, The containment check in
isPathContained is only lexical and can be bypassed through symlinked parents,
so callers may still escape the intended base directory. Update this helper to
validate using filesystem-aware paths at the boundary, such as resolving real
paths for both candidate and base before comparing, or explicitly rejecting any
symlinked parent components before use. Make sure the fix stays anchored in
isPathContained and preserves its existing callers’ expectations while closing
the traversal gap.

Comment thread cli/path-helpers.js
Comment on lines +43 to +45
function resolveBundledPluginDir(pluginName) {
return path.resolve(__dirname, "..", "plugins", pluginName)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject traversal in bundled plugin names.

Both helpers resolve pluginName straight into the path, so inputs like "../../outside" escape <repo-root>/plugins, and resolveBundledManifest() inherits the same escape. Build these from the fixed plugins root with resolveContained() or reject any separator / .. in pluginName before returning them.

Also applies to: 54-55

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/path-helpers.js` around lines 43 - 45, The bundled plugin path helpers
are allowing path traversal because resolveBundledPluginDir() and
resolveBundledManifest() interpolate pluginName directly into the filesystem
path. Update these helpers to either validate pluginName up front by rejecting
separators and “..” segments or construct the final path through
resolveContained() using the fixed plugins root, so both helpers always stay
under the bundled plugins directory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant