Skip to content

refactor(ralph-loop): sync with official ralph-wiggum plugin - #446

Closed
baleen37 wants to merge 2 commits into
mainfrom
sync-ralph-loop-from-official
Closed

refactor(ralph-loop): sync with official ralph-wiggum plugin#446
baleen37 wants to merge 2 commits into
mainfrom
sync-ralph-loop-from-official

Conversation

@baleen37

@baleen37 baleen37 commented Feb 7, 2026

Copy link
Copy Markdown
Owner

Summary

Syncs ralph-loop plugin architecture with the official ralph-wiggum plugin, simplifying from a multi-session enhanced version back to the official single-session approach.

Changes

Removed Features

  • SessionStart hook: Removed multi-session initialization
  • Project UUID system: Removed project-scoped state files
  • Session ID validation: Removed complex session management
  • State library: Removed scripts/lib/state.sh dependency

Updated Files

  • hooks/hooks.json: Removed SessionStart hook, matches official
  • hooks/stop-hook.sh: Reverted to official implementation (177 lines)
  • commands/cancel-ralph.md: Uses inline operations vs script
  • commands/ralph-loop.md: Minor description update
  • commands/help.md: Fixed markdownlint issues
  • README.md: Fixed markdownlint issues

Deleted Files

  • hooks/session-start-hook.sh
  • scripts/cancel-ralph.sh
  • scripts/lib/state.sh
  • package.json, bun.lock (not needed for plugins)
  • tests/ralph-loop-specific.bats

Preserved Local Differences

  • Plugin name: ralph-loop (branding preference)
  • Version: 5.7.2 (local versioning scheme)
  • Safety improvement: Empty array handling in setup script
  • Marketplace configuration for publishing

Motivation

The official plugin's simpler architecture is sufficient for current use cases. The multi-session enhancements added complexity without clear benefit, and maintaining them in sync with official updates was becoming difficult.

Testing

  • All ralph-loop functionality verified with direct script testing
  • Setup script creates state file correctly
  • Stop hook manages iteration counting properly
  • Cancel command removes state file as expected
  • Markdownlint issues resolved

Auto Merge

This PR is ready for auto-merge when CI passes.

Summary by CodeRabbit

  • Documentation

    • Rebranded plugin to "Ralph Wiggum", refreshed guides/examples, and switched completion promise examples to a tagged format.
    • Simplified command docs and consolidated state-file path references.
  • Refactor

    • Unified loop state to a single local state file; simplified start/stop/cancel flows and removed multi-session state handling.
  • Tests

    • Overhauled test infrastructure: new helpers, many new unit/integration/performance tests, and refactored test suites.
  • Chores

    • Version bumps (5.7.1 → 5.7.2).

Revert to simpler single-session architecture matching official:
- Remove SessionStart hook and session-start-hook.sh
- Remove project UUID system and lib/state.sh
- Use simple .claude/ralph-loop.local.md state file
- Update hooks.json to match official structure
- Consolidate cancel-ralph logic into command file

Keep local differences:
- Plugin name: ralph-loop (branding preference)
- Version: 5.7.2 (continuing local versioning)
- Safety: empty array handling in setup script
- Fix markdownlint issues (line length, code blocks)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 7, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The Ralph Loop plugin is comprehensively refactored, consolidating state management from a multi-session, per-session directory architecture to a single local .claude/ralph-loop.local.md file. The plugin is renamed from "Ralph Loop" to "Ralph Wiggum," and session-start hooks and the state library are removed. Documentation and command behaviors are updated accordingly.

Changes

