Skip to content

feat(stac_cli): add unit testing infrastructure and core command tests#472

Open
Pratikdate wants to merge 12 commits into
StacDev:devfrom
Pratikdate:dev
Open

feat(stac_cli): add unit testing infrastructure and core command tests#472
Pratikdate wants to merge 12 commits into
StacDev:devfrom
Pratikdate:dev

Conversation

@Pratikdate
Copy link
Copy Markdown

@Pratikdate Pratikdate commented Apr 3, 2026

Description

This PR adds a comprehensive unit testing layer to the stac_cli package and integrates it into the monorepo's CI pipeline. Key changes include:

  • Infrastructure Set-up: Initialized the test/ directory within packages/stac_cli to manage command and utility tests.
  • Environment Mocking: Added a mechanism to safely stub out required environment variables (like STAC_BASE_API_URL and STAC_GOOGLE_CLIENT_ID) during tests to prevent initialization crashes without real API keys.
  • Core Command Tests: Implemented tests for BuildCommand, InitCommand, and DeployCommand to verify registration, name, and descriptions.
  • Utility Reliability: Added tests for FileUtils to validate file system operations (create, read, delete) with automated sandbox cleanup.
  • CI Pipeline Integration:
    • Added a test command to the root melos.yaml.
    • Updated .github/workflows/all_plugins.yaml to include a global test step for all packages.

Related Issues

Closes # (Insert issue number if applicable)

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Code refactor
  • Build configuration change
  • Documentation
  • Chore

Summary by CodeRabbit

  • Tests
    • Added test suite verifying CLI commands (build, init, deploy) are properly registered and configured.
    • Added test file validating file utility functions, including home directory access, configuration directory management, and file operations (read, write, delete).

@Pratikdate Pratikdate mentioned this pull request Apr 3, 2026
3 tasks
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 5, 2026

CLA assistant check
All committers have signed the CLA.

@Pratikdate Pratikdate marked this pull request as draft April 11, 2026 06:49
@Pratikdate Pratikdate marked this pull request as ready for review April 11, 2026 06:49
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Too many files changed? Review this PR in Change Stack to see how the pieces fit before you dive in.

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b27d9126-d925-4a9b-a07a-c496bec56a2a

📥 Commits

Reviewing files that changed from the base of the PR and between 3897043 and 0df44b9.

⛔ Files ignored due to path filters (1)
  • packages/stac_cli/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • packages/stac_cli/test/commands/cli_commands_test.dart
💤 Files with no reviewable changes (1)
  • packages/stac_cli/test/commands/cli_commands_test.dart

📝 Walkthrough

Walkthrough

This PR adds two test suites to the STAC CLI package: one verifying FileUtils directory access and file I/O operations, and another verifying that Build, Init, and Deploy commands are properly registered in the CLI runner.

Changes

Test Infrastructure for FileUtils and CLI Commands

Layer / File(s) Summary
FileUtils integration and file operation tests
packages/stac_cli/test/utils/file_utils_test.dart
Tests confirm homeDirectory and configDirectory are non-empty and exist as directories; ensureConfigDirectory() creates the config directory if absent; async temp-file test verifies writeFile, readFile, and deleteFile round-trip with guaranteed cleanup via finally block.
CLI command registration and runner tests
packages/stac_cli/test/commands/cli_commands_test.dart
Tests construct a CommandRunner<int> with mocked environment in setUp, register BuildCommand, InitCommand, and DeployCommand, and assert each command exists in runner.commands with the expected name and non-empty description. Environment configuration is cleared in tearDown.

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Our tests now bloom with coverage bright,
FileUtils and commands tested right,
Each assertion stands so proud and tall,
The CLI infrastructure conquers all! 🌟

🚥 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 objective of the changeset: adding unit testing infrastructure and core command tests to the stac_cli package.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/stac_cli/test/utils/file_utils_test.dart (1)

10-18: ⚡ Quick win

Consider validating directory paths beyond non-empty checks.

The current tests only verify that homeDirectory and configDirectory return non-empty strings. They don't validate that these are actual valid directory paths or that they exist. While acceptable for initial test infrastructure, consider adding path validity checks in future iterations.