Cohort / File(s) Summary
Metadata & Configuration
plugins/ralph-loop/.claude-plugin/marketplace.json, plugins/ralph-loop/.claude-plugin/plugin.json, plugins/ralph-loop/hooks/hooks.json
Manifest updates and version bumps; $schema removed from hooks.json; SessionStart hook removed, Stop hook retained.
Documentation
plugins/ralph-loop/README.md, plugins/ralph-loop/commands/help.md, plugins/ralph-loop/commands/ralph-loop.md
Rename to "Ralph Wiggum" and editorial/formatting updates; state path references moved to .claude/ralph-loop.local.md; examples now use <promise>...</promise> tag; multi-session support docs removed.
Command Docs
plugins/ralph-loop/commands/cancel-ralph.md
Cancel flow changed from invoking a script to explicit step-by-step Bash checks and removal of the fixed .claude/ralph-loop.local.md file.
Setup & CLI Script
plugins/ralph-loop/scripts/setup-ralph-loop.sh
Argument parsing rewritten; state model migrated from per-session files to a single .claude/ralph-loop.local.md YAML-frontmatter file with fields: active, iteration, max_iterations, completion_promise, started_at; removed project UUID/session-id logic.
Stop Hook
plugins/ralph-loop/hooks/stop-hook.sh
Refactored to read/write the single fixed state file .claude/ralph-loop.local.md; frontmatter parsing simplified (sed/grep); state variable renamed to RALPH_STATE_FILE; validation and error messaging simplified.
Removed scripts & libs
plugins/ralph-loop/hooks/session-start-hook.sh, plugins/ralph-loop/scripts/cancel-ralph.sh, plugins/ralph-loop/scripts/lib/state.sh, plugins/ralph-loop/tests/ralph-loop-specific.bats
Deleted session-start hook, cancel script, and the state utility library; corresponding per-session tests removed.
Tests & Test infra
tests/helpers/*, tests/*, plugins/conversation-memory/src/core/*, tests/integration/*, etc.
Large addition and rework of test helpers, fixture factories, new assertion helpers, many new unit/integration/performance tests; several parser tests added and one parser test deleted; extensive BATS refactors and new test utilities introduced.
Parser logic
plugins/conversation-memory/src/core/parser.ts
Emit exchanges when tool calls exist even without assistant text; exchange ID generation now uses MD5 hex digest of path+line range; tool calls assigned exchangeId; empty-message filtering adjusted to treat tool_result-only messages as non-empty.

Sequence Diagram(s)

sequenceDiagram
  participant User as User (CLI)
  participant Setup as setup-ralph-loop.sh
  participant FS as FileSystem (.claude/ralph-loop.local.md)
  participant Stop as stop-hook.sh

  User->>Setup: run /ralph-loop (with flags)
  Setup->>FS: create/write `.claude/ralph-loop.local.md` (frontmatter: active, iteration, max_iterations, completion_promise, started_at)
  Note right of FS: Single fixed per-repo state file
  User->>Stop: trigger Stop hook (e.g., stop event)
  Stop->>FS: read `.claude/ralph-loop.local.md`
  Stop->>FS: update iteration / optionally delete file
  Stop-->>User: emit JSON decision block / status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

released

Poem

🐰 I hopped through loops both wide and deep,
Now one small file keeps the promises I keep.
No sessions scattered in a thousand places,
Just .claude/ralph-loop.local.md with tidy traces.
Hooray — one loop, one home, many carrot embraces! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: syncing the ralph-loop plugin with the official ralph-wiggum plugin architecture.
Docstring Coverage ✅ Passed Docstring coverage is 97.06% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sync-ralph-loop-from-official

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.

@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: 3

🤖 Fix all issues with AI agents
In `@plugins/ralph-loop/hooks/stop-hook.sh`:
- Around line 56-67: The jq call that sets TRANSCRIPT_PATH can fail if
HOOK_INPUT is empty/malformed, which would abort the script under set -e and
skip the cleanup; wrap or guard the jq invocation so it cannot cause a non-zero
exit (e.g., capture jq stderr and use a fallback/quiet failure or temporarily
disable set -e around the command) and then perform a robust check (if [[ -z
"$TRANSCRIPT_PATH" || ! -f "$TRANSCRIPT_PATH" ]]) to run the same cleanup (rm
"$RALPH_STATE_FILE" and exit 0) when jq fails or returns an empty path; update
the code around the TRANSCRIPT_PATH assignment and the subsequent file-existence
branch to ensure cleanup always runs even if jq fails.
- Around line 90-105: The jq failure check is unreachable under set -e because
command substitution will abort the script; change the flow to run jq in a
conditional so its non-zero exit is handled (e.g. use if ! LAST_OUTPUT=$(echo
"$LAST_LINE" | jq -r '...'' 2>&1); then ... fi). Specifically, replace the
current assignment + subsequent "$?" check with an if-not wrapper that captures
LAST_OUTPUT and, on failure, writes the same stderr messages, removes
RALPH_STATE_FILE and exits 0; ensure you refer to LAST_LINE, LAST_OUTPUT, jq,
and RALPH_STATE_FILE when making the change.

In `@plugins/ralph-loop/scripts/setup-ralph-loop.sh`:
- Around line 137-142: The current YAML quoting of COMPLETION_PROMISE can
produce malformed YAML when the value contains quotes or backslashes; update the
block that sets COMPLETION_PROMISE_YAML to validate and reject problematic
characters instead of blindly quoting: in the script around the
COMPLETION_PROMISE / COMPLETION_PROMISE_YAML logic, add a check on
COMPLETION_PROMISE for characters like double-quote (") and backslash (\) (and
optionally newlines) and if present print a clear error and exit non-zero,
otherwise safely wrap the value in double quotes as you do now; reference
COMPLETION_PROMISE and COMPLETION_PROMISE_YAML in your change so the guard runs
before assigning COMPLETION_PROMISE_YAML.
🧹 Nitpick comments (1)
plugins/ralph-loop/scripts/setup-ralph-loop.sh (1)

144-154: The active: true frontmatter field is written but never read.

The stop hook (lines 15–17) checks file existence to determine if a loop is active — it never reads the active field. This is dead metadata. Not harmful, but worth noting for future cleanup or to start using it (e.g., for pausing without deleting the file).

Comment on lines 56 to 67

# Get transcript path from hook input
TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path')

if [[ ! -f "$TRANSCRIPT_PATH" ]]; then
echo "⚠️ Ralph loop: Transcript file not found" >&2
echo " Expected: $TRANSCRIPT_PATH" >&2
echo " This is unusual and may indicate a Claude Code internal issue." >&2
echo " Ralph loop is stopping." >&2
rm "$STATE_FILE"
exit 0
echo "⚠️ Ralph loop: Transcript file not found" >&2
echo " Expected: $TRANSCRIPT_PATH" >&2
echo " This is unusual and may indicate a Claude Code internal issue." >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same set -e risk applies to the jq call on line 58.

If HOOK_INPUT is empty or malformed, jq -r '.transcript_path' will fail, and the script will abort without running the cleanup on lines 61–66. Though the risk is lower (Claude provides the hook input), defensive handling is still warranted for the same reason as above.

Proposed fix
-TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path')
+TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path' 2>/dev/null) || TRANSCRIPT_PATH=""
📝 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
# Get transcript path from hook input
TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path')
if [[ ! -f "$TRANSCRIPT_PATH" ]]; then
echo "⚠️ Ralph loop: Transcript file not found" >&2
echo " Expected: $TRANSCRIPT_PATH" >&2
echo " This is unusual and may indicate a Claude Code internal issue." >&2
echo " Ralph loop is stopping." >&2
rm "$STATE_FILE"
exit 0
echo "⚠️ Ralph loop: Transcript file not found" >&2
echo " Expected: $TRANSCRIPT_PATH" >&2
echo " This is unusual and may indicate a Claude Code internal issue." >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
# Get transcript path from hook input
TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path' 2>/dev/null) || TRANSCRIPT_PATH=""
if [[ ! -f "$TRANSCRIPT_PATH" ]]; then
echo "⚠️ Ralph loop: Transcript file not found" >&2
echo " Expected: $TRANSCRIPT_PATH" >&2
echo " This is unusual and may indicate a Claude Code internal issue." >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
🤖 Prompt for AI Agents
In `@plugins/ralph-loop/hooks/stop-hook.sh` around lines 56 - 67, The jq call that
sets TRANSCRIPT_PATH can fail if HOOK_INPUT is empty/malformed, which would
abort the script under set -e and skip the cleanup; wrap or guard the jq
invocation so it cannot cause a non-zero exit (e.g., capture jq stderr and use a
fallback/quiet failure or temporarily disable set -e around the command) and
then perform a robust check (if [[ -z "$TRANSCRIPT_PATH" || ! -f
"$TRANSCRIPT_PATH" ]]) to run the same cleanup (rm "$RALPH_STATE_FILE" and exit
0) when jq fails or returns an empty path; update the code around the
TRANSCRIPT_PATH assignment and the subsequent file-existence branch to ensure
cleanup always runs even if jq fails.

Comment on lines 90 to 105
LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
.message.content |
map(select(.type == "text")) |
map(.text) |
join("\n")
' 2>/dev/null)
.message.content |
map(select(.type == "text")) |
map(.text) |
join("\n")
' 2>&1)

# Check if jq succeeded
if ! echo "$LAST_LINE" | jq -r '.message.content' >/dev/null 2>&1; then
echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
echo " This may indicate a transcript format issue" >&2
echo " Ralph loop is stopping." >&2
rm "$STATE_FILE"
exit 0
if [[ $? -ne 0 ]]; then
echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
echo " Error: $LAST_OUTPUT" >&2
echo " This may indicate a transcript format issue" >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

$? check is unreachable under set -e — jq failure will abort the script.

With set -euo pipefail active, if jq returns a non-zero exit code, the assignment LAST_OUTPUT=$(...) on line 90 causes the script to exit immediately. The graceful error handling on lines 98–105 is never reached.

This means a malformed transcript entry will crash the hook ungracefully instead of cleaning up the state file and allowing exit.

Proposed fix: capture exit code properly
-LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
+LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
   .message.content |
   map(select(.type == "text")) |
   map(.text) |
   join("\n")
-' 2>&1)
-
-# Check if jq succeeded
-if [[ $? -ne 0 ]]; then
+' 2>/dev/null) || {
   echo "⚠️  Ralph loop: Failed to parse assistant message JSON" >&2
-  echo "   Error: $LAST_OUTPUT" >&2
   echo "   This may indicate a transcript format issue" >&2
   echo "   Ralph loop is stopping." >&2
   rm "$RALPH_STATE_FILE"
   exit 0
-fi
+}

As per coding guidelines, hook scripts must use stderr for error messages — the error output is correct, but the control flow around $? defeats the purpose of the error handling.

📝 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
LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
.message.content |
map(select(.type == "text")) |
map(.text) |
join("\n")
' 2>/dev/null)
.message.content |
map(select(.type == "text")) |
map(.text) |
join("\n")
' 2>&1)
# Check if jq succeeded
if ! echo "$LAST_LINE" | jq -r '.message.content' >/dev/null 2>&1; then
echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
echo " This may indicate a transcript format issue" >&2
echo " Ralph loop is stopping." >&2
rm "$STATE_FILE"
exit 0
if [[ $? -ne 0 ]]; then
echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
echo " Error: $LAST_OUTPUT" >&2
echo " This may indicate a transcript format issue" >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
.message.content |
map(select(.type == "text")) |
map(.text) |
join("\n")
' 2>/dev/null) || {
echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
echo " This may indicate a transcript format issue" >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
}
🤖 Prompt for AI Agents
In `@plugins/ralph-loop/hooks/stop-hook.sh` around lines 90 - 105, The jq failure
check is unreachable under set -e because command substitution will abort the
script; change the flow to run jq in a conditional so its non-zero exit is
handled (e.g. use if ! LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '...'' 2>&1);
then ... fi). Specifically, replace the current assignment + subsequent "$?"
check with an if-not wrapper that captures LAST_OUTPUT and, on failure, writes
the same stderr messages, removes RALPH_STATE_FILE and exits 0; ensure you refer
to LAST_LINE, LAST_OUTPUT, jq, and RALPH_STATE_FILE when making the change.

Comment on lines 137 to 142
# Quote completion promise for YAML if it contains special chars or is not null
if [[ -n "$COMPLETION_PROMISE" ]] && [[ "$COMPLETION_PROMISE" != "null" ]]; then
COMPLETION_PROMISE_YAML="\"$COMPLETION_PROMISE\""
COMPLETION_PROMISE_YAML="\"$COMPLETION_PROMISE\""
else
COMPLETION_PROMISE_YAML="null"
COMPLETION_PROMISE_YAML="null"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

YAML quoting is fragile for special characters in COMPLETION_PROMISE.

If the user supplies a promise containing double quotes, backslashes, or other YAML-special characters (e.g., --completion-promise 'say "done"'), the resulting YAML will be malformed — completion_promise: "say "done"" — and the stop hook's sed/grep parsing will extract the wrong value or fail silently.

Consider escaping or single-quoting the value, or rejecting promises with problematic characters.

Proposed minimal fix: reject quotes in the promise
     --completion-promise)
       if [[ -z "${2:-}" ]]; then
         ...
       fi
+      if [[ "$2" == *'"'* ]] || [[ "$2" == *'\'* ]]; then
+        echo "❌ Error: --completion-promise must not contain double quotes or backslashes" >&2
+        exit 1
+      fi
       COMPLETION_PROMISE="$2"
🤖 Prompt for AI Agents
In `@plugins/ralph-loop/scripts/setup-ralph-loop.sh` around lines 137 - 142, The
current YAML quoting of COMPLETION_PROMISE can produce malformed YAML when the
value contains quotes or backslashes; update the block that sets
COMPLETION_PROMISE_YAML to validate and reject problematic characters instead of
blindly quoting: in the script around the COMPLETION_PROMISE /
COMPLETION_PROMISE_YAML logic, add a check on COMPLETION_PROMISE for characters
like double-quote (") and backslash (\) (and optionally newlines) and if present
print a clear error and exit non-zero, otherwise safely wrap the value in double
quotes as you do now; reference COMPLETION_PROMISE and COMPLETION_PROMISE_YAML
in your change so the guard runs before assigning COMPLETION_PROMISE_YAML.

- Create test utilities layer (tests/helpers/test_utils.bash)
  - Add plugin discovery functions: find_all_plugins, get_plugin_manifest
  - Add validation wrappers: assert_valid_plugin, assert_valid_skill
  - Add file iteration helpers: for_each_skill_file, for_each_command_file
  - Add comprehensive validation: validate_plugin_manifest_comprehensive

- Standardize fixture creation (tests/helpers/fixture_factory.bash)
  - Add create_marketplace_json, create_hooks_json, create_skill_md
  - Add create_plugin_with_custom_fields for custom JSON fields
  - Refactor negative_tests, edge_cases, validate_plugin_manifest

- Split parser.test.ts into 5 focused test files
  - parser-basic.test.ts (basic parsing, 11 tests)
  - parser-exclusion.test.ts (exclusion markers, 8 tests)
  - parser-metadata.test.ts (metadata extraction, 5 tests)
  - parser-tool-calls.test.ts (tool call handling, 8 tests)
  - parser-edge-cases.test.ts (edge cases, 9 tests)

- Create common plugin validation module
  - Add validate_plugin_manifest_comprehensive for complete validation
  - Add check_all_plugin_manifests, count_valid_plugins, get_invalid_plugins
  - Create tests/plugin_validation_common.bats with reusable patterns

- Standardize assertion patterns (tests/helpers/bats_helper.bash)
  - Add assert_json_field, assert_json_field_type
  - Add assert_output_contains, assert_output_matches
  - Add assert_exit_code for consistent exit code checking

- Fix missing matcher field in ralph-loop/hooks/hooks.json

Results: All 162 tests passing, ~30-40% reduction in duplication

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@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: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/TESTING.md (1)

302-310: ⚠️ Potential issue | 🟡 Minor

Example uses old load path.

Line 304 says Source the helper: load bats_helper.bash but the actual convention used throughout the codebase (and shown correctly in line 310) is load helpers/bats_helper. The instruction text on line 304 should match.

🤖 Fix all issues with AI agents
In @.claude/ralph-loop.local.md:
- Around line 1-7: Add .claude/ralph-loop.local.md and
.claude/settings.local.json (or the patterns "*.local.md" and
"settings.local.json" under the .claude directory) to .gitignore, then remove
the tracked copies from Git index (git rm --cached) and commit the change so
those ephemeral runtime files are no longer committed; update .gitignore to
include the new patterns and ensure you stage and commit the removal of
.claude/ralph-loop.local.md and .claude/settings.local.json from the repository.

In `@docs/TESTING.md`:
- Around line 46-54: The "### Test Count" block lists category counts that sum
to 235 but shows a total of 178; update that block under the heading "### Test
Count" so the total is correct (change **Total: 178 tests** to **Total: 235
tests**) or explicitly indicate which categories are subsets of Unit Tests
(e.g., annotate "Error Handling Tests", "Edge Case Tests", "Negative Tests" as
subsets) and then adjust the displayed totals accordingly to remove
double-counting.
- Around line 265-272: Update the Fixture Factory section to reflect the new
signatures: change the `create_minimal_plugin` and `create_full_plugin` entries
to accept two optional author parameters (`[author_name] [author_email]`)
instead of a single `[author]`; keep the other function signatures as-is and
ensure the descriptions note that author name and email are separate optional
arguments so callers pass both when available.
- Around line 215-220: Remove the duplicated "Performance benchmarks for
critical operations" bullet list that was copy-pasted under the
"edge_cases.bats" section; specifically locate the block that starts with
"Performance benchmarks for critical operations:" and the bullets ("Plugin list
caching effectiveness", "JSON parsing speed", "File operation efficiency", "Test
execution timing") in the edge_cases.bats section and delete that redundant
block so only the original list (the one in Lines 188–193) remains.

In `@plugins/conversation-memory/src/core/parser-basic.test.ts`:
- Around line 160-172: The test case name and assertions are misleading because
parseConversationFile(tempFilePath) uses an absolute tempFilePath which will
always have ≥2 path segments and thus never hit the "unknown" fallback; update
the test by either renaming it to reflect that it only asserts project is a
defined string, or modify the input to actually exercise the fallback by passing
a path with fewer than 2 segments (e.g., just 'test-conversation.jsonl' or a
bare filename) to parseConversationFile and then assert result.project ===
'unknown'; reference the existing test function and
tempFilePath/parseConversationFile identifiers when making the change.

In `@plugins/conversation-memory/src/core/parser-tool-calls.test.ts`:
- Around line 201-203: Update the stale test comment in
plugins/conversation-memory/src/core/parser-tool-calls.test.ts to reflect that
this is a regression test for a previously-fixed parser bug: rephrase the line
referencing "This is the bug: parser requires assistantMessages.length > 0" to
state that the parser bug was fixed (see parser.ts check for toolCalls.length >
0) and that the test ensures conversations with only tool_use blocks (no
assistantMessages) still produce an exchange; reference symbols
assistantMessages and toolCalls and the parser.ts fix in the comment for
clarity.

In `@tests/edge_cases.bats`:
- Around line 139-146: The test passes coincidentally because create_skill_md in
fixture_factory.bash injects a literal "\n" instead of an actual newline,
producing malformed frontmatter with name and description on the same line; fix
create_skill_md to emit a real newline between frontmatter fields (use
printf/heredoc or proper newline expansion rather than a backslash-n literal) so
files generated by create_skill_md produce valid YAML frontmatter, and then
update tests (e.g., this case in tests/edge_cases.bats) to also assert the
description field via has_frontmatter_field to prevent regressions.

In `@tests/helpers/fixture_factory.bash`:
- Around line 330-346: The frontmatter string uses literal "\n" inside double
quotes so newlines are not being produced; update the construction of the
frontmatter variable (the local variable frontmatter and the conditional
appending of additional_frontmatter) to use an actual newline (for example via
$'\n' quoting or printf) so that name and description are on separate lines in
the generated SKILL.md; ensure the code that writes SKILL.md still uses the
frontmatter variable unchanged and that additional_frontmatter is appended with
the same newline semantics.

In `@tests/integration/plugin_loading.bats`:
- Around line 133-137: The inline Python call uses
yaml.safe_load(open('$workflow')) which breaks and allows shell-injection when
$workflow contains quotes; change the invocation to pass the path as an argument
(use python3 -c with sys.argv[1]) so the script reads the file from argv instead
of interpolating $workflow into the command string, keeping the surrounding
logic (the elif branch that checks python3 and the invalid_count increment)
unchanged.

In `@tests/negative_tests.bats`:
- Around line 170-179: The test "rejects plugin.json with wrong field type"
currently only creates a fixture with create_plugin_with_custom_fields and
asserts the manifest's keywords is a string via assert_json_field_type; change
it to actually run the type-check validator and assert it fails: after building
the manifest (using create_plugin_with_custom_fields and manifest path), invoke
the validator command/function used elsewhere in tests (e.g., run validate_json
or the CLI validator) against "$manifest" and assert a non-zero exit (e.g., use
run .. and assert_failure or equivalent) and optionally assert the validator
output mentions "keywords" or "type"; keep the existing helpers
validate_json/assert_json_field_type for verifying fixture setup but add the
run+failure assertion to ensure the validator rejects the wrong-type "keywords".
- Around line 128-136: The test "rejects marketplace.json with duplicate plugin
names" currently calls validate_json without running or asserting failure;
change the test to invoke validate_json via the test harness (use run
validate_json "$marketplace_file") and then assert a non-zero exit (e.g., [
"$status" -ne 0 ] or run's failure assertion) and/or assert the stderr contains
a duplicate-name error; update the test block referencing validate_json and the
test name to ensure it actually fails when duplicate names ("dup-plugin") are
present.
- Around line 51-59: The test named "rejects hooks.json with invalid structure"
is asserting success against validate_json, which only checks JSON validity;
either update the test to actually assert rejection by calling the structural
validator (replace or follow validate_json with the schema/structure check
function—e.g., call validate_hooks or validate_json_schema on "$hooks_file") and
assert a non-zero exit status, or rename the test string to reflect current
behavior (e.g., "accepts valid JSON with invalid hooks structure") and adjust
the inline comment; locate the test by the `@test` "rejects hooks.json with
invalid structure" line and modify the assertion around validate_json
accordingly.

In `@tests/new_assertions.bats`:
- Around line 19-22: Add a failing test for assert_dir_not_exists by creating a
directory under TEST_TEMP_DIR and asserting the helper returns a non-zero
status; specifically add a test named like "assert_dir_not_exists fails when
directory exists" that creates the directory (mkdir -p), runs
assert_dir_not_exists on that path, and checks that "$status" is non-zero to
verify the assertion fails. Reference the existing test pattern in
tests/new_assertions.bats and reuse TEST_TEMP_DIR and a local variable (e.g.,
existing) to locate where to add the new test.

In `@tests/performance/benchmarks.bats`:
- Around line 42-62: The test divides total_time by plugin_count (and later by
file_count) which will cause a division-by-zero if no plugins/files matched;
update the benchmark test to guard the division by checking plugin_count (and
file_count) > 0 before computing avg_time or asserting — e.g., after the loop
that uses parse_plugin_json, if plugin_count is zero, either skip/fail the test
with a clear message or set avg_time to 0 and avoid the division; apply the same
pattern for the file_count block that uses benchmark_start/benchmark_end; use
the existing symbols parse_plugin_json, benchmark_start, benchmark_end,
total_time, plugin_count, file_count, and avg_time to locate and modify the
code.
- Around line 12-21: The current benchmark_start/benchmark_end uses date +%s%N
which is not portable (macOS emits a literal %N); replace that with a portable
helper get_time_ns() that returns an integer nanosecond timestamp: try date
+%s%N and detect if the output contains a literal "%N" (or if date supports %N),
and if not fall back to a reliable interpreter (e.g., python3 -c "import time;
print(int(time.time()*1e9))") or another portable method; then change
benchmark_start to set _BENCHMARK_START_TIME=$(get_time_ns) and benchmark_end to
read end_time=$(get_time_ns) and compute milliseconds via $(( (end_time -
_BENCHMARK_START_TIME) / 1000000 )), keeping the function names benchmark_start,
benchmark_end and the variable _BENCHMARK_START_TIME unchanged.

In `@tests/plugin_validation_common.bats`:
- Around line 202-211: The final assertion in the test "common:
validate_plugin_manifest_comprehensive works on valid plugin" is masked by a
trailing "|| true", which makes the check always pass; update the assertion that
inspects the output variable lines (the expression currently using [
"${`#lines`[@]}" -eq 0 ] || [[ "${lines[*]}" == *"all"*"valid"* ]]) by removing
the "|| true" and leaving the two-part check so the test fails when there are
lines that do not contain the expected "all" and "valid" text; target the test
block name and the variables plugin_path and lines to locate and fix the
assertion.
🧹 Nitpick comments (16)
tests/helpers/fixture_factory.bash (1)

246-291: Heredoc-based JSON generation is fragile with special characters in inputs.

create_marketplace_json uses raw string interpolation into JSON heredocs. If marketplace_name, marketplace_description, or owner_name contain characters like " or \, the output will be malformed JSON. Since this is a test fixture factory with controlled inputs, this is low-risk, but worth noting.

Consider using jq (already available via $JQ_BIN) for generating JSON safely if you ever need to handle arbitrary inputs.

tests/helpers/test_utils.bash (3)

128-181: Inconsistent required fields between assert_valid_plugin and validate_plugin_manifest_comprehensive.

assert_valid_plugin (Line 148-173) requires name and version, while validate_plugin_manifest_comprehensive (Line 707) requires name, description, and author — but not version. Test authors using one vs. the other will get different validation behavior. Consider aligning the required fields or clearly documenting the intended difference.

Also applies to: 682-745


296-307: for_each_skill_file excludes .worktrees but not node_modules.

Other iteration helpers don't need this since they search under $PROJECT_ROOT/plugins, but for_each_skill_file searches from $PROJECT_ROOT (the entire project tree). You have is_under_node_modules defined below — consider adding node_modules exclusion to the grep -v chain to avoid picking up vendored SKILL.md files.

♻️ Suggested fix
-    skill_files=$(find "$PROJECT_ROOT" -name "SKILL.md" -type f 2>/dev/null | grep -v ".worktrees" | sort)
+    skill_files=$(find "$PROJECT_ROOT" -name "SKILL.md" -type f 2>/dev/null | grep -v ".worktrees" | grep -v "node_modules" | sort)

474-488: count_matches relies on word-splitting of $find_args — intentional but fragile.

Unquoted $find_args on Line 487 enables multi-argument passing but will break if any argument value contains spaces. This is fine for the documented usage pattern ("-name plugin.json -type f"), but worth a brief inline comment noting the intentional word-splitting.

tests/performance/benchmarks.bats (1)

110-132: Glob ** requires shopt -s globstar to recurse directories.

Line 114 uses "${PROJECT_ROOT}"/**/SKILL.md which only recurses if globstar is enabled. Without it, ** matches a single directory level. The skip on file_count=0 prevents hard failure, but the test may silently skip when it shouldn't.

♻️ Use `find` for portability
-    for skill_file in "${PROJECT_ROOT}"/**/SKILL.md; do
+    while IFS= read -r skill_file; do
-        if [ -f "$skill_file" ]; then
             benchmark_start
             has_frontmatter_delimiter "$skill_file"
             local elapsed
@@ -120,7 +120,7 @@
             total_time=$((total_time + elapsed))
             file_count=$((file_count + 1))
-        fi
-    done
+    done < <(find "${PROJECT_ROOT}" -name "SKILL.md" -type f 2>/dev/null)
plugins/conversation-memory/src/core/parser.ts (1)

158-171: Non-deterministic tool call IDs contrast with deterministic exchange IDs.

Exchange IDs are stable MD5 digests (line 94-97), but tool call IDs use crypto.randomUUID() (line 161), meaning re-parsing the same file produces different tool call IDs each time. If these IDs are persisted or used for deduplication/diffing, this creates inconsistency.

Consider deriving tool call IDs deterministically, e.g., hashing archivePath + lineNumber + blockIndex.

plugins/conversation-memory/src/core/parser-edge-cases.test.ts (1)

24-40: Weak assertion on malformed JSON handling.

The test constructs 2 valid user-assistant exchanges interleaved with invalid lines, but Line 39 only asserts toBeGreaterThanOrEqual(1). This makes the test pass even if one valid exchange is silently dropped. Consider asserting expect(exchanges).toHaveLength(2) to ensure all valid exchanges survive.