📁 Example enhancement to validate paths
   test('homeDirectory returns a non-empty string on this OS', () {
     final home = FileUtils.homeDirectory;
     expect(home, isNotEmpty);
+    expect(Directory(home).existsSync(), isTrue, 
+      reason: 'homeDirectory should point to an existing directory');
   });

   test('configDirectory path is generated', () {
     final config = FileUtils.configDirectory;
     expect(config, isNotEmpty);
+    expect(path.isAbsolute(config), isTrue,
+      reason: 'configDirectory should be an absolute path');
   });
🤖 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 `@packages/stac_cli/test/utils/file_utils_test.dart` around lines 10 - 18, The
tests only assert non-empty strings; update the tests for
FileUtils.homeDirectory and FileUtils.configDirectory to also validate that the
returned paths point to existing directories by constructing a Directory from
each path and asserting Directory(path).exists() (and optionally
Directory(path).stat() indicates a directory) so the tests ensure the paths are
valid and are directories rather than merely non-empty strings.
🤖 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 `@packages/stac_cli/test/commands/cli_commands_test.dart`:
- Around line 13-26: The tests set environment variables in the setUp using
configureEnvironment but never clear them, so add a tearDown block that resets
or clears those keys after each test; implement tearDown(() {
configureEnvironment({'STAC_BASE_API_URL': null, 'STAC_GOOGLE_CLIENT_ID': null,
'STAC_FIREBASE_API_KEY': null}); }) or call your existing environment-reset
helper to remove those variables so configureEnvironment and setUp no longer
leak state between tests.

---

Nitpick comments:
In `@packages/stac_cli/test/utils/file_utils_test.dart`:
- Around line 10-18: The tests only assert non-empty strings; update the tests
for FileUtils.homeDirectory and FileUtils.configDirectory to also validate that
the returned paths point to existing directories by constructing a Directory
from each path and asserting Directory(path).exists() (and optionally
Directory(path).stat() indicates a directory) so the tests ensure the paths are
valid and are directories rather than merely non-empty strings.
🪄 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: 4105edd3-5e6e-4cc4-b1e5-44517536bfa9

📥 Commits

Reviewing files that changed from the base of the PR and between 2907c6a and f48feea.

📒 Files selected for processing (2)
  • packages/stac_cli/test/commands/cli_commands_test.dart
  • packages/stac_cli/test/utils/file_utils_test.dart

Comment thread packages/stac_cli/test/commands/cli_commands_test.dart
@divyanshub024
Copy link
Copy Markdown
Member

Hey @Pratikdate!! Can you please add all the proper test cases for the cli?

@Pratikdate
Copy link
Copy Markdown
Author

Yes , I do update

- Add SkillsCommand with 'stac skills add' subcommand
- AddCommand fetches repo ZIP, parses skills/catalog.json,
  and copies skill dirs into .agents/skills/ — no Node/npm required
- Prompt users in 'stac init' to optionally install agent skills
- Register SkillsCommand in the CLI runner
- Add archive: ^4.0.9 dependency for ZIP extraction
- Update docs/skills.mdx to show Dart-native path first,
  keeping npx as an alternative
- Add tests for SkillsCommand and AddCommand (7 tests pass)