Suggested fix
-      expect(exchanges.length).toBeGreaterThanOrEqual(1);
+      expect(exchanges).toHaveLength(2);
tests/plugin_validation_common.bats (2)

187-200: Test name is misleading — it doesn't assert all plugins are valid.

The test is named "common: get_invalid_plugins returns empty list when all are valid" but it unconditionally passes even when invalid plugins are found (Lines 193-199 only validate the format of the invalid plugin names). The test name should reflect that it validates the function's output format, not that all plugins are valid.


75-161: Significant test overlap with tests/plugin_json.bats.

The common test cases here (valid JSON, required fields, non-empty values, naming convention, allowed fields, comprehensive validation) largely duplicate what's already tested in tests/plugin_json.bats. Since this file is documented as a "reusable module" that can be sourced by others, consider whether the @test blocks should be removed in favor of exporting only the helper functions, leaving the actual test assertions to consuming files.

plugins/conversation-memory/src/core/parser-exclusion.test.ts (1)

120-134: Prefer static import for parseConversationFile.

parseConversationWithResult is already statically imported from ./parser.js at Line 4. The dynamic await import('./parser.js') on Line 128 is unnecessary and inconsistent. Add parseConversationFile to the static import.

Suggested fix

At line 4:

-import { parseConversationWithResult } from './parser.js';
+import { parseConversationWithResult, parseConversationFile } from './parser.js';