Closes StacDev#480
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@packages/stac_cli/lib/src/commands/init_command.dart`:
- Around line 93-96: The skills install block currently calls
AddCommand().execute() which runs against Directory.current instead of the
intended targetDir and ignores non-zero exit codes; change the call to execute
the AddCommand within the targetDir (either by passing targetDir into AddCommand
or setting the working directory before calling AddCommand().execute()) and
capture its return/exit code, logging an error via ConsoleLogger.error and
aborting or returning non-zero when the installer fails so success messages
printed later are conditional on a successful install. Ensure you update
references to shouldInstallSkills / AddCommand().execute() to use the
targetDir-aware invocation and explicitly check the result to handle failures.

In `@packages/stac_cli/lib/src/commands/skills/add_command.dart`:
- Around line 56-89: The tempDir created with
Directory.systemTemp.createTemp('stac_skills_') is only cleaned up on the
success path; wrap the download/extract/validation logic in a try/finally
(declare tempDir before the try) and in the finally always call await
tempDir.delete(recursive: true) to remove the temporary files even on early
returns or exceptions; update the function containing this logic (e.g., the add
command function that uses tempDir, zipFile, extractDir and catalogFile) so all
exit paths go through the finally block and keep existing return codes and error
logging behavior.
- Around line 101-116: The code uses untrusted catalog values (skillName,
skillPath) to build sourceSkillDir and targetSkillDir and perform filesystem
ops; validate and canonicalize both values before any filesystem access: reject
empty, absolute paths, or values containing path traversal or path separators
for skillName (enforce a single safe token), normalize skillPath and compute its
absolute/resolved path and ensure it is a descendant of repoRoot (use resolved
canonical paths) before reading/copying, and ensure targetSkillDir resolved path
is inside targetAgentsDir before deleting or writing; if checks fail, log a
warning and skip the entry. Use the existing variables skillName, skillPath,
sourceSkillDir, targetSkillDir, repoRoot, and targetAgentsDir when adding these
validations.

In `@packages/stac_cli/test/commands/cli_commands_test.dart`:
- Around line 64-65: The test performs a direct cast with "as SkillsCommand"
which can throw before assertions run; change it to first assert that
runner.commands['skills'] is not null and is a SkillsCommand (e.g.
expect(runner.commands['skills'], isNotNull) and
expect(runner.commands['skills'], isA<SkillsCommand>())), then safely cast into
a local variable (e.g. final skillsCommand = runner.commands['skills'] as
SkillsCommand) and finally assert that skillsCommand.subcommands['add']
isNotNull; reference the runner.commands map, the SkillsCommand type, and the
subcommands['add'] lookup when making these changes.
🪄 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: 58b6229c-c406-4453-9120-352eca8263d2

📥 Commits

Reviewing files that changed from the base of the PR and between 0e2a6e1 and 3897043.

⛔ Files ignored due to path filters (1)
  • packages/stac_cli/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • docs/skills.mdx
  • packages/stac_cli/bin/stac_cli.dart
  • packages/stac_cli/lib/src/commands/init_command.dart
  • packages/stac_cli/lib/src/commands/skills/add_command.dart
  • packages/stac_cli/lib/src/commands/skills_command.dart
  • packages/stac_cli/pubspec.yaml
  • packages/stac_cli/test/commands/cli_commands_test.dart
✅ Files skipped from review due to trivial changes (1)
  • docs/skills.mdx

Comment on lines +93 to +96
if (shouldInstallSkills) {
ConsoleLogger.info('Installing skills...');
await AddCommand().execute();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Install location and failure handling are disconnected from init --directory.

At Line 95, AddCommand().execute() runs against Directory.current, not targetDir, so skills can be installed in a different project than the one being initialized. Also, non-zero exit from execute() is ignored, yet success is still printed later.

Pass targetDir into the skill installer (or run with that working dir) and handle non-zero exit codes explicitly.

🤖 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 `@packages/stac_cli/lib/src/commands/init_command.dart` around lines 93 - 96,
The skills install block currently calls AddCommand().execute() which runs
against Directory.current instead of the intended targetDir and ignores non-zero
exit codes; change the call to execute the AddCommand within the targetDir
(either by passing targetDir into AddCommand or setting the working directory
before calling AddCommand().execute()) and capture its return/exit code, logging
an error via ConsoleLogger.error and aborting or returning non-zero when the
installer fails so success messages printed later are conditional on a
successful install. Ensure you update references to shouldInstallSkills /
AddCommand().execute() to use the targetDir-aware invocation and explicitly
check the result to handle failures.

Comment thread packages/stac_cli/lib/src/commands/skills/add_command.dart Outdated
Comment on lines +101 to +116
for (final skill in catalog) {
final skillName = skill['name'];
final skillPath = skill['path'];

if (skillName == null || skillPath == null) continue;

final sourceSkillDir = Directory(path.join(repoRoot.path, skillPath));
if (!await sourceSkillDir.exists()) {
ConsoleLogger.warning('Skill directory $skillPath not found, skipping.');
continue;
}

final targetSkillDir = Directory(path.join(targetAgentsDir.path, skillName));
if (await targetSkillDir.exists()) {
await targetSkillDir.delete(recursive: true);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Validate and constrain skillName/skillPath before filesystem operations.

At Line 107 and Line 113-116, untrusted values from catalog.json are used to build source/target paths and to recursively delete directories. A crafted name or path (for example with .. or absolute paths) can escape the repo/.agents/skills boundaries and overwrite/delete arbitrary local directories.

Use canonical paths and enforce boundary checks before read/copy/delete.

Suggested fix
+      String _canonical(String p) => Directory(p).resolveSymbolicLinksSync();
+
       for (final skill in catalog) {
-        final skillName = skill['name'];
-        final skillPath = skill['path'];
+        final skillName = skill['name'] as String?;
+        final skillPath = skill['path'] as String?;
 
         if (skillName == null || skillPath == null) continue;
+        if (path.isAbsolute(skillName) || path.isAbsolute(skillPath)) {
+          ConsoleLogger.warning('Invalid absolute path in catalog, skipping.');
+          continue;
+        }
 
         final sourceSkillDir = Directory(path.join(repoRoot.path, skillPath));
+        final canonicalSource = _canonical(sourceSkillDir.path);
+        final canonicalRepoRoot = _canonical(repoRoot.path);
+        if (!path.isWithin(canonicalRepoRoot, canonicalSource)) {
+          ConsoleLogger.warning('Skill path escapes repository root, skipping.');
+          continue;
+        }
         if (!await sourceSkillDir.exists()) {
           ConsoleLogger.warning('Skill directory $skillPath not found, skipping.');
           continue;
         }
 
         final targetSkillDir = Directory(path.join(targetAgentsDir.path, skillName));
+        final canonicalTargetBase = _canonical(targetAgentsDir.path);
+        final canonicalTarget = _canonical(targetSkillDir.path);
+        if (!path.isWithin(canonicalTargetBase, canonicalTarget)) {
+          ConsoleLogger.warning('Skill name escapes target directory, skipping.');
+          continue;
+        }
         if (await targetSkillDir.exists()) {
           await targetSkillDir.delete(recursive: true);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (final skill in catalog) {
final skillName = skill['name'];
final skillPath = skill['path'];
if (skillName == null || skillPath == null) continue;
final sourceSkillDir = Directory(path.join(repoRoot.path, skillPath));
if (!await sourceSkillDir.exists()) {
ConsoleLogger.warning('Skill directory $skillPath not found, skipping.');
continue;
}
final targetSkillDir = Directory(path.join(targetAgentsDir.path, skillName));
if (await targetSkillDir.exists()) {
await targetSkillDir.delete(recursive: true);
}
String _canonical(String p) => Directory(p).resolveSymbolicLinksSync();
for (final skill in catalog) {
final skillName = skill['name'] as String?;
final skillPath = skill['path'] as String?;
if (skillName == null || skillPath == null) continue;
if (path.isAbsolute(skillName) || path.isAbsolute(skillPath)) {
ConsoleLogger.warning('Invalid absolute path in catalog, skipping.');
continue;
}
final sourceSkillDir = Directory(path.join(repoRoot.path, skillPath));
final canonicalSource = _canonical(sourceSkillDir.path);
final canonicalRepoRoot = _canonical(repoRoot.path);
if (!path.isWithin(canonicalRepoRoot, canonicalSource)) {
ConsoleLogger.warning('Skill path escapes repository root, skipping.');
continue;
}
if (!await sourceSkillDir.exists()) {
ConsoleLogger.warning('Skill directory $skillPath not found, skipping.');
continue;
}
final targetSkillDir = Directory(path.join(targetAgentsDir.path, skillName));
final canonicalTargetBase = _canonical(targetAgentsDir.path);
final canonicalTarget = _canonical(targetSkillDir.path);
if (!path.isWithin(canonicalTargetBase, canonicalTarget)) {
ConsoleLogger.warning('Skill name escapes target directory, skipping.');
continue;
}
if (await targetSkillDir.exists()) {
await targetSkillDir.delete(recursive: true);
}
🤖 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 `@packages/stac_cli/lib/src/commands/skills/add_command.dart` around lines 101
- 116, The code uses untrusted catalog values (skillName, skillPath) to build
sourceSkillDir and targetSkillDir and perform filesystem ops; validate and
canonicalize both values before any filesystem access: reject empty, absolute
paths, or values containing path traversal or path separators for skillName
(enforce a single safe token), normalize skillPath and compute its
absolute/resolved path and ensure it is a descendant of repoRoot (use resolved
canonical paths) before reading/copying, and ensure targetSkillDir resolved path
is inside targetAgentsDir before deleting or writing; if checks fail, log a
warning and skip the entry. Use the existing variables skillName, skillPath,
sourceSkillDir, targetSkillDir, repoRoot, and targetAgentsDir when adding these
validations.

Comment thread packages/stac_cli/test/commands/cli_commands_test.dart Outdated
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.

3 participants