At line 128:

-      const { parseConversationFile } = await import('./parser.js');
-      const result = await parseConversationFile(tempFilePath);
+      const result = await parseConversationFile(tempFilePath);
tests/validate_paths.bats (1)

52-58: The .git exclusion test is a no-op.

This test only checks if .git exists and then unconditionally passes with true. It doesn't verify that any path-checking logic actually excludes .git. Consider either implementing a real assertion or marking this as a TODO/skip with a reason.

tests/integration/cross_plugin_interactions.bats (2)

55-72: Consider skip instead of hard failure when no skills exist.

Line 71 asserts found_skills > 0, which will fail if the repo has no skills. If skills are optional, this should skip rather than fail, similar to how other tests handle optional features.


140-161: Same concern: hard failure when no agents found.

Line 160 asserts found_agents > 0. If agents are optional, consider using skip when none are found.

tests/plugin_json.bats (1)

77-96: json_get returns "null" string for null-valued JSON fields — not caught by empty check.

On Line 87, [ -z "$value" ] won't catch a JSON field with an explicit null value, since jq -r outputs the literal string "null". Consider adding a null check for robustness:

Suggested fix
-                if [ -z "$value" ]; then
+                if [ -z "$value" ] || [ "$value" = "null" ]; then
tests/negative_tests.bats (1)

138-144: Consider adding individual assertion messages for each invalid format.

Each run ! silently overwrites $status/$output, so if an intermediate case incorrectly passes (returns 0), BATS will fail the test but won't indicate which format was wrongly accepted. Splitting into separate @test blocks or adding per-case diagnostics would improve debuggability.

.claude/ralph-loop.local.md (1)

3-4: max_iterations: 0 for "unlimited" is ambiguous.

A value of 0 could reasonably mean "zero iterations allowed" or "unlimited." If this is the convention, it should be documented. A sentinel like -1 or a separate unlimited: true field would be clearer.

Comment on lines +1 to +7
---
active: true
iteration: 2
max_iterations: 0
completion_promise: "베스트프렉티스로 적용된 test 로 전부 되었을때"
started_at: "2026-02-07T05:31:19Z"
---

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if .claude/ralph-loop.local.md is gitignored
echo "=== Checking .gitignore for .local.md patterns ==="
if [ -f .gitignore ]; then
  cat .gitignore | grep -E "(\.local|ralph-loop)" || echo "No matching .gitignore entries found"
else
  echo "No .gitignore found"
fi

echo ""
echo "=== Checking if file is tracked ==="
git ls-files --error-unmatch .claude/ralph-loop.local.md 2>&1

echo ""
echo "=== Any .local.md files in git index? ==="
git ls-files '*.local.md' 2>/dev/null || echo "No .local.md files found"

echo ""
echo "=== Files in .claude/ directory ==="
git ls-files '.claude/*' 2>/dev/null | head -20

echo ""
echo "=== Check if .claude directory is in git ==="
git ls-tree -r HEAD .claude/ 2>/dev/null || echo "Not in current HEAD"

Repository: baleen37/claude-plugins

Length of output: 591


Add .local.md files to .gitignore.

The file .claude/ralph-loop.local.md is tracked in git and contains ephemeral runtime state (active: true, iteration: 2, started_at timestamp). Files matching the .local.md convention should not be committed. Additionally, .claude/settings.local.json has the same issue. Add both to .gitignore and remove them from git tracking.

🤖 Prompt for AI Agents
In @.claude/ralph-loop.local.md around lines 1 - 7, Add
.claude/ralph-loop.local.md and .claude/settings.local.json (or the patterns
"*.local.md" and "settings.local.json" under the .claude directory) to
.gitignore, then remove the tracked copies from Git index (git rm --cached) and
commit the change so those ephemeral runtime files are no longer committed;
update .gitignore to include the new patterns and ensure you stage and commit
the removal of .claude/ralph-loop.local.md and .claude/settings.local.json from
the repository.

Comment thread docs/TESTING.md
Comment on lines +46 to +54
### Test Count

- Unit Tests: 148 tests
- Integration Tests: 20 tests (10 plugin loading + 10 cross-plugin interactions)
- Performance Tests: 10 tests
- Error Handling Tests: 23 tests
- Edge Case Tests: 17 tests
- Negative Tests: 17 tests
- **Total: 178 tests**

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test count total doesn't add up.

The individual counts sum to 235 (148 + 20 + 10 + 23 + 17 + 17), but the total is listed as 178. Either the total should be updated or some categories (Error Handling, Edge Cases, Negative) are subsets of Unit Tests — which should be clarified.

🤖 Prompt for AI Agents
In `@docs/TESTING.md` around lines 46 - 54, The "### Test Count" block lists
category counts that sum to 235 but shows a total of 178; update that block
under the heading "### Test Count" so the total is correct (change **Total: 178
tests** to **Total: 235 tests**) or explicitly indicate which categories are
subsets of Unit Tests (e.g., annotate "Error Handling Tests", "Edge Case Tests",
"Negative Tests" as subsets) and then adjust the displayed totals accordingly to
remove double-counting.

Comment thread docs/TESTING.md
Comment on lines +215 to +220
Performance benchmarks for critical operations:

- Plugin list caching effectiveness
- JSON parsing speed
- File operation efficiency
- Test execution timing

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate content: performance benchmarks list repeated.

Lines 188–193 already describe performance benchmarks. Lines 215–220 repeat the exact same list under the "edge_cases.bats" section. This appears to be a copy-paste error.

🐛 Proposed fix — remove duplicate block
 - Minimal/maximum valid configurations
 
-Performance benchmarks for critical operations:
-
-- Plugin list caching effectiveness
-- JSON parsing speed
-- File operation efficiency
-- Test execution timing
-
 ## Test Helpers
📝 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
Performance benchmarks for critical operations:
- Plugin list caching effectiveness
- JSON parsing speed
- File operation efficiency
- Test execution timing
- Minimal/maximum valid configurations
## Test Helpers
🤖 Prompt for AI Agents
In `@docs/TESTING.md` around lines 215 - 220, Remove the duplicated "Performance
benchmarks for critical operations" bullet list that was copy-pasted under the
"edge_cases.bats" section; specifically locate the block that starts with
"Performance benchmarks for critical operations:" and the bullets ("Plugin list
caching effectiveness", "JSON parsing speed", "File operation efficiency", "Test
execution timing") in the edge_cases.bats section and delete that redundant
block so only the original list (the one in Lines 188–193) remains.

Comment thread docs/TESTING.md
Comment on lines +265 to +272
### Fixture Factory (`tests/helpers/fixture_factory.bash`)

- `create_minimal_plugin <base_dir> <name> [version] [author]`: Create minimal plugin
- `create_full_plugin <base_dir> <name> [version] [author]`: Create complete plugin
- `create_command_file <commands_dir> <name> <description>`: Create command file
- `create_agent_file <agents_dir> <name> <description> [model]`: Create agent file
- `create_skill_file <skills_dir> <name> <description> [content]`: Create skill directory
- `cleanup_fixtures <fixture_root>`: Clean up test fixtures

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fixture factory signatures in docs are stale.

The documentation shows [author] as a single optional parameter, but create_minimal_plugin and create_full_plugin now accept [author_name] [author_email] as separate parameters.

📝 Proposed fix
-- `create_minimal_plugin <base_dir> <name> [version] [author]`: Create minimal plugin
-- `create_full_plugin <base_dir> <name> [version] [author]`: Create complete plugin
+- `create_minimal_plugin <base_dir> <name> [version] [author_name] [author_email]`: Create minimal plugin
+- `create_full_plugin <base_dir> <name> [version] [author_name] [author_email]`: Create complete plugin
🤖 Prompt for AI Agents
In `@docs/TESTING.md` around lines 265 - 272, Update the Fixture Factory section
to reflect the new signatures: change the `create_minimal_plugin` and
`create_full_plugin` entries to accept two optional author parameters
(`[author_name] [author_email]`) instead of a single `[author]`; keep the other
function signatures as-is and ensure the descriptions note that author name and
email are separate optional arguments so callers pass both when available.

Comment on lines +160 to +172
test('returns "unknown" for project when path has insufficient parts', async () => {
const jsonlContent = [
{ type: 'user', message: { role: 'user', content: 'Hello' }, timestamp: '2024-01-01T00:00:00.000Z' },
{ type: 'assistant', message: { role: 'assistant', content: 'Hi!' }, timestamp: '2024-01-01T00:00:01.000Z' }
].map((obj) => JSON.stringify(obj));

fs.writeFileSync(tempFilePath, jsonlContent.join('\n'));
const result = await parseConversationFile(tempFilePath);

// tempFilePath ends with 'test-conversation.jsonl', so parent is the tempDir name
expect(result.project).toBeDefined();
expect(typeof result.project).toBe('string');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test name is misleading — it doesn't actually exercise the "unknown" project fallback.

tempFilePath is an absolute path (e.g., /tmp/parser-test-xxx/test-conversation.jsonl) which always has ≥ 2 parts, so project will be the temp directory name, never "unknown". The assertions (lines 170-171) only check that project is a defined string, which every path satisfies.

To actually test the "unknown" fallback, you'd need a path with fewer than 2 segments (e.g., just a filename with no directory). Consider renaming the test to reflect what it actually validates, or adjusting the test to cover the edge case.

🤖 Prompt for AI Agents
In `@plugins/conversation-memory/src/core/parser-basic.test.ts` around lines 160 -
172, The test case name and assertions are misleading because
parseConversationFile(tempFilePath) uses an absolute tempFilePath which will
always have ≥2 path segments and thus never hit the "unknown" fallback; update
the test by either renaming it to reflect that it only asserts project is a
defined string, or modify the input to actually exercise the fallback by passing
a path with fewer than 2 segments (e.g., just 'test-conversation.jsonl' or a
bare filename) to parseConversationFile and then assert result.project ===
'unknown'; reference the existing test function and
tempFilePath/parseConversationFile identifiers when making the change.

Comment thread tests/negative_tests.bats
Comment on lines +170 to +179
@test "rejects plugin.json with wrong field type" {
local FIXTURE_ROOT="${TEST_TEMP_DIR}/negative_tests"
local plugin_path
plugin_path=$(create_plugin_with_custom_fields "$FIXTURE_ROOT" "wrong-type-test" '"keywords": "should-be-array"')

local manifest="$plugin_path/.claude-plugin/plugin.json"
validate_json "$manifest"
# JSON is valid but type is wrong - verify it's a string not array
assert_json_field_type "$manifest" "keywords" "string" "keywords field should be string (wrong type for this test)"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test verifies the fixture, not the rejection of wrong types.

"rejects plugin.json with wrong field type" confirms that keywords is indeed a string (the intentionally wrong type), but never asserts that a validator rejects it. If the goal is to test that a type-checking validator flags this, a run + failure assertion on a type-validation function is needed.

🤖 Prompt for AI Agents
In `@tests/negative_tests.bats` around lines 170 - 179, The test "rejects
plugin.json with wrong field type" currently only creates a fixture with
create_plugin_with_custom_fields and asserts the manifest's keywords is a string
via assert_json_field_type; change it to actually run the type-check validator
and assert it fails: after building the manifest (using
create_plugin_with_custom_fields and manifest path), invoke the validator
command/function used elsewhere in tests (e.g., run validate_json or the CLI
validator) against "$manifest" and assert a non-zero exit (e.g., use run .. and
assert_failure or equivalent) and optionally assert the validator output
mentions "keywords" or "type"; keep the existing helpers
validate_json/assert_json_field_type for verifying fixture setup but add the
run+failure assertion to ensure the validator rejects the wrong-type "keywords".

Comment thread tests/new_assertions.bats
Comment on lines +19 to +22
@test "assert_dir_not_exists passes when directory does not exist" {
local non_existent="${TEST_TEMP_DIR}/does-not-exist"
assert_dir_not_exists "$non_existent" "Directory should not exist"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing failure-case test for assert_dir_not_exists.

Other assertion tests include both passing and failing cases (e.g., assert_file_not_exists has both at lines 6-17), but assert_dir_not_exists only tests the passing case. Consider adding a test that creates a directory and verifies the assertion fails.

Suggested addition after line 22
`@test` "assert_dir_not_exists fails when directory exists" {
    local existing="${TEST_TEMP_DIR}/exists-dir"
    mkdir -p "$existing"

    run assert_dir_not_exists "$existing"
    [ "$status" -ne 0 ]
}
🤖 Prompt for AI Agents
In `@tests/new_assertions.bats` around lines 19 - 22, Add a failing test for
assert_dir_not_exists by creating a directory under TEST_TEMP_DIR and asserting
the helper returns a non-zero status; specifically add a test named like
"assert_dir_not_exists fails when directory exists" that creates the directory
(mkdir -p), runs assert_dir_not_exists on that path, and checks that "$status"
is non-zero to verify the assertion fails. Reference the existing test pattern
in tests/new_assertions.bats and reuse TEST_TEMP_DIR and a local variable (e.g.,
existing) to locate where to add the new test.

Comment on lines +12 to +21
benchmark_start() {
_BENCHMARK_START_TIME=$(date +%s%N 2>/dev/null || date +%s000000000)
}

# Helper: End benchmark and return elapsed milliseconds
benchmark_end() {
local end_time
end_time=$(date +%s%N 2>/dev/null || date +%s000000000)
echo $(( (end_time - _BENCHMARK_START_TIME) / 1000000 ))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

date +%s%N is not portable to macOS — benchmark timing will be broken.

On macOS, date +%s%N succeeds (exit 0) but outputs literal %N in the result (e.g., 1707300000%N), so the || fallback never triggers. Subsequent arithmetic in benchmark_end will fail or produce nonsensical values.

🐛 Proposed fix — use a portable nanosecond timer with proper detection
 benchmark_start() {
-    _BENCHMARK_START_TIME=$(date +%s%N 2>/dev/null || date +%s000000000)
+    local ns
+    ns=$(date +%s%N 2>/dev/null)
+    if [[ "$ns" == *N* ]] || [[ -z "$ns" ]]; then
+        # macOS date doesn't support %N — fall back to seconds with zero-padded ns
+        ns=$(date +%s000000000)
+    fi
+    _BENCHMARK_START_TIME="$ns"
 }
 
 benchmark_end() {
     local end_time
-    end_time=$(date +%s%N 2>/dev/null || date +%s000000000)
+    end_time=$(date +%s%N 2>/dev/null)
+    if [[ "$end_time" == *N* ]] || [[ -z "$end_time" ]]; then
+        end_time=$(date +%s000000000)
+    fi
     echo $(( (end_time - _BENCHMARK_START_TIME) / 1000000 ))
 }
📝 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
benchmark_start() {
_BENCHMARK_START_TIME=$(date +%s%N 2>/dev/null || date +%s000000000)
}
# Helper: End benchmark and return elapsed milliseconds
benchmark_end() {
local end_time
end_time=$(date +%s%N 2>/dev/null || date +%s000000000)
echo $(( (end_time - _BENCHMARK_START_TIME) / 1000000 ))
}
benchmark_start() {
local ns
ns=$(date +%s%N 2>/dev/null)
if [[ "$ns" == *N* ]] || [[ -z "$ns" ]]; then
# macOS date doesn't support %N — fall back to seconds with zero-padded ns
ns=$(date +%s000000000)
fi
_BENCHMARK_START_TIME="$ns"
}
# Helper: End benchmark and return elapsed milliseconds
benchmark_end() {
local end_time
end_time=$(date +%s%N 2>/dev/null)
if [[ "$end_time" == *N* ]] || [[ -z "$end_time" ]]; then
end_time=$(date +%s000000000)
fi
echo $(( (end_time - _BENCHMARK_START_TIME) / 1000000 ))
}
🤖 Prompt for AI Agents
In `@tests/performance/benchmarks.bats` around lines 12 - 21, The current
benchmark_start/benchmark_end uses date +%s%N which is not portable (macOS emits
a literal %N); replace that with a portable helper get_time_ns() that returns an
integer nanosecond timestamp: try date +%s%N and detect if the output contains a
literal "%N" (or if date supports %N), and if not fall back to a reliable
interpreter (e.g., python3 -c "import time; print(int(time.time()*1e9))") or
another portable method; then change benchmark_start to set
_BENCHMARK_START_TIME=$(get_time_ns) and benchmark_end to read
end_time=$(get_time_ns) and compute milliseconds via $(( (end_time -
_BENCHMARK_START_TIME) / 1000000 )), keeping the function names benchmark_start,
benchmark_end and the variable _BENCHMARK_START_TIME unchanged.

Comment on lines +42 to +62
@test "plugin JSON parsing is reasonably fast" {
local total_time=0
local plugin_count=0

for plugin_dir in "${PROJECT_ROOT}"/plugins/*/; do
if [ -d "$plugin_dir" ]; then
benchmark_start
parse_plugin_json "$plugin_dir" > /dev/null
local elapsed
elapsed=$(benchmark_end)
total_time=$((total_time + elapsed))
plugin_count=$((plugin_count + 1))
fi
done

local avg_time=$((total_time / plugin_count))
echo "Parsed $plugin_count plugins in ${total_time}ms (avg: ${avg_time}ms each)"

# Each plugin should parse in less than 100ms
assert_lt "$avg_time" "100" "Average parse time should be under 100ms"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Division by zero if no plugins or JSON files are found.

Lines 57 and 91 compute $((total_time / plugin_count)) and $((total_time / file_count)) respectively. If the glob matches nothing, the counter stays at 0, causing a divide-by-zero arithmetic error.

🐛 Proposed fix (example for lines 56-58)
+    if [ "$plugin_count" -eq 0 ]; then
+        skip "No plugins found"
+    fi
     local avg_time=$((total_time / plugin_count))

Apply the same pattern before Line 91 for file_count.

Also applies to: 76-96

🤖 Prompt for AI Agents
In `@tests/performance/benchmarks.bats` around lines 42 - 62, The test divides
total_time by plugin_count (and later by file_count) which will cause a
division-by-zero if no plugins/files matched; update the benchmark test to guard
the division by checking plugin_count (and file_count) > 0 before computing
avg_time or asserting — e.g., after the loop that uses parse_plugin_json, if
plugin_count is zero, either skip/fail the test with a clear message or set
avg_time to 0 and avoid the division; apply the same pattern for the file_count
block that uses benchmark_start/benchmark_end; use the existing symbols
parse_plugin_json, benchmark_start, benchmark_end, total_time, plugin_count,
file_count, and avg_time to locate and modify the code.

Comment on lines +202 to +211
@test "common: validate_plugin_manifest_comprehensive works on valid plugin" {
local FIXTURE_ROOT="${TEST_TEMP_DIR}/comprehensive_test"
local plugin_path
plugin_path=$(create_minimal_plugin "$FIXTURE_ROOT" "test-valid-plugin")

run validate_plugin_manifest_comprehensive "$plugin_path"
# Should pass with exit code 0
[ "$status" -eq 0 ]
[ "${#lines[@]}" -eq 0 ] || [[ "${lines[*]}" == *"all"*"valid"* ]] || true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assertion on Line 210 always passes due to || true.

The expression [ "${#lines[@]}" -eq 0 ] || [[ "${lines[*]}" == *"all"*"valid"* ]] || true short-circuits to success regardless of output content. If the intent is to verify output when present, drop the || true:

Suggested fix
-    [ "${`#lines`[@]}" -eq 0 ] || [[ "${lines[*]}" == *"all"*"valid"* ]] || true
+    [ "${`#lines`[@]}" -eq 0 ] || [[ "${lines[*]}" == *"all"*"valid"* ]]
📝 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
@test "common: validate_plugin_manifest_comprehensive works on valid plugin" {
local FIXTURE_ROOT="${TEST_TEMP_DIR}/comprehensive_test"
local plugin_path
plugin_path=$(create_minimal_plugin "$FIXTURE_ROOT" "test-valid-plugin")
run validate_plugin_manifest_comprehensive "$plugin_path"
# Should pass with exit code 0
[ "$status" -eq 0 ]
[ "${#lines[@]}" -eq 0 ] || [[ "${lines[*]}" == *"all"*"valid"* ]] || true
}
`@test` "common: validate_plugin_manifest_comprehensive works on valid plugin" {
local FIXTURE_ROOT="${TEST_TEMP_DIR}/comprehensive_test"
local plugin_path
plugin_path=$(create_minimal_plugin "$FIXTURE_ROOT" "test-valid-plugin")
run validate_plugin_manifest_comprehensive "$plugin_path"
# Should pass with exit code 0
[ "$status" -eq 0 ]
[ "${`#lines`[@]}" -eq 0 ] || [[ "${lines[*]}" == *"all"*"valid"* ]]
}
🤖 Prompt for AI Agents
In `@tests/plugin_validation_common.bats` around lines 202 - 211, The final
assertion in the test "common: validate_plugin_manifest_comprehensive works on
valid plugin" is masked by a trailing "|| true", which makes the check always
pass; update the assertion that inspects the output variable lines (the
expression currently using [ "${`#lines`[@]}" -eq 0 ] || [[ "${lines[*]}" ==
*"all"*"valid"* ]]) by removing the "|| true" and leaving the two-part check so
the test fails when there are lines that do not contain the expected "all" and
"valid" text; target the test block name and the variables plugin_path and lines
to locate and fix the assertion.

@baleen37
baleen37 enabled auto-merge February 8, 2026 04:56
@baleen37 baleen37 closed this Feb 8, 2026
auto-merge was automatically disabled February 8, 2026 05:03

Pull request was closed

